Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

jobs/runner/src/collect.rs

//! **The runner's own look at a job's output**, outside the job: every file it
//! wrote is a regular file, exactly the ones its answer names, each hashed here.
//! Anything else refuses the job by name, and a refused job hands over nothing.

use std::collections::BTreeSet;
use std::path::Path;

use heimdall_core::{Output, Verdict};

use crate::wire::Answer;

/// The verdict and the listed outputs of a job that answered `answer` and wrote
/// into `out`. A refusal (the job's own or this check's) empties `out`.
pub fn collect(answer: &Answer, out: &Path) -> (Verdict, Vec<Output>) {
    match check(answer, out) {
        Ok(outputs) => (Verdict::Accepted, outputs),
        Err(v) => {
            empty(out);
            (v, Vec::new())
        }
    }
}

fn check(answer: &Answer, out: &Path) -> Result<Vec<Output>, Verdict> {
    use holger_errcode::{
        SANDBOX_JOB_ANSWER_INVALID as INVALID, SANDBOX_JOB_UNDECLARED as UNDECLARED,
    };
    if let Verdict::Refused { .. } = &answer.verdict {
        return Err(answer.verdict.clone());
    }
    let mut declared = BTreeSet::new();
    for n in &answer.outputs {
        if heimdall_core::validate_output_name(n).is_err()
            || !n
                .bytes()
                .all(|b| b.is_ascii_alphanumeric() || b"._-+".contains(&b))
        {
            return Err(refused(
                &INVALID,
                format!("the answer names {n:?}, which is not one plain file name"),
            ));
        }
        if !declared.insert(n.as_str()) {
            return Err(refused(&INVALID, format!("the answer names {n:?} twice")));
        }
    }
    let entries = std::fs::read_dir(out).map_err(|e| {
        refused(
            &INVALID,
            format!("the output directory {}: {e}", out.display()),
        )
    })?;
    let mut there = BTreeSet::new();
    for e in entries {
        let e = e.map_err(|e| refused(&INVALID, format!("the output directory: {e}")))?;
        let name = e.file_name().to_string_lossy().into_owned();
        let ft = e
            .file_type()
            .map_err(|e| refused(&INVALID, format!("{name}: {e}")))?;
        if !ft.is_file() {
            return Err(refused(
                &UNDECLARED,
                format!("the job wrote {name:?}, which is not a regular file"),
            ));
        }
        if !declared.contains(name.as_str()) {
            return Err(refused(
                &UNDECLARED,
                format!("the job wrote {name:?} and did not declare it"),
            ));
        }
        there.insert(name);
    }
    if let Some(n) = declared.iter().find(|n| !there.contains(**n)) {
        return Err(refused(
            &INVALID,
            format!("the answer names {n:?}, which the job did not write"),
        ));
    }
    let mut outputs = Vec::new();
    for n in declared {
        let p = out.join(n);
        let bytes = std::fs::symlink_metadata(&p)
            .map_err(|e| refused(&INVALID, format!("{n}: {e}")))?
            .len();
        let sha256 = sha256_file(&p).map_err(|e| refused(&INVALID, format!("{n}: {e}")))?;
        outputs.push(Output {
            name: n.to_string(),
            sha256,
            bytes,
        });
    }
    Ok(outputs)
}

/// A refused verdict with a frozen code.
pub fn refused(code: &holger_errcode::ErrCode, reason: impl std::fmt::Display) -> Verdict {
    Verdict::Refused {
        code: code.code.into(),
        reason: code.tag(reason),
    }
}

/// Remove every entry of `dir` (best effort): a refused job hands over nothing.
pub fn empty(dir: &Path) {
    for e in std::fs::read_dir(dir).into_iter().flatten().flatten() {
        let p = e.path();
        let _ = match e.file_type() {
            Ok(t) if t.is_dir() => std::fs::remove_dir_all(&p),
            _ => std::fs::remove_file(&p),
        };
    }
}

/// The sha256 of the file at `path`, read in pieces, as Heimdall's digest.
pub fn sha256_file(path: &Path) -> std::io::Result<heimdall_core::Sha256Digest> {
    use sha2::Digest as _;
    use std::io::Read as _;
    let mut f = std::fs::File::open(path)?;
    let mut h = sha2::Sha256::new();
    let mut buf = vec![0u8; 1 << 16];
    loop {
        let n = f.read(&mut buf)?;
        if n == 0 {
            break;
        }
        h.update(&buf[..n]);
    }
    heimdall_core::Sha256Digest::from_hex(&hex::encode(h.finalize()))
        .ok_or_else(|| std::io::Error::other("a sha256 is 32 bytes"))
}