Skip to main content

kcode_rust_library_toolchain/
lib.rs

1//! Disposable validation and publication for managed Rust library source.
2
3use std::error::Error as StdError;
4use std::ffi::{OsStr, OsString};
5use std::fmt;
6use std::fs;
7use std::io;
8use std::path::{Path, PathBuf};
9use std::process::{Command, ExitStatus};
10use std::sync::atomic::{AtomicU64, Ordering};
11use std::time::{SystemTime, UNIX_EPOCH};
12
13use kcode_rust_source::Source;
14
15const CONTAINERFILE: &str = include_str!("assets/Containerfile");
16const CHROMIUM_OUTER_SANDBOX_CONFIG: &str = include_str!("assets/ChromiumOuterSandbox.conf");
17static UNIQUE: AtomicU64 = AtomicU64::new(0);
18
19/// A toolchain failure with complete process diagnostics.
20pub struct Error(String);
21
22/// Result type returned by this crate.
23pub type Result<T> = std::result::Result<T, Error>;
24
25impl Error {
26    fn new(category: &str, message: impl fmt::Display) -> Self {
27        Self(format!("{category}: {message}"))
28    }
29
30    fn io(operation: &str, path: impl AsRef<Path>, source: io::Error) -> Self {
31        Self::new(
32            "io",
33            format!("{operation} at {}: {source}", path.as_ref().display()),
34        )
35    }
36
37    fn redact(mut self, secret: &str) -> Self {
38        if !secret.is_empty() {
39            self.0 = self.0.replace(secret, "[REDACTED]");
40        }
41        self
42    }
43}
44
45impl fmt::Display for Error {
46    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
47        formatter.write_str(&self.0)
48    }
49}
50
51impl fmt::Debug for Error {
52    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
53        formatter.debug_tuple("Error").field(&self.0).finish()
54    }
55}
56
57impl StdError for Error {}
58
59/// Formats and validates a disposable copy through all six release stages.
60pub fn check(source: &Source) -> Result<()> {
61    check_with(source, OsStr::new("podman"), &tool_image())
62}
63
64/// Revalidates and publishes the formatted disposable copy to crates.io.
65pub fn publish(source: &Source, crates_io_registry_token: &str) -> Result<()> {
66    publish_with(
67        source,
68        crates_io_registry_token,
69        OsStr::new("podman"),
70        &tool_image(),
71    )
72}
73
74fn check_with(source: &Source, podman: &OsStr, image: &str) -> Result<()> {
75    checked_run(source, podman, image).map(|_| ())
76}
77
78fn checked_run(source: &Source, podman: &OsStr, image: &str) -> Result<RunDirectory> {
79    ensure_image(podman, image)?;
80    let run = RunDirectory::new(source, "check")?;
81    for stage in stages() {
82        run_stage(podman, image, &run, stage)?;
83    }
84    Ok(run)
85}
86
87fn publish_with(source: &Source, token: &str, podman: &OsStr, image: &str) -> Result<()> {
88    let token = token.trim();
89    if token.is_empty() {
90        return Err(Error::new(
91            "invalid_token",
92            "the crates.io registry token is empty",
93        ));
94    }
95
96    let run = checked_run(source, podman, image)?;
97    let mut command = podman_command(podman, &run, "/tmp", true);
98    command
99        .env("CARGO_REGISTRY_TOKEN", token)
100        .arg("--env=CARGO_REGISTRY_TOKEN")
101        .arg(image)
102        .arg("cargo")
103        .arg("--quiet")
104        .arg("--config")
105        .arg("registry.global-credential-providers=[\"cargo:token\"]")
106        .arg("publish")
107        .arg("--manifest-path=/workspace/Cargo.toml")
108        .arg("--registry=crates-io")
109        .arg("--no-verify")
110        .arg("--color=never");
111    run_checked(command, "publish", "cargo publish").map_err(|error| error.redact(token))
112}
113
114#[derive(Clone, Copy)]
115struct Stage {
116    category: &'static str,
117    label: &'static str,
118    arguments: &'static [&'static str],
119    network: bool,
120}
121
122fn stages() -> [Stage; 6] {
123    [
124        Stage {
125            category: "check.fetch",
126            label: "cargo fetch",
127            arguments: &["fetch", "--color", "never"],
128            network: true,
129        },
130        Stage {
131            category: "check.format",
132            label: "cargo fmt",
133            arguments: &["fmt", "--all"],
134            network: false,
135        },
136        Stage {
137            category: "check.build",
138            label: "cargo build",
139            arguments: &[
140                "build",
141                "--workspace",
142                "--all-targets",
143                "--all-features",
144                "--locked",
145                "--offline",
146                "--color",
147                "never",
148            ],
149            network: false,
150        },
151        Stage {
152            category: "check.clippy",
153            label: "cargo clippy",
154            arguments: &[
155                "clippy",
156                "--workspace",
157                "--all-targets",
158                "--all-features",
159                "--locked",
160                "--offline",
161                "--color",
162                "never",
163                "--",
164                "-D",
165                "warnings",
166            ],
167            network: false,
168        },
169        Stage {
170            category: "check.test",
171            label: "cargo test",
172            arguments: &[
173                "test",
174                "--workspace",
175                "--all-targets",
176                "--all-features",
177                "--locked",
178                "--offline",
179                "--no-fail-fast",
180                "--color",
181                "never",
182            ],
183            network: false,
184        },
185        Stage {
186            category: "check.doc_test",
187            label: "cargo doc tests",
188            arguments: &[
189                "test",
190                "--workspace",
191                "--all-features",
192                "--doc",
193                "--locked",
194                "--offline",
195                "--no-fail-fast",
196                "--color",
197                "never",
198            ],
199            network: false,
200        },
201    ]
202}
203
204fn run_stage(podman: &OsStr, image: &str, run: &RunDirectory, stage: Stage) -> Result<()> {
205    let mut command = podman_command(podman, run, "/workspace", stage.network);
206    command
207        .arg(image)
208        .arg("cargo")
209        .arg("--quiet")
210        .args(stage.arguments);
211    run_checked(command, stage.category, stage.label)
212}
213
214fn ensure_image(podman: &OsStr, image: &str) -> Result<()> {
215    let mut inspect = Command::new(podman);
216    inspect.arg("image").arg("exists").arg(image);
217    let inspected = run_capture(inspect, "inspect Podman image")?;
218    if inspected.status.success() {
219        return Ok(());
220    }
221    if inspected.status.code() != Some(1) {
222        return Err(command_error(
223            "sandbox.image",
224            "Podman image inspection",
225            inspected,
226        ));
227    }
228
229    let build = TemporaryDirectory::new("image")?;
230    let containerfile = build.path().join("Containerfile");
231    fs::write(&containerfile, CONTAINERFILE)
232        .map_err(|error| Error::io("write embedded Containerfile", &containerfile, error))?;
233    let chromium_config = build.path().join("ChromiumOuterSandbox.conf");
234    fs::write(&chromium_config, CHROMIUM_OUTER_SANDBOX_CONFIG).map_err(|error| {
235        Error::io(
236            "write embedded Chromium outer-sandbox configuration",
237            &chromium_config,
238            error,
239        )
240    })?;
241
242    let mut command = Command::new(podman);
243    command
244        .arg("build")
245        .arg("--tag")
246        .arg(image)
247        .arg("--file")
248        .arg(&containerfile)
249        .arg(build.path());
250    run_checked(command, "sandbox.image", "Podman image build")
251}
252
253fn podman_command(podman: &OsStr, run: &RunDirectory, workdir: &str, network: bool) -> Command {
254    let mut command = Command::new(podman);
255    command
256        .arg("run")
257        .arg("--rm")
258        .arg("--read-only")
259        .arg("--cap-drop=all")
260        .arg("--security-opt=no-new-privileges")
261        .arg("--userns=keep-id")
262        .arg("--tmpfs=/tmp:rw,nosuid,nodev")
263        .arg("--workdir")
264        .arg(workdir)
265        .arg("--env=CARGO_HOME=/cargo-home")
266        .arg("--env=CARGO_TARGET_DIR=/target")
267        .arg("--env=CARGO_TERM_COLOR=never");
268    if !network {
269        command.arg("--network=none");
270    }
271    command
272        .arg("--pull=never")
273        .arg("--volume")
274        .arg(volume_spec(&run.workspace, "/workspace"))
275        .arg("--volume")
276        .arg(volume_spec(&run.cargo_home, "/cargo-home"))
277        .arg("--volume")
278        .arg(volume_spec(&run.target, "/target"));
279    command
280}
281
282fn volume_spec(source: &Path, destination: &str) -> OsString {
283    let mut specification = source.as_os_str().to_os_string();
284    specification.push(":");
285    specification.push(destination);
286    specification.push(":rw,Z");
287    specification
288}
289
290fn run_checked(command: Command, category: &'static str, label: &'static str) -> Result<()> {
291    let output = run_capture(command, label)?;
292    if output.status.success() {
293        Ok(())
294    } else {
295        Err(command_error(category, label, output))
296    }
297}
298
299fn run_capture(mut command: Command, label: &str) -> Result<CapturedOutput> {
300    let output = command
301        .output()
302        .map_err(|error| Error::new("sandbox.start", format!("could not run {label}: {error}")))?;
303    Ok(CapturedOutput {
304        status: output.status,
305        stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
306        stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
307    })
308}
309
310fn command_error(category: &str, label: &str, output: CapturedOutput) -> Error {
311    let status = output
312        .status
313        .code()
314        .map_or_else(|| "signal".to_owned(), |code| code.to_string());
315    Error::new(
316        category,
317        format!(
318            "{label} exited with {status}\n--- stdout ---\n{}\n--- stderr ---\n{}",
319            output.stdout, output.stderr
320        ),
321    )
322}
323
324struct CapturedOutput {
325    status: ExitStatus,
326    stdout: String,
327    stderr: String,
328}
329
330struct RunDirectory {
331    _root: TemporaryDirectory,
332    workspace: PathBuf,
333    cargo_home: PathBuf,
334    target: PathBuf,
335}
336
337impl RunDirectory {
338    fn new(source: &Source, label: &str) -> Result<Self> {
339        let root = TemporaryDirectory::new(label)?;
340        let workspace = root.path().join("workspace");
341        let cargo_home = root.path().join("cargo-home");
342        let target = root.path().join("target");
343        fs::create_dir(&workspace)
344            .map_err(|error| Error::io("create disposable workspace", &workspace, error))?;
345        materialize(&workspace, source)?;
346        fs::create_dir(&cargo_home)
347            .map_err(|error| Error::io("create disposable Cargo home", &cargo_home, error))?;
348        fs::create_dir(&target)
349            .map_err(|error| Error::io("create disposable target", &target, error))?;
350        Ok(Self {
351            _root: root,
352            workspace,
353            cargo_home,
354            target,
355        })
356    }
357}
358
359fn materialize(root: &Path, source: &Source) -> Result<()> {
360    for file in source.files() {
361        let destination = root.join(&file.path);
362        let parent = destination.parent().ok_or_else(|| {
363            Error::new(
364                "unsafe_path",
365                format!("source path has no parent: {:?}", file.path),
366            )
367        })?;
368        fs::create_dir_all(parent)
369            .map_err(|error| Error::io("create source parent", parent, error))?;
370        fs::write(&destination, file.contents.as_bytes())
371            .map_err(|error| Error::io("write source file", &destination, error))?;
372    }
373    Ok(())
374}
375
376struct TemporaryDirectory(PathBuf);
377
378impl TemporaryDirectory {
379    fn new(label: &str) -> Result<Self> {
380        let parent = std::env::temp_dir().join("kcode-rust-library-toolchain");
381        fs::create_dir_all(&parent)
382            .map_err(|error| Error::io("create temporary work root", &parent, error))?;
383        loop {
384            let nanos = SystemTime::now()
385                .duration_since(UNIX_EPOCH)
386                .unwrap_or_default()
387                .as_nanos();
388            let counter = UNIQUE.fetch_add(1, Ordering::Relaxed);
389            let path = parent.join(format!(
390                "{label}-{}-{nanos:x}-{counter:x}",
391                std::process::id()
392            ));
393            match fs::create_dir(&path) {
394                Ok(()) => return Ok(Self(path)),
395                Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {}
396                Err(error) => {
397                    return Err(Error::io("create temporary directory", path, error));
398                }
399            }
400        }
401    }
402
403    fn path(&self) -> &Path {
404        &self.0
405    }
406}
407
408impl Drop for TemporaryDirectory {
409    fn drop(&mut self) {
410        let _ = fs::remove_dir_all(&self.0);
411    }
412}
413
414fn tool_image() -> String {
415    let mut hash = 0xcbf29ce484222325_u64;
416    for byte in CONTAINERFILE
417        .bytes()
418        .chain(CHROMIUM_OUTER_SANDBOX_CONFIG.bytes())
419    {
420        hash ^= u64::from(byte);
421        hash = hash.wrapping_mul(0x100000001b3);
422    }
423    format!(
424        "localhost/kcode-rust-library-toolchain:{}-{hash:016x}",
425        env!("CARGO_PKG_VERSION")
426    )
427}
428
429#[cfg(all(test, unix))]
430mod tests {
431    use std::ffi::OsStr;
432    use std::fs;
433    use std::os::unix::fs::PermissionsExt;
434    use std::path::{Path, PathBuf};
435
436    use kcode_rust_source::{File, Source};
437
438    use super::{RunDirectory, check_with, podman_command, publish_with};
439
440    struct Fixture(PathBuf);
441
442    impl Fixture {
443        fn new(label: &str) -> Self {
444            let path = std::env::temp_dir().join(format!(
445                "kcode-rust-library-toolchain-test-{label}-{}-{}",
446                std::process::id(),
447                super::UNIQUE.fetch_add(1, super::Ordering::Relaxed)
448            ));
449            fs::create_dir(&path).unwrap();
450            Self(path)
451        }
452
453        fn path(&self) -> &Path {
454            &self.0
455        }
456
457        fn script(&self, contents: &str) -> PathBuf {
458            let path = self.path().join("fake-podman");
459            fs::write(&path, contents).unwrap();
460            let mut permissions = fs::metadata(&path).unwrap().permissions();
461            permissions.set_mode(0o755);
462            fs::set_permissions(&path, permissions).unwrap();
463            path
464        }
465    }
466
467    impl Drop for Fixture {
468        fn drop(&mut self) {
469            let _ = fs::remove_dir_all(&self.0);
470        }
471    }
472
473    fn source() -> Source {
474        Source::validate(
475            &[
476                File {
477                    path: "Cargo.toml".to_owned(),
478                    contents:
479                        "[package]\nname = \"demo\"\nversion = \"0.1.0\"\nedition = \"2024\"\n"
480                            .to_owned(),
481                },
482                File {
483                    path: "Documentation.md".to_owned(),
484                    contents: "docs\n".to_owned(),
485                },
486                File {
487                    path: "src/lib.rs".to_owned(),
488                    contents: String::new(),
489                },
490            ],
491            "demo",
492        )
493        .unwrap()
494    }
495
496    #[test]
497    fn every_cargo_invocation_is_globally_quiet() {
498        let fixture = Fixture::new("quiet");
499        let log = fixture.path().join("arguments.log");
500        let podman = fixture.script(&format!(
501            "#!/bin/sh\nif [ \"$1\" = image ]; then exit 0; fi\nprintf '%s\\n' \"$*\" >> '{}'\nexit 0\n",
502            log.display()
503        ));
504        check_with(&source(), podman.as_os_str(), "test-image").unwrap();
505        publish_with(&source(), "private-token", podman.as_os_str(), "test-image").unwrap();
506
507        let arguments = fs::read_to_string(log).unwrap();
508        let cargo_lines = arguments
509            .lines()
510            .filter(|line| line.contains(" cargo "))
511            .collect::<Vec<_>>();
512        assert_eq!(cargo_lines.len(), 13);
513        assert!(
514            cargo_lines
515                .iter()
516                .all(|line| line.contains("test-image cargo --quiet "))
517        );
518        assert!(
519            cargo_lines
520                .iter()
521                .any(|line| line.contains(" cargo --quiet fmt --all"))
522        );
523        assert!(!arguments.contains("private-token"));
524        assert!(!arguments.contains("fmt --all --check"));
525    }
526
527    #[test]
528    fn failed_commands_retain_large_complete_stdout_and_stderr() {
529        let fixture = Fixture::new("complete-output");
530        let podman = fixture.script(
531            "#!/bin/sh\nif [ \"$1\" = image ]; then exit 0; fi\n\
532             printf 'stdout-start\\n'\n\
533             i=0; while [ $i -lt 4000 ]; do printf '0123456789'; i=$((i+1)); done\n\
534             printf '\\nstdout-end\\n'\n\
535             printf 'stderr-start\\n' >&2\n\
536             i=0; while [ $i -lt 4000 ]; do printf 'abcdefghij' >&2; i=$((i+1)); done\n\
537             printf '\\nstderr-end\\n' >&2\n\
538             exit 1\n",
539        );
540        let rendered = check_with(&source(), podman.as_os_str(), "test-image")
541            .unwrap_err()
542            .to_string();
543        assert!(rendered.len() > 75 * 1024);
544        for marker in ["stdout-start", "stdout-end", "stderr-start", "stderr-end"] {
545            assert!(rendered.contains(marker));
546        }
547        assert!(!rendered.contains("truncated"));
548    }
549
550    #[test]
551    fn publication_redacts_the_token_across_complete_output() {
552        let fixture = Fixture::new("redaction");
553        let podman = fixture.script(
554            "#!/bin/sh\nif [ \"$1\" = image ]; then exit 0; fi\n\
555             case \"$*\" in\n\
556               *' publish '*)\n\
557                 printf 'stdout-before %s stdout-after\\n' \"$CARGO_REGISTRY_TOKEN\"\n\
558                 printf 'stderr-before %s stderr-after\\n' \"$CARGO_REGISTRY_TOKEN\" >&2\n\
559                 exit 1\n\
560                 ;;\n\
561             esac\n\
562             exit 0\n",
563        );
564        let rendered = publish_with(&source(), "private-token", podman.as_os_str(), "test-image")
565            .unwrap_err()
566            .to_string();
567        assert!(!rendered.contains("private-token"));
568        assert_eq!(rendered.matches("[REDACTED]").count(), 2);
569        for marker in [
570            "stdout-before",
571            "stdout-after",
572            "stderr-before",
573            "stderr-after",
574        ] {
575            assert!(rendered.contains(marker));
576        }
577    }
578
579    #[test]
580    fn image_and_outer_container_preserve_browser_hardening() {
581        assert!(super::CONTAINERFILE.contains("chromium"));
582        assert!(
583            super::CONTAINERFILE
584                .contains("COPY ChromiumOuterSandbox.conf /etc/chromium.d/kcode-outer-sandbox")
585        );
586        assert!(
587            super::CHROMIUM_OUTER_SANDBOX_CONFIG
588                .lines()
589                .any(|line| line == r#"CHROMIUM_FLAGS="$CHROMIUM_FLAGS --no-sandbox""#)
590        );
591
592        let run = RunDirectory::new(&source(), "outer-sandbox-test").unwrap();
593        let command = podman_command(OsStr::new("podman"), &run, "/workspace", false);
594        let arguments = command
595            .get_args()
596            .map(|argument| argument.to_string_lossy().into_owned())
597            .collect::<Vec<_>>();
598        for expected in [
599            "--read-only",
600            "--cap-drop=all",
601            "--security-opt=no-new-privileges",
602            "--userns=keep-id",
603            "--tmpfs=/tmp:rw,nosuid,nodev",
604            "--network=none",
605        ] {
606            assert!(arguments.iter().any(|argument| argument == expected));
607        }
608    }
609}