use std::fs;
use greentic_bundle::bundle_fs::{
BackhandBundleFsReader, BackhandBundleFsWriter, BundleFsReader, BundleFsWriter,
read_bundle_file,
};
use sha2::{Digest, Sha256};
use tempfile::TempDir;
fn staged_bundle() -> TempDir {
let temp = TempDir::new().expect("tempdir");
fs::write(temp.path().join("pack.cbor"), b"pack").expect("pack");
fs::write(temp.path().join("bundle.cbor"), b"bundle").expect("bundle");
fs::create_dir_all(temp.path().join("assets")).expect("assets dir");
fs::write(temp.path().join("assets").join("example.txt"), b"example").expect("asset");
temp
}
fn create_bundle(input: &TempDir, output_name: &str) -> (TempDir, std::path::PathBuf) {
let out = TempDir::new().expect("out dir");
let bundle = out.path().join(output_name);
BackhandBundleFsWriter
.write_bundle(input.path(), &bundle)
.expect("write bundle");
(out, bundle)
}
#[test]
fn creates_gtbundle_with_backhand() {
let input = staged_bundle();
let (_out, bundle) = create_bundle(&input, "example.gtbundle");
assert!(bundle.exists());
assert!(bundle.metadata().expect("metadata").len() > 0);
}
#[test]
fn backhand_bundle_contains_expected_files() {
let input = staged_bundle();
let (_out, bundle) = create_bundle(&input, "example.gtbundle");
let mut paths: Vec<_> = BackhandBundleFsReader
.list_bundle(&bundle)
.expect("list bundle")
.into_iter()
.map(|entry| entry.path)
.collect();
paths.sort();
assert!(paths.contains(&"assets".to_string()));
assert!(paths.contains(&"assets/example.txt".to_string()));
assert!(paths.contains(&"bundle.cbor".to_string()));
assert!(paths.contains(&"pack.cbor".to_string()));
}
#[test]
fn backhand_bundle_uses_normalized_paths() {
let input = staged_bundle();
let (_out, bundle) = create_bundle(&input, "example.gtbundle");
let paths: Vec<_> = BackhandBundleFsReader
.list_bundle(&bundle)
.expect("list bundle")
.into_iter()
.map(|entry| entry.path)
.collect();
assert!(paths.iter().all(|path| !path.contains('\\')));
assert!(paths.iter().all(|path| !path.starts_with('/')));
}
#[test]
fn backhand_bundle_extracts_expected_files() {
let input = staged_bundle();
let (_out, bundle) = create_bundle(&input, "example.gtbundle");
let extract = TempDir::new().expect("extract dir");
BackhandBundleFsReader
.extract_bundle(&bundle, extract.path())
.expect("extract bundle");
assert_eq!(
fs::read_to_string(extract.path().join("assets").join("example.txt")).expect("asset"),
"example"
);
assert_eq!(
read_bundle_file(&bundle, "bundle.cbor").expect("read bundle.cbor"),
b"bundle"
);
}
#[test]
fn backhand_bundle_rebuild_is_deterministic() {
let input = staged_bundle();
let (_out_one, bundle_one) = create_bundle(&input, "one.gtbundle");
let (_out_two, bundle_two) = create_bundle(&input, "two.gtbundle");
let hash_one = Sha256::digest(fs::read(&bundle_one).expect("bundle one"));
let hash_two = Sha256::digest(fs::read(&bundle_two).expect("bundle two"));
assert_eq!(hash_one[..], hash_two[..]);
}
#[cfg(unix)]
#[test]
fn extract_refuses_relative_symlink_escaping_root() {
let temp = TempDir::new().expect("input dir");
let input = temp.path();
fs::create_dir_all(input.join("packs")).expect("packs dir");
fs::write(input.join("packs/manifest.json"), b"{}").expect("seed");
std::os::unix::fs::symlink("../../outside", input.join("packs/escape"))
.expect("escaping symlink");
let out_dir = TempDir::new().expect("out dir");
let bundle = out_dir.path().join("malicious.gtbundle");
BackhandBundleFsWriter
.write_bundle(input, &bundle)
.expect("write bundle");
let extract = TempDir::new().expect("extract dir");
let err = BackhandBundleFsReader
.extract_bundle(&bundle, extract.path())
.expect_err("must refuse escaping symlink");
assert!(format!("{err:#}").contains("escapes extract root"));
}
#[cfg(unix)]
#[test]
fn extract_accepts_benign_symlink_inside_root() {
let temp = TempDir::new().expect("input dir");
let input = temp.path();
fs::create_dir_all(input.join("packs")).expect("packs dir");
fs::write(input.join("packs/target.txt"), b"hello").expect("seed");
std::os::unix::fs::symlink("target.txt", input.join("packs/alias")).expect("benign symlink");
let out_dir = TempDir::new().expect("out dir");
let bundle = out_dir.path().join("benign.gtbundle");
BackhandBundleFsWriter
.write_bundle(input, &bundle)
.expect("write bundle");
let extract = TempDir::new().expect("extract dir");
BackhandBundleFsReader
.extract_bundle(&bundle, extract.path())
.expect("benign symlink must extract");
assert!(extract.path().join("packs/target.txt").is_file());
let alias_meta =
fs::symlink_metadata(extract.path().join("packs/alias")).expect("symlink metadata");
assert!(alias_meta.file_type().is_symlink());
}