use std::collections::{BTreeMap, HashMap};
use std::io::Read;
use std::sync::mpsc::{Receiver, SyncSender, TrySendError, sync_channel};
use std::sync::{Arc, Mutex};
use std::thread;
use crate::Result;
use crate::block::BlockDevice;
use crate::fs::DeviceKind;
use crate::fs::FileSource;
use crate::fs::squashfs::Compression;
use crate::fs::squashfs::inode::{
INODE_BASIC_BLOCK, INODE_BASIC_CHAR, INODE_BASIC_DIR, INODE_BASIC_FIFO, INODE_BASIC_FILE,
INODE_BASIC_SOCKET, INODE_BASIC_SYMLINK, INODE_EXT_BLOCK, INODE_EXT_CHAR, INODE_EXT_DIR,
INODE_EXT_FIFO, INODE_EXT_FILE, INODE_EXT_SOCKET, INODE_EXT_SYMLINK,
};
use crate::fs::squashfs::metablock::{compression_to_algo, encode_metablock};
use crate::fs::squashfs::xattr::Xattr;
pub const DEFAULT_BLOCK_SIZE: u32 = 131_072;
#[derive(Debug, Clone, Copy)]
pub struct EntryMeta {
pub mode: u16,
pub uid: u32,
pub gid: u32,
pub mtime: u32,
}
impl Default for EntryMeta {
fn default() -> Self {
Self {
mode: 0o644,
uid: 0,
gid: 0,
mtime: 0,
}
}
}
struct FileLayout {
blocks_start: u64,
block_size_words: Vec<u32>,
fragment_index: u32,
fragment_offset: u32,
file_size: u64,
}
#[allow(dead_code)]
enum BuiltKind {
Dir,
File,
Symlink(String),
Hardlink(String),
Device {
kind: DeviceKind,
major: u32,
minor: u32,
},
}
struct BuiltEntry {
kind: BuiltKind,
meta: EntryMeta,
xattrs: Vec<Xattr>,
}
pub struct WriteState {
pub block_size: u32,
pub compression: Compression,
dirs: BTreeMap<String, EntryDir>,
files: BTreeMap<String, BuiltEntry>,
compress_threads: Option<usize>,
data_pipe: Option<BlockPipeline>,
data_next_offset: u64,
frag_buf: Vec<u8>,
fragment_entries: Vec<(u64, u32)>,
frag_block_count: u32,
file_layouts: BTreeMap<String, FileLayout>,
data_scratch: Vec<u8>,
data_started: bool,
}
struct EntryDir {
meta: EntryMeta,
xattrs: Vec<Xattr>,
}
impl WriteState {
pub fn new(block_size: u32, compression: Compression) -> Self {
let mut dirs = BTreeMap::new();
dirs.insert(
"/".to_string(),
EntryDir {
meta: EntryMeta {
mode: 0o755,
..Default::default()
},
xattrs: Vec::new(),
},
);
Self {
block_size,
compression,
dirs,
files: BTreeMap::new(),
compress_threads: None,
data_pipe: None,
data_next_offset: 0,
frag_buf: Vec::new(),
fragment_entries: Vec::new(),
frag_block_count: 0,
file_layouts: BTreeMap::new(),
data_scratch: Vec::new(),
data_started: false,
}
}
pub fn create_dir(&mut self, path: &str, meta: EntryMeta, xattrs: Vec<Xattr>) -> Result<()> {
let p = normalise_path(path)?;
if p == "/" {
if let Some(d) = self.dirs.get_mut("/") {
d.meta = meta;
d.xattrs = xattrs;
}
return Ok(());
}
let parent = parent_path(&p);
self.ensure_parent(parent)?;
self.dirs.insert(p, EntryDir { meta, xattrs });
Ok(())
}
pub fn create_file(
&mut self,
dev: &mut dyn BlockDevice,
path: &str,
src: FileSource,
meta: EntryMeta,
xattrs: Vec<Xattr>,
) -> Result<()> {
let p = normalise_path(path)?;
if p == "/" {
return Err(crate::Error::InvalidArgument(
"squashfs: cannot create file at /".into(),
));
}
let parent = parent_path(&p);
self.ensure_parent(parent)?;
self.ensure_data_phase(dev)?;
let (mut r, total) = src
.open()
.map_err(|e| crate::Error::Io(std::io::Error::other(e)))?;
self.stream_file(dev, &p, &mut r, total)?;
self.files.insert(
p,
BuiltEntry {
kind: BuiltKind::File,
meta,
xattrs,
},
);
Ok(())
}
pub fn create_file_streaming(
&mut self,
dev: &mut dyn BlockDevice,
path: &str,
body: &mut dyn Read,
len: u64,
meta: EntryMeta,
xattrs: Vec<Xattr>,
) -> Result<()> {
let p = normalise_path(path)?;
if p == "/" {
return Err(crate::Error::InvalidArgument(
"squashfs: cannot create file at /".into(),
));
}
let parent = parent_path(&p);
self.ensure_parent(parent)?;
self.ensure_data_phase(dev)?;
self.stream_file(dev, &p, body, len)?;
self.files.insert(
p,
BuiltEntry {
kind: BuiltKind::File,
meta,
xattrs,
},
);
Ok(())
}
fn ensure_data_phase(&mut self, dev: &mut dyn BlockDevice) -> Result<()> {
if self.data_started {
return Ok(());
}
let mut next_disk_offset: u64 = 96; ensure_size(dev, 96)?;
dev.write_at(0, &[0u8; 96])?;
if matches!(self.compression, Compression::Lz4) {
let header = ((8u16) | 0x8000).to_le_bytes();
let body = [
1u32.to_le_bytes(),
0u32.to_le_bytes(), ]
.concat();
ensure_size(dev, 96 + 2 + body.len() as u64)?;
dev.write_at(96, &header)?;
dev.write_at(98, &body)?;
next_disk_offset = 96 + 2 + body.len() as u64;
}
let threads = self.compress_threads.unwrap_or_else(compress_threads);
self.data_pipe = Some(BlockPipeline::new(
self.compression,
next_disk_offset,
threads,
));
self.data_next_offset = next_disk_offset;
if self.data_scratch.is_empty() {
self.data_scratch = vec![0u8; 65_536];
}
self.data_started = true;
Ok(())
}
fn submit_frag(&mut self, dev: &mut dyn BlockDevice) -> Result<()> {
if self.frag_buf.is_empty() {
return Ok(());
}
let buf = std::mem::take(&mut self.frag_buf);
let entry_idx = self.frag_block_count;
self.fragment_entries.push((0, 0)); self.frag_block_count += 1;
let pipe = self.data_pipe.as_mut().expect("data phase started");
pipe.submit(dev, buf, EmitTarget::Fragment { entry_idx })
}
fn stream_file(
&mut self,
dev: &mut dyn BlockDevice,
path: &str,
reader: &mut dyn Read,
total: u64,
) -> Result<()> {
let block_size = self.block_size;
let mut layout = FileLayout {
blocks_start: 0,
block_size_words: Vec::new(),
fragment_index: 0xFFFF_FFFF,
fragment_offset: 0,
file_size: total,
};
if total == 0 {
self.file_layouts.insert(path.to_string(), layout);
return Ok(());
}
if total < block_size as u64 {
if self.frag_buf.len() as u64 + total > block_size as u64 {
self.submit_frag(dev)?;
}
let off = self.frag_buf.len();
let mut scratch = std::mem::take(&mut self.data_scratch);
let res = copy_to_buf(reader, &mut scratch, total, &mut self.frag_buf);
self.data_scratch = scratch;
res?;
layout.fragment_index = self.frag_block_count;
layout.fragment_offset = off as u32;
self.file_layouts.insert(path.to_string(), layout);
return Ok(());
}
let mut consumed: u64 = 0;
while total - consumed >= block_size as u64 {
let mut block_buf = vec![0u8; block_size as usize];
read_exact(reader, &mut block_buf)?;
layout.block_size_words.push(0); let block_idx = layout.block_size_words.len() - 1;
let pipe = self.data_pipe.as_mut().expect("data phase started");
pipe.submit(
dev,
block_buf,
EmitTarget::Data {
file_key: path.to_string(),
block_idx,
first: block_idx == 0,
},
)?;
consumed += block_size as u64;
}
let tail = (total - consumed) as usize;
if tail > 0 {
if self.frag_buf.len() + tail > block_size as usize {
self.submit_frag(dev)?;
}
let off = self.frag_buf.len();
let mut scratch = std::mem::take(&mut self.data_scratch);
let res = copy_to_buf(reader, &mut scratch, tail as u64, &mut self.frag_buf);
self.data_scratch = scratch;
res?;
layout.fragment_index = self.frag_block_count;
layout.fragment_offset = off as u32;
}
self.file_layouts.insert(path.to_string(), layout);
Ok(())
}
pub fn create_symlink(
&mut self,
path: &str,
target: &str,
meta: EntryMeta,
xattrs: Vec<Xattr>,
) -> Result<()> {
let p = normalise_path(path)?;
if p == "/" {
return Err(crate::Error::InvalidArgument(
"squashfs: cannot create symlink at /".into(),
));
}
let parent = parent_path(&p);
self.ensure_parent(parent)?;
self.files.insert(
p,
BuiltEntry {
kind: BuiltKind::Symlink(target.to_string()),
meta,
xattrs,
},
);
Ok(())
}
pub fn create_hardlink(&mut self, src_path: &str, dst_path: &str) -> Result<()> {
let src = normalise_path(src_path)?;
let dst = normalise_path(dst_path)?;
if dst == "/" {
return Err(crate::Error::InvalidArgument(
"squashfs: cannot create hardlink at /".into(),
));
}
if dst == src {
return Err(crate::Error::InvalidArgument(
"squashfs: hardlink source and destination are identical".into(),
));
}
let (real_src, src_meta) = {
let Some(entry) = self.files.get(&src) else {
if self.dirs.contains_key(&src) {
return Err(crate::Error::InvalidArgument(
"squashfs: hardlinks to directories are not allowed".into(),
));
}
return Err(crate::Error::InvalidArgument(format!(
"squashfs: hardlink source {src:?} does not exist"
)));
};
let real_src = match &entry.kind {
BuiltKind::Hardlink(s) => s.clone(),
_ => src.clone(),
};
(real_src, entry.meta)
};
let parent = parent_path(&dst);
self.ensure_parent(parent)?;
self.files.insert(
dst,
BuiltEntry {
kind: BuiltKind::Hardlink(real_src),
meta: src_meta,
xattrs: Vec::new(),
},
);
Ok(())
}
pub fn create_device(
&mut self,
path: &str,
kind: DeviceKind,
major: u32,
minor: u32,
meta: EntryMeta,
xattrs: Vec<Xattr>,
) -> Result<()> {
let p = normalise_path(path)?;
if p == "/" {
return Err(crate::Error::InvalidArgument(
"squashfs: cannot create device at /".into(),
));
}
let parent = parent_path(&p);
self.ensure_parent(parent)?;
self.files.insert(
p,
BuiltEntry {
kind: BuiltKind::Device { kind, major, minor },
meta,
xattrs,
},
);
Ok(())
}
fn ensure_parent(&mut self, parent: String) -> Result<()> {
if parent == "/" {
return Ok(());
}
if !self.dirs.contains_key(&parent) {
let pp = parent_path(&parent);
self.ensure_parent(pp)?;
self.dirs.insert(
parent,
EntryDir {
meta: EntryMeta {
mode: 0o755,
..Default::default()
},
xattrs: Vec::new(),
},
);
}
Ok(())
}
pub fn flush(&mut self, dev: &mut dyn BlockDevice) -> Result<super::Superblock> {
let mut inode_numbers: BTreeMap<String, u32> = BTreeMap::new();
let mut hardlink_count: BTreeMap<String, u32> = BTreeMap::new();
for entry in self.files.values() {
if let BuiltKind::Hardlink(src) = &entry.kind {
*hardlink_count.entry(src.clone()).or_insert(0) += 1;
}
}
let mut next_inode: u32 = 1;
inode_numbers.insert("/".into(), next_inode);
next_inode += 1;
let mut all_paths: Vec<&String> = Vec::new();
for p in self.dirs.keys() {
if p != "/" {
all_paths.push(p);
}
}
for (p, e) in &self.files {
if !matches!(e.kind, BuiltKind::Hardlink(_)) {
all_paths.push(p);
}
}
all_paths.sort();
for p in &all_paths {
inode_numbers.insert((*p).clone(), next_inode);
next_inode += 1;
}
for (p, e) in &self.files {
if let BuiltKind::Hardlink(src) = &e.kind {
let src_inode = *inode_numbers.get(src).ok_or_else(|| {
crate::Error::InvalidArgument(format!(
"squashfs: hardlink source {src:?} not found at flush time"
))
})?;
inode_numbers.insert(p.clone(), src_inode);
}
}
let total_inodes = next_inode - 1;
let mut id_table: Vec<u32> = Vec::new();
let mut id_index: BTreeMap<u32, u16> = BTreeMap::new();
let mut intern_id = |v: u32| -> Result<u16> {
if let Some(&i) = id_index.get(&v) {
return Ok(i);
}
if id_table.len() >= u16::MAX as usize {
return Err(crate::Error::InvalidArgument(
"squashfs: id table overflow (>65535 distinct uids/gids)".into(),
));
}
let i = id_table.len() as u16;
id_table.push(v);
id_index.insert(v, i);
Ok(i)
};
let mut xattr_sets: Vec<Vec<Xattr>> = Vec::new();
let mut xattr_set_index: BTreeMap<Vec<(String, Vec<u8>)>, u32> = BTreeMap::new();
let intern_xattr = |xs: &Vec<Xattr>,
xattr_sets: &mut Vec<Vec<Xattr>>,
xattr_set_index: &mut BTreeMap<Vec<(String, Vec<u8>)>, u32>|
-> u32 {
if xs.is_empty() {
return u32::MAX;
}
let key: Vec<(String, Vec<u8>)> = xs
.iter()
.map(|x| (x.key.clone(), x.value.clone()))
.collect();
if let Some(&i) = xattr_set_index.get(&key) {
return i;
}
let i = xattr_sets.len() as u32;
xattr_sets.push(xs.clone());
xattr_set_index.insert(key, i);
i
};
let block_size = self.block_size;
self.ensure_data_phase(dev)?;
self.submit_frag(dev)?;
let mut data_next_offset = self.data_next_offset;
let (targets, results) = self
.data_pipe
.take()
.expect("data phase started")
.finish(dev, &mut data_next_offset)?;
self.data_next_offset = data_next_offset;
for (seq, target) in targets.iter().enumerate() {
let (off, size_word) = results[seq];
match target {
EmitTarget::Data {
file_key,
block_idx,
first,
} => {
let layout = self
.file_layouts
.get_mut(file_key)
.expect("data block targets an existing file layout");
layout.block_size_words[*block_idx] = size_word;
if *first {
layout.blocks_start = off;
}
}
EmitTarget::Fragment { entry_idx } => {
self.fragment_entries[*entry_idx as usize] = (off, size_word);
}
}
}
let mut next_disk_offset = self.data_next_offset;
let file_layouts = std::mem::take(&mut self.file_layouts);
let fragment_entries = std::mem::take(&mut self.fragment_entries);
let file_keys: Vec<String> = self.files.keys().cloned().collect();
let mut inode_table_raw: Vec<u8> = Vec::new();
let mut inode_positions: BTreeMap<String, (u32, u16)> = BTreeMap::new();
let emit_inode = |raw: &mut Vec<u8>, bytes: &[u8]| -> usize {
let off = raw.len();
raw.extend_from_slice(bytes);
off
};
let mut nondir_raw_offsets: BTreeMap<String, usize> = BTreeMap::new();
for path in &file_keys {
let entry = self.files.get(path).unwrap();
if matches!(entry.kind, BuiltKind::Hardlink(_)) {
continue;
}
let uid_idx = intern_id(entry.meta.uid)?;
let gid_idx = intern_id(entry.meta.gid)?;
let inode_no = inode_numbers[path];
let link_count: u32 = 1 + hardlink_count.get(path).copied().unwrap_or(0);
let xattr_idx = intern_xattr(&entry.xattrs, &mut xattr_sets, &mut xattr_set_index);
let force_ext = link_count > 1;
match &entry.kind {
BuiltKind::File => {
let layout = &file_layouts[path];
if layout.blocks_start > u32::MAX as u64 {
return Err(crate::Error::InvalidImage(
"squashfs: blocks_start > 4GiB not supported by writer".into(),
));
}
if layout.file_size > u32::MAX as u64 {
return Err(crate::Error::InvalidImage(
"squashfs: file > 4GiB not supported by writer".into(),
));
}
let mut bytes = Vec::with_capacity(64 + layout.block_size_words.len() * 4);
if xattr_idx == u32::MAX && !force_ext {
bytes.extend_from_slice(&INODE_BASIC_FILE.to_le_bytes());
bytes.extend_from_slice(&entry.meta.mode.to_le_bytes());
bytes.extend_from_slice(&uid_idx.to_le_bytes());
bytes.extend_from_slice(&gid_idx.to_le_bytes());
bytes.extend_from_slice(&entry.meta.mtime.to_le_bytes());
bytes.extend_from_slice(&inode_no.to_le_bytes());
bytes.extend_from_slice(&(layout.blocks_start as u32).to_le_bytes());
bytes.extend_from_slice(&layout.fragment_index.to_le_bytes());
bytes.extend_from_slice(&layout.fragment_offset.to_le_bytes());
bytes.extend_from_slice(&(layout.file_size as u32).to_le_bytes());
} else {
bytes.extend_from_slice(&INODE_EXT_FILE.to_le_bytes());
bytes.extend_from_slice(&entry.meta.mode.to_le_bytes());
bytes.extend_from_slice(&uid_idx.to_le_bytes());
bytes.extend_from_slice(&gid_idx.to_le_bytes());
bytes.extend_from_slice(&entry.meta.mtime.to_le_bytes());
bytes.extend_from_slice(&inode_no.to_le_bytes());
bytes.extend_from_slice(&layout.blocks_start.to_le_bytes());
bytes.extend_from_slice(&layout.file_size.to_le_bytes());
bytes.extend_from_slice(&0u64.to_le_bytes()); bytes.extend_from_slice(&link_count.to_le_bytes());
bytes.extend_from_slice(&layout.fragment_index.to_le_bytes());
bytes.extend_from_slice(&layout.fragment_offset.to_le_bytes());
bytes.extend_from_slice(&xattr_idx.to_le_bytes());
}
for sw in &layout.block_size_words {
bytes.extend_from_slice(&sw.to_le_bytes());
}
let off = emit_inode(&mut inode_table_raw, &bytes);
nondir_raw_offsets.insert(path.clone(), off);
}
BuiltKind::Symlink(target) => {
let mut bytes = Vec::new();
if xattr_idx == u32::MAX {
bytes.extend_from_slice(&INODE_BASIC_SYMLINK.to_le_bytes());
} else {
bytes.extend_from_slice(&INODE_EXT_SYMLINK.to_le_bytes());
}
bytes.extend_from_slice(&entry.meta.mode.to_le_bytes());
bytes.extend_from_slice(&uid_idx.to_le_bytes());
bytes.extend_from_slice(&gid_idx.to_le_bytes());
bytes.extend_from_slice(&entry.meta.mtime.to_le_bytes());
bytes.extend_from_slice(&inode_no.to_le_bytes());
bytes.extend_from_slice(&link_count.to_le_bytes());
bytes.extend_from_slice(&(target.len() as u32).to_le_bytes());
bytes.extend_from_slice(target.as_bytes());
if xattr_idx != u32::MAX {
bytes.extend_from_slice(&xattr_idx.to_le_bytes());
}
let off = emit_inode(&mut inode_table_raw, &bytes);
nondir_raw_offsets.insert(path.clone(), off);
}
BuiltKind::Device { kind, major, minor } => {
let dev_word: u32 = ((*major & 0xFFF) << 20) | (*minor & 0xF_FFFF);
let (basic_id, ext_id) = match kind {
DeviceKind::Block => (INODE_BASIC_BLOCK, INODE_EXT_BLOCK),
DeviceKind::Char => (INODE_BASIC_CHAR, INODE_EXT_CHAR),
DeviceKind::Fifo => (INODE_BASIC_FIFO, INODE_EXT_FIFO),
DeviceKind::Socket => (INODE_BASIC_SOCKET, INODE_EXT_SOCKET),
};
let use_ext = xattr_idx != u32::MAX || force_ext;
let mut bytes = Vec::new();
if use_ext {
bytes.extend_from_slice(&ext_id.to_le_bytes());
} else {
bytes.extend_from_slice(&basic_id.to_le_bytes());
}
bytes.extend_from_slice(&entry.meta.mode.to_le_bytes());
bytes.extend_from_slice(&uid_idx.to_le_bytes());
bytes.extend_from_slice(&gid_idx.to_le_bytes());
bytes.extend_from_slice(&entry.meta.mtime.to_le_bytes());
bytes.extend_from_slice(&inode_no.to_le_bytes());
bytes.extend_from_slice(&link_count.to_le_bytes());
match kind {
DeviceKind::Block | DeviceKind::Char => {
bytes.extend_from_slice(&dev_word.to_le_bytes());
}
DeviceKind::Fifo | DeviceKind::Socket => {}
}
if use_ext {
bytes.extend_from_slice(&xattr_idx.to_le_bytes());
}
let off = emit_inode(&mut inode_table_raw, &bytes);
nondir_raw_offsets.insert(path.clone(), off);
}
BuiltKind::Hardlink(_) => unreachable!(),
BuiltKind::Dir => unreachable!(),
}
}
let mut listings: BTreeMap<String, Vec<(String, String, u16)>> = BTreeMap::new();
for d in self.dirs.keys() {
listings.insert(d.clone(), Vec::new());
}
for p in &file_keys {
let parent = parent_path(p);
let name = leaf_name(p).to_string();
let (kind, target_path) = match &self.files[p].kind {
BuiltKind::File => (INODE_BASIC_FILE, p.clone()),
BuiltKind::Symlink(_) => (INODE_BASIC_SYMLINK, p.clone()),
BuiltKind::Dir => (INODE_BASIC_DIR, p.clone()),
BuiltKind::Hardlink(src) => {
let k = match &self.files[src].kind {
BuiltKind::File => INODE_BASIC_FILE,
BuiltKind::Symlink(_) => INODE_BASIC_SYMLINK,
BuiltKind::Device { kind, .. } => match kind {
DeviceKind::Block => INODE_BASIC_BLOCK,
DeviceKind::Char => INODE_BASIC_CHAR,
DeviceKind::Fifo => INODE_BASIC_FIFO,
DeviceKind::Socket => INODE_BASIC_SOCKET,
},
BuiltKind::Dir | BuiltKind::Hardlink(_) => INODE_BASIC_FILE,
};
(k, src.clone())
}
BuiltKind::Device { kind, .. } => {
let id = match kind {
DeviceKind::Block => INODE_BASIC_BLOCK,
DeviceKind::Char => INODE_BASIC_CHAR,
DeviceKind::Fifo => INODE_BASIC_FIFO,
DeviceKind::Socket => INODE_BASIC_SOCKET,
};
(id, p.clone())
}
};
listings
.get_mut(&parent)
.unwrap()
.push((name, target_path, kind));
}
for d in self.dirs.keys() {
if d == "/" {
continue;
}
let parent = parent_path(d);
let name = leaf_name(d).to_string();
listings
.get_mut(&parent)
.unwrap()
.push((name, d.clone(), INODE_BASIC_DIR));
}
for v in listings.values_mut() {
v.sort_by(|a, b| a.0.cmp(&b.0));
}
let mut dir_raw_offsets: BTreeMap<String, usize> = BTreeMap::new();
let mut dir_table_raw: Vec<u8> = Vec::new();
let mut dir_listing_offsets: BTreeMap<String, (usize, usize)> = BTreeMap::new();
let mut dir_is_ext: BTreeMap<String, bool> = BTreeMap::new();
for d in self.dirs.keys() {
let mut upper: usize = 0;
if let Some(entries) = listings.get(d) {
for (name, _, _) in entries {
upper += 12 + 8 + name.len();
}
}
let needs_ext_for_size = upper > 65_532;
let needs_ext_for_xattr = !self.dirs[d].xattrs.is_empty();
dir_is_ext.insert(d.clone(), needs_ext_for_size || needs_ext_for_xattr);
}
for d in self.dirs.keys() {
let off = inode_table_raw.len();
let sz = if dir_is_ext[d] { 40 } else { 32 };
inode_table_raw.extend_from_slice(&vec![0u8; sz]);
dir_raw_offsets.insert(d.clone(), off);
}
let (inode_block_rel_map, _) = chunk_raw_to_metablocks(&inode_table_raw, self.compression)?;
let raw_to_pos = |raw_off: usize| -> (u32, u16) {
let entry = inode_block_rel_map[raw_off / 8192];
(entry, (raw_off % 8192) as u16)
};
for path in nondir_raw_offsets.keys() {
let (b, o) = raw_to_pos(nondir_raw_offsets[path]);
inode_positions.insert(path.clone(), (b, o));
}
for path in dir_raw_offsets.keys() {
let (b, o) = raw_to_pos(dir_raw_offsets[path]);
inode_positions.insert(path.clone(), (b, o));
}
for d in self.dirs.keys() {
let entries = &listings[d];
let raw_start = dir_table_raw.len();
if entries.is_empty() {
dir_listing_offsets.insert(d.clone(), (raw_start, 0));
continue;
}
let mut idx = 0;
while idx < entries.len() {
let (_name0, child0_path, _kind0) = &entries[idx];
let (start_block, _) = inode_positions[child0_path];
let base_inode = inode_numbers[child0_path];
let mut run_entries: Vec<&(String, String, u16)> = Vec::new();
while idx < entries.len() {
let (_n, cp, _k) = &entries[idx];
let (cb, _co) = inode_positions[cp];
if cb != start_block {
break;
}
let ci = inode_numbers[cp];
let diff = ci as i64 - base_inode as i64;
if !(-32768..=32767).contains(&diff) {
break;
}
if run_entries.len() >= 256 {
break;
}
run_entries.push(&entries[idx]);
idx += 1;
}
let count_minus_one = (run_entries.len() - 1) as u32;
dir_table_raw.extend_from_slice(&count_minus_one.to_le_bytes());
dir_table_raw.extend_from_slice(&start_block.to_le_bytes());
dir_table_raw.extend_from_slice(&base_inode.to_le_bytes());
for (name, child_path, kind) in run_entries {
let (_b, in_off) = inode_positions[child_path];
let ci = inode_numbers[child_path];
let diff = ci as i64 - base_inode as i64;
let signed = diff as i16;
dir_table_raw.extend_from_slice(&in_off.to_le_bytes());
dir_table_raw.extend_from_slice(&signed.to_le_bytes());
dir_table_raw.extend_from_slice(&kind.to_le_bytes());
let name_bytes = name.as_bytes();
let name_size = (name_bytes.len() - 1) as u16;
dir_table_raw.extend_from_slice(&name_size.to_le_bytes());
dir_table_raw.extend_from_slice(name_bytes);
}
}
let raw_size = dir_table_raw.len() - raw_start;
dir_listing_offsets.insert(d.clone(), (raw_start, raw_size));
}
let (dir_block_rel_map, dir_disk_payload) =
chunk_raw_to_metablocks(&dir_table_raw, self.compression)?;
let dir_raw_to_pos = |raw_off: usize| -> (u32, u16) {
let entry = dir_block_rel_map[raw_off / 8192];
(entry, (raw_off % 8192) as u16)
};
for d in self.dirs.keys() {
let (raw_off, listing_size) = dir_listing_offsets[d];
let (block_index, block_offset) = dir_raw_to_pos(raw_off);
let parent_inode = if d == "/" {
inode_numbers["/"]
} else {
let p = parent_path(d);
inode_numbers[&p]
};
let inode_no = inode_numbers[d];
let dir_meta = &self.dirs[d];
let uid_idx = intern_id(dir_meta.meta.uid)?;
let gid_idx = intern_id(dir_meta.meta.gid)?;
let off = dir_raw_offsets[d];
let link_count = count_subdirs(&listings, d) as u32 + 2;
let xattr_idx = intern_xattr(&dir_meta.xattrs, &mut xattr_sets, &mut xattr_set_index);
if dir_is_ext[d] {
let mut buf = [0u8; 40];
buf[0..2].copy_from_slice(&INODE_EXT_DIR.to_le_bytes());
buf[2..4].copy_from_slice(&dir_meta.meta.mode.to_le_bytes());
buf[4..6].copy_from_slice(&uid_idx.to_le_bytes());
buf[6..8].copy_from_slice(&gid_idx.to_le_bytes());
buf[8..12].copy_from_slice(&dir_meta.meta.mtime.to_le_bytes());
buf[12..16].copy_from_slice(&inode_no.to_le_bytes());
buf[16..20].copy_from_slice(&link_count.to_le_bytes());
let stored: u32 = (listing_size as u32).saturating_add(3);
buf[20..24].copy_from_slice(&stored.to_le_bytes());
buf[24..28].copy_from_slice(&block_index.to_le_bytes());
buf[28..32].copy_from_slice(&parent_inode.to_le_bytes());
buf[32..34].copy_from_slice(&0u16.to_le_bytes()); buf[34..36].copy_from_slice(&block_offset.to_le_bytes());
buf[36..40].copy_from_slice(&xattr_idx.to_le_bytes());
inode_table_raw[off..off + 40].copy_from_slice(&buf);
} else {
let mut buf = [0u8; 32];
buf[0..2].copy_from_slice(&INODE_BASIC_DIR.to_le_bytes());
buf[2..4].copy_from_slice(&dir_meta.meta.mode.to_le_bytes());
buf[4..6].copy_from_slice(&uid_idx.to_le_bytes());
buf[6..8].copy_from_slice(&gid_idx.to_le_bytes());
buf[8..12].copy_from_slice(&dir_meta.meta.mtime.to_le_bytes());
buf[12..16].copy_from_slice(&inode_no.to_le_bytes());
buf[16..20].copy_from_slice(&block_index.to_le_bytes());
buf[20..24].copy_from_slice(&link_count.to_le_bytes());
let stored = if listing_size == 0 {
3u16
} else {
(listing_size as u16).saturating_add(3)
};
buf[24..26].copy_from_slice(&stored.to_le_bytes());
buf[26..28].copy_from_slice(&block_offset.to_le_bytes());
buf[28..32].copy_from_slice(&parent_inode.to_le_bytes());
inode_table_raw[off..off + 32].copy_from_slice(&buf);
}
}
let (_, inode_disk_payload) = chunk_raw_to_metablocks(&inode_table_raw, self.compression)?;
let inode_table_start = next_disk_offset;
ensure_size(dev, next_disk_offset + inode_disk_payload.len() as u64)?;
dev.write_at(inode_table_start, &inode_disk_payload)?;
next_disk_offset += inode_disk_payload.len() as u64;
let directory_table_start = next_disk_offset;
ensure_size(dev, next_disk_offset + dir_disk_payload.len() as u64)?;
dev.write_at(directory_table_start, &dir_disk_payload)?;
next_disk_offset += dir_disk_payload.len() as u64;
let fragment_count = fragment_entries.len() as u32;
let fragment_table_start = if fragment_count == 0 {
u64::MAX
} else {
let mut frag_raw = Vec::with_capacity(fragment_entries.len() * 16);
for (start, size_word) in &fragment_entries {
frag_raw.extend_from_slice(&start.to_le_bytes());
frag_raw.extend_from_slice(&size_word.to_le_bytes());
frag_raw.extend_from_slice(&0u32.to_le_bytes()); }
let mb = encode_metablock(&frag_raw, self.compression)?;
let mb_disk_offset = next_disk_offset;
ensure_size(dev, next_disk_offset + mb.len() as u64)?;
dev.write_at(mb_disk_offset, &mb)?;
next_disk_offset += mb.len() as u64;
let l1_offset = next_disk_offset;
ensure_size(dev, next_disk_offset + 8)?;
dev.write_at(l1_offset, &mb_disk_offset.to_le_bytes())?;
next_disk_offset += 8;
l1_offset
};
let export_table_start = if total_inodes == 0 {
u64::MAX
} else {
let mut inv: BTreeMap<u32, String> = BTreeMap::new();
for (p, &i) in &inode_numbers {
if let Some(e) = self.files.get(p)
&& matches!(e.kind, BuiltKind::Hardlink(_))
{
continue;
}
inv.insert(i, p.clone());
}
let mut raw: Vec<u8> = Vec::with_capacity(total_inodes as usize * 8);
for i in 1..=total_inodes {
let p = inv.get(&i).ok_or_else(|| {
crate::Error::InvalidImage("squashfs: gap in inode numbers".into())
})?;
let (block_rel, in_off) = inode_positions[p];
let iref = ((block_rel as u64) << 16) | (in_off as u64);
raw.extend_from_slice(&iref.to_le_bytes());
}
let mut mb_offsets_abs: Vec<u64> = Vec::new();
let mut pos = 0usize;
while pos < raw.len() {
let end = (pos + 8192).min(raw.len());
let mb = encode_metablock(&raw[pos..end], self.compression)?;
let mb_off = next_disk_offset;
ensure_size(dev, next_disk_offset + mb.len() as u64)?;
dev.write_at(mb_off, &mb)?;
next_disk_offset += mb.len() as u64;
mb_offsets_abs.push(mb_off);
pos = end;
}
let l1_offset = next_disk_offset;
let mut l1 = Vec::with_capacity(mb_offsets_abs.len() * 8);
for o in &mb_offsets_abs {
l1.extend_from_slice(&o.to_le_bytes());
}
ensure_size(dev, next_disk_offset + l1.len() as u64)?;
dev.write_at(l1_offset, &l1)?;
next_disk_offset += l1.len() as u64;
l1_offset
};
let id_count = id_table.len() as u16;
let id_table_start = if id_count == 0 {
u64::MAX
} else {
let mut raw: Vec<u8> = Vec::with_capacity(id_table.len() * 4);
for v in &id_table {
raw.extend_from_slice(&v.to_le_bytes());
}
let mut mb_offsets_abs: Vec<u64> = Vec::new();
let mut pos = 0usize;
while pos < raw.len() {
let end = (pos + 8192).min(raw.len());
let mb = encode_metablock(&raw[pos..end], self.compression)?;
let mb_off = next_disk_offset;
ensure_size(dev, next_disk_offset + mb.len() as u64)?;
dev.write_at(mb_off, &mb)?;
next_disk_offset += mb.len() as u64;
mb_offsets_abs.push(mb_off);
pos = end;
}
let l1_offset = next_disk_offset;
let mut l1 = Vec::with_capacity(mb_offsets_abs.len() * 8);
for o in &mb_offsets_abs {
l1.extend_from_slice(&o.to_le_bytes());
}
ensure_size(dev, next_disk_offset + l1.len() as u64)?;
dev.write_at(l1_offset, &l1)?;
next_disk_offset += l1.len() as u64;
l1_offset
};
let xattr_id_table_start = if xattr_sets.is_empty() {
u64::MAX
} else {
let base = next_disk_offset;
let (payload, hdr_off) =
super::xattr::encode_xattr_table(&xattr_sets, base, self.compression)?;
ensure_size(dev, base + payload.len() as u64)?;
dev.write_at(base, &payload)?;
next_disk_offset += payload.len() as u64;
base + hdr_off
};
let bytes_used = next_disk_offset;
let block_log = block_size.trailing_zeros() as u16;
let root_ref = {
let (b, o) = inode_positions["/"];
((b as u64) << 16) | (o as u64)
};
let comp_id = match self.compression {
Compression::Gzip => 1,
Compression::Lzma => 2,
Compression::Lzo => 3,
Compression::Xz => 4,
Compression::Lz4 => 5,
Compression::Zstd => 6,
Compression::Unknown(_) => 0,
};
let mut sb = vec![0u8; 96];
sb[0..4].copy_from_slice(&super::SQUASHFS_MAGIC.to_le_bytes());
sb[4..8].copy_from_slice(&total_inodes.to_le_bytes());
sb[8..12].copy_from_slice(&0u32.to_le_bytes()); sb[12..16].copy_from_slice(&block_size.to_le_bytes());
sb[16..20].copy_from_slice(&fragment_count.to_le_bytes());
sb[20..22].copy_from_slice(&(comp_id as u16).to_le_bytes());
sb[22..24].copy_from_slice(&block_log.to_le_bytes());
let mut flags: u16 = 0;
if export_table_start != u64::MAX {
flags |= 0x0080;
}
if xattr_id_table_start == u64::MAX {
flags |= 0x0200;
}
if matches!(self.compression, Compression::Lz4) {
flags |= 0x0400;
}
sb[24..26].copy_from_slice(&flags.to_le_bytes());
sb[26..28].copy_from_slice(&id_count.to_le_bytes());
sb[28..30].copy_from_slice(&4u16.to_le_bytes()); sb[30..32].copy_from_slice(&0u16.to_le_bytes()); sb[32..40].copy_from_slice(&root_ref.to_le_bytes());
sb[40..48].copy_from_slice(&bytes_used.to_le_bytes());
sb[48..56].copy_from_slice(&id_table_start.to_le_bytes());
sb[56..64].copy_from_slice(&xattr_id_table_start.to_le_bytes());
sb[64..72].copy_from_slice(&inode_table_start.to_le_bytes());
sb[72..80].copy_from_slice(&directory_table_start.to_le_bytes());
sb[80..88].copy_from_slice(&fragment_table_start.to_le_bytes());
sb[88..96].copy_from_slice(&export_table_start.to_le_bytes());
dev.write_at(0, &sb)?;
dev.sync()?;
super::Superblock::decode(&sb).ok_or_else(|| {
crate::Error::InvalidImage("squashfs: writer produced invalid superblock".into())
})
}
}
fn chunk_raw_to_metablocks(raw: &[u8], compression: Compression) -> Result<(Vec<u32>, Vec<u8>)> {
let mut rel_offsets: Vec<u32> = Vec::new();
let mut out: Vec<u8> = Vec::new();
let mut pos = 0usize;
if raw.is_empty() {
return Ok((rel_offsets, out));
}
while pos < raw.len() {
rel_offsets.push(out.len() as u32);
let end = (pos + 8192).min(raw.len());
let mb = encode_metablock(&raw[pos..end], compression)?;
out.extend_from_slice(&mb);
pos = end;
}
Ok((rel_offsets, out))
}
fn compress_block(compression: Compression, block: Vec<u8>) -> (Vec<u8>, u32) {
let algo = compression_to_algo(compression);
if let Some(a) = algo
&& a.enabled()
&& let Ok(c) = crate::compression::compress(a, &block)
&& !c.is_empty()
&& c.len() < block.len()
{
let sw = c.len() as u32;
(c, sw)
} else {
let sw = block.len() as u32 | 0x0100_0000;
(block, sw)
}
}
fn compress_threads() -> usize {
if let Ok(v) = std::env::var("FSTOOL_COMPRESS_THREADS")
&& let Ok(n) = v.trim().parse::<usize>()
{
return n.max(1);
}
std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1)
}
type BlockResult = (u64, u32);
enum EmitTarget {
Data {
file_key: String,
block_idx: usize,
first: bool,
},
Fragment { entry_idx: u32 },
}
struct BlockPipeline {
parallel: bool,
compression: Compression,
work_tx: Option<SyncSender<(u64, Vec<u8>)>>,
res_rx: Option<Receiver<(u64, Vec<u8>, u32)>>,
workers: Vec<thread::JoinHandle<()>>,
targets: Vec<EmitTarget>,
next_write_seq: u64,
reorder: HashMap<u64, (Vec<u8>, u32)>,
results: Vec<BlockResult>,
next_disk_offset: u64,
}
impl BlockPipeline {
fn new(compression: Compression, start_offset: u64, threads: usize) -> Self {
let n = threads.max(1);
let mut pipe = Self {
parallel: n > 1,
compression,
work_tx: None,
res_rx: None,
workers: Vec::new(),
targets: Vec::new(),
next_write_seq: 0,
reorder: HashMap::new(),
results: Vec::new(),
next_disk_offset: start_offset,
};
if !pipe.parallel {
return pipe;
}
let (work_tx, work_rx) = sync_channel::<(u64, Vec<u8>)>(n * 2);
let (res_tx, res_rx) = sync_channel::<(u64, Vec<u8>, u32)>(n * 2);
let work_rx = Arc::new(Mutex::new(work_rx));
for _ in 0..n {
let rx = Arc::clone(&work_rx);
let tx = res_tx.clone();
pipe.workers.push(thread::spawn(move || {
loop {
let job = {
let guard = rx.lock().unwrap();
guard.recv()
};
match job {
Ok((seq, block)) => {
let (payload, sw) = compress_block(compression, block);
if tx.send((seq, payload, sw)).is_err() {
break;
}
}
Err(_) => break,
}
}
}));
}
drop(res_tx);
pipe.work_tx = Some(work_tx);
pipe.res_rx = Some(res_rx);
pipe
}
fn write_block(&mut self, dev: &mut dyn BlockDevice, payload: Vec<u8>, sw: u32) -> Result<()> {
let off = self.next_disk_offset;
ensure_size(dev, off + payload.len() as u64)?;
dev.write_at(off, &payload)?;
self.results.push((off, sw));
self.next_disk_offset += payload.len() as u64;
Ok(())
}
fn flush_ready(&mut self, dev: &mut dyn BlockDevice) -> Result<()> {
while let Some((payload, sw)) = self.reorder.remove(&self.next_write_seq) {
self.write_block(dev, payload, sw)?;
self.next_write_seq += 1;
}
Ok(())
}
fn collect_ready(&mut self, dev: &mut dyn BlockDevice) -> Result<()> {
while let Ok((seq, payload, sw)) = self.res_rx.as_ref().unwrap().try_recv() {
self.reorder.insert(seq, (payload, sw));
}
self.flush_ready(dev)
}
fn drain_one(&mut self, dev: &mut dyn BlockDevice) -> Result<()> {
let (seq, payload, sw) = self.res_rx.as_ref().unwrap().recv().map_err(|_| {
crate::Error::Io(std::io::Error::other(
"squashfs compression workers disconnected",
))
})?;
self.reorder.insert(seq, (payload, sw));
self.flush_ready(dev)
}
fn submit(
&mut self,
dev: &mut dyn BlockDevice,
block: Vec<u8>,
target: EmitTarget,
) -> Result<()> {
let seq = self.targets.len() as u64;
self.targets.push(target);
if !self.parallel {
let (payload, sw) = compress_block(self.compression, block);
return self.write_block(dev, payload, sw);
}
self.collect_ready(dev)?;
let mut item = (seq, block);
loop {
match self.work_tx.as_ref().unwrap().try_send(item) {
Ok(()) => break,
Err(TrySendError::Full(back)) => {
item = back;
self.drain_one(dev)?;
}
Err(TrySendError::Disconnected(_)) => {
return Err(crate::Error::Io(std::io::Error::other(
"squashfs compression workers disconnected",
)));
}
}
}
Ok(())
}
fn finish(
mut self,
dev: &mut dyn BlockDevice,
next_disk_offset: &mut u64,
) -> Result<(Vec<EmitTarget>, Vec<BlockResult>)> {
if self.parallel {
self.work_tx.take();
let total = self.targets.len() as u64;
while self.next_write_seq < total {
self.drain_one(dev)?;
}
for h in self.workers.drain(..) {
let _ = h.join();
}
}
*next_disk_offset = self.next_disk_offset;
Ok((self.targets, self.results))
}
}
fn ensure_size(dev: &mut dyn BlockDevice, len: u64) -> Result<()> {
if dev.total_size() < len {
return Err(crate::Error::OutOfBounds {
offset: 0,
len,
size: dev.total_size(),
});
}
Ok(())
}
fn read_exact(r: &mut dyn Read, out: &mut [u8]) -> Result<()> {
let mut filled = 0;
while filled < out.len() {
let n = r.read(&mut out[filled..]).map_err(crate::Error::Io)?;
if n == 0 {
return Err(crate::Error::InvalidImage(
"squashfs: source reader returned EOF before file size reached".into(),
));
}
filled += n;
}
Ok(())
}
fn copy_to_buf(r: &mut dyn Read, scratch: &mut [u8], n: u64, dst: &mut Vec<u8>) -> Result<()> {
let mut remaining = n;
while remaining > 0 {
let want = remaining.min(scratch.len() as u64) as usize;
let buf = &mut scratch[..want];
let mut filled = 0;
while filled < buf.len() {
let m = r.read(&mut buf[filled..]).map_err(crate::Error::Io)?;
if m == 0 {
return Err(crate::Error::InvalidImage(
"squashfs: source reader returned EOF before declared length".into(),
));
}
filled += m;
}
dst.extend_from_slice(buf);
remaining -= want as u64;
}
Ok(())
}
fn normalise_path(p: &str) -> Result<String> {
if p.is_empty() {
return Err(crate::Error::InvalidArgument("squashfs: empty path".into()));
}
let mut parts: Vec<&str> = Vec::new();
for c in p.split('/') {
match c {
"" | "." => continue,
".." => {
return Err(crate::Error::InvalidArgument(
"squashfs: '..' not allowed in writer paths".into(),
));
}
other => parts.push(other),
}
}
if parts.is_empty() {
Ok("/".to_string())
} else {
Ok(format!("/{}", parts.join("/")))
}
}
fn parent_path(p: &str) -> String {
if p == "/" {
return "/".into();
}
let trimmed = p.trim_end_matches('/');
match trimmed.rsplit_once('/') {
Some(("", _)) => "/".into(),
Some((parent, _)) => parent.into(),
None => "/".into(),
}
}
fn leaf_name(p: &str) -> &str {
let trimmed = p.trim_end_matches('/');
match trimmed.rsplit_once('/') {
Some((_, leaf)) => leaf,
None => trimmed,
}
}
fn count_subdirs(listings: &BTreeMap<String, Vec<(String, String, u16)>>, path: &str) -> usize {
listings
.get(path)
.map(|v| v.iter().filter(|(_, _, k)| *k == INODE_BASIC_DIR).count())
.unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::block::MemoryBackend;
#[test]
fn normalise_path_handles_common_inputs() {
assert_eq!(normalise_path("/").unwrap(), "/");
assert_eq!(normalise_path("/a").unwrap(), "/a");
assert_eq!(normalise_path("/a/").unwrap(), "/a");
assert_eq!(normalise_path("/a//b/./c/").unwrap(), "/a/b/c");
assert!(normalise_path("").is_err());
assert!(normalise_path("/a/../b").is_err());
}
#[test]
fn parent_and_leaf_round_trip() {
assert_eq!(parent_path("/"), "/");
assert_eq!(parent_path("/a"), "/");
assert_eq!(parent_path("/a/b"), "/a");
assert_eq!(parent_path("/a/b/c"), "/a/b");
assert_eq!(leaf_name("/"), "");
assert_eq!(leaf_name("/a"), "a");
assert_eq!(leaf_name("/a/b"), "b");
}
#[test]
fn write_then_read_multi_block_file() {
let mut dev = MemoryBackend::new(8 * 1024 * 1024);
let mut state = WriteState::new(4096, Compression::Unknown(0));
let mut payload = Vec::with_capacity(4096 + 1234);
for i in 0..(4096 + 1234) {
payload.push((i % 251) as u8);
}
state
.create_file(
&mut dev,
"/big.bin",
FileSource::Reader {
reader: Box::new(std::io::Cursor::new(payload.clone())),
len: payload.len() as u64,
},
EntryMeta::default(),
Vec::new(),
)
.unwrap();
state.flush(&mut dev).unwrap();
let s = super::super::Squashfs::open(&mut dev).unwrap();
let mut r = s.open_file_reader(&mut dev, "/big.bin").unwrap();
let mut buf = Vec::new();
std::io::Read::read_to_end(&mut r, &mut buf).unwrap();
drop(r);
assert_eq!(buf, payload);
}
#[test]
fn parallel_compression_matches_serial() {
if !crate::compression::Algo::Zlib.enabled() {
return;
}
let build = |threads: usize| -> Vec<u8> {
let cap = 16 * 1024 * 1024;
let mut dev = MemoryBackend::new(cap);
let mut state = WriteState::new(4096, Compression::Gzip);
state.compress_threads = Some(threads);
let sizes = [
0usize,
100,
4096,
4096 + 7,
4096 * 5 + 123,
9000,
200,
4096 * 3,
];
for (i, &n) in sizes.iter().enumerate() {
let payload: Vec<u8> = (0..n).map(|j| ((i * 31 + j) % 97) as u8).collect();
state
.create_file(
&mut dev,
&format!("/f{i:02}.bin"),
FileSource::Reader {
reader: Box::new(std::io::Cursor::new(payload.clone())),
len: payload.len() as u64,
},
EntryMeta::default(),
Vec::new(),
)
.unwrap();
}
let sb = state.flush(&mut dev).unwrap();
let len = sb.bytes_used as usize;
let mut out = vec![0u8; len];
crate::block::BlockDevice::read_at(&mut dev, 0, &mut out).unwrap();
out
};
let serial = build(1);
let parallel = build(4);
assert_eq!(
serial.len(),
parallel.len(),
"image length differs between 1 and 4 worker threads"
);
assert!(
serial == parallel,
"parallel compression produced a different image than serial"
);
}
#[test]
fn write_then_read_minimal_image() {
let mut dev = MemoryBackend::new(1024 * 1024);
let mut state = WriteState::new(DEFAULT_BLOCK_SIZE, Compression::Unknown(0));
state
.create_dir(
"/etc",
EntryMeta {
mode: 0o755,
uid: 0,
gid: 0,
mtime: 12345,
},
Vec::new(),
)
.unwrap();
state
.create_file(
&mut dev,
"/etc/hello.txt",
FileSource::Reader {
reader: Box::new(std::io::Cursor::new(b"hello".to_vec())),
len: 5,
},
EntryMeta {
mode: 0o644,
uid: 1000,
gid: 1000,
mtime: 12345,
},
vec![Xattr {
key: "user.color".into(),
value: b"orange".to_vec(),
}],
)
.unwrap();
state
.create_symlink(
"/lnk",
"etc/hello.txt",
EntryMeta {
mode: 0o777,
uid: 0,
gid: 0,
mtime: 0,
},
Vec::new(),
)
.unwrap();
let sb = state.flush(&mut dev).unwrap();
assert_eq!(sb.magic, super::super::SQUASHFS_MAGIC);
assert_eq!(sb.major, 4);
assert_eq!(sb.inode_count, 4);
let s = super::super::Squashfs::open(&mut dev).unwrap();
let entries = s.list_path(&mut dev, "/").unwrap();
let names: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect();
assert!(names.contains(&"etc"));
assert!(names.contains(&"lnk"));
let etc = s.list_path(&mut dev, "/etc").unwrap();
assert_eq!(etc.len(), 1);
assert_eq!(etc[0].name, "hello.txt");
let mut r = s.open_file_reader(&mut dev, "/etc/hello.txt").unwrap();
let mut buf = Vec::new();
std::io::Read::read_to_end(&mut r, &mut buf).unwrap();
drop(r);
assert_eq!(buf, b"hello");
let tgt = s.read_symlink(&mut dev, "/lnk").unwrap();
assert_eq!(tgt, "etc/hello.txt");
}
}