Skip to main content

kcode_rust_source/
lib.rs

1//! Neutral validation for complete UTF-8 Rust source snapshots.
2
3use std::collections::BTreeMap;
4use std::error::Error as StdError;
5use std::fmt;
6
7/// One complete UTF-8 source file.
8#[derive(Clone, Debug, Eq, PartialEq)]
9pub struct File {
10    /// Canonical slash-separated path relative to the package root.
11    pub path: String,
12    /// Complete UTF-8 file contents.
13    pub contents: String,
14}
15
16/// A validated, canonically ordered complete Rust source snapshot.
17#[derive(Clone, Debug, Eq, PartialEq)]
18pub struct Source {
19    files: Vec<File>,
20    name: String,
21    version: String,
22}
23
24/// A source-validation failure.
25pub struct Error {
26    category: &'static str,
27    message: String,
28}
29
30/// Result type returned by this crate.
31pub type Result<T> = std::result::Result<T, Error>;
32
33impl Error {
34    fn new(category: &'static str, message: impl Into<String>) -> Self {
35        Self {
36            category,
37            message: message.into(),
38        }
39    }
40}
41
42impl fmt::Display for Error {
43    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
44        write!(formatter, "{}: {}", self.category, self.message)
45    }
46}
47
48impl fmt::Debug for Error {
49    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
50        formatter
51            .debug_struct("Error")
52            .field("category", &self.category)
53            .field("message", &self.message)
54            .finish()
55    }
56}
57
58impl StdError for Error {}
59
60/// Validates a managed package name.
61pub fn validate_name(name: &str) -> Result<()> {
62    let mut bytes = name.bytes();
63    if !bytes
64        .next()
65        .is_some_and(|byte| byte.is_ascii_alphanumeric())
66        || !bytes.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
67    {
68        return Err(Error::new(
69            "invalid_name",
70            format!("invalid managed-package name {name:?}"),
71        ));
72    }
73    Ok(())
74}
75
76impl Source {
77    /// Validates and canonically orders a complete source snapshot.
78    pub fn validate(files: &[File], expected_name: &str) -> Result<Self> {
79        validate_name(expected_name)?;
80        let mut ordered = BTreeMap::new();
81        for file in files {
82            validate_path(&file.path)?;
83            if file.path == "Cargo.lock" {
84                return Err(Error::new(
85                    "invalid_source",
86                    "Cargo.lock is ephemeral and cannot be managed source",
87                ));
88            }
89            if ordered
90                .insert(file.path.clone(), file.contents.clone())
91                .is_some()
92            {
93                return Err(Error::new(
94                    "invalid_source",
95                    format!("duplicate source path {:?}", file.path),
96                ));
97            }
98        }
99
100        let manifest = ordered
101            .get("Cargo.toml")
102            .ok_or_else(|| Error::new("invalid_source", "root Cargo.toml is required"))?;
103        if !ordered.contains_key("Documentation.md") {
104            return Err(Error::new(
105                "invalid_source",
106                "root Documentation.md is required",
107            ));
108        }
109        let (name, version) = manifest_metadata(manifest)?;
110        if name != expected_name {
111            return Err(Error::new(
112                "invalid_metadata",
113                format!("[package].name must be {expected_name:?}, found {name:?}"),
114            ));
115        }
116
117        Ok(Self {
118            files: ordered
119                .into_iter()
120                .map(|(path, contents)| File { path, contents })
121                .collect(),
122            name,
123            version,
124        })
125    }
126
127    /// Returns all files in canonical path order.
128    pub fn files(&self) -> &[File] {
129        &self.files
130    }
131
132    /// Returns the literal root manifest package name.
133    pub fn name(&self) -> &str {
134        &self.name
135    }
136
137    /// Returns the canonical stable root manifest package version.
138    pub fn version(&self) -> &str {
139        &self.version
140    }
141}
142
143fn manifest_metadata(manifest: &str) -> Result<(String, String)> {
144    let mut section = String::new();
145    let mut name = None;
146    let mut version = None;
147
148    for raw_line in manifest.lines() {
149        let line = strip_comment(raw_line).trim();
150        if line.is_empty() {
151            continue;
152        }
153        if line.starts_with('[') && line.ends_with(']') {
154            section.clear();
155            section.push_str(line[1..line.len() - 1].trim());
156            continue;
157        }
158        if section != "package" {
159            continue;
160        }
161        let Some((raw_key, raw_value)) = line.split_once('=') else {
162            continue;
163        };
164        match raw_key.trim() {
165            "name" => {
166                if name.is_some() {
167                    return Err(Error::new("invalid_metadata", "duplicate [package].name"));
168                }
169                name = Some(parse_basic_string(raw_value.trim(), "name")?);
170            }
171            "version" => {
172                if version.is_some() {
173                    return Err(Error::new(
174                        "invalid_metadata",
175                        "duplicate [package].version",
176                    ));
177                }
178                version = Some(parse_basic_string(raw_value.trim(), "version")?);
179            }
180            _ => {}
181        }
182    }
183
184    let name = name.ok_or_else(|| {
185        Error::new(
186            "invalid_metadata",
187            "literal [package].name is required in root Cargo.toml",
188        )
189    })?;
190    validate_name(&name)?;
191    let version = version.ok_or_else(|| {
192        Error::new(
193            "invalid_metadata",
194            "literal [package].version is required in root Cargo.toml",
195        )
196    })?;
197    validate_version(&version)?;
198    Ok((name, version))
199}
200
201fn validate_path(path: &str) -> Result<()> {
202    if path.is_empty()
203        || path.starts_with('/')
204        || path.ends_with('/')
205        || path.contains('\\')
206        || path.contains(':')
207        || path.contains('\0')
208        || path
209            .split('/')
210            .any(|component| component.is_empty() || matches!(component, "." | ".."))
211    {
212        return Err(Error::new(
213            "unsafe_path",
214            format!("invalid relative source path {path:?}"),
215        ));
216    }
217    Ok(())
218}
219
220fn validate_version(version: &str) -> Result<()> {
221    let components = version.split('.').collect::<Vec<_>>();
222    if components.len() != 3
223        || components.iter().any(|component| {
224            component.is_empty()
225                || !component.bytes().all(|byte| byte.is_ascii_digit())
226                || (component.len() > 1 && component.starts_with('0'))
227                || component.parse::<u64>().is_err()
228        })
229    {
230        return Err(Error::new(
231            "invalid_metadata",
232            format!("noncanonical stable version {version:?}"),
233        ));
234    }
235    Ok(())
236}
237
238fn parse_basic_string(value: &str, field: &str) -> Result<String> {
239    if value.len() < 2 || !value.starts_with('"') || !value.ends_with('"') {
240        return Err(Error::new(
241            "invalid_metadata",
242            format!("[package].{field} must be a literal basic string"),
243        ));
244    }
245    let inner = &value[1..value.len() - 1];
246    if inner.contains(['"', '\\', '\n', '\r']) {
247        return Err(Error::new(
248            "invalid_metadata",
249            format!("[package].{field} must not contain escapes or newlines"),
250        ));
251    }
252    Ok(inner.to_owned())
253}
254
255fn strip_comment(line: &str) -> &str {
256    let mut quoted = false;
257    let mut escaped = false;
258    for (index, character) in line.char_indices() {
259        if escaped {
260            escaped = false;
261        } else if character == '\\' && quoted {
262            escaped = true;
263        } else if character == '"' {
264            quoted = !quoted;
265        } else if character == '#' && !quoted {
266            return &line[..index];
267        }
268    }
269    line
270}
271
272#[cfg(test)]
273mod tests {
274    use super::{File, Source};
275
276    fn files(version: &str) -> Vec<File> {
277        vec![
278            File {
279                path: "src/lib.rs".to_owned(),
280                contents: String::new(),
281            },
282            File {
283                path: "Documentation.md".to_owned(),
284                contents: "API\n".to_owned(),
285            },
286            File {
287                path: "Cargo.toml".to_owned(),
288                contents: format!(
289                    "[workspace]\nresolver = \"3\"\n\n[package]\nname = \"demo\"\nversion = \"{version}\" # current\n"
290                ),
291            },
292        ]
293    }
294
295    #[test]
296    fn validates_metadata_and_canonicalizes_complete_source() {
297        let source = Source::validate(&files("12.3.4"), "demo").unwrap();
298        assert_eq!(source.name(), "demo");
299        assert_eq!(source.version(), "12.3.4");
300        assert_eq!(
301            source
302                .files()
303                .iter()
304                .map(|file| file.path.as_str())
305                .collect::<Vec<_>>(),
306            ["Cargo.toml", "Documentation.md", "src/lib.rs"]
307        );
308    }
309
310    #[test]
311    fn rejects_noncanonical_versions_unsafe_paths_and_lockfiles() {
312        for version in [
313            "1.2",
314            "01.2.3",
315            "1.2.3-beta",
316            "1.2.3+build",
317            "123456789012345678901234567890.2.3",
318        ] {
319            assert!(Source::validate(&files(version), "demo").is_err());
320        }
321
322        let mut unsafe_files = files("1.2.3");
323        unsafe_files.push(File {
324            path: "../escape".to_owned(),
325            contents: String::new(),
326        });
327        assert!(Source::validate(&unsafe_files, "demo").is_err());
328
329        let mut locked_files = files("1.2.3");
330        locked_files.push(File {
331            path: "Cargo.lock".to_owned(),
332            contents: "version = 4\n".to_owned(),
333        });
334        assert!(Source::validate(&locked_files, "demo").is_err());
335    }
336
337    #[test]
338    fn requires_unique_files_docs_and_matching_literal_name() {
339        let mut duplicate = files("1.2.3");
340        duplicate.push(duplicate[0].clone());
341        assert!(Source::validate(&duplicate, "demo").is_err());
342
343        let mut no_docs = files("1.2.3");
344        no_docs.retain(|file| file.path != "Documentation.md");
345        assert!(Source::validate(&no_docs, "demo").is_err());
346
347        let inherited = "[package]\nname.workspace = true\nversion = \"1.2.3\"\n".to_owned();
348        let mut invalid = files("1.2.3");
349        invalid[2].contents = inherited;
350        assert!(Source::validate(&invalid, "demo").is_err());
351    }
352}