use bytes::Bytes;
use std::borrow::Cow;
use std::path::{Component, Path};
#[cfg(target_os = "macos")]
pub fn resolve(path: &str) -> Cow<'_, str> {
let p = Path::new(path);
let mut components = p.components();
if components.next() != Some(Component::RootDir) {
return Cow::Borrowed(path);
}
let Some(first) = components.next() else {
return Cow::Borrowed(path);
};
let top = Path::new("/").join(first);
let Ok(target) = top.read_link() else {
return Cow::Borrowed(path);
};
let resolved = if target.is_relative() {
Path::new("/").join(&target)
} else {
target
};
let rest: std::path::PathBuf = components.collect();
let full = if rest.as_os_str().is_empty() {
resolved
} else {
resolved.join(rest)
};
let s = full.to_string_lossy();
if s == path {
Cow::Borrowed(path)
} else {
Cow::Owned(s.into_owned())
}
}
#[cfg(not(target_os = "macos"))]
pub fn resolve(path: &str) -> Cow<'_, str> {
Cow::Borrowed(path)
}
pub fn rewrite_create_body(body: Bytes) -> Bytes {
let Ok(mut v) = serde_json::from_slice::<serde_json::Value>(&body) else {
return body;
};
let mut changed = false;
if let Some(binds) = v
.pointer_mut("/HostConfig/Binds")
.and_then(|v| v.as_array_mut())
{
for entry in binds {
if let Some(s) = entry.as_str() {
if let Some(rewritten) = rewrite_bind_entry(s) {
*entry = serde_json::Value::String(rewritten);
changed = true;
}
}
}
}
if let Some(mounts) = v
.pointer_mut("/HostConfig/Mounts")
.and_then(|v| v.as_array_mut())
{
for mount in mounts {
if mount.get("Type").and_then(|t| t.as_str()) != Some("bind") {
continue;
}
if let Some(src) = mount.get("Source").and_then(|s| s.as_str()) {
if let Cow::Owned(resolved) = resolve(src) {
mount["Source"] = serde_json::Value::String(resolved);
changed = true;
}
}
}
}
if changed {
serde_json::to_vec(&v).map_or(body, Bytes::from)
} else {
body
}
}
fn rewrite_bind_entry(entry: &str) -> Option<String> {
let colon = entry.find(':')?;
let host = &entry[..colon];
if let Cow::Owned(resolved) = resolve(host) {
Some(format!("{resolved}{}", &entry[colon..]))
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolve_leaves_non_symlink_paths_unchanged() {
assert_eq!(resolve("/Users/me/project"), "/Users/me/project");
assert_eq!(resolve("/nonexistent/path"), "/nonexistent/path");
assert_eq!(resolve("/"), "/");
}
#[test]
#[cfg(target_os = "macos")]
fn resolve_follows_macos_tmp_symlink() {
assert_eq!(resolve("/tmp"), "/private/tmp");
assert_eq!(resolve("/tmp/foo"), "/private/tmp/foo");
assert_eq!(
resolve("/tmp/deep/nested/path"),
"/private/tmp/deep/nested/path"
);
}
#[test]
#[cfg(target_os = "macos")]
fn resolve_follows_macos_var_symlink() {
assert_eq!(resolve("/var"), "/private/var");
assert_eq!(resolve("/var/folders/xx/yy"), "/private/var/folders/xx/yy");
assert_eq!(resolve("/var/tmp/foo"), "/private/var/tmp/foo");
}
#[test]
#[cfg(target_os = "macos")]
fn resolve_follows_macos_etc_symlink() {
assert_eq!(resolve("/etc"), "/private/etc");
assert_eq!(resolve("/etc/hosts"), "/private/etc/hosts");
}
#[test]
#[cfg(target_os = "macos")]
fn resolve_already_private_is_unchanged() {
assert_eq!(resolve("/private/tmp/foo"), "/private/tmp/foo");
assert_eq!(
resolve("/private/var/folders/xx"),
"/private/var/folders/xx"
);
}
#[test]
fn rewrite_bind_entry_with_options() {
if resolve("/tmp") == "/tmp" {
return; }
assert_eq!(
rewrite_bind_entry("/tmp/foo:/app:ro"),
Some("/private/tmp/foo:/app:ro".to_string())
);
}
#[test]
fn rewrite_bind_entry_no_change() {
assert_eq!(rewrite_bind_entry("/Users/me/proj:/app"), None);
}
#[test]
fn rewrite_create_body_binds() {
if resolve("/tmp") == "/tmp" {
return;
}
let body = serde_json::json!({
"Image": "alpine",
"HostConfig": {
"Binds": ["/tmp/foo:/app", "/Users/me:/home:ro"]
}
});
let input = Bytes::from(serde_json::to_vec(&body).unwrap());
let output = rewrite_create_body(input);
let v: serde_json::Value = serde_json::from_slice(&output).unwrap();
let binds = v["HostConfig"]["Binds"].as_array().unwrap();
assert_eq!(binds[0], "/private/tmp/foo:/app");
assert_eq!(binds[1], "/Users/me:/home:ro");
}
#[test]
fn rewrite_create_body_mounts() {
if resolve("/tmp") == "/tmp" {
return;
}
let body = serde_json::json!({
"Image": "alpine",
"HostConfig": {
"Mounts": [
{"Type": "bind", "Source": "/tmp/ctx", "Target": "/app"},
{"Type": "volume", "Source": "myvolume", "Target": "/data"},
{"Type": "bind", "Source": "/Users/me", "Target": "/home"}
]
}
});
let input = Bytes::from(serde_json::to_vec(&body).unwrap());
let output = rewrite_create_body(input);
let v: serde_json::Value = serde_json::from_slice(&output).unwrap();
let mounts = v["HostConfig"]["Mounts"].as_array().unwrap();
assert_eq!(mounts[0]["Source"], "/private/tmp/ctx");
assert_eq!(mounts[1]["Source"], "myvolume"); assert_eq!(mounts[2]["Source"], "/Users/me"); }
#[test]
fn rewrite_create_body_no_host_config_is_noop() {
let body = Bytes::from(br#"{"Image":"alpine"}"#.to_vec());
let output = rewrite_create_body(body.clone());
assert_eq!(output, body);
}
#[test]
fn rewrite_create_body_invalid_json_is_noop() {
let body = Bytes::from(b"not json".to_vec());
let output = rewrite_create_body(body.clone());
assert_eq!(output, body);
}
}