use std::{
borrow::Cow,
collections::{BTreeMap, VecDeque},
ffi::CString,
io::{Cursor, Write},
};
#[cfg(unix)]
use std::{io::Read, path::Path};
use apple_bom::{
BomPath, BomPathType,
format::{
BomBlock, BomBlockBomInfo, BomBlockFile, BomBlockPathInfoIndex, BomBlockPathRecord,
BomBlockPaths, BomBlockTree, BomBlockVIndex, BomBlocksEntry, BomBlocksIndex, BomHeader,
BomInfoEntry, BomPathsEntry,
},
};
use chrono::{DateTime, Utc};
use scroll::IOwrite;
use crate::{Error, Result};
const fn cksum_table() -> [u32; 256] {
let mut table = [0u32; 256];
let mut i = 0;
while i < 256 {
let mut crc = (i as u32) << 24;
let mut bit = 0;
while bit < 8 {
crc = if crc & 0x8000_0000 != 0 {
(crc << 1) ^ 0x04C1_1DB7
} else {
crc << 1
};
bit += 1;
}
table[i] = crc;
i += 1;
}
table
}
const CKSUM_TABLE: [u32; 256] = cksum_table();
#[derive(Default)]
struct Cksum {
crc: u32,
len: usize,
}
impl Cksum {
fn step(crc: u32, byte: u8) -> u32 {
(crc << 8) ^ CKSUM_TABLE[(((crc >> 24) as u8) ^ byte) as usize]
}
fn update(&mut self, bytes: &[u8]) {
for byte in bytes {
self.crc = Self::step(self.crc, *byte);
}
self.len += bytes.len();
}
fn finish(self) -> (u32, usize) {
let mut crc = self.crc;
let mut length = self.len;
while length != 0 {
crc = Self::step(crc, (length & 0xff) as u8);
length >>= 8;
}
(!crc, self.len)
}
}
#[cfg(unix)]
fn cksum_path(path: &Path) -> std::io::Result<(u32, usize)> {
let mut fh = std::fs::File::open(path)?;
let mut buffer = [0u8; 32768];
let mut cksum = Cksum::default();
loop {
let bytes_read = fh.read(&mut buffer)?;
if bytes_read == 0 {
break;
}
cksum.update(&buffer[..bytes_read]);
}
Ok(cksum.finish())
}
fn vars_index_to_vec(vars: &[(u32, &str)]) -> Vec<u8> {
let mut out = Vec::new();
out.extend((vars.len() as u32).to_be_bytes());
for (block_index, name) in vars {
out.extend(block_index.to_be_bytes());
out.push(name.len() as u8);
out.extend(name.as_bytes());
}
out
}
fn validate_bom_path(s: &str) -> Result<()> {
if s.starts_with('.') || s.starts_with('/') || s.contains('\\') {
Err(Error::BadBomPath(s.to_string()))
} else {
Ok(())
}
}
#[derive(Clone, Debug)]
pub struct BomBuilder {
paths: BTreeMap<String, BomPath>,
default_mtime: DateTime<Utc>,
default_uid: u32,
default_gid: u32,
default_mode_file: u16,
default_mode_dir: u16,
}
impl Default for BomBuilder {
fn default() -> Self {
Self {
paths: Default::default(),
default_mtime: Utc::now(),
default_uid: 0,
default_gid: 0,
default_mode_file: 0o100644,
default_mode_dir: 0o40755,
}
}
}
impl BomBuilder {
fn default_file_path(&self) -> Result<BomPath> {
let record = BomBlockPathRecord {
path_type: BomPathType::File.into(),
a: 1,
architecture: 15,
mode: self.default_mode_file,
user: self.default_uid,
group: self.default_gid,
mtime: self.default_mtime.timestamp() as u32,
size: 0,
b: 1,
checksum_or_type: 0,
link_name_length: 0,
link_name: None,
};
Ok(BomPath::from_record(String::new(), &record)?)
}
pub fn default_user_id(&mut self, uid: u32) {
self.default_uid = uid;
}
pub fn default_group_id(&mut self, gid: u32) {
self.default_gid = gid;
}
pub fn default_mtime(&mut self, mtime: DateTime<Utc>) {
self.default_mtime = mtime;
}
#[cfg(unix)]
pub fn add_file_from_path(
&mut self,
bom_path: impl ToString,
path: impl AsRef<Path>,
) -> Result<&mut BomPath> {
let (cksum, file_size) = cksum_path(path.as_ref())?;
self.add_file(bom_path, cksum, file_size)
}
pub fn add_file_from_bytes(
&mut self,
bom_path: impl ToString,
bytes: &[u8],
) -> Result<&mut BomPath> {
let mut cksum = Cksum::default();
cksum.update(bytes);
let (cksum, file_size) = cksum.finish();
self.add_file(bom_path, cksum, file_size)
}
fn add_file(
&mut self,
bom_path: impl ToString,
cksum: u32,
file_size: usize,
) -> Result<&mut BomPath> {
let bom_path = bom_path.to_string();
validate_bom_path(&bom_path)?;
let mut path = self.default_file_path()?;
path.set_size(file_size);
path.set_crc32(Some(cksum));
self.paths.insert(bom_path.clone(), path);
Ok(self.paths.get_mut(&bom_path).unwrap())
}
pub fn add_directory(&mut self, bom_path: impl ToString, mode: u16) -> Result<()> {
let bom_path = bom_path.to_string();
validate_bom_path(&bom_path)?;
let mut record = self.directory_record();
record.mode = mode;
let path = BomPath::from_record(String::new(), &record)?;
self.paths.insert(bom_path, path);
Ok(())
}
fn directory_record(&self) -> BomBlockPathRecord<'static> {
BomBlockPathRecord {
path_type: BomPathType::Directory.into(),
a: 1,
architecture: 15,
mode: self.default_mode_dir,
user: self.default_uid,
group: self.default_gid,
mtime: self.default_mtime.timestamp() as u32,
size: 0,
b: 1,
checksum_or_type: 0,
link_name_length: 0,
link_name: None,
}
}
pub fn build_bom(&self) -> Result<Vec<u8>> {
let mut records = Vec::with_capacity(self.paths.len() + 1);
records.push((
1u32,
self.directory_record(),
BomBlockFile {
parent_path_id: 0,
name: Cow::from(CString::new(".").expect("valid C string")),
},
));
#[derive(Default)]
struct TreeNode<'p> {
children: BTreeMap<&'p str, TreeNode<'p>>,
entry: Option<&'p BomPath>,
}
let mut root = TreeNode::default();
for (index_path, entry) in &self.paths {
let mut node = &mut root;
for part in index_path.split('/') {
node = node.children.entry(part).or_default();
}
node.entry = Some(entry);
}
let mut next_id = 2u32;
let mut queue = VecDeque::from([(1u32, root)]);
while let Some((parent_path_id, node)) = queue.pop_front() {
for (name, child) in node.children {
let path_id = next_id;
next_id += 1;
let path_record = if let Some(entry) = child.entry {
BomBlockPathRecord {
path_type: entry.path_type().into(),
a: 1,
architecture: 15,
mode: entry.file_mode(),
user: entry.user_id(),
group: entry.group_id(),
mtime: entry.modified_time().timestamp() as _,
size: entry.size() as _,
b: 1,
checksum_or_type: entry.crc32().unwrap_or(0),
link_name_length: if let Some(link_name) = entry.link_name() {
link_name.len() as u32 + 1
} else {
0
},
link_name: entry.link_name_cstring().map(Cow::from),
}
} else {
self.directory_record()
};
let file = BomBlockFile {
parent_path_id,
name: Cow::from(CString::new(name).expect("valid C string")),
};
records.push((path_id, path_record, file));
queue.push_back((path_id, child));
}
}
let mut blocks = vec![];
let mut paths_entries = Vec::with_capacity(records.len());
blocks.push(BomBlock::Empty);
blocks.push(BomBlock::BomInfo(BomBlockBomInfo {
version: 1,
number_of_paths: records.len() as u32 + 1,
number_of_info_entries: 3,
entries: vec![
BomInfoEntry {
a: 0,
b: 0,
c: 8546296,
d: 0,
},
BomInfoEntry {
a: 16777223,
b: 0,
c: 37959280,
d: 0,
},
BomInfoEntry {
a: 16777228,
b: 0,
c: 25620800,
d: 0,
},
],
}));
let mut vars: Vec<(u32, &str)> = vec![(blocks.len() as u32 - 1, "BomInfo")];
for (path_id, path_record, file) in records {
let path_record_index = blocks.len() as u32;
blocks.push(BomBlock::PathRecord(path_record));
let file_index = blocks.len() as u32;
blocks.push(BomBlock::File(file));
let path_info_index = blocks.len() as u32;
blocks.push(BomBlock::PathInfoIndex(BomBlockPathInfoIndex {
path_id,
path_record_index,
}));
paths_entries.push(BomPathsEntry {
block_index: path_info_index,
file_index,
});
}
const PATHS_BLOCK_SIZE: u32 = 4096;
vars.push((blocks.len() as u32, "Paths"));
blocks.push(BomBlock::Tree(BomBlockTree {
block_paths_index: blocks.len() as u32 + 1,
block_size: PATHS_BLOCK_SIZE,
path_count: paths_entries.len() as u32,
..Default::default()
}));
let mut paths_blocks = vec![];
let mut paths_block = BomBlockPaths {
is_path_info: 1,
..Default::default()
};
for path_entry in paths_entries {
paths_block.count += 1;
paths_block.paths.push(path_entry);
let remaining_bytes = PATHS_BLOCK_SIZE - 12 - 8 * paths_block.count as u32;
if remaining_bytes < 16 {
paths_blocks.push(paths_block.clone());
paths_block = BomBlockPaths {
is_path_info: 1,
..Default::default()
};
}
}
if paths_block.count > 0 || paths_blocks.is_empty() {
paths_blocks.push(paths_block);
}
if paths_blocks.len() > 1 {
let leaves_start = blocks.len() as u32 + 1;
blocks.push(BomBlock::Paths(BomBlockPaths {
is_path_info: 0,
count: paths_blocks.len() as u16,
paths: paths_blocks
.iter()
.enumerate()
.map(|(i, paths)| BomPathsEntry {
block_index: leaves_start + i as u32,
file_index: paths
.paths
.last()
.map(|entry| entry.file_index)
.unwrap_or(0),
})
.collect(),
..Default::default()
}));
}
for (i, paths) in paths_blocks.iter().enumerate() {
blocks.push(BomBlock::Paths(BomBlockPaths {
is_path_info: paths.is_path_info,
count: paths.count,
next_paths_block_index: if i == paths_blocks.len() - 1 {
0
} else {
blocks.len() as u32 + 1
},
previous_paths_block_index: if i == 0 { 0 } else { blocks.len() as u32 - 1 },
paths: paths.paths.clone(),
}));
}
vars.push((blocks.len() as u32, "HLIndex"));
blocks.push(BomBlock::Tree(BomBlockTree {
block_paths_index: blocks.len() as u32 + 1,
block_size: PATHS_BLOCK_SIZE,
..Default::default()
}));
blocks.push(BomBlock::Paths(BomBlockPaths {
is_path_info: 1,
..Default::default()
}));
vars.push((blocks.len() as u32, "VIndex"));
blocks.push(BomBlock::VIndex(BomBlockVIndex {
a: 1,
tree_block_index: blocks.len() as u32 + 1,
b: 0,
c: 0,
}));
blocks.push(BomBlock::Tree(BomBlockTree {
block_paths_index: blocks.len() as u32 + 1,
block_size: PATHS_BLOCK_SIZE,
..Default::default()
}));
blocks.push(BomBlock::Paths(BomBlockPaths {
is_path_info: 1,
..Default::default()
}));
vars.push((blocks.len() as u32, "Size64"));
blocks.push(BomBlock::Tree(BomBlockTree {
block_paths_index: blocks.len() as u32 + 1,
block_size: PATHS_BLOCK_SIZE,
..Default::default()
}));
blocks.push(BomBlock::Paths(BomBlockPaths {
is_path_info: 1,
..Default::default()
}));
const BLOCK_DATA_FILE_OFFSET: u32 = 512;
let mut blocks_index = BomBlocksIndex::default();
let mut blocks_writer = Cursor::new(Vec::<u8>::new());
for block in &blocks {
let start_offset = blocks_writer.position();
block.write(&mut blocks_writer)?;
if let BomBlock::PathRecord(record) = block {
if record.path_type == u8::from(BomPathType::File) {
blocks_writer.write_all(&[0u8; 4])?;
}
}
let end_offset = blocks_writer.position();
blocks_index.count += 1;
blocks_index.blocks.push(BomBlocksEntry {
file_offset: BLOCK_DATA_FILE_OFFSET + start_offset as u32,
length: (end_offset - start_offset) as _,
});
}
let blocks_data = blocks_writer.into_inner();
let vars_index_data = vars_index_to_vec(&vars);
let mut blocks_index_data = blocks_index.to_vec()?;
blocks_index_data.extend([0u8; 20]);
const VARS_INDEX_OFFSET: u32 = 128;
let blocks_index_offset = BLOCK_DATA_FILE_OFFSET
+ blocks_data.len() as u32
+ (64 - blocks_data.len() % 64) as u32;
let header = BomHeader {
magic: *b"BOMStore",
version: 1,
number_of_blocks: blocks.len() as u32 - 1,
blocks_index_offset,
blocks_index_length: blocks_index_data.len() as _,
vars_index_offset: VARS_INDEX_OFFSET,
vars_index_length: vars_index_data.len() as _,
};
let mut writer = Cursor::new(Vec::<u8>::new());
writer.iowrite_with(header, scroll::BE)?;
for _ in 0..VARS_INDEX_OFFSET - writer.position() as u32 {
writer.write_all(b"\0")?;
}
writer.write_all(&vars_index_data)?;
for _ in 0..BLOCK_DATA_FILE_OFFSET - writer.position() as u32 {
writer.write_all(b"\0")?;
}
writer.write_all(&blocks_data)?;
for _ in 0..blocks_index_offset - writer.position() as u32 {
writer.write_all(b"\0")?;
}
writer.write_all(&blocks_index_data)?;
Ok(writer.into_inner())
}
}