Skip to main content

kcode_rust_libs_v2/
model.rs

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