641 lines
22 KiB
Rust
641 lines
22 KiB
Rust
use proxywarden_lib::component_catalog::{
|
|
parse_bundled_catalog_if_present, parse_catalog, validate_bundle, AssetArch, ComponentId,
|
|
TargetArch,
|
|
};
|
|
use serde_json::{json, Value};
|
|
use sha2::{Digest, Sha256};
|
|
use std::fs;
|
|
use std::path::{Path, PathBuf};
|
|
#[cfg(windows)]
|
|
use std::process::Command;
|
|
use uuid::Uuid;
|
|
|
|
#[test]
|
|
fn parses_exact_x64_catalog_and_all_trust_policy_variants() {
|
|
let catalog = parse_value(&valid_catalog()).expect("valid catalog must parse");
|
|
|
|
assert_eq!(catalog.target_arch, TargetArch::X64);
|
|
assert_eq!(catalog.components.len(), 5);
|
|
assert_eq!(
|
|
catalog
|
|
.components
|
|
.iter()
|
|
.find(|component| component.id == ComponentId::Winsw)
|
|
.expect("WinSW entry")
|
|
.asset_arch,
|
|
AssetArch::Anycpu
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_unknown_schema_arch_fields_duplicates_and_incomplete_set() {
|
|
assert_rejected(mutate(|catalog| catalog["schemaVersion"] = json!(2)));
|
|
assert_rejected(mutate(|catalog| catalog["targetArch"] = json!("arm64")));
|
|
assert_rejected(mutate(|catalog| catalog["unexpected"] = json!(true)));
|
|
assert_rejected(mutate(|catalog| {
|
|
component_mut(catalog, "proxifyre")["unexpected"] = json!(true);
|
|
}));
|
|
assert_rejected(mutate(|catalog| {
|
|
component_mut(catalog, "windows-packet-filter")["sourceUrl"] = json!(
|
|
"https://github.com/attacker/ndisapi/releases/download/v3.6.2/Windows.Packet.Filter.3.6.2.1.x64.msi"
|
|
);
|
|
}));
|
|
assert_rejected(mutate(|catalog| {
|
|
component_mut(catalog, "vc-runtime")["sourceUrl"] =
|
|
json!("https://attacker.example/vc_redist.x64.exe");
|
|
component_mut(catalog, "vc-runtime")["updateTrustPolicy"]["allowedSourceHosts"] =
|
|
json!(["attacker.example"]);
|
|
}));
|
|
assert_rejected(mutate(|catalog| {
|
|
component_mut(catalog, "proxifyre")["license"]["unexpected"] = json!(true);
|
|
}));
|
|
assert_rejected(mutate(|catalog| {
|
|
component_mut(catalog, "proxifyre")["updateTrustPolicy"]["unexpected"] = json!(true);
|
|
}));
|
|
assert_rejected(mutate(|catalog| {
|
|
component_mut(catalog, "windows-packet-filter")["id"] = json!("proxifyre");
|
|
}));
|
|
assert_rejected(mutate(|catalog| {
|
|
component_mut(catalog, "windows-packet-filter")["installRole"] = json!("proxifyre-runtime");
|
|
}));
|
|
assert_rejected(mutate(|catalog| {
|
|
catalog["components"]
|
|
.as_array_mut()
|
|
.expect("components array")
|
|
.pop();
|
|
}));
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_wrong_component_role_or_architecture() {
|
|
assert_rejected(mutate(|catalog| {
|
|
component_mut(catalog, "proxifyre")["installRole"] = json!("packet-filter-driver");
|
|
}));
|
|
assert_rejected(mutate(|catalog| {
|
|
component_mut(catalog, "proxifyre")["assetArch"] = json!("anycpu");
|
|
}));
|
|
assert_rejected(mutate(|catalog| {
|
|
component_mut(catalog, "winsw")["assetArch"] = json!("x64");
|
|
}));
|
|
assert_rejected(mutate(|catalog| {
|
|
component_mut(catalog, "winsw")["effectiveTarget"] = json!("anycpu");
|
|
}));
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_unsafe_paths_hash_size_license_version_and_source() {
|
|
for invalid_path in [
|
|
"../asset.zip",
|
|
"proxifyre/../asset.zip",
|
|
"proxifyre\\asset.zip",
|
|
"/proxifyre/asset.zip",
|
|
"proxifyre/CON.zip",
|
|
"other/asset.zip",
|
|
] {
|
|
assert_rejected(mutate(|catalog| {
|
|
component_mut(catalog, "proxifyre")["assetPath"] = json!(invalid_path);
|
|
}));
|
|
}
|
|
|
|
assert_rejected(mutate(|catalog| {
|
|
component_mut(catalog, "proxifyre")["license"]["path"] = json!("../LICENSE.txt");
|
|
}));
|
|
assert_rejected(mutate(|catalog| {
|
|
component_mut(catalog, "windows-packet-filter")["license"]["path"] =
|
|
json!("proxifyre/WPF-LICENSE.txt");
|
|
}));
|
|
assert_rejected(mutate(|catalog| {
|
|
component_mut(catalog, "windows-packet-filter")["license"]["path"] =
|
|
json!("proxifyre/LICENSE.txt");
|
|
}));
|
|
assert_rejected(mutate(|catalog| {
|
|
component_mut(catalog, "proxifyre")["license"]["id"] = json!("GPL 3");
|
|
}));
|
|
assert_rejected(mutate(|catalog| {
|
|
component_mut(catalog, "proxifyre")["sha256"] = json!("A".repeat(64));
|
|
}));
|
|
assert_rejected(mutate(|catalog| {
|
|
component_mut(catalog, "proxifyre")["sha256"] = json!("a".repeat(63));
|
|
}));
|
|
assert_rejected(mutate(|catalog| {
|
|
component_mut(catalog, "proxifyre")["size"] = json!(0);
|
|
}));
|
|
assert_rejected(mutate(|catalog| {
|
|
component_mut(catalog, "proxifyre")["version"] = json!("2.4.0-beta.1");
|
|
}));
|
|
assert_rejected(mutate(|catalog| {
|
|
component_mut(catalog, "winsw")["productVersion"] = json!("2.12.0-rc.1");
|
|
}));
|
|
assert_rejected(mutate(|catalog| {
|
|
component_mut(catalog, "proxifyre")["sourceUrl"] = json!(
|
|
"http://github.com/wiresock/proxifyre/releases/download/v2.4.0/ProxiFyre-v2.4.0-x64-signed.zip"
|
|
);
|
|
}));
|
|
assert_rejected(mutate(|catalog| {
|
|
component_mut(catalog, "proxifyre")["sourceUrl"] = json!(
|
|
"https://user:secret@github.com/wiresock/proxifyre/releases/download/v2.4.0/ProxiFyre-v2.4.0-x64-signed.zip"
|
|
);
|
|
}));
|
|
assert_rejected(mutate(|catalog| {
|
|
component_mut(catalog, "proxifyre")["sourceUrl"] =
|
|
json!("https://github.com/wiresock/proxifyre/releases/download/v2.4.0/wrong.zip");
|
|
}));
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_invalid_trust_policies() {
|
|
for id in ["proxifyre", "windows-packet-filter", "sing-box"] {
|
|
assert_rejected(mutate(|catalog| {
|
|
component_mut(catalog, id)["updateTrustPolicy"] = json!({
|
|
"type": "bundledOnlyNoIndependentProof",
|
|
"reason": "Wrong policy for this component."
|
|
});
|
|
}));
|
|
}
|
|
assert_rejected(mutate(|catalog| {
|
|
component_mut(catalog, "vc-runtime")["updateTrustPolicy"] = json!({
|
|
"type": "bundledOnlyNoIndependentProof",
|
|
"reason": "Wrong policy for this component."
|
|
});
|
|
}));
|
|
assert_rejected(mutate(|catalog| {
|
|
component_mut(catalog, "winsw")["updateTrustPolicy"] = json!({
|
|
"type": "githubReleaseDigest",
|
|
"repository": "winsw/winsw",
|
|
"tagPattern": "v*",
|
|
"assetPattern": "WinSW.NET461.exe",
|
|
"requireStable": true
|
|
});
|
|
}));
|
|
assert_rejected(mutate(|catalog| {
|
|
component_mut(catalog, "proxifyre")["updateTrustPolicy"]["requireStable"] = json!(false);
|
|
}));
|
|
assert_rejected(mutate(|catalog| {
|
|
component_mut(catalog, "proxifyre")["updateTrustPolicy"]["repository"] =
|
|
json!("attacker/proxifyre");
|
|
}));
|
|
assert_rejected(mutate(|catalog| {
|
|
component_mut(catalog, "proxifyre")["updateTrustPolicy"]["tagPattern"] = json!("v**");
|
|
}));
|
|
assert_rejected(mutate(|catalog| {
|
|
component_mut(catalog, "proxifyre")["updateTrustPolicy"]["authenticodePublishers"] =
|
|
json!([]);
|
|
}));
|
|
assert_rejected(mutate(|catalog| {
|
|
component_mut(catalog, "vc-runtime")["updateTrustPolicy"]["allowedSourceHosts"] = json!([]);
|
|
}));
|
|
assert_rejected(mutate(|catalog| {
|
|
component_mut(catalog, "vc-runtime")["updateTrustPolicy"]["publishers"] = json!([" "]);
|
|
}));
|
|
assert_rejected(mutate(|catalog| {
|
|
component_mut(catalog, "vc-runtime")["updateTrustPolicy"]["assetPattern"] =
|
|
json!("other.exe");
|
|
}));
|
|
assert_rejected(mutate(|catalog| {
|
|
component_mut(catalog, "winsw")["updateTrustPolicy"]["type"] = json!("unknownPolicy");
|
|
}));
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_component_policy_allowlist_expansion() {
|
|
assert_rejected(mutate(|catalog| {
|
|
component_mut(catalog, "proxifyre")["updateTrustPolicy"]["repository"] =
|
|
json!("Wiresock/proxifyre");
|
|
}));
|
|
assert_rejected(mutate(|catalog| {
|
|
component_mut(catalog, "proxifyre")["updateTrustPolicy"]["tagPattern"] = json!("v2.*");
|
|
}));
|
|
assert_rejected(mutate(|catalog| {
|
|
component_mut(catalog, "proxifyre")["updateTrustPolicy"]["assetPattern"] = json!("*");
|
|
}));
|
|
assert_rejected(mutate(|catalog| {
|
|
component_mut(catalog, "proxifyre")["updateTrustPolicy"]["authenticodePublishers"] =
|
|
Value::Null;
|
|
}));
|
|
assert_rejected(mutate(|catalog| {
|
|
component_mut(catalog, "windows-packet-filter")["updateTrustPolicy"]["assetPattern"] =
|
|
json!("Windows.Packet.Filter.*");
|
|
}));
|
|
assert_rejected(mutate(|catalog| {
|
|
component_mut(catalog, "windows-packet-filter")["updateTrustPolicy"]
|
|
["authenticodePublishers"] =
|
|
json!(["The Anti-Cloud Corporation", "Unexpected Publisher"]);
|
|
}));
|
|
assert_rejected(mutate(|catalog| {
|
|
component_mut(catalog, "sing-box")["updateTrustPolicy"]["repository"] =
|
|
json!("sagernet/sing-box");
|
|
}));
|
|
assert_rejected(mutate(|catalog| {
|
|
component_mut(catalog, "sing-box")["updateTrustPolicy"]["authenticodePublishers"] =
|
|
json!(["Unexpected Publisher"]);
|
|
}));
|
|
assert_rejected(mutate(|catalog| {
|
|
component_mut(catalog, "vc-runtime")["updateTrustPolicy"]["allowedSourceHosts"] =
|
|
json!(["aka.ms", "attacker.example"]);
|
|
}));
|
|
assert_rejected(mutate(|catalog| {
|
|
component_mut(catalog, "vc-runtime")["updateTrustPolicy"]["assetPattern"] = json!("*");
|
|
}));
|
|
assert_rejected(mutate(|catalog| {
|
|
component_mut(catalog, "vc-runtime")["updateTrustPolicy"]["publishers"] =
|
|
json!(["Microsoft Corporation", "Unexpected Publisher"]);
|
|
}));
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_wrong_component_license_ids() {
|
|
for (id, wrong_license) in [
|
|
("proxifyre", "MIT"),
|
|
("windows-packet-filter", "GPL-3.0-only"),
|
|
("vc-runtime", "LicenseRef-Microsoft-VCRedist"),
|
|
("sing-box", "GPL-3.0-or-later"),
|
|
("winsw", "AGPL-3.0-only"),
|
|
] {
|
|
assert_rejected(mutate(|catalog| {
|
|
component_mut(catalog, id)["license"]["id"] = json!(wrong_license);
|
|
}));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_unpinned_or_wrong_vc_runtime_source() {
|
|
for source in [
|
|
"https://aka.ms/vs/17/release/vc_redist.x64.exe",
|
|
"https://aka.ms/vs/18/release/vc_redist.x64.exe",
|
|
"https://aka.ms/vs/18/release/14.50.35719/VC_redist.x64.exe",
|
|
] {
|
|
assert_rejected(mutate(|catalog| {
|
|
component_mut(catalog, "vc-runtime")["sourceUrl"] = json!(source);
|
|
}));
|
|
}
|
|
assert_rejected(mutate(|catalog| {
|
|
component_mut(catalog, "vc-runtime")["version"] = json!("14.50.35719.0");
|
|
}));
|
|
}
|
|
|
|
#[test]
|
|
fn validates_exact_bundle_contents_hashes_sizes_and_licenses() {
|
|
let bundle = TestBundle::new();
|
|
let catalog = validate_bundle(bundle.path()).expect("complete bundle must validate");
|
|
|
|
assert_eq!(catalog.components.len(), 5);
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_missing_extra_or_changed_package_assets() {
|
|
let missing = TestBundle::new();
|
|
fs::remove_file(missing.path().join(asset_path("proxifyre"))).expect("remove fixture asset");
|
|
assert!(validate_bundle(missing.path()).is_err());
|
|
|
|
let extra = TestBundle::new();
|
|
fs::write(extra.path().join("unexpected.bin"), b"extra").expect("write extra file");
|
|
assert!(validate_bundle(extra.path()).is_err());
|
|
|
|
let changed = TestBundle::new();
|
|
let path = changed.path().join(asset_path("proxifyre"));
|
|
let original = fs::read(&path).expect("read fixture asset");
|
|
fs::write(&path, vec![b'x'; original.len()]).expect("change fixture asset");
|
|
assert!(validate_bundle(changed.path()).is_err());
|
|
|
|
let wrong_size = TestBundle::new();
|
|
let mut catalog: Value = serde_json::from_slice(
|
|
&fs::read(wrong_size.path().join("catalog.json")).expect("read fixture catalog"),
|
|
)
|
|
.expect("parse fixture catalog");
|
|
component_mut(&mut catalog, "proxifyre")["size"] = json!(999);
|
|
write_catalog(wrong_size.path(), &catalog);
|
|
assert!(validate_bundle(wrong_size.path()).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_missing_or_empty_license_copy() {
|
|
let missing = TestBundle::new();
|
|
fs::remove_file(missing.path().join("proxifyre/LICENSE.txt")).expect("remove fixture license");
|
|
assert!(validate_bundle(missing.path()).is_err());
|
|
|
|
let empty = TestBundle::new();
|
|
fs::write(empty.path().join("proxifyre/LICENSE.txt"), b"").expect("empty fixture license");
|
|
assert!(validate_bundle(empty.path()).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn optional_bundle_parse_is_none_only_when_catalog_is_absent() {
|
|
let absent = TempDirectory::new();
|
|
assert!(parse_bundled_catalog_if_present(absent.path())
|
|
.expect("absent catalog is allowed")
|
|
.is_none());
|
|
|
|
let present = TestBundle::new();
|
|
assert!(parse_bundled_catalog_if_present(present.path())
|
|
.expect("present catalog must validate")
|
|
.is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn production_bundle_validates_when_catalog_exists() {
|
|
let root = Path::new(env!("CARGO_MANIFEST_DIR"))
|
|
.join("bundled")
|
|
.join("components");
|
|
|
|
let catalog = validate_bundle(&root).expect("production component bundle must validate");
|
|
assert_eq!(catalog.components.len(), 5);
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
#[test]
|
|
fn rejects_reparse_bundle_root_and_nested_directory() {
|
|
let target = TestBundle::new();
|
|
let junctions = TempDirectory::new();
|
|
let root_junction = junctions.path().join("bundle-root-junction");
|
|
let root_guard = create_junction(&root_junction, target.path());
|
|
assert!(validate_bundle(&root_junction).is_err());
|
|
drop(root_guard);
|
|
|
|
let nested = TestBundle::new();
|
|
let proxifyre_target = junctions.path().join("proxifyre-target");
|
|
fs::rename(nested.path().join("proxifyre"), &proxifyre_target)
|
|
.expect("move fixture component behind a junction");
|
|
let nested_guard = create_junction(&nested.path().join("proxifyre"), &proxifyre_target);
|
|
assert!(validate_bundle(nested.path()).is_err());
|
|
drop(nested_guard);
|
|
}
|
|
|
|
fn valid_catalog() -> Value {
|
|
json!({
|
|
"schemaVersion": 1,
|
|
"targetArch": "x64",
|
|
"components": [
|
|
component(
|
|
"proxifyre",
|
|
"2.4.0",
|
|
"ProxiFyre-v2.4.0-x64-signed.zip",
|
|
"x64",
|
|
"https://github.com/wiresock/proxifyre/releases/download/v2.4.0/ProxiFyre-v2.4.0-x64-signed.zip",
|
|
json!({
|
|
"type": "githubReleaseDigest",
|
|
"repository": "wiresock/proxifyre",
|
|
"tagPattern": "v*",
|
|
"assetPattern": "ProxiFyre-v*-x64-signed.zip",
|
|
"requireStable": true,
|
|
"authenticodePublishers": ["The Anti-Cloud Corporation"]
|
|
})
|
|
),
|
|
component(
|
|
"windows-packet-filter",
|
|
"3.6.2",
|
|
"Windows.Packet.Filter.3.6.2.1.x64.msi",
|
|
"x64",
|
|
"https://github.com/wiresock/ndisapi/releases/download/v3.6.2/Windows.Packet.Filter.3.6.2.1.x64.msi",
|
|
json!({
|
|
"type": "githubReleaseDigest",
|
|
"repository": "wiresock/ndisapi",
|
|
"tagPattern": "v*",
|
|
"assetPattern": "Windows.Packet.Filter.*.x64.msi",
|
|
"requireStable": true,
|
|
"authenticodePublishers": ["The Anti-Cloud Corporation"]
|
|
})
|
|
),
|
|
component(
|
|
"vc-runtime",
|
|
"14.51.36247.0",
|
|
"VC_redist.x64.exe",
|
|
"x64",
|
|
"https://aka.ms/vs/18/release/14.51.36247/VC_redist.x64.exe",
|
|
json!({
|
|
"type": "buildTimeOnlyAuthenticode",
|
|
"allowedSourceHosts": ["aka.ms"],
|
|
"assetPattern": "VC_redist.x64.exe",
|
|
"publishers": ["Microsoft Corporation"]
|
|
})
|
|
),
|
|
component(
|
|
"sing-box",
|
|
"1.13.19",
|
|
"sing-box-1.13.19-windows-amd64.zip",
|
|
"x64",
|
|
"https://github.com/SagerNet/sing-box/releases/download/v1.13.19/sing-box-1.13.19-windows-amd64.zip",
|
|
json!({
|
|
"type": "githubReleaseDigest",
|
|
"repository": "SagerNet/sing-box",
|
|
"tagPattern": "v*",
|
|
"assetPattern": "sing-box-*-windows-amd64.zip",
|
|
"requireStable": true
|
|
})
|
|
),
|
|
component(
|
|
"winsw",
|
|
"2.12.0",
|
|
"WinSW.NET461.exe",
|
|
"anycpu",
|
|
"https://github.com/winsw/winsw/releases/download/v2.12.0/WinSW.NET461.exe",
|
|
json!({
|
|
"type": "bundledOnlyNoIndependentProof",
|
|
"reason": "Upstream provides no independent digest or Authenticode proof for this asset."
|
|
})
|
|
)
|
|
]
|
|
})
|
|
}
|
|
|
|
fn component(
|
|
id: &str,
|
|
version: &str,
|
|
asset_name: &str,
|
|
asset_arch: &str,
|
|
source_url: &str,
|
|
update_trust_policy: Value,
|
|
) -> Value {
|
|
let bytes = asset_bytes(id);
|
|
let (license_id, install_role) = match id {
|
|
"proxifyre" => ("AGPL-3.0-only", "proxifyre-runtime"),
|
|
"windows-packet-filter" => ("MIT", "packet-filter-driver"),
|
|
"vc-runtime" => (
|
|
"LicenseRef-Microsoft-Visual-Cpp-v14-Redistributable-2026",
|
|
"vc-runtime-prerequisite",
|
|
),
|
|
"sing-box" => ("LicenseRef-Sing-Box-Project", "sing-box-runtime"),
|
|
"winsw" => ("MIT", "sing-box-service-wrapper"),
|
|
_ => panic!("unknown fixture component"),
|
|
};
|
|
json!({
|
|
"id": id,
|
|
"version": version,
|
|
"fileVersion": if id == "windows-packet-filter" { "3.6.2.1" } else { version },
|
|
"productVersion": match id {
|
|
"windows-packet-filter" => "3.6.2.1",
|
|
"winsw" => "2.12.0+eef5c6a",
|
|
_ => version
|
|
},
|
|
"assetPath": format!("{id}/{asset_name}"),
|
|
"assetArch": asset_arch,
|
|
"effectiveTarget": "x64",
|
|
"sha256": sha256(bytes),
|
|
"size": bytes.len(),
|
|
"sourceUrl": source_url,
|
|
"license": {
|
|
"id": license_id,
|
|
"path": format!("{id}/LICENSE.txt")
|
|
},
|
|
"installRole": install_role,
|
|
"updateTrustPolicy": update_trust_policy
|
|
})
|
|
}
|
|
|
|
fn asset_bytes(id: &str) -> &'static [u8] {
|
|
match id {
|
|
"proxifyre" => b"fixture-proxifyre-asset",
|
|
"windows-packet-filter" => b"fixture-packet-filter-asset",
|
|
"vc-runtime" => b"fixture-vc-runtime-asset",
|
|
"sing-box" => b"fixture-sing-box-asset",
|
|
"winsw" => b"fixture-winsw-asset",
|
|
_ => panic!("unknown fixture component"),
|
|
}
|
|
}
|
|
|
|
fn asset_path(id: &str) -> String {
|
|
valid_catalog()["components"]
|
|
.as_array()
|
|
.expect("components array")
|
|
.iter()
|
|
.find(|component| component["id"] == id)
|
|
.expect("fixture component")["assetPath"]
|
|
.as_str()
|
|
.expect("asset path")
|
|
.to_string()
|
|
}
|
|
|
|
fn sha256(bytes: &[u8]) -> String {
|
|
format!("{:x}", Sha256::digest(bytes))
|
|
}
|
|
|
|
fn mutate(change: impl FnOnce(&mut Value)) -> Value {
|
|
let mut catalog = valid_catalog();
|
|
change(&mut catalog);
|
|
catalog
|
|
}
|
|
|
|
fn component_mut<'a>(catalog: &'a mut Value, id: &str) -> &'a mut Value {
|
|
catalog["components"]
|
|
.as_array_mut()
|
|
.expect("components array")
|
|
.iter_mut()
|
|
.find(|component| component["id"] == id)
|
|
.expect("fixture component")
|
|
}
|
|
|
|
fn parse_value(
|
|
value: &Value,
|
|
) -> Result<
|
|
proxywarden_lib::component_catalog::ComponentCatalog,
|
|
proxywarden_lib::component_catalog::ComponentCatalogError,
|
|
> {
|
|
parse_catalog(&serde_json::to_vec(value).expect("serialize fixture catalog"))
|
|
}
|
|
|
|
fn assert_rejected(value: Value) {
|
|
assert!(
|
|
parse_value(&value).is_err(),
|
|
"catalog unexpectedly passed: {value}"
|
|
);
|
|
}
|
|
|
|
fn write_catalog(root: &Path, catalog: &Value) {
|
|
fs::write(
|
|
root.join("catalog.json"),
|
|
serde_json::to_vec_pretty(catalog).expect("serialize fixture catalog"),
|
|
)
|
|
.expect("write fixture catalog");
|
|
}
|
|
|
|
struct TestBundle {
|
|
directory: TempDirectory,
|
|
}
|
|
|
|
impl TestBundle {
|
|
fn new() -> Self {
|
|
let directory = TempDirectory::new();
|
|
let catalog = valid_catalog();
|
|
for component in catalog["components"].as_array().expect("components array") {
|
|
let id = component["id"].as_str().expect("component id");
|
|
let asset_path = component["assetPath"].as_str().expect("asset path");
|
|
let license_path = component["license"]["path"].as_str().expect("license path");
|
|
fs::create_dir_all(
|
|
directory
|
|
.path()
|
|
.join(asset_path)
|
|
.parent()
|
|
.expect("asset parent"),
|
|
)
|
|
.expect("create component directory");
|
|
fs::write(directory.path().join(asset_path), asset_bytes(id))
|
|
.expect("write fixture asset");
|
|
fs::write(
|
|
directory.path().join(license_path),
|
|
format!("License fixture for {id}\n"),
|
|
)
|
|
.expect("write fixture license");
|
|
}
|
|
write_catalog(directory.path(), &catalog);
|
|
Self { directory }
|
|
}
|
|
|
|
fn path(&self) -> &Path {
|
|
self.directory.path()
|
|
}
|
|
}
|
|
|
|
struct TempDirectory {
|
|
path: PathBuf,
|
|
}
|
|
|
|
impl TempDirectory {
|
|
fn new() -> Self {
|
|
let path = std::env::temp_dir().join(format!(
|
|
"proxywarden-component-catalog-test-{}",
|
|
Uuid::new_v4()
|
|
));
|
|
fs::create_dir_all(&path).expect("create temporary test directory");
|
|
Self { path }
|
|
}
|
|
|
|
fn path(&self) -> &Path {
|
|
&self.path
|
|
}
|
|
}
|
|
|
|
impl Drop for TempDirectory {
|
|
fn drop(&mut self) {
|
|
let _ = fs::remove_dir_all(&self.path);
|
|
}
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
struct JunctionGuard {
|
|
path: PathBuf,
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
impl Drop for JunctionGuard {
|
|
fn drop(&mut self) {
|
|
let _ = fs::remove_dir(&self.path);
|
|
}
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
fn create_junction(path: &Path, target: &Path) -> JunctionGuard {
|
|
let output = Command::new("cmd")
|
|
.args(["/d", "/c", "mklink", "/J"])
|
|
.arg(path)
|
|
.arg(target)
|
|
.output()
|
|
.expect("run mklink for reparse-point fixture");
|
|
assert!(
|
|
output.status.success(),
|
|
"mklink failed: {}",
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
JunctionGuard {
|
|
path: path.to_path_buf(),
|
|
}
|
|
}
|