pub mod iso9660;
pub mod tree;
pub mod udf;
pub use tree::TreeNode;
use std::fs::{create_dir_all, File};
use std::io::{self, Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
pub type Error = Box<dyn std::error::Error + Send + Sync + 'static>;
pub type Result<T> = std::result::Result<T, Error>;
pub fn detect_and_parse_filesystem(file: &mut File, filename: &str) -> Result<TreeNode> {
detect_and_parse_filesystem_verbose(file, filename, false)
}
pub fn detect_and_parse_filesystem_verbose(
file: &mut File,
filename: &str,
verbose: bool,
) -> Result<TreeNode> {
let mut errors = Vec::new();
if verbose {
let file_size = file.seek(SeekFrom::End(0))?;
file.seek(SeekFrom::Start(0))?;
eprintln!(
"File size: {} bytes ({:.2} GB)",
file_size,
file_size as f64 / (1024.0 * 1024.0 * 1024.0)
);
eprintln!("Scanning key sectors for filesystem signatures...");
for (sector, desc) in [
(16, "ISO 9660 PVD / UDF VRS"),
(17, "UDF VRS"),
(256, "UDF AVDP"),
]
.iter()
{
file.seek(SeekFrom::Start(*sector as u64 * 2048))?;
let mut buf = [0u8; 32];
if file.read_exact(&mut buf).is_ok() {
let printable: String = buf
.iter()
.map(|&b| {
if (0x20..0x7f).contains(&b) {
b as char
} else {
'.'
}
})
.collect();
eprintln!(" Sector {:>3} ({}): {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} |{}|",
sector, desc, buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7], &printable[..8]);
}
}
file.seek(SeekFrom::Start(0))?;
}
if verbose {
eprintln!("\nAttempting ISO 9660 parsing...");
}
match iso9660::parse_iso9660_verbose(file, verbose) {
Ok(root) => return Ok(root),
Err(e) => {
if verbose {
eprintln!(" ISO 9660 parsing failed: {}", e);
}
errors.push(format!("ISO 9660: {}", e));
}
}
file.seek(SeekFrom::Start(0))?;
if verbose {
eprintln!("\nAttempting UDF parsing...");
}
match udf::parse_udf_verbose(file, verbose) {
Ok(root) => return Ok(root),
Err(e) => {
if verbose {
eprintln!(" UDF parsing failed: {}", e);
}
errors.push(format!("UDF: {}", e));
}
}
let mut msg = format!("Unable to detect supported filesystem in {}", filename);
if !errors.is_empty() {
msg.push_str("\nDetails:\n - ");
msg.push_str(&errors.join("\n - "));
}
Err(msg.into())
}
pub fn cat_node<W: Write>(file: &mut File, node: &TreeNode, writer: &mut W) -> Result<()> {
if node.is_directory {
return Err(format!("'{}' is a directory, not a file", node.name).into());
}
let (location, length) = match (node.file_location, node.file_length) {
(Some(l), Some(n)) => (l, n),
_ => return Err("File location information not available".into()),
};
file.seek(SeekFrom::Start(location))?;
let mut remaining: u64 = length;
let buf_cap = remaining.min(EXTRACT_CHUNK_SIZE as u64) as usize;
let mut buffer = vec![0u8; buf_cap];
while remaining > 0 {
let to_read = remaining.min(EXTRACT_CHUNK_SIZE as u64) as usize;
let buf = &mut buffer[..to_read];
file.read_exact(buf)?;
match writer.write_all(buf) {
Ok(()) => {}
Err(e) if e.kind() == io::ErrorKind::BrokenPipe => return Ok(()),
Err(e) => return Err(e.into()),
}
remaining -= to_read as u64;
}
Ok(())
}
pub fn extract_node(file: &mut File, node: &TreeNode, output_path: &str) -> Result<()> {
create_dir_all(output_path)
.map_err(|e| format!("cannot create output directory '{}': {}", output_path, e))?;
let root = std::fs::canonicalize(output_path).map_err(|e| {
format!(
"cannot canonicalize output directory '{}': {}",
output_path, e
)
})?;
if node.is_directory && node.name == "/" {
for child in &node.children {
extract_into(file, child, &root, &root)?;
}
Ok(())
} else {
extract_into(file, node, &root, &root)
}
}
const EXTRACT_CHUNK_SIZE: usize = 8 * 1024 * 1024;
fn validate_entry_name(name: &str) -> Result<()> {
if name.is_empty() || name == "." || name == ".." {
return Err(format!("refusing to extract entry with unsafe name {:?}", name).into());
}
if name.contains('/') || name.contains('\\') || name.contains('\0') {
return Err(format!(
"refusing to extract entry whose name contains a path separator or NUL byte: {:?}",
name
)
.into());
}
Ok(())
}
fn safe_join(root: &Path, here: &Path, name: &str) -> Result<PathBuf> {
validate_entry_name(name)?;
let target = here.join(name);
if !target.starts_with(root) {
return Err(format!(
"path escape: entry '{}' would write outside output directory {}",
name,
root.display()
)
.into());
}
Ok(target)
}
fn extract_into(file: &mut File, node: &TreeNode, root: &Path, here: &Path) -> Result<()> {
let target = safe_join(root, here, &node.name)?;
if node.is_directory {
create_dir_all(&target)?;
eprintln!("Created directory: {}", target.display());
for child in &node.children {
extract_into(file, child, root, &target)?;
}
} else {
extract_file_at(file, node, &target)?;
}
Ok(())
}
fn extract_file_at(file: &mut File, node: &TreeNode, target: &Path) -> Result<()> {
let (location, length) = match (node.file_location, node.file_length) {
(Some(l), Some(n)) => (l, n),
_ => return Err("File location information not available for extraction".into()),
};
file.seek(SeekFrom::Start(location))?;
if let Some(parent) = target.parent() {
create_dir_all(parent)?;
}
let mut output_file = std::fs::File::create(target)
.map_err(|e| format!("cannot create '{}': {}", target.display(), e))?;
let mut remaining: u64 = length;
let buf_cap = remaining.min(EXTRACT_CHUNK_SIZE as u64) as usize;
let mut buffer = vec![0u8; buf_cap];
while remaining > 0 {
let to_read = remaining.min(EXTRACT_CHUNK_SIZE as u64) as usize;
let buf = &mut buffer[..to_read];
file.read_exact(buf)?;
output_file.write_all(buf)?;
remaining -= to_read as u64;
if length > 100 * 1024 * 1024 {
let done = length - remaining;
eprint!(
"\r Extracting {}: {:.1}%",
node.name,
done as f64 / length as f64 * 100.0
);
}
}
if length > 100 * 1024 * 1024 {
eprintln!();
}
eprintln!("Extracted: {}", target.display());
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
use std::path::Path;
fn test_file_path(filename: &str) -> String {
format!("test_data/{}", filename)
}
fn require_test_file(name: &str) -> Option<File> {
let path = test_file_path(name);
if !Path::new(&path).exists() {
eprintln!(
"Skipping test: {} not found (run `make test-data` to generate)",
path
);
return None;
}
Some(File::open(&path).unwrap_or_else(|_| panic!("Failed to open test file: {}", path)))
}
fn parse_linux_iso() -> Option<(File, TreeNode)> {
let mut file = require_test_file("test_linux.iso")?;
let root = detect_and_parse_filesystem(&mut file, "test_linux.iso")
.expect("Failed to parse test_linux.iso");
Some((file, root))
}
fn parse_macos_iso() -> Option<(File, TreeNode)> {
let mut file = require_test_file("test_macos.iso")?;
let root = detect_and_parse_filesystem(&mut file, "test_macos.iso")
.expect("Failed to parse test_macos.iso");
Some((file, root))
}
#[test]
fn test_iso9660_parsing() {
for test_file in &["test_linux.iso", "test_macos.iso"] {
if let Some(mut file) = require_test_file(test_file) {
let root = iso9660::parse_iso9660(&mut file)
.unwrap_or_else(|e| panic!("ISO 9660 parsing failed for {}: {}", test_file, e));
assert_eq!(root.name, "/");
assert!(root.is_directory);
assert!(
!root.children.is_empty(),
"{} should have children",
test_file
);
}
}
}
#[test]
fn test_filesystem_detection() {
for test_file in &["test_linux.iso", "test_macos.iso"] {
if let Some(mut file) = require_test_file(test_file) {
let root = detect_and_parse_filesystem(&mut file, test_file).unwrap_or_else(|e| {
panic!("Filesystem detection failed for {}: {}", test_file, e)
});
assert_eq!(root.name, "/");
assert!(root.is_directory);
}
}
}
#[test]
fn test_invalid_file_handling() {
assert!(File::open(test_file_path("nonexistent.iso")).is_err());
}
#[test]
fn test_garbage_data_rejected() {
let dir = std::env::temp_dir().join("isomage_test");
std::fs::create_dir_all(&dir).unwrap();
let garbage_path = dir.join("garbage.iso");
std::fs::write(&garbage_path, b"this is not an ISO file at all").unwrap();
let mut file = File::open(&garbage_path).unwrap();
let result = detect_and_parse_filesystem(&mut file, "garbage.iso");
assert!(result.is_err(), "Garbage data should fail to parse");
let err = result.unwrap_err().to_string();
assert!(
err.contains("Unable to detect"),
"Error should mention detection failure, got: {}",
err
);
std::fs::remove_file(&garbage_path).ok();
}
#[test]
fn test_linux_iso_expected_directories() {
if let Some((_file, root)) = parse_linux_iso() {
for dir_name in &["boot", "etc", "home", "usr", "var"] {
let node = root
.find_node(dir_name)
.unwrap_or_else(|| panic!("Expected directory '{}' not found", dir_name));
assert!(node.is_directory, "'{}' should be a directory", dir_name);
}
}
}
#[test]
fn test_linux_iso_expected_files() {
if let Some((_file, root)) = parse_linux_iso() {
let expected_files = [
"boot/grub.cfg",
"etc/hostname",
"etc/hosts",
"home/user/.bashrc",
"usr/bin/hello",
"var/log/system.log",
];
for path in &expected_files {
let node = root
.find_node(path)
.unwrap_or_else(|| panic!("Expected file '{}' not found", path));
assert!(!node.is_directory, "'{}' should be a file", path);
assert!(node.size > 0, "'{}' should have non-zero size", path);
assert!(
node.file_location.is_some(),
"'{}' should have a file location",
path
);
assert!(
node.file_length.is_some(),
"'{}' should have a file length",
path
);
}
}
}
#[test]
fn test_linux_iso_nested_structure() {
if let Some((_file, root)) = parse_linux_iso() {
let home = root.find_node("home").expect("home not found");
assert!(home.is_directory);
let user = home.find_node("user").expect("user not found in home");
assert!(user.is_directory);
let bashrc = user
.find_node(".bashrc")
.expect(".bashrc not found in user");
assert!(!bashrc.is_directory);
}
}
#[test]
fn test_macos_iso_expected_structure() {
if let Some((_file, root)) = parse_macos_iso() {
for dir_name in &["Applications", "System", "Users", "private"] {
let node = root.find_node(dir_name).unwrap_or_else(|| {
panic!("Expected directory '{}' not found in macOS ISO", dir_name)
});
assert!(node.is_directory);
}
let expected_files = [
"Applications/readme.txt",
"System/Library/info.txt",
"Users/user/welcome.txt",
"private/var/log/system.log",
];
for path in &expected_files {
let node = root
.find_node(path)
.unwrap_or_else(|| panic!("Expected file '{}' not found in macOS ISO", path));
assert!(!node.is_directory);
assert!(node.size > 0);
}
}
}
#[test]
fn test_tree_structure_validation() {
for (name, parser) in [
("linux", parse_linux_iso as fn() -> Option<(File, TreeNode)>),
("macos", parse_macos_iso),
] {
if let Some((_file, root)) = parser() {
validate_tree_structure(&root, 0, name);
}
}
}
fn validate_tree_structure(node: &TreeNode, depth: usize, iso_name: &str) {
assert!(
!node.name.is_empty(),
"Node name should not be empty in {}",
iso_name
);
assert!(depth <= 10, "Tree depth exceeded limit in {}", iso_name);
if !node.is_directory {
assert!(
node.children.is_empty(),
"File '{}' should not have children in {}",
node.name,
iso_name
);
}
for child in &node.children {
validate_tree_structure(child, depth + 1, iso_name);
}
}
#[test]
fn test_tree_node_creation() {
let dir_node = TreeNode::new_directory("test_dir".to_string());
assert!(dir_node.is_directory);
assert_eq!(dir_node.name, "test_dir");
assert_eq!(dir_node.size, 0);
assert!(dir_node.children.is_empty());
assert!(dir_node.file_location.is_none());
let file_node = TreeNode::new_file("test_file.txt".to_string(), 1024);
assert!(!file_node.is_directory);
assert_eq!(file_node.name, "test_file.txt");
assert_eq!(file_node.size, 1024);
assert!(file_node.file_location.is_none());
let located = TreeNode::new_file_with_location("f.bin".to_string(), 512, 4096, 512);
assert_eq!(located.file_location, Some(4096));
assert_eq!(located.file_length, Some(512));
}
#[test]
fn test_directory_size_calculation() {
let mut root = TreeNode::new_directory("root".to_string());
root.add_child(TreeNode::new_file("file1.txt".to_string(), 100));
root.add_child(TreeNode::new_file("file2.txt".to_string(), 200));
let mut subdir = TreeNode::new_directory("subdir".to_string());
subdir.add_child(TreeNode::new_file("file3.txt".to_string(), 300));
root.add_child(subdir);
root.calculate_directory_size();
assert_eq!(root.size, 600);
let sub = root.find_node("subdir").unwrap();
assert_eq!(sub.size, 300);
}
#[test]
fn test_find_node_with_leading_slash() {
if let Some((_file, root)) = parse_linux_iso() {
assert!(root.find_node("/etc/hostname").is_some());
assert!(root.find_node("etc/hostname").is_some());
assert!(root.find_node("///etc/hostname").is_some());
}
}
#[test]
fn test_find_node_root_paths() {
if let Some((_file, root)) = parse_linux_iso() {
let by_empty = root.find_node("").unwrap();
assert_eq!(by_empty.name, "/");
let by_slash = root.find_node("/").unwrap();
assert_eq!(by_slash.name, "/");
}
}
#[test]
fn test_find_node_nonexistent() {
if let Some((_file, root)) = parse_linux_iso() {
assert!(root.find_node("nonexistent").is_none());
assert!(root.find_node("etc/nonexistent").is_none());
assert!(root.find_node("a/b/c/d/e/f").is_none());
}
}
#[test]
fn test_cat_file_to_buffer() {
if let Some((mut file, root)) = parse_linux_iso() {
let node = root
.find_node("etc/hostname")
.expect("etc/hostname not found");
let mut output = Vec::new();
cat_node(&mut file, node, &mut output).expect("cat_node failed");
let content = String::from_utf8(output).expect("Not valid UTF-8");
assert!(
content.contains("test-linux-system"),
"Expected hostname content, got: {:?}",
content
);
}
}
#[test]
fn test_cat_preserves_exact_bytes() {
if let Some((mut file, root)) = parse_linux_iso() {
let node = root
.find_node("etc/hostname")
.expect("etc/hostname not found");
let mut output = Vec::new();
cat_node(&mut file, node, &mut output).expect("cat_node failed");
assert_eq!(
output.len() as u64,
node.size,
"cat output length {} doesn't match node size {}",
output.len(),
node.size
);
}
}
#[test]
fn test_cat_rejects_directory() {
if let Some((mut file, root)) = parse_linux_iso() {
let node = root.find_node("etc").expect("etc/ not found");
let mut output = Vec::new();
let result = cat_node(&mut file, node, &mut output);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("directory"));
assert!(
output.is_empty(),
"No bytes should be written for a directory"
);
}
}
#[test]
fn test_cat_node_without_location() {
let node = TreeNode::new_file("orphan.txt".to_string(), 100);
let dir = std::env::temp_dir().join("isomage_test");
std::fs::create_dir_all(&dir).unwrap();
let dummy_path = dir.join("dummy.bin");
std::fs::write(&dummy_path, b"x").unwrap();
let mut file = File::open(&dummy_path).unwrap();
let mut output = Vec::new();
let result = cat_node(&mut file, &node, &mut output);
assert!(result.is_err(), "cat on file without location should error");
assert!(result.unwrap_err().to_string().contains("not available"));
std::fs::remove_file(&dummy_path).ok();
}
#[test]
fn test_cat_every_file_in_linux_iso() {
if let Some((mut file, root)) = parse_linux_iso() {
let files = [
("boot/grub.cfg", "GRUB"),
("etc/hostname", "test-linux-system"),
("etc/hosts", "127.0.0.1"),
("home/user/.bashrc", "Bash"),
("usr/bin/hello", "Hello World"),
("var/log/system.log", "System started"),
];
for (path, expected) in &files {
let node = root
.find_node(path)
.unwrap_or_else(|| panic!("{} not found", path));
let mut output = Vec::new();
cat_node(&mut file, node, &mut output)
.unwrap_or_else(|e| panic!("cat failed for {}: {}", path, e));
let content = String::from_utf8(output).expect("Not valid UTF-8");
assert!(
content.contains(expected),
"Expected '{}' in {}, got: {:?}",
expected,
path,
content
);
}
}
}
#[test]
fn test_cat_every_file_in_macos_iso() {
if let Some((mut file, root)) = parse_macos_iso() {
let files = [
("Applications/readme.txt", "Application Data"),
("System/Library/info.txt", "System Library"),
("Users/user/welcome.txt", "Welcome to macOS"),
("private/var/log/system.log", "macOS system log"),
];
for (path, expected) in &files {
let node = root
.find_node(path)
.unwrap_or_else(|| panic!("{} not found in macOS ISO", path));
let mut output = Vec::new();
cat_node(&mut file, node, &mut output)
.unwrap_or_else(|e| panic!("cat failed for {}: {}", path, e));
let content = String::from_utf8(output).expect("Not valid UTF-8");
assert!(
content.contains(expected),
"Expected '{}' in {}, got: {:?}",
expected,
path,
content
);
}
}
}
#[test]
fn test_extract_single_file() {
if let Some((mut file, root)) = parse_linux_iso() {
let dir = std::env::temp_dir().join("isomage_test_extract_single");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let node = root
.find_node("etc/hostname")
.expect("etc/hostname not found");
extract_node(&mut file, node, dir.to_str().unwrap()).expect("extract failed");
let extracted = std::fs::read_to_string(dir.join("hostname")).unwrap();
assert!(extracted.contains("test-linux-system"));
std::fs::remove_dir_all(&dir).ok();
}
}
#[test]
fn test_extract_directory() {
if let Some((mut file, root)) = parse_linux_iso() {
let dir = std::env::temp_dir().join("isomage_test_extract_dir");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let node = root.find_node("etc").expect("etc not found");
extract_node(&mut file, node, dir.to_str().unwrap()).expect("extract failed");
assert!(dir.join("etc/hostname").exists(), "hostname should exist");
assert!(dir.join("etc/hosts").exists(), "hosts should exist");
let hostname = std::fs::read_to_string(dir.join("etc/hostname")).unwrap();
assert!(hostname.contains("test-linux-system"));
std::fs::remove_dir_all(&dir).ok();
}
}
#[test]
fn test_extract_root() {
if let Some((mut file, root)) = parse_linux_iso() {
let dir = std::env::temp_dir().join("isomage_test_extract_root");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
extract_node(&mut file, &root, dir.to_str().unwrap()).expect("extract root failed");
for name in &["boot", "etc", "home", "usr", "var"] {
assert!(dir.join(name).is_dir(), "{} directory should exist", name);
}
assert!(
dir.join("home/user/.bashrc").exists(),
".bashrc should exist"
);
std::fs::remove_dir_all(&dir).ok();
}
}
#[test]
fn test_extract_matches_cat() {
if let Some((mut file, root)) = parse_linux_iso() {
let dir = std::env::temp_dir().join("isomage_test_extract_vs_cat");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let node = root.find_node("etc/hosts").expect("etc/hosts not found");
let mut cat_output = Vec::new();
cat_node(&mut file, node, &mut cat_output).expect("cat failed");
extract_node(&mut file, node, dir.to_str().unwrap()).expect("extract failed");
let extracted = std::fs::read(dir.join("hosts")).unwrap();
assert_eq!(
cat_output, extracted,
"cat and extract should produce identical bytes"
);
std::fs::remove_dir_all(&dir).ok();
}
}
fn dummy_iso() -> (File, std::path::PathBuf) {
let dir = std::env::temp_dir().join("isomage_test_security");
std::fs::create_dir_all(&dir).unwrap();
let p = dir.join("dummy.bin");
std::fs::write(&p, b"hostile payload bytes").unwrap();
let f = File::open(&p).unwrap();
(f, p)
}
#[test]
fn test_extract_rejects_dotdot_in_name() {
let (mut file, _payload) = dummy_iso();
let mut root = TreeNode::new_directory("/".to_string());
root.add_child(TreeNode::new_file_with_location(
"../escapee.txt".to_string(),
21,
0,
21,
));
let out = std::env::temp_dir().join("isomage_test_extract_dotdot_out");
let _ = std::fs::remove_dir_all(&out);
std::fs::create_dir_all(&out).unwrap();
let result = extract_node(&mut file, &root, out.to_str().unwrap());
assert!(result.is_err(), "extract must refuse '../escapee.txt'");
let err = result.unwrap_err().to_string();
assert!(
err.contains("unsafe") || err.contains("path"),
"error should mention unsafe/path traversal, got: {}",
err
);
assert!(
!out.parent().unwrap().join("escapee.txt").exists(),
"no file should have been written outside the output directory"
);
std::fs::remove_dir_all(&out).ok();
}
#[test]
fn test_extract_rejects_slash_in_name() {
let (mut file, _payload) = dummy_iso();
let mut root = TreeNode::new_directory("/".to_string());
root.add_child(TreeNode::new_file_with_location(
"subdir/file.txt".to_string(),
21,
0,
21,
));
let out = std::env::temp_dir().join("isomage_test_extract_slash_out");
let _ = std::fs::remove_dir_all(&out);
std::fs::create_dir_all(&out).unwrap();
let result = extract_node(&mut file, &root, out.to_str().unwrap());
assert!(result.is_err(), "extract must refuse a name containing '/'");
std::fs::remove_dir_all(&out).ok();
}
#[test]
fn test_extract_rejects_absolute_name() {
let (mut file, _payload) = dummy_iso();
let mut root = TreeNode::new_directory("/".to_string());
root.add_child(TreeNode::new_file_with_location(
"/etc/passwd".to_string(),
21,
0,
21,
));
let out = std::env::temp_dir().join("isomage_test_extract_abs_out");
let _ = std::fs::remove_dir_all(&out);
std::fs::create_dir_all(&out).unwrap();
let result = extract_node(&mut file, &root, out.to_str().unwrap());
assert!(
result.is_err(),
"extract must refuse an absolute-looking name"
);
std::fs::remove_dir_all(&out).ok();
}
#[test]
fn test_extract_rejects_nul_byte() {
let (mut file, _payload) = dummy_iso();
let mut root = TreeNode::new_directory("/".to_string());
root.add_child(TreeNode::new_file_with_location(
"good\0name.txt".to_string(),
21,
0,
21,
));
let out = std::env::temp_dir().join("isomage_test_extract_nul_out");
let _ = std::fs::remove_dir_all(&out);
std::fs::create_dir_all(&out).unwrap();
let result = extract_node(&mut file, &root, out.to_str().unwrap());
assert!(
result.is_err(),
"extract must refuse a name with a NUL byte"
);
std::fs::remove_dir_all(&out).ok();
}
#[test]
fn test_validate_entry_name_unit() {
assert!(validate_entry_name("hostname").is_ok());
assert!(validate_entry_name(".bashrc").is_ok());
assert!(validate_entry_name("name with spaces").is_ok());
assert!(validate_entry_name("").is_err());
assert!(validate_entry_name(".").is_err());
assert!(validate_entry_name("..").is_err());
assert!(validate_entry_name("../etc/passwd").is_err());
assert!(validate_entry_name("foo/bar").is_err());
assert!(validate_entry_name("foo\\bar").is_err());
assert!(validate_entry_name("/etc/passwd").is_err());
assert!(validate_entry_name("a\0b").is_err());
}
struct BrokenPipeAfter {
budget: usize,
written: usize,
}
impl Write for BrokenPipeAfter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
if self.written >= self.budget {
return Err(io::Error::new(
io::ErrorKind::BrokenPipe,
"downstream closed",
));
}
let take = buf.len().min(self.budget - self.written);
self.written += take;
if take == 0 {
Err(io::Error::new(
io::ErrorKind::BrokenPipe,
"downstream closed",
))
} else {
Ok(take)
}
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
#[test]
fn test_cat_node_swallows_broken_pipe() {
if let Some((mut file, root)) = parse_linux_iso() {
let node = root
.find_node("etc/hostname")
.expect("etc/hostname not found");
let mut w = BrokenPipeAfter {
budget: 0,
written: 0,
};
let result = cat_node(&mut file, node, &mut w);
assert!(
result.is_ok(),
"cat_node should treat BrokenPipe as Ok, got: {:?}",
result
);
}
}
}