Skip to content

Commit eb03952

Browse files
committed
trustpub: Implement basic validation functions for GitHub configs
1 parent 0a25aee commit eb03952

File tree

5 files changed

+167
-0
lines changed

5 files changed

+167
-0
lines changed

Cargo.lock

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/crates_io_trustpub/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,9 @@ edition = "2024"
88
workspace = true
99

1010
[dependencies]
11+
regex = "=1.11.1"
12+
thiserror = "=2.0.12"
1113

1214
[dev-dependencies]
15+
claims = "=0.8.0"
16+
insta = "=1.43.1"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub mod validation;
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
use std::sync::LazyLock;
2+
3+
const MAX_FIELD_LENGTH: usize = 255;
4+
5+
#[derive(Debug, thiserror::Error)]
6+
pub enum ValidationError {
7+
#[error("GitHub repository owner name may not be empty")]
8+
OwnerEmpty,
9+
#[error("GitHub repository owner name is too long (maximum is {MAX_FIELD_LENGTH} characters)")]
10+
OwnerTooLong,
11+
#[error("Invalid GitHub repository owner name")]
12+
OwnerInvalid,
13+
14+
#[error("GitHub repository name may not be empty")]
15+
RepoEmpty,
16+
#[error("GitHub repository name is too long (maximum is {MAX_FIELD_LENGTH} characters)")]
17+
RepoTooLong,
18+
#[error("Invalid GitHub repository name")]
19+
RepoInvalid,
20+
21+
#[error("Workflow filename may not be empty")]
22+
WorkflowFilenameEmpty,
23+
#[error("Workflow filename is too long (maximum is {MAX_FIELD_LENGTH} characters)")]
24+
WorkflowFilenameTooLong,
25+
#[error("Workflow filename must end with `.yml` or `.yaml`")]
26+
WorkflowFilenameMissingSuffix,
27+
#[error("Workflow filename must be a filename only, without directories")]
28+
WorkflowFilenameContainsSlash,
29+
30+
#[error("Environment name may not be empty (use `null` to omit)")]
31+
EnvironmentEmptyString,
32+
#[error("Environment name is too long (maximum is {MAX_FIELD_LENGTH} characters)")]
33+
EnvironmentTooLong,
34+
#[error("Environment name may not start with whitespace")]
35+
EnvironmentStartsWithWhitespace,
36+
#[error("Environment name may not end with whitespace")]
37+
EnvironmentEndsWithWhitespace,
38+
#[error(r#"Environment name must not contain non-printable characters or the characters "'", """, "`", ",", ";", "\""#)]
39+
EnvironmentInvalidChars,
40+
}
41+
42+
pub fn validate_owner(owner: &str) -> Result<(), ValidationError> {
43+
static RE_VALID_GITHUB_OWNER: LazyLock<regex::Regex> =
44+
LazyLock::new(|| regex::Regex::new(r"^[a-zA-Z0-9][a-zA-Z0-9-]*$").unwrap());
45+
46+
if owner.is_empty() {
47+
Err(ValidationError::OwnerEmpty)
48+
} else if owner.len() > MAX_FIELD_LENGTH {
49+
Err(ValidationError::OwnerTooLong)
50+
} else if !RE_VALID_GITHUB_OWNER.is_match(owner) {
51+
Err(ValidationError::OwnerInvalid)
52+
} else {
53+
Ok(())
54+
}
55+
}
56+
57+
pub fn validate_repo(repo: &str) -> Result<(), ValidationError> {
58+
static RE_VALID_GITHUB_REPO: LazyLock<regex::Regex> =
59+
LazyLock::new(|| regex::Regex::new(r"^[a-zA-Z0-9-_.]+$").unwrap());
60+
61+
if repo.is_empty() {
62+
Err(ValidationError::RepoEmpty)
63+
} else if repo.len() > MAX_FIELD_LENGTH {
64+
Err(ValidationError::RepoTooLong)
65+
} else if !RE_VALID_GITHUB_REPO.is_match(repo) {
66+
Err(ValidationError::RepoInvalid)
67+
} else {
68+
Ok(())
69+
}
70+
}
71+
72+
pub fn validate_workflow_filename(filename: &str) -> Result<(), ValidationError> {
73+
if filename.is_empty() {
74+
Err(ValidationError::WorkflowFilenameEmpty)
75+
} else if filename.len() > MAX_FIELD_LENGTH {
76+
Err(ValidationError::WorkflowFilenameTooLong)
77+
} else if !filename.ends_with(".yml") && !filename.ends_with(".yaml") {
78+
Err(ValidationError::WorkflowFilenameMissingSuffix)
79+
} else if filename.contains('/') {
80+
Err(ValidationError::WorkflowFilenameContainsSlash)
81+
} else {
82+
Ok(())
83+
}
84+
}
85+
86+
pub fn validate_environment(env: &str) -> Result<(), ValidationError> {
87+
static RE_INVALID_ENVIRONMENT_CHARS: LazyLock<regex::Regex> =
88+
LazyLock::new(|| regex::Regex::new(r#"[\x00-\x1F\x7F'"`,;\\]"#).unwrap());
89+
90+
if env.is_empty() {
91+
Err(ValidationError::EnvironmentEmptyString)
92+
} else if env.len() > MAX_FIELD_LENGTH {
93+
Err(ValidationError::EnvironmentTooLong)
94+
} else if env.starts_with(" ") {
95+
Err(ValidationError::EnvironmentStartsWithWhitespace)
96+
} else if env.ends_with(" ") {
97+
Err(ValidationError::EnvironmentEndsWithWhitespace)
98+
} else if RE_INVALID_ENVIRONMENT_CHARS.is_match(env) {
99+
Err(ValidationError::EnvironmentInvalidChars)
100+
} else {
101+
Ok(())
102+
}
103+
}
104+
105+
#[cfg(test)]
106+
mod tests {
107+
use super::*;
108+
use claims::assert_err;
109+
use insta::assert_snapshot;
110+
111+
#[test]
112+
fn test_validate_owner() {
113+
assert_snapshot!(assert_err!(validate_owner("")), @"GitHub repository owner name may not be empty");
114+
assert_snapshot!(assert_err!(validate_owner(&"x".repeat(256))), @"GitHub repository owner name is too long (maximum is 255 characters)");
115+
assert_snapshot!(assert_err!(validate_owner("invalid_characters@")), @"Invalid GitHub repository owner name");
116+
}
117+
118+
#[test]
119+
fn test_validate_repo() {
120+
assert_snapshot!(assert_err!(validate_repo("")), @"GitHub repository name may not be empty");
121+
assert_snapshot!(assert_err!(validate_repo(&"x".repeat(256))), @"GitHub repository name is too long (maximum is 255 characters)");
122+
assert_snapshot!(assert_err!(validate_repo("$invalid#characters")), @"Invalid GitHub repository name");
123+
}
124+
125+
#[test]
126+
fn test_validate_workflow_filename() {
127+
assert_snapshot!(assert_err!(validate_workflow_filename("")), @"Workflow filename may not be empty");
128+
assert_snapshot!(assert_err!(validate_workflow_filename(&"x".repeat(256))), @"Workflow filename is too long (maximum is 255 characters)");
129+
assert_snapshot!(assert_err!(validate_workflow_filename("missing_suffix")), @"Workflow filename must end with `.yml` or `.yaml`");
130+
assert_snapshot!(assert_err!(validate_workflow_filename("/slash")), @"Workflow filename must end with `.yml` or `.yaml`");
131+
assert_snapshot!(assert_err!(validate_workflow_filename("/many/slashes")), @"Workflow filename must end with `.yml` or `.yaml`");
132+
assert_snapshot!(assert_err!(validate_workflow_filename("/slash.yml")), @"Workflow filename must be a filename only, without directories");
133+
}
134+
135+
#[test]
136+
fn test_validate_environment() {
137+
assert_snapshot!(assert_err!(validate_environment("")), @"Environment name may not be empty (use `null` to omit)");
138+
assert_snapshot!(assert_err!(validate_environment(&"x".repeat(256))), @"Environment name is too long (maximum is 255 characters)");
139+
assert_snapshot!(assert_err!(validate_environment(" foo")), @"Environment name may not start with whitespace");
140+
assert_snapshot!(assert_err!(validate_environment("foo ")), @"Environment name may not end with whitespace");
141+
assert_snapshot!(assert_err!(validate_environment("'")), @r#"Environment name must not contain non-printable characters or the characters "'", """, "`", ",", ";", "\""#);
142+
assert_snapshot!(assert_err!(validate_environment("\"")), @r#"Environment name must not contain non-printable characters or the characters "'", """, "`", ",", ";", "\""#);
143+
assert_snapshot!(assert_err!(validate_environment("`")), @r#"Environment name must not contain non-printable characters or the characters "'", """, "`", ",", ";", "\""#);
144+
assert_snapshot!(assert_err!(validate_environment(",")), @r#"Environment name must not contain non-printable characters or the characters "'", """, "`", ",", ";", "\""#);
145+
assert_snapshot!(assert_err!(validate_environment(";")), @r#"Environment name must not contain non-printable characters or the characters "'", """, "`", ",", ";", "\""#);
146+
assert_snapshot!(assert_err!(validate_environment("\\")), @r#"Environment name must not contain non-printable characters or the characters "'", """, "`", ",", ";", "\""#);
147+
assert_snapshot!(assert_err!(validate_environment("\x00")), @r#"Environment name must not contain non-printable characters or the characters "'", """, "`", ",", ";", "\""#);
148+
assert_snapshot!(assert_err!(validate_environment("\x1f")), @r#"Environment name must not contain non-printable characters or the characters "'", """, "`", ",", ";", "\""#);
149+
assert_snapshot!(assert_err!(validate_environment("\x7f")), @r#"Environment name must not contain non-printable characters or the characters "'", """, "`", ",", ";", "\""#);
150+
assert_snapshot!(assert_err!(validate_environment("\t")), @r#"Environment name must not contain non-printable characters or the characters "'", """, "`", ",", ";", "\""#);
151+
assert_snapshot!(assert_err!(validate_environment("\r")), @r#"Environment name must not contain non-printable characters or the characters "'", """, "`", ",", ";", "\""#);
152+
assert_snapshot!(assert_err!(validate_environment("\n")), @r#"Environment name must not contain non-printable characters or the characters "'", """, "`", ",", ";", "\""#);
153+
}
154+
}

crates/crates_io_trustpub/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
#![doc = include_str!("../README.md")]
2+
3+
pub mod github;

0 commit comments

Comments
 (0)