use std::{
borrow::Cow,
hash::{Hash, Hasher},
num::NonZero,
time::{Duration, SystemTime},
};
use dwarfs::metadata;
use indexmap::IndexSet;
use crate::{Error, ErrorInner, Result};
const S_IFSOCK: u32 = 0o0140000;
const S_IFLNK: u32 = 0o0120000;
const S_IFREG: u32 = 0o0100000;
const S_IFBLK: u32 = 0o0060000;
const S_IFDIR: u32 = 0o0040000;
const S_IFCHR: u32 = 0o0020000;
const S_IFIFO: u32 = 0o0010000;
#[derive(Debug, Clone)]
pub struct Config {
block_size: NonZero<u32>,
mtime_only: bool,
time_resolution_sec: NonZero<u32>,
source_date_epoch: u64,
creator: Option<Cow<'static, str>>,
created_timestamp: Option<u64>,
}
impl Default for Config {
fn default() -> Self {
Self {
block_size: NonZero::new(16 << 20).expect("not zero"),
mtime_only: false,
time_resolution_sec: NonZero::new(1).expect("not zero"),
source_date_epoch: u64::MAX,
creator: Some(Cow::Borrowed(Self::DEFAULT_CREATOR_VERSION)),
created_timestamp: None,
}
}
}
impl Config {
const DEFAULT_CREATOR_VERSION: &str =
concat!(env!("CARGO_PKG_NAME"), " ", env!("CARGO_PKG_VERSION"));
pub fn block_size(&mut self, bytes: NonZero<u32>) -> &mut Self {
assert!(bytes.is_power_of_two());
self.block_size = bytes;
self
}
pub fn mtime_only(&mut self, yes: bool) -> &mut Self {
self.mtime_only = yes;
self
}
pub fn time_resolution_sec(&mut self, sec: NonZero<u32>) -> &mut Self {
self.time_resolution_sec = sec;
self
}
pub fn source_date_epoch(&mut self, timestamp: u64) -> &mut Self {
self.source_date_epoch = timestamp;
self.clamp_timestamp();
self
}
#[doc = concat!("`\"", env!("CARGO_PKG_NAME"), " ", env!("CARGO_PKG_VERSION"), "\"`.")]
pub fn creator(&mut self, info: impl Into<Option<Cow<'static, str>>>) -> &mut Self {
self.creator = info.into();
self
}
pub fn created_timestamp(&mut self, ts: impl Into<Option<u64>>) -> &mut Self {
self.created_timestamp = ts.into();
self.clamp_timestamp();
self
}
fn clamp_timestamp(&mut self) {
if let Some(t) = &mut self.created_timestamp {
*t = self.source_date_epoch.min(*t);
}
}
}
#[derive(Debug)]
pub struct Builder {
config: Config,
inodes: Vec<InodeData>,
dir_entries: IndexSet<DirEntry>,
chunks: Vec<Chunk>,
file_chunk_start: Vec<u32>,
symlink_target_idxs: Vec<u32>,
devices: Vec<u64>,
name_table: IndexSet<String>,
symlink_table: IndexSet<String>,
modes: IndexSet<u32>,
uids: IndexSet<u32>,
gids: IndexSet<u32>,
}
impl Builder {
pub fn new(root_meta: &InodeMetadata) -> Self {
Self::new_with_config(&Config::default(), root_meta)
}
pub fn new_with_config(config: &Config, root_meta: &InodeMetadata) -> Self {
let mut this = Self {
config: config.clone(),
inodes: Default::default(),
dir_entries: Default::default(),
chunks: Default::default(),
file_chunk_start: Default::default(),
symlink_target_idxs: Default::default(),
devices: Default::default(),
name_table: Default::default(),
symlink_table: Default::default(),
modes: Default::default(),
uids: Default::default(),
gids: Default::default(),
};
this.put_inode(S_IFDIR, InodeKind::Dir, root_meta)
.expect("no overflow");
this
}
pub fn block_size(&self) -> NonZero<u32> {
self.config.block_size
}
#[inline]
pub fn root(&self) -> DirId {
DirId(0)
}
fn put_inode(&mut self, file_type: u32, kind: InodeKind, meta: &InodeMetadata) -> Result<u32> {
let ino = u32::try_from(self.inodes.len())
.ok()
.ok_or(ErrorInner::Limit("inode count exceeds 2^32"))?;
let cvt_time = |time: SystemTime| {
let timestamp = time
.duration_since(SystemTime::UNIX_EPOCH)
.map_err(|_| ErrorInner::Limit("timestamp before UNIX epoch is unsupported"))?
.as_secs();
let multiples = timestamp.min(self.config.source_date_epoch)
/ u64::from(self.config.time_resolution_sec.get());
u32::try_from(multiples)
.map_err(|_| Error::from(ErrorInner::Limit("relative timestamp exceeds 2^32")))
};
let mtime_offset = cvt_time(meta.mtime)?;
let (atime_offset, ctime_offset) = if self.config.mtime_only {
(0, 0)
} else {
(cvt_time(meta.atime)?, cvt_time(meta.ctime)?)
};
let mode = file_type | meta.mode_without_type;
let mode_idx = self.modes.insert_full(mode).0 as u32;
let uid_idx = self.uids.insert_full(meta.uid).0 as u32;
let gid_idx = self.gids.insert_full(meta.gid).0 as u32;
self.inodes.push(InodeData {
kind,
orig_ino: ino,
mode_idx,
uid_idx,
gid_idx,
mtime_offset,
atime_offset,
ctime_offset,
});
Ok(ino)
}
fn put_entry_inner(&mut self, parent: DirId, name: &str, child: u32) -> Result<()> {
u32::try_from(self.dir_entries.len())
.ok()
.ok_or(ErrorInner::Limit("directory entry count exceeds 2^32"))?;
let name_idx = self.name_table.insert_full(name.into()).0 as u32;
let (_, inserted) = self.dir_entries.insert_full(DirEntry {
parent: parent.0,
name_idx,
child,
});
if !inserted {
return Err(ErrorInner::DuplicatedEntry.into());
}
Ok(())
}
#[inline]
pub fn put_dir(&mut self, parent: DirId, name: &str, meta: &InodeMetadata) -> Result<DirId> {
let ino = self.put_inode(S_IFDIR, InodeKind::Dir, meta)?;
self.put_entry_inner(parent, name, ino)?;
Ok(DirId(ino))
}
pub fn put_hard_link(
&mut self,
parent: DirId,
name: &str,
inode: impl Into<LinkableInodeId>,
) -> Result<()> {
self.put_entry_inner(parent, name, inode.into().0)
}
pub fn put_file(
&mut self,
parent: DirId,
name: &str,
meta: &InodeMetadata,
chunks: impl IntoIterator<Item = Chunk>,
) -> Result<FileId> {
let chunk_start = self.chunks.len() as u32;
self.chunks.extend(chunks);
u32::try_from(self.chunks.len())
.ok()
.ok_or(ErrorInner::Limit("file chunk count exceeds 2^32"))?;
if let Some(c) = self.chunks[chunk_start as usize..].iter().find(|c| {
c.offset
.checked_add(c.size)
.is_none_or(|end| end > self.config.block_size.get())
}) {
panic!(
"invalid chunk for block size {}B: {:?}",
self.config.block_size, c,
);
}
let ino = self.put_inode(S_IFREG, InodeKind::UniqueFile, meta)?;
self.file_chunk_start.push(chunk_start);
self.put_entry_inner(parent, name, ino)?;
Ok(FileId(ino))
}
#[inline]
pub fn put_symlink(
&mut self,
parent: DirId,
name: &str,
meta: &InodeMetadata,
target: &str,
) -> Result<LinkableInodeId> {
let ino = self.put_inode(S_IFLNK, InodeKind::Symlink, meta)?;
let tgt_idx = self.symlink_table.insert_full(target.into()).0 as u32;
self.symlink_target_idxs.push(tgt_idx);
self.put_entry_inner(parent, name, ino)?;
Ok(LinkableInodeId(ino))
}
#[inline]
pub fn put_block_device(
&mut self,
parent: DirId,
name: &str,
meta: &InodeMetadata,
device_id: u64,
) -> Result<LinkableInodeId> {
let ino = self.put_inode(S_IFBLK, InodeKind::Device, meta)?;
self.devices.push(device_id);
self.put_entry_inner(parent, name, ino)?;
Ok(LinkableInodeId(ino))
}
#[inline]
pub fn put_char_device(
&mut self,
parent: DirId,
name: &str,
meta: &InodeMetadata,
device_id: u64,
) -> Result<LinkableInodeId> {
let ino = self.put_inode(S_IFCHR, InodeKind::Device, meta)?;
self.devices.push(device_id);
self.put_entry_inner(parent, name, ino)?;
Ok(LinkableInodeId(ino))
}
#[inline]
pub fn put_fifo(
&mut self,
parent: DirId,
name: &str,
meta: &InodeMetadata,
) -> Result<LinkableInodeId> {
let ino = self.put_inode(S_IFIFO, InodeKind::Ipc, meta)?;
self.put_entry_inner(parent, name, ino)?;
Ok(LinkableInodeId(ino))
}
#[inline]
pub fn put_socket(
&mut self,
parent: DirId,
name: &str,
meta: &InodeMetadata,
) -> Result<LinkableInodeId> {
let ino = self.put_inode(S_IFSOCK, InodeKind::Ipc, meta)?;
self.put_entry_inner(parent, name, ino)?;
Ok(LinkableInodeId(ino))
}
fn build_string_table(
tbl: IndexSet<String>,
err_msg: &'static str,
) -> Result<Option<metadata::StringTable>> {
if tbl.is_empty() {
return Ok(None);
}
let mut out = metadata::StringTable::default();
out.packed_index = true;
let total_len = tbl.iter().map(|s| s.len()).sum::<usize>();
u32::try_from(total_len)
.ok()
.ok_or(ErrorInner::Limit(err_msg))?;
out.buffer.reserve(total_len);
out.index.reserve(tbl.len());
for s in tbl {
out.buffer.extend_from_slice(s.as_bytes());
out.index.push(s.len() as u32);
}
Ok(Some(out))
}
pub fn finish(mut self) -> Result<metadata::Metadata> {
let mut out = metadata::Metadata::default();
let opts = out.options.insert(metadata::FsOptions::default());
opts.mtime_only = self.config.mtime_only;
opts.time_resolution_sec = (self.config.time_resolution_sec.get() != 1)
.then_some(self.config.time_resolution_sec.get());
self.inodes.sort_by_key(|inode| inode.kind as u8);
let orig_ino_to_final = {
let mut map = vec![0u32; self.inodes.len()];
for (final_ino, inode) in self.inodes.iter().enumerate() {
map[inode.orig_ino as usize] = final_ino as u32;
}
map
};
out.inodes = self
.inodes
.iter()
.map(|inode| {
let mut data = metadata::InodeData::default();
data.mode_index = inode.mode_idx;
data.owner_index = inode.uid_idx;
data.group_index = inode.gid_idx;
data.atime_offset = inode.atime_offset;
data.mtime_offset = inode.mtime_offset;
data.ctime_offset = inode.ctime_offset;
data
})
.collect();
let dir_cnt = self
.inodes
.iter()
.take_while(|data| data.kind == InodeKind::Dir)
.count();
assert_ne!(dir_cnt, 0, "root exists");
let mut dir_entries = std::iter::once(DirEntry {
parent: 0,
child: 0,
name_idx: 0,
})
.chain(self.dir_entries)
.collect::<Vec<_>>();
dir_entries[1..]
.sort_by_key(|ent| (ent.parent, &self.name_table[ent.name_idx as usize][..]));
debug_assert!(
dir_entries[1..]
.windows(2)
.all(|w| (w[0].parent, w[0].name_idx) != (w[1].parent, w[1].name_idx))
);
{
out.directories = vec![Default::default(); dir_cnt + 1];
let mut offset = 1u32;
for (final_ino, inode) in self.inodes[..dir_cnt].iter().enumerate() {
let dir = &mut out.directories[final_ino];
dir.first_entry = offset;
let parent_entry = dir.self_entry;
while let Some(ent) = dir_entries
.get(offset as usize)
.filter(|ent| ent.parent == inode.orig_ino)
{
let child_final_ino = orig_ino_to_final[ent.child as usize] as usize;
if let Some(subdir) = out.directories.get_mut(child_final_ino) {
subdir.self_entry = offset;
subdir.parent_entry = parent_entry;
}
offset += 1;
}
}
debug_assert_eq!(offset as usize, dir_entries.len());
out.directories.last_mut().unwrap().first_entry = dir_entries.len() as u32;
}
out.dir_entries = Some(
dir_entries
.into_iter()
.map(|ent| {
let mut out = metadata::DirEntry::default();
out.name_index = ent.name_idx;
out.inode_num = orig_ino_to_final[ent.child as usize];
out
})
.collect(),
);
out.compact_names =
Self::build_string_table(self.name_table, "total file name length exceeds 2^32")?;
out.compact_symlinks =
Self::build_string_table(self.symlink_table, "total symlink length exceeds 2^32")?;
out.block_size = self.config.block_size.get();
out.total_fs_size = 0; out.dwarfs_version = self.config.creator.map(|s| String::from(s).into());
out.create_timestamp = self.config.created_timestamp;
out.symlink_table = self.symlink_target_idxs;
out.modes = self.modes.into_iter().collect();
out.uids = self.uids.into_iter().collect();
out.gids = self.gids.into_iter().collect();
out.devices = (!self.devices.is_empty()).then_some(self.devices);
out.chunk_table = self.file_chunk_start;
out.chunk_table.push(self.chunks.len() as u32);
out.chunks = self
.chunks
.into_iter()
.map(|chunk| {
let mut data = metadata::Chunk::default();
data.block = chunk.section_idx;
data.offset = chunk.offset;
data.size = chunk.size;
data
})
.collect();
Ok(out)
}
}
#[derive(Debug)]
struct DirEntry {
parent: u32,
name_idx: u32,
child: u32,
}
impl Hash for DirEntry {
fn hash<H: Hasher>(&self, h: &mut H) {
h.write_u64(u64::from(self.parent) | u64::from(self.name_idx) << 32);
}
}
impl PartialEq for DirEntry {
fn eq(&self, other: &Self) -> bool {
(self.parent, self.name_idx) == (other.parent, other.name_idx)
}
}
impl Eq for DirEntry {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Chunk {
pub section_idx: u32,
pub offset: u32,
pub size: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum InodeKind {
Dir,
Symlink,
UniqueFile,
Device,
Ipc,
}
#[derive(Debug)]
struct InodeData {
kind: InodeKind,
orig_ino: u32,
mode_idx: u32,
uid_idx: u32,
gid_idx: u32,
mtime_offset: u32,
atime_offset: u32,
ctime_offset: u32,
}
#[derive(Debug, Clone)]
pub struct InodeMetadata {
mode_without_type: u32,
uid: u32,
gid: u32,
mtime: SystemTime,
atime: SystemTime,
ctime: SystemTime,
}
impl From<&std::fs::Metadata> for InodeMetadata {
fn from(meta: &std::fs::Metadata) -> Self {
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
#[cfg(unix)]
let mode = meta.mode() & 0o777;
#[cfg(not(unix))]
let mode = if meta.is_dir() { 0o755 } else { 0o644 };
let mut ret = InodeMetadata::new(mode);
if let Ok(mtime) = meta.modified() {
ret.mtime(mtime);
}
if let Ok(atime) = meta.accessed() {
ret.atime(atime);
}
#[cfg(unix)]
{
let ctime = meta.ctime();
let ctime = if ctime >= 0 {
SystemTime::UNIX_EPOCH + Duration::from_secs(meta.ctime() as u64)
} else {
SystemTime::UNIX_EPOCH - Duration::from_secs(-meta.ctime() as u64)
};
ret.ctime(ctime).uid(meta.uid()).gid(meta.gid());
}
ret
}
}
impl InodeMetadata {
pub const fn new(mode_without_type: u32) -> Self {
assert!(
mode_without_type & !0o777 == 0,
"`mode_without_type` should only have 0o7777 bits set",
);
Self {
mode_without_type,
uid: 0,
gid: 0,
mtime: SystemTime::UNIX_EPOCH,
atime: SystemTime::UNIX_EPOCH,
ctime: SystemTime::UNIX_EPOCH,
}
}
pub fn uid(&mut self, uid: u32) -> &mut Self {
self.uid = uid;
self
}
pub fn gid(&mut self, gid: u32) -> &mut Self {
self.gid = gid;
self
}
pub fn mtime(&mut self, timestamp: SystemTime) -> &mut Self {
self.mtime = timestamp;
self
}
pub fn atime(&mut self, timestamp: SystemTime) -> &mut Self {
self.atime = timestamp;
self
}
pub fn ctime(&mut self, timestamp: SystemTime) -> &mut Self {
self.ctime = timestamp;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct DirId(u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct LinkableInodeId(u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct FileId(u32);
impl From<FileId> for LinkableInodeId {
fn from(i: FileId) -> Self {
Self(i.0)
}
}