Skip to main content

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