use anyhow::{Context, Result};
use sha2::{Digest, Sha256};
use std::fs;
use std::io::{BufReader, Read, Seek, SeekFrom};
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArchiveFormat {
Zip,
Squashfs,
ElfSquashfs(u64),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MountName {
Relative(String),
Fixed(PathBuf),
}
impl MountName {
pub fn as_str(&self) -> &str {
match self {
MountName::Relative(s) => s.as_str(),
MountName::Fixed(p) => p.file_name().and_then(|n| n.to_str()).unwrap_or(""),
}
}
}
pub struct ArchiveSpec {
pub mount: MountName,
pub file: PathBuf,
}
impl ArchiveSpec {
pub fn parse(arg: &str) -> Result<Self> {
if let Some(colon) = arg.find(':') {
let whole = Path::new(arg);
if whole.is_file() {
let mount = parse_mount_name(arg, arg)?;
return Ok(Self {
mount,
file: whole
.canonicalize()
.with_context(|| format!("failed to resolve path {}", whole.display()))?,
});
}
let name_part = &arg[..colon];
let mount = parse_mount_name(name_part, arg)?;
let file = Path::new(&arg[colon + 1..]);
if !file.is_file() {
anyhow::bail!("archive file not found: {}", file.display());
}
Ok(Self {
mount,
file: file
.canonicalize()
.with_context(|| format!("failed to resolve path {}", file.display()))?,
})
} else {
let file = Path::new(arg);
if !file.is_file() {
anyhow::bail!("archive file not found: {}", file.display());
}
let name = stem(arg);
let mount = parse_mount_name(&name, arg)?;
Ok(Self {
mount,
file: file
.canonicalize()
.with_context(|| format!("failed to resolve path {}", file.display()))?,
})
}
}
}
pub struct EmptySpec {
pub mount: MountName,
}
impl EmptySpec {
pub fn parse(arg: &str) -> Result<Self> {
let mount = parse_mount_name(arg, arg)?;
Ok(Self { mount })
}
}
pub fn parse_mount_name(name: &str, source: &str) -> Result<MountName> {
if let Some(rest) = name.strip_prefix("/run/fuselage/") {
if rest.is_empty() || rest.contains('/') || rest.contains('\0') {
anyhow::bail!(
"fixed mount path {:?} must have exactly one component after /run/fuselage/ \
(e.g. /run/fuselage/myapp)",
name
);
}
if rest == "." || rest == ".." {
anyhow::bail!("fixed mount path {:?} is not a valid directory name", name);
}
Ok(MountName::Fixed(PathBuf::from(name)))
} else if name.starts_with('/') {
anyhow::bail!(
"absolute mount path {:?} (from {:?}) is not allowed; \
only /run/fuselage/NAME paths are permitted",
name,
source
);
} else {
validate_name(name, source)?;
Ok(MountName::Relative(name.to_string()))
}
}
fn validate_name(name: &str, source: &str) -> Result<()> {
if name.is_empty() {
anyhow::bail!(
"archive name derived from {:?} is empty; use NAME:FILE to provide an explicit name",
source
);
}
if name == "." || name == ".." {
anyhow::bail!(
"archive name {:?} (derived from {:?}) is not a valid directory name; \
use NAME:FILE to provide an explicit name",
name,
source
);
}
if name.contains('/') || name.contains('\0') {
anyhow::bail!(
"archive name {:?} (derived from {:?}) contains an invalid character; \
use NAME:FILE to provide an explicit name",
name,
source
);
}
Ok(())
}
fn stem(path: &str) -> String {
let base = Path::new(path)
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| path.to_string());
let base = base.strip_suffix(".sfs").unwrap_or(&base);
let base = base.strip_suffix(".zip").unwrap_or(base);
let base = base.strip_suffix(".b64").unwrap_or(base);
base.to_string()
}
pub fn try_decode_base64(src: &Path, dest: &Path) -> Result<bool> {
use std::io::{BufRead, BufReader};
let f = fs::File::open(src).with_context(|| format!("failed to open {}", src.display()))?;
let reader = BufReader::new(f);
let tmp_dest = dest.with_extension("tmp");
let mut dec: Option<crate::b64stream::B64Decoder<fs::File>> = None;
let result = (|| {
for line in reader.lines() {
let line = line.with_context(|| format!("failed to read {}", src.display()))?;
if line.starts_with('#') {
continue;
}
let trimmed = line.trim_end();
if trimmed.is_empty() {
continue;
}
if dec.is_none() {
let out = fs::File::create(&tmp_dest)
.with_context(|| format!("failed to create {}", tmp_dest.display()))?;
dec = Some(crate::b64stream::B64Decoder::new(out));
}
let accepted = dec
.as_mut()
.unwrap()
.push_line(trimmed)
.with_context(|| format!("base64 decode failed for {}", src.display()))?;
if !accepted {
return Ok(false);
}
}
let Some(dec) = dec else {
return Ok(false);
};
dec.finish()
.with_context(|| format!("base64 decode failed for {}", src.display()))?;
fs::rename(&tmp_dest, dest).with_context(|| {
format!(
"failed to rename {} to {}",
tmp_dest.display(),
dest.display()
)
})?;
Ok(true)
})();
if result.is_err() {
let _ = fs::remove_file(&tmp_dest);
}
result
}
pub fn detect_format(path: &Path) -> Result<ArchiveFormat> {
let mut f =
fs::File::open(path).with_context(|| format!("failed to open {}", path.display()))?;
let mut magic = [0u8; 4];
f.read_exact(&mut magic)
.with_context(|| format!("failed to read magic bytes from {}", path.display()))?;
match &magic {
b"PK\x03\x04" => Ok(ArchiveFormat::Zip),
b"hsqs" | b"sqsh" => Ok(ArchiveFormat::Squashfs),
b"\x7fELF" => detect_elf_squashfs(f, path),
_ => anyhow::bail!(
"{}: unrecognised archive format (magic {:02x?})",
path.display(),
magic
),
}
}
fn detect_elf_squashfs(mut f: fs::File, path: &Path) -> Result<ArchiveFormat> {
let mut e_ident_rest = [0u8; 12];
f.read_exact(&mut e_ident_rest)
.with_context(|| format!("failed to read ELF ident from {}", path.display()))?;
let class = e_ident_rest[0]; let data = e_ident_rest[1];
let squashfs_offset = match (class, data) {
(1, 1) => elf_squashfs_offset_32::<byteorder::LittleEndian>(&mut f, path),
(1, 2) => elf_squashfs_offset_32::<byteorder::BigEndian>(&mut f, path),
(2, 1) => elf_squashfs_offset_64::<byteorder::LittleEndian>(&mut f, path),
(2, 2) => elf_squashfs_offset_64::<byteorder::BigEndian>(&mut f, path),
_ => anyhow::bail!(
"{}: ELF file has unsupported class/data encoding ({}/{})",
path.display(),
class,
data
),
}?;
f.seek(SeekFrom::Start(squashfs_offset))
.with_context(|| format!("failed to seek in {}", path.display()))?;
let mut sfs_magic = [0u8; 4];
f.read_exact(&mut sfs_magic).with_context(|| {
format!(
"{}: ELF file has no squashfs image at expected offset {}",
path.display(),
squashfs_offset
)
})?;
if &sfs_magic != b"hsqs" && &sfs_magic != b"sqsh" {
anyhow::bail!(
"{}: ELF file has no squashfs image at expected offset {} (found {:02x?})",
path.display(),
squashfs_offset,
sfs_magic
);
}
Ok(ArchiveFormat::ElfSquashfs(squashfs_offset))
}
fn elf_squashfs_offset_32<B: byteorder::ByteOrder>(f: &mut fs::File, path: &Path) -> Result<u64> {
use byteorder::ReadBytesExt;
let _e_type = f.read_u16::<B>()?;
let _e_machine = f.read_u16::<B>()?;
let _e_version = f.read_u32::<B>()?;
let _e_entry = f.read_u32::<B>()?;
let e_phoff = f.read_u32::<B>()? as u64;
let e_shoff = f.read_u32::<B>()? as u64;
let _e_flags = f.read_u32::<B>()?;
let _e_ehsize = f.read_u16::<B>()?;
let e_phentsize = f.read_u16::<B>()? as u64;
let e_phnum = f.read_u16::<B>()? as u64;
let e_shentsize = f.read_u16::<B>()? as u64;
let e_shnum = f.read_u16::<B>()? as u64;
let load_end = highest_pt_load_end_32::<B>(f, path, e_phoff, e_phentsize, e_phnum)?;
Ok(elf_image_offset(
load_end,
e_phoff,
e_phentsize,
e_phnum,
e_shoff,
e_shentsize,
e_shnum,
))
}
fn elf_squashfs_offset_64<B: byteorder::ByteOrder>(f: &mut fs::File, path: &Path) -> Result<u64> {
use byteorder::ReadBytesExt;
let _e_type = f.read_u16::<B>()?;
let _e_machine = f.read_u16::<B>()?;
let _e_version = f.read_u32::<B>()?;
let _e_entry = f.read_u64::<B>()?;
let e_phoff = f.read_u64::<B>()?;
let e_shoff = f.read_u64::<B>()?;
let _e_flags = f.read_u32::<B>()?;
let _e_ehsize = f.read_u16::<B>()?;
let e_phentsize = f.read_u16::<B>()? as u64;
let e_phnum = f.read_u16::<B>()? as u64;
let e_shentsize = f.read_u16::<B>()? as u64;
let e_shnum = f.read_u16::<B>()? as u64;
let load_end = highest_pt_load_end_64::<B>(f, path, e_phoff, e_phentsize, e_phnum)?;
Ok(elf_image_offset(
load_end,
e_phoff,
e_phentsize,
e_phnum,
e_shoff,
e_shentsize,
e_shnum,
))
}
#[allow(clippy::too_many_arguments)]
fn elf_image_offset(
load_end: u64,
e_phoff: u64,
e_phentsize: u64,
e_phnum: u64,
e_shoff: u64,
e_shentsize: u64,
e_shnum: u64,
) -> u64 {
let pht_end = e_phoff.saturating_add(e_phnum.saturating_mul(e_phentsize));
let sht_end = e_shoff.saturating_add(e_shnum.saturating_mul(e_shentsize));
let image_end = load_end.max(pht_end).max(sht_end);
align_up(image_end, 4096)
}
fn highest_pt_load_end_32<B: byteorder::ByteOrder>(
f: &mut fs::File,
path: &Path,
e_phoff: u64,
e_phentsize: u64,
e_phnum: u64,
) -> Result<u64> {
use byteorder::ReadBytesExt;
const PT_LOAD: u32 = 1;
let mut highest_end: u64 = 0;
for i in 0..e_phnum {
let entry_offset = e_phoff + i * e_phentsize;
f.seek(SeekFrom::Start(entry_offset)).with_context(|| {
format!(
"failed to seek to program header {} in {}",
i,
path.display()
)
})?;
let p_type = f.read_u32::<B>()?;
let p_offset = f.read_u32::<B>()? as u64;
let _p_vaddr = f.read_u32::<B>()?;
let _p_paddr = f.read_u32::<B>()?;
let p_filesz = f.read_u32::<B>()? as u64;
if p_type == PT_LOAD {
let end = p_offset.saturating_add(p_filesz);
if end > highest_end {
highest_end = end;
}
}
}
anyhow::ensure!(
highest_end > 0,
"{}: ELF file has no PT_LOAD segments",
path.display()
);
Ok(highest_end)
}
fn highest_pt_load_end_64<B: byteorder::ByteOrder>(
f: &mut fs::File,
path: &Path,
e_phoff: u64,
e_phentsize: u64,
e_phnum: u64,
) -> Result<u64> {
use byteorder::ReadBytesExt;
const PT_LOAD: u32 = 1;
let mut highest_end: u64 = 0;
for i in 0..e_phnum {
let entry_offset = e_phoff + i * e_phentsize;
f.seek(SeekFrom::Start(entry_offset)).with_context(|| {
format!(
"failed to seek to program header {} in {}",
i,
path.display()
)
})?;
let p_type = f.read_u32::<B>()?;
let _p_flags = f.read_u32::<B>()?;
let p_offset = f.read_u64::<B>()?;
let _p_vaddr = f.read_u64::<B>()?;
let _p_paddr = f.read_u64::<B>()?;
let p_filesz = f.read_u64::<B>()?;
if p_type == PT_LOAD {
let end = p_offset.saturating_add(p_filesz);
if end > highest_end {
highest_end = end;
}
}
}
anyhow::ensure!(
highest_end > 0,
"{}: ELF file has no PT_LOAD segments",
path.display()
);
Ok(highest_end)
}
fn align_up(value: u64, align: u64) -> u64 {
(value + align - 1) & !(align - 1)
}
pub fn compute_sha256(path: &Path) -> Result<String> {
let mut f =
fs::File::open(path).with_context(|| format!("failed to open {}", path.display()))?;
let mut hasher = Sha256::new();
let mut buf = vec![0u8; 65536];
loop {
let n = f.read(&mut buf)?;
if n == 0 {
break;
}
hasher.update(&buf[..n]);
}
let result = hasher.finalize();
Ok(hex::encode(&result[..8])) }
pub fn extract_zip(archive: &Path, dest: &Path) -> Result<()> {
let file =
fs::File::open(archive).with_context(|| format!("failed to open {}", archive.display()))?;
let mut zip = zip::ZipArchive::new(file)
.with_context(|| format!("not a valid zip archive: {}", archive.display()))?;
zip.extract(dest).with_context(|| {
format!(
"failed to extract {} into {}",
archive.display(),
dest.display()
)
})?;
Ok(())
}
pub fn extract_squashfs(sfs: &Path, dest: &Path, offset: u64) -> Result<()> {
use backhand::{FilesystemReader, InnerNode};
let file = BufReader::new(
fs::File::open(sfs).with_context(|| format!("failed to open {}", sfs.display()))?,
);
let filesystem = FilesystemReader::from_reader_with_offset(file, offset)
.with_context(|| format!("failed to read squashfs image {}", sfs.display()))?;
for node in filesystem.files() {
let rel = node.fullpath.strip_prefix("/").unwrap_or(&node.fullpath);
let out = dest.join(rel);
match &node.inner {
InnerNode::Dir(_) if out != dest => {
fs::create_dir_all(&out)
.with_context(|| format!("failed to create dir {}", out.display()))?;
}
InnerNode::Dir(_) => {}
InnerNode::File(file_reader) => {
if let Some(parent) = out.parent() {
fs::create_dir_all(parent)?;
}
let mut out_file = fs::File::create(&out)
.with_context(|| format!("failed to create {}", out.display()))?;
let mut reader = filesystem.file(file_reader).reader();
std::io::copy(&mut reader, &mut out_file)
.with_context(|| format!("failed to write {}", out.display()))?;
fs::set_permissions(
&out,
fs::Permissions::from_mode(node.header.permissions as u32),
)?;
}
InnerNode::Symlink(sym) => {
if let Some(parent) = out.parent() {
fs::create_dir_all(parent)?;
}
std::os::unix::fs::symlink(&sym.link, &out)
.with_context(|| format!("failed to create symlink {}", out.display()))?;
}
_ => {}
}
}
Ok(())
}
pub fn zip_to_squashfs(zip: &Path, sfs_dest: &Path, tmp_dir: &Path) -> Result<bool> {
if std::process::Command::new("mksquashfs")
.arg("-version")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.is_err()
{
return Ok(false);
}
extract_zip(zip, tmp_dir)?;
let status = std::process::Command::new("mksquashfs")
.args([
tmp_dir.as_os_str(),
sfs_dest.as_os_str(),
"-comp".as_ref(),
"zstd".as_ref(),
"-Xcompression-level".as_ref(),
"1".as_ref(),
"-noappend".as_ref(),
"-quiet".as_ref(),
])
.status()
.context("failed to run mksquashfs")?;
if !status.success() {
anyhow::bail!("mksquashfs exited with status {:?}", status.code());
}
Ok(true)
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
#[test]
fn stem_no_extension() {
assert_eq!(stem("archive"), "archive");
}
#[test]
fn stem_zip() {
assert_eq!(stem("archive.zip"), "archive");
}
#[test]
fn stem_sfs() {
assert_eq!(stem("archive.sfs"), "archive");
}
#[test]
fn stem_b64() {
assert_eq!(stem("archive.b64"), "archive");
}
#[test]
fn stem_with_directory() {
assert_eq!(stem("/some/path/archive.zip"), "archive");
}
#[test]
fn stem_double_known_extension_zip_b64() {
assert_eq!(stem("archive.zip.b64"), "archive.zip");
}
#[test]
fn stem_double_known_extension_sfs_zip() {
assert_eq!(stem("archive.sfs.zip"), "archive.sfs");
}
#[test]
fn stem_extension_only() {
assert_eq!(stem(".zip"), "");
}
#[test]
fn stem_unknown_extension() {
assert_eq!(stem("archive.tar.gz"), "archive.tar.gz");
}
fn write_tmp(bytes: &[u8]) -> tempfile::NamedTempFile {
let mut f = tempfile::NamedTempFile::new().unwrap();
f.write_all(bytes).unwrap();
f
}
#[test]
fn detect_format_zip() {
let f = write_tmp(b"PK\x03\x04extra bytes");
assert_eq!(detect_format(f.path()).unwrap(), ArchiveFormat::Zip);
}
#[test]
fn detect_format_squashfs_little_endian() {
let f = write_tmp(b"hsqsextra");
assert_eq!(detect_format(f.path()).unwrap(), ArchiveFormat::Squashfs);
}
#[test]
fn detect_format_squashfs_big_endian() {
let f = write_tmp(b"sqshextra");
assert_eq!(detect_format(f.path()).unwrap(), ArchiveFormat::Squashfs);
}
#[test]
fn detect_format_unknown_magic() {
let f = write_tmp(b"\x00\x01\x02\x03");
assert!(detect_format(f.path()).is_err());
}
#[test]
fn detect_format_too_small() {
let f = write_tmp(b"PK\x03"); assert!(detect_format(f.path()).is_err());
}
#[test]
fn detect_format_nonexistent() {
assert!(detect_format(std::path::Path::new("/nonexistent/path.zip")).is_err());
}
#[test]
fn compute_sha256_empty_file() {
let f = write_tmp(b"");
assert_eq!(compute_sha256(f.path()).unwrap(), "e3b0c44298fc1c14");
}
#[test]
fn compute_sha256_known_content() {
let f = write_tmp(b"hello");
assert_eq!(compute_sha256(f.path()).unwrap(), "2cf24dba5fb0a30e");
}
#[test]
fn compute_sha256_returns_16_chars() {
let f = write_tmp(b"any content");
assert_eq!(compute_sha256(f.path()).unwrap().len(), 16);
}
#[test]
fn compute_sha256_nonexistent() {
assert!(compute_sha256(std::path::Path::new("/nonexistent")).is_err());
}
#[test]
fn decode_base64_pure_content() {
let src = write_tmp(b"aGVsbG8=");
let dest = tempfile::NamedTempFile::new().unwrap();
let ok = try_decode_base64(src.path(), dest.path()).unwrap();
assert!(ok);
assert_eq!(fs::read(dest.path()).unwrap(), b"hello");
}
#[test]
fn decode_base64_with_hash_comments() {
let src = write_tmp(b"# This is a comment\n## Another comment\naGVsbG8=\n");
let dest = tempfile::NamedTempFile::new().unwrap();
let ok = try_decode_base64(src.path(), dest.path()).unwrap();
assert!(ok);
assert_eq!(fs::read(dest.path()).unwrap(), b"hello");
}
#[test]
fn decode_base64_multiline_data() {
let src = write_tmp(b"# comment\naGVs\nbG8=\n");
let dest = tempfile::NamedTempFile::new().unwrap();
let ok = try_decode_base64(src.path(), dest.path()).unwrap();
assert!(ok);
assert_eq!(fs::read(dest.path()).unwrap(), b"hello");
}
#[test]
fn decode_base64_binary_content_is_not_base64() {
let src = write_tmp(b"PK\x03\x04");
let dest = tempfile::NamedTempFile::new().unwrap();
let ok = try_decode_base64(src.path(), dest.path()).unwrap();
assert!(!ok);
}
#[test]
fn decode_base64_only_comments_is_not_base64() {
let src = write_tmp(b"# no data here\n# nothing\n");
let dest = tempfile::NamedTempFile::new().unwrap();
let ok = try_decode_base64(src.path(), dest.path()).unwrap();
assert!(!ok);
}
#[test]
fn validate_name_accepts_normal_name() {
assert!(validate_name("mydata", "mydata.zip").is_ok());
}
#[test]
fn validate_name_rejects_empty() {
assert!(validate_name("", ".zip").is_err());
}
#[test]
fn validate_name_rejects_dot() {
assert!(validate_name(".", "./file.zip").is_err());
}
#[test]
fn validate_name_rejects_dotdot() {
assert!(validate_name("..", "../file.zip").is_err());
}
#[test]
fn validate_name_rejects_slash() {
assert!(validate_name("foo/bar", "foo/bar.zip").is_err());
}
#[test]
fn validate_name_rejects_null_byte() {
assert!(validate_name("foo\0bar", "foo\0bar.zip").is_err());
}
fn make_elf64_le_with_squashfs(sfs_magic: &[u8; 4]) -> Vec<u8> {
let mut buf = vec![0u8; 4096 + 8];
buf[0..4].copy_from_slice(b"\x7fELF");
buf[4] = 2; buf[5] = 1; buf[6] = 1;
buf[16..18].copy_from_slice(&2u16.to_le_bytes()); buf[18..20].copy_from_slice(&0x3eu16.to_le_bytes()); buf[20..24].copy_from_slice(&1u32.to_le_bytes());
buf[24..32].copy_from_slice(&0u64.to_le_bytes()); buf[32..40].copy_from_slice(&64u64.to_le_bytes()); buf[40..48].copy_from_slice(&0u64.to_le_bytes());
buf[48..52].copy_from_slice(&0u32.to_le_bytes()); buf[52..54].copy_from_slice(&64u16.to_le_bytes()); buf[54..56].copy_from_slice(&56u16.to_le_bytes()); buf[56..58].copy_from_slice(&1u16.to_le_bytes()); buf[58..60].copy_from_slice(&64u16.to_le_bytes()); buf[60..62].copy_from_slice(&0u16.to_le_bytes()); buf[62..64].copy_from_slice(&0u16.to_le_bytes());
buf[64..68].copy_from_slice(&1u32.to_le_bytes()); buf[68..72].copy_from_slice(&5u32.to_le_bytes()); buf[72..80].copy_from_slice(&0u64.to_le_bytes()); buf[80..88].copy_from_slice(&0u64.to_le_bytes()); buf[88..96].copy_from_slice(&0u64.to_le_bytes()); buf[96..104].copy_from_slice(&120u64.to_le_bytes()); buf[104..112].copy_from_slice(&120u64.to_le_bytes()); buf[112..120].copy_from_slice(&4096u64.to_le_bytes());
buf[4096..4100].copy_from_slice(sfs_magic);
buf
}
#[test]
fn detect_format_elf64_le_squashfs_little_endian() {
let bytes = make_elf64_le_with_squashfs(b"hsqs");
let f = write_tmp(&bytes);
match detect_format(f.path()).unwrap() {
ArchiveFormat::ElfSquashfs(offset) => assert_eq!(offset, 4096),
other => panic!("expected ElfSquashfs, got {other:?}"),
}
}
#[test]
fn detect_format_elf64_le_squashfs_big_endian() {
let bytes = make_elf64_le_with_squashfs(b"sqsh");
let f = write_tmp(&bytes);
match detect_format(f.path()).unwrap() {
ArchiveFormat::ElfSquashfs(offset) => assert_eq!(offset, 4096),
other => panic!("expected ElfSquashfs, got {other:?}"),
}
}
fn make_elf64_le_with_trailing_sht(sfs_magic: &[u8; 4]) -> Vec<u8> {
let mut buf = vec![0u8; 8192 + 8];
buf[0..4].copy_from_slice(b"\x7fELF");
buf[4] = 2; buf[5] = 1; buf[6] = 1;
buf[16..18].copy_from_slice(&2u16.to_le_bytes()); buf[18..20].copy_from_slice(&0x3eu16.to_le_bytes()); buf[20..24].copy_from_slice(&1u32.to_le_bytes());
buf[24..32].copy_from_slice(&0u64.to_le_bytes()); buf[32..40].copy_from_slice(&64u64.to_le_bytes()); buf[40..48].copy_from_slice(&4097u64.to_le_bytes());
buf[48..52].copy_from_slice(&0u32.to_le_bytes()); buf[52..54].copy_from_slice(&64u16.to_le_bytes()); buf[54..56].copy_from_slice(&56u16.to_le_bytes()); buf[56..58].copy_from_slice(&1u16.to_le_bytes()); buf[58..60].copy_from_slice(&64u16.to_le_bytes()); buf[60..62].copy_from_slice(&2u16.to_le_bytes()); buf[62..64].copy_from_slice(&0u16.to_le_bytes());
buf[64..68].copy_from_slice(&1u32.to_le_bytes()); buf[68..72].copy_from_slice(&5u32.to_le_bytes()); buf[72..80].copy_from_slice(&0u64.to_le_bytes()); buf[80..88].copy_from_slice(&0u64.to_le_bytes()); buf[88..96].copy_from_slice(&0u64.to_le_bytes()); buf[96..104].copy_from_slice(&120u64.to_le_bytes()); buf[104..112].copy_from_slice(&120u64.to_le_bytes()); buf[112..120].copy_from_slice(&4096u64.to_le_bytes());
buf[8192..8196].copy_from_slice(sfs_magic);
buf
}
#[test]
fn detect_format_elf_offset_follows_section_header_table() {
let bytes = make_elf64_le_with_trailing_sht(b"hsqs");
let f = write_tmp(&bytes);
match detect_format(f.path()).unwrap() {
ArchiveFormat::ElfSquashfs(offset) => assert_eq!(offset, 8192),
other => panic!("expected ElfSquashfs, got {other:?}"),
}
}
#[test]
fn detect_format_elf_without_squashfs_is_error() {
let mut bytes = make_elf64_le_with_squashfs(b"hsqs");
bytes[4096..4100].copy_from_slice(b"\x00\x00\x00\x00");
let f = write_tmp(&bytes);
assert!(
detect_format(f.path()).is_err(),
"ELF without squashfs should be an error"
);
}
#[test]
fn align_up_already_aligned() {
assert_eq!(align_up(4096, 4096), 4096);
}
#[test]
fn align_up_one_past() {
assert_eq!(align_up(4097, 4096), 8192);
}
#[test]
fn align_up_zero() {
assert_eq!(align_up(0, 4096), 0);
}
}