Skip to main content

kcode_rust_libs/
repository.rs

1use std::collections::HashSet;
2use std::fmt;
3use std::fs;
4use std::io;
5use std::path::{Path, PathBuf};
6use std::sync::Arc;
7
8use crate::files::{
9    absolute_lexical, load_files, paths_overlap, validate_rust_lib_name, validate_write_target,
10    write_file,
11};
12use crate::sandbox;
13use crate::{CheckResult, Error, Result, RustLibDocs, RustLibFile};
14
15const DOCUMENTATION_FILE: &str = "Documentation.md";
16const MANIFEST_FILE: &str = "Cargo.toml";
17
18#[derive(Clone)]
19struct RegistryToken(Arc<str>);
20
21impl RegistryToken {
22    fn new(value: impl Into<String>) -> Result<Self> {
23        let value = value.into();
24        let value = value.trim();
25        if value.is_empty() {
26            return Err(Error::InvalidRegistryToken);
27        }
28        Ok(Self(Arc::from(value)))
29    }
30
31    fn expose(&self) -> &str {
32        self.0.as_ref()
33    }
34}
35
36impl fmt::Debug for RegistryToken {
37    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38        f.write_str("RegistryToken([REDACTED])")
39    }
40}
41
42/// Entry point for creating and opening managed Rust libraries.
43#[derive(Clone, Debug)]
44pub struct KcodeRustLibs {
45    rust_libs_root: PathBuf,
46    work_root: PathBuf,
47    registry_token: RegistryToken,
48}
49
50impl KcodeRustLibs {
51    /// Use `rust_libs_root` as the persistent library directory and retain the supplied crates.io
52    /// registry token in memory for publication.
53    pub fn new(
54        rust_libs_root: impl Into<PathBuf>,
55        crates_io_registry_token: impl Into<String>,
56    ) -> Result<Self> {
57        let registry_token = RegistryToken::new(crates_io_registry_token)?;
58        let rust_libs_lexical = absolute_lexical(&rust_libs_root.into())?;
59        let work_lexical = absolute_lexical(
60            &std::env::temp_dir().join(format!("kcode-rust-libs-work-{}", std::process::id())),
61        )?;
62        if paths_overlap(&rust_libs_lexical, &work_lexical) {
63            return Err(Error::RootsOverlap {
64                rust_libs_root: rust_libs_lexical,
65                work_root: work_lexical,
66            });
67        }
68
69        create_root(&rust_libs_lexical, "create Rust libraries root")?;
70        create_root(&work_lexical, "create work root")?;
71
72        let rust_libs_root = fs::canonicalize(&rust_libs_lexical).map_err(|source| {
73            Error::io(
74                "canonicalize Rust libraries root",
75                &rust_libs_lexical,
76                source,
77            )
78        })?;
79        let work_root = fs::canonicalize(&work_lexical)
80            .map_err(|source| Error::io("canonicalize work root", &work_lexical, source))?;
81
82        if paths_overlap(&rust_libs_root, &work_root) {
83            return Err(Error::RootsOverlap {
84                rust_libs_root,
85                work_root,
86            });
87        }
88
89        Ok(Self {
90            rust_libs_root,
91            work_root,
92            registry_token,
93        })
94    }
95
96    /// Create a minimal managed Rust library and return it already loaded.
97    pub fn create_rust_lib(&self, name: &str) -> Result<OpenedRustLib> {
98        validate_rust_lib_name(name)?;
99        let root = self.rust_libs_root.join(name);
100
101        match fs::create_dir(&root) {
102            Ok(()) => {}
103            Err(source) if source.kind() == io::ErrorKind::AlreadyExists => {
104                return Err(Error::RustLibAlreadyExists(name.to_owned()));
105            }
106            Err(source) => return Err(Error::io("create Rust library", root, source)),
107        }
108
109        let created = (|| {
110            let source_directory = root.join("src");
111            fs::create_dir(&source_directory).map_err(|source| {
112                Error::io(
113                    "create Rust library source directory",
114                    &source_directory,
115                    source,
116                )
117            })?;
118
119            let manifest = format!(
120                "[package]\nname = \"{name}\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[dependencies]\n"
121            );
122            write_new_file(&root.join(MANIFEST_FILE), &manifest, "write Cargo manifest")?;
123            write_new_file(
124                &root.join(DOCUMENTATION_FILE),
125                "",
126                "write Rust library documentation",
127            )?;
128            write_new_file(&source_directory.join("lib.rs"), "", "write library source")?;
129            Ok(())
130        })();
131
132        if let Err(error) = created {
133            let _ = fs::remove_dir_all(&root);
134            return Err(error);
135        }
136
137        self.open_validated(name, root)
138    }
139
140    /// Open an existing managed Rust library and recursively load all of its files.
141    pub fn open_rust_lib(&self, name: &str) -> Result<OpenedRustLib> {
142        validate_rust_lib_name(name)?;
143        let root = self.rust_libs_root.join(name);
144        let metadata = match fs::symlink_metadata(&root) {
145            Ok(metadata) => metadata,
146            Err(source) if source.kind() == io::ErrorKind::NotFound => {
147                return Err(Error::RustLibNotFound(name.to_owned()));
148            }
149            Err(source) => return Err(Error::io("inspect Rust library", root, source)),
150        };
151
152        if metadata.file_type().is_symlink() {
153            return Err(Error::SymlinkNotAllowed(root));
154        }
155        if !metadata.is_dir() {
156            return Err(Error::RustLibIsNotDirectory(name.to_owned()));
157        }
158
159        self.open_validated(name, root)
160    }
161
162    fn open_validated(&self, name: &str, root: PathBuf) -> Result<OpenedRustLib> {
163        let files = load_files(&root)?;
164        let docs = validate_rust_lib_metadata(&files)?;
165        Ok(OpenedRustLib {
166            name: name.to_owned(),
167            root,
168            work_root: self.work_root.clone(),
169            registry_token: self.registry_token.clone(),
170            files,
171            docs,
172        })
173    }
174}
175
176fn create_root(path: &Path, action: &'static str) -> Result<()> {
177    fs::create_dir_all(path).map_err(|source| Error::io(action, path, source))?;
178    let metadata = fs::metadata(path).map_err(|source| Error::io("inspect root", path, source))?;
179    if !metadata.is_dir() {
180        return Err(Error::io(
181            "use root as a directory",
182            path,
183            io::Error::new(io::ErrorKind::NotADirectory, "path is not a directory"),
184        ));
185    }
186    Ok(())
187}
188
189fn write_new_file(path: &Path, contents: &str, action: &'static str) -> Result<()> {
190    fs::write(path, contents).map_err(|source| Error::io(action, path, source))
191}
192
193/// A managed Rust library whose complete file set was loaded when it was opened.
194#[derive(Debug)]
195pub struct OpenedRustLib {
196    name: String,
197    root: PathBuf,
198    work_root: PathBuf,
199    registry_token: RegistryToken,
200    files: Vec<RustLibFile>,
201    docs: RustLibDocs,
202}
203
204impl OpenedRustLib {
205    pub fn name(&self) -> &str {
206        &self.name
207    }
208
209    pub fn files(&self) -> &[RustLibFile] {
210        &self.files
211    }
212
213    /// Return only the canonical manifest version and agent-facing documentation text.
214    pub fn docs(&self) -> &RustLibDocs {
215        &self.docs
216    }
217
218    /// Create or completely overwrite every file in the supplied batch.
219    pub fn write(&mut self, files: &[RustLibFile]) -> Result<()> {
220        let mut unique_paths = HashSet::with_capacity(files.len());
221        for file in files {
222            if !unique_paths.insert(file.path.clone()) {
223                return Err(Error::DuplicateWritePath(file.path.to_string()));
224            }
225            validate_write_target(&self.root, &file.path)?;
226        }
227
228        let current_files = load_files(&self.root)?;
229        validate_projected_metadata(&current_files, files)?;
230
231        for file in files {
232            write_file(&self.root, file)?;
233        }
234
235        let reloaded = load_files(&self.root)?;
236        let docs = validate_rust_lib_metadata(&reloaded)?;
237        self.files = reloaded;
238        self.docs = docs;
239        Ok(())
240    }
241
242    /// Run the complete Rust quality and test pipeline in disposable Podman containers.
243    pub fn check(&self) -> Result<CheckResult> {
244        sandbox::check(&self.root, &self.work_root)
245    }
246
247    /// Check the Rust library and publish its root Cargo package to crates.io using the token
248    /// supplied when `KcodeRustLibs` was initialized.
249    pub fn publish(&self) -> Result<()> {
250        let check = self.check()?;
251        if !check.passed() {
252            return Err(Error::CheckFailed(check));
253        }
254
255        sandbox::publish(&self.root, &self.work_root, self.registry_token.expose())
256    }
257}
258
259fn validate_projected_metadata(
260    current: &[RustLibFile],
261    writes: &[RustLibFile],
262) -> Result<RustLibDocs> {
263    let projected = |path: &str| {
264        writes
265            .iter()
266            .find(|file| file.path.as_str() == path)
267            .or_else(|| current.iter().find(|file| file.path.as_str() == path))
268            .map(|file| file.contents.as_str())
269    };
270    validate_metadata_text(projected(DOCUMENTATION_FILE), projected(MANIFEST_FILE))
271}
272
273fn validate_rust_lib_metadata(files: &[RustLibFile]) -> Result<RustLibDocs> {
274    let contents = |path: &str| {
275        files
276            .iter()
277            .find(|file| file.path.as_str() == path)
278            .map(|file| file.contents.as_str())
279    };
280    validate_metadata_text(contents(DOCUMENTATION_FILE), contents(MANIFEST_FILE))
281}
282
283fn validate_metadata_text(
284    documentation: Option<&str>,
285    manifest: Option<&str>,
286) -> Result<RustLibDocs> {
287    let documentation = documentation.ok_or(Error::MissingRequiredFile(DOCUMENTATION_FILE))?;
288    let manifest = manifest.ok_or(Error::MissingRequiredFile(MANIFEST_FILE))?;
289    let version = parse_version(&manifest_package_version(manifest)?)?;
290    Ok(RustLibDocs {
291        version,
292        documentation: documentation.to_owned(),
293    })
294}
295
296fn parse_version(value: &str) -> Result<String> {
297    let components = value.split('.').collect::<Vec<_>>();
298    let valid_component = |component: &str| {
299        !component.is_empty()
300            && component.bytes().all(|byte| byte.is_ascii_digit())
301            && (component == "0" || !component.starts_with('0'))
302    };
303
304    if components.len() != 3 || !components.into_iter().all(valid_component) {
305        return Err(Error::InvalidVersion(value.to_owned()));
306    }
307
308    Ok(value.to_owned())
309}
310
311fn manifest_package_version(manifest: &str) -> Result<String> {
312    let mut in_package = false;
313    let mut version = None;
314
315    for raw_line in manifest.lines() {
316        let line = strip_toml_comment(raw_line).trim();
317        if line.is_empty() {
318            continue;
319        }
320        if line.starts_with('[') {
321            in_package = line
322                .strip_prefix('[')
323                .and_then(|value| value.strip_suffix(']'))
324                .is_some_and(|value| value.trim() == "package");
325            continue;
326        }
327        if !in_package {
328            continue;
329        }
330
331        let Some((key, value)) = line.split_once('=') else {
332            continue;
333        };
334        if key.trim() != "version" {
335            continue;
336        }
337        if version.is_some() {
338            return Err(Error::InvalidCargoManifest(
339                "[package] declares version more than once".to_owned(),
340            ));
341        }
342
343        let value = value.trim();
344        let unquoted = value
345            .strip_prefix('"')
346            .and_then(|value| value.strip_suffix('"'))
347            .or_else(|| {
348                value
349                    .strip_prefix('\'')
350                    .and_then(|value| value.strip_suffix('\''))
351            })
352            .filter(|value| !value.contains(['"', '\'', '\\']))
353            .ok_or_else(|| {
354                Error::InvalidCargoManifest(
355                    "[package].version must be a single-line literal string".to_owned(),
356                )
357            })?;
358        version = Some(unquoted.to_owned());
359    }
360
361    version.ok_or_else(|| {
362        Error::InvalidCargoManifest(
363            "the root manifest needs a literal version in [package]".to_owned(),
364        )
365    })
366}
367
368fn strip_toml_comment(line: &str) -> &str {
369    let mut quote = None;
370    let mut escaped = false;
371    for (index, character) in line.char_indices() {
372        match quote {
373            Some('"') if escaped => escaped = false,
374            Some('"') if character == '\\' => escaped = true,
375            Some(active) if character == active => quote = None,
376            Some(_) => {}
377            None if character == '"' || character == '\'' => quote = Some(character),
378            None if character == '#' => return &line[..index],
379            None => {}
380        }
381    }
382    line
383}
384
385#[cfg(test)]
386mod tests {
387    use super::{parse_version, validate_rust_lib_metadata};
388    use crate::{Error, RustLibFile, RustLibPath};
389
390    fn file(path: &str, contents: &str) -> RustLibFile {
391        RustLibFile::new(RustLibPath::new(path).unwrap(), contents)
392    }
393
394    #[test]
395    fn accepts_only_canonical_stable_versions() {
396        for value in ["0.1.0", "12.34.56"] {
397            assert!(parse_version(value).is_ok(), "{value:?}");
398        }
399        for value in [
400            "v0.1.0",
401            "01.2.3",
402            "0.1",
403            "0.1.0-beta.1",
404            "0.1.0+build",
405            " 0.1.0 ",
406        ] {
407            assert!(
408                matches!(parse_version(value), Err(Error::InvalidVersion(_))),
409                "{value:?}"
410            );
411        }
412    }
413
414    #[test]
415    fn reads_the_canonical_version_from_the_root_manifest() {
416        let files = vec![
417            file(
418                "Cargo.toml",
419                "[package]\nname = \"demo#one\"\nversion = '2.0.0' # current\n",
420            ),
421            file("Documentation.md", "docs\n"),
422        ];
423        let docs = validate_rust_lib_metadata(&files).unwrap();
424        assert_eq!(docs.version, "2.0.0");
425        assert_eq!(docs.documentation, "docs\n");
426
427        let inherited = vec![
428            file(
429                "Cargo.toml",
430                "[package]\nname = \"demo\"\nversion.workspace = true\n",
431            ),
432            file("Documentation.md", ""),
433        ];
434        assert!(matches!(
435            validate_rust_lib_metadata(&inherited),
436            Err(Error::InvalidCargoManifest(_))
437        ));
438    }
439}