#![cfg(windows)]
use iron_shell::env::Env;
use iron_shell::value::Value;
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::sync::Mutex;
static SERIAL: Mutex<()> = Mutex::new(());
const ACCESS_PATH_COUNT: &str = r#"(Get-Partition | Where-Object { $_.AccessPaths -contains ($env:AE_DST + '\') } | Measure-Object).Count"#;
const IMAGE_ATTACHED: &str = r#"(Get-DiskImage -ImagePath $env:AE_SRC).Attached"#;
const CLEANUP: &str = r#"
Get-Partition | Where-Object { $_.AccessPaths -contains ($env:AE_DST + '\') } | ForEach-Object { $_ | Remove-PartitionAccessPath -AccessPath ($env:AE_DST + '\') }
Dismount-DiskImage -ImagePath $env:AE_SRC -ErrorAction SilentlyContinue | Out-Null
"#;
fn disposable() -> bool {
std::env::var("AE_DISPOSABLE_HOST").as_deref() == Ok("1")
}
fn s(text: &str) -> Value {
Value::Str(text.to_string())
}
fn rec(pairs: &[(&str, Value)]) -> Value {
Value::Record(
pairs
.iter()
.map(|(key, value)| (key.to_string(), value.clone()))
.collect::<BTreeMap<String, Value>>(),
)
}
fn call(name: &str, args: Vec<Value>) -> anyhow::Result<Value> {
let mut env = Env::new();
iron_shell::builtins::call(name, args, &mut env)
}
fn call_ok(name: &str, args: Vec<Value>) -> Value {
call(name, args).unwrap_or_else(|e| panic!("{name} refused a valid call: {e}"))
}
fn code_of(e: &anyhow::Error) -> String {
let text = e.to_string();
match text.find("E_") {
Some(i) => text[i..]
.split(|c: char| !(c.is_ascii_uppercase() || c == '_'))
.next()
.unwrap_or("E_?")
.to_string(),
None => format!("NO_CODE: {text}"),
}
}
fn expect_code(result: anyhow::Result<Value>, want: &str) {
match result {
Ok(v) => panic!("answered {v:?}; expected {want}"),
Err(e) => assert_eq!(code_of(&e), want, "wrong refusal: {e}"),
}
}
fn field<'a>(v: &'a Value, key: &str) -> &'a Value {
match v {
Value::Record(m) => m.get(key).unwrap_or_else(|| panic!("no {key} in {v:?}")),
other => panic!("not a record: {other:?}"),
}
}
fn text(v: &Value, key: &str) -> String {
match field(v, key) {
Value::Str(s) => s.clone(),
other => panic!("{key} is not a String: {other:?}"),
}
}
fn flag(v: &Value, key: &str) -> bool {
match field(v, key) {
Value::Bool(b) => *b,
other => panic!("{key} is not a Bool: {other:?}"),
}
}
fn run_powershell(script: &str, vars: &[(&str, &str)]) -> Output {
let mut cmd = Command::new("powershell.exe");
cmd.args(["-NoProfile", "-NonInteractive", "-Command", script]);
cmd.env_remove("PSModulePath");
for (name, value) in vars {
cmd.env(name, value);
}
cmd.output().expect("powershell.exe should start")
}
fn powershell(script: &str, vars: &[(&str, &str)]) -> String {
let out = run_powershell(script, vars);
assert!(
out.status.success(),
"PowerShell oracle failed: {}",
String::from_utf8_lossy(&out.stderr)
);
String::from_utf8_lossy(&out.stdout).trim().to_string()
}
fn access_paths(folder: &str) -> usize {
powershell(ACCESS_PATH_COUNT, &[("AE_DST", folder)])
.parse()
.expect("the access path count is a number")
}
fn attached(image: &str) -> bool {
powershell(IMAGE_ATTACHED, &[("AE_SRC", image)]) == "True"
}
fn canon(path: &Path) -> PathBuf {
let text = std::fs::canonicalize(path)
.expect("the path should exist")
.to_string_lossy()
.into_owned();
PathBuf::from(text.trim_start_matches(r"\\?\"))
}
struct Fixture {
dir: PathBuf,
image: String,
folder: String,
}
impl Fixture {
fn new() -> Self {
let raw = std::env::temp_dir().join(format!("aemnt-{}", uuid::Uuid::new_v4()));
std::fs::create_dir_all(&raw).unwrap();
let dir = canon(&raw);
let image = dir.join("img.vhdx");
let folder = dir.join("mnt");
std::fs::create_dir(&folder).unwrap();
let script = dir.join("make.txt");
let commands = format!(
"create vdisk file=\"{image}\" maximum=16 type=expandable\n\
select vdisk file=\"{image}\"\n\
attach vdisk\n\
create partition primary\n\
format fs=ntfs quick label=AECI\n\
detach vdisk\n",
image = image.display()
);
std::fs::write(&script, commands).unwrap();
let out = Command::new("diskpart")
.arg("/s")
.arg(&script)
.output()
.expect("diskpart should start");
assert!(
out.status.success(),
"diskpart could not make the image: {}",
String::from_utf8_lossy(&out.stdout)
);
Self {
image: image.to_string_lossy().into_owned(),
folder: folder.to_string_lossy().into_owned(),
dir,
}
}
fn assert_unchanged(&self) {
assert_eq!(access_paths(&self.folder), 0, "a mount was left behind");
assert!(!attached(&self.image), "the image was left attached");
}
}
impl Drop for Fixture {
fn drop(&mut self) {
let _ = run_powershell(
CLEANUP,
&[("AE_DST", &self.folder), ("AE_SRC", &self.image)],
);
let _ = std::fs::remove_dir_all(&self.dir);
}
}
#[test]
#[ignore]
fn aemnt_mount_round_trip_matches_powershell() {
if !disposable() {
return;
}
let _serial = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
let fx = Fixture::new();
let made = call_ok("fs_mount", vec![s(&fx.image), s(&fx.folder)]);
assert_eq!(text(&made, "source"), fx.image);
assert_eq!(text(&made, "target"), fx.folder);
assert_eq!(text(&made, "fstype"), "NTFS");
assert!(!flag(&made, "readonly"));
assert_eq!(access_paths(&fx.folder), 1);
assert!(attached(&fx.image));
let file = Path::new(&fx.folder).join("hello.txt");
std::fs::write(&file, "round trip").unwrap();
assert_eq!(std::fs::read_to_string(&file).unwrap(), "round trip");
let gone = call_ok("fs_unmount", vec![s(&fx.folder)]);
assert!(flag(&gone, "unmounted"));
assert_eq!(text(&gone, "target"), fx.folder);
assert_eq!(access_paths(&fx.folder), 0);
assert!(!attached(&fx.image));
assert_eq!(std::fs::read_dir(&fx.folder).unwrap().count(), 0);
let ro = call_ok(
"fs_mount",
vec![
s(&fx.image),
s(&fx.folder),
rec(&[("readonly", Value::Bool(true))]),
],
);
assert!(flag(&ro, "readonly"));
assert_eq!(access_paths(&fx.folder), 1);
assert!(std::fs::write(Path::new(&fx.folder).join("denied.txt"), "x").is_err());
call_ok("fs_unmount", vec![s(&fx.folder)]);
fx.assert_unchanged();
}
#[test]
#[ignore]
fn aemnt_refusals_change_nothing() {
if !disposable() {
return;
}
let _serial = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
let fx = Fixture::new();
let occupied = Path::new(&fx.folder).join("occupied.txt");
std::fs::write(&occupied, "x").unwrap();
expect_code(
call("fs_mount", vec![s(&fx.image), s(&fx.folder)]),
"E_BAD_ARG",
);
fx.assert_unchanged();
std::fs::remove_file(&occupied).unwrap();
expect_code(
call(
"fs_mount",
vec![
s(&fx.image),
s(&fx.folder),
rec(&[("bind", Value::Bool(true))]),
],
),
"E_BAD_ARG",
);
fx.assert_unchanged();
expect_code(
call(
"fs_mount",
vec![s(&fx.image), s(&fx.folder), rec(&[("fstype", s("ntfs"))])],
),
"E_BAD_ARG",
);
fx.assert_unchanged();
let note = Path::new(&fx.dir).join("note.txt");
std::fs::write(¬e, "not an image").unwrap();
expect_code(
call("fs_mount", vec![s(¬e.to_string_lossy()), s(&fx.folder)]),
"E_BAD_ARG",
);
fx.assert_unchanged();
let system = std::env::var("SystemRoot").unwrap_or_else(|_| r"C:\Windows".to_string());
expect_code(
call("fs_mount", vec![s(&fx.image), s(&system)]),
"E_POLICY_DENY",
);
fx.assert_unchanged();
expect_code(call("fs_unmount", vec![s(&fx.folder)]), "E_NOT_FOUND");
fx.assert_unchanged();
call_ok("fs_mount", vec![s(&fx.image), s(&fx.folder)]);
assert_eq!(access_paths(&fx.folder), 1);
expect_code(
call(
"fs_unmount",
vec![s(&fx.folder), rec(&[("lazy", Value::Bool(true))])],
),
"E_BAD_ARG",
);
assert_eq!(
access_paths(&fx.folder),
1,
"the refused unmount changed the mount"
);
call_ok("fs_unmount", vec![s(&fx.folder)]);
fx.assert_unchanged();
}