use byteorder::{BigEndian, ByteOrder};
use std::cmp::Ordering;
use std::sync::OnceLock;
use disk_core::filesystem::FilesystemError;
pub use disk_core::bits::{
bitmap_clear_bit_be, bitmap_collect_clear_runs_be, bitmap_find_clear_bit_be,
bitmap_find_clear_run_be, bitmap_find_set_bit_be, bitmap_set_bit_be, bitmap_test_bit_be,
};
const MAC_EPOCH_DELTA: u64 = 2_082_844_800;
pub fn hfs_now() -> u32 {
let unix_secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
u32::try_from(unix_secs.saturating_add(MAC_EPOCH_DELTA)).unwrap_or(u32::MAX)
}
pub fn format_mac_date(mac_secs: u32) -> Option<String> {
if mac_secs == 0 {
return None;
}
let unix = mac_secs as i64 - MAC_EPOCH_DELTA as i64;
Some(disk_core::unix_common::inode::format_unix_timestamp(unix))
}
pub fn mac_date_to_unix(mac_secs: u32) -> Option<u64> {
if mac_secs == 0 || (mac_secs as u64) < MAC_EPOCH_DELTA {
return None;
}
Some(mac_secs as u64 - MAC_EPOCH_DELTA)
}
pub fn parse_mac_date(s: &str) -> Option<u32> {
parse_mac_date_checked(s).ok()
}
pub fn parse_mac_date_checked(s: &str) -> Result<u32, String> {
let s = s.trim();
if s.is_empty() || s == "0" {
return Ok(0);
}
let fmt = "expected YYYY-MM-DD HH:MM:SS";
let (date, time) = s.split_once(' ').ok_or(fmt)?;
let d: Vec<&str> = date.split('-').collect();
let t: Vec<&str> = time.split(':').collect();
if d.len() != 3 || t.len() != 3 {
return Err(fmt.into());
}
let num = |x: &str, what: &str| -> Result<i64, String> {
x.trim()
.parse::<i64>()
.map_err(|_| format!("'{}' is not a number ({what})", x.trim()))
};
let (y, mo, day) = (num(d[0], "year")?, num(d[1], "month")?, num(d[2], "day")?);
let (h, mi, se) = (
num(t[0], "hour")?,
num(t[1], "minute")?,
num(t[2], "second")?,
);
if !(1..=12).contains(&mo) {
return Err(format!("month {mo} is out of range (1-12)"));
}
let nd = chrono::NaiveDate::from_ymd_opt(y as i32, mo as u32, day as u32)
.ok_or_else(|| format!("day {day} is invalid for {y}-{mo:02}"))?;
let nt = chrono::NaiveTime::from_hms_opt(h as u32, mi as u32, se as u32)
.ok_or_else(|| format!("time {h:02}:{mi:02}:{se:02} is out of range"))?;
let mac = chrono::NaiveDateTime::new(nd, nt).and_utc().timestamp() + MAC_EPOCH_DELTA as i64;
if (0..=u32::MAX as i64).contains(&mac) {
Ok(mac as u32)
} else {
Err("date is outside the representable range".into())
}
}
struct ExtensionMap {
entries: Vec<(String, [u8; 4], [u8; 4])>, }
fn load_extension_map() -> &'static ExtensionMap {
static MAP: OnceLock<ExtensionMap> = OnceLock::new();
MAP.get_or_init(|| {
let json_bytes = include_str!("../assets/hfs_file_types.json");
let parsed: serde_json::Value = serde_json::from_str(json_bytes).unwrap_or_default();
let mut entries = Vec::new();
if let Some(exts) = parsed.get("extensions").and_then(|v| v.as_object()) {
for (ext, val) in exts {
if ext.starts_with('_') {
continue;
}
if let (Some(tc), Some(cc)) = (
val.get("type").and_then(|v| v.as_str()),
val.get("creator").and_then(|v| v.as_str()),
) {
entries.push((ext.to_lowercase(), encode_fourcc(tc), encode_fourcc(cc)));
}
}
}
ExtensionMap { entries }
})
}
#[derive(Debug, Clone)]
pub struct TypeCreatorEntry {
pub type_code: [u8; 4],
pub creator_code: [u8; 4],
pub description: String,
}
impl TypeCreatorEntry {
pub fn type_str(&self) -> String {
String::from_utf8_lossy(&self.type_code).to_string()
}
pub fn creator_str(&self) -> String {
String::from_utf8_lossy(&self.creator_code).to_string()
}
}
fn load_type_creator_entries() -> &'static Vec<TypeCreatorEntry> {
static ENTRIES: OnceLock<Vec<TypeCreatorEntry>> = OnceLock::new();
ENTRIES.get_or_init(|| {
let json_bytes = include_str!("../assets/hfs_file_types.json");
let parsed: serde_json::Value = serde_json::from_str(json_bytes).unwrap_or_default();
let mut type_desc: std::collections::HashMap<String, String> =
std::collections::HashMap::new();
if let Some(types) = parsed.get("types").and_then(|v| v.as_object()) {
for (tc, val) in types {
if let Some(desc) = val.get("description").and_then(|v| v.as_str()) {
type_desc.insert(tc.clone(), desc.to_string());
}
}
}
let mut seen: std::collections::HashSet<(String, String)> =
std::collections::HashSet::new();
let mut out: Vec<TypeCreatorEntry> = Vec::new();
if let Some(arr) = parsed.get("entries").and_then(|v| v.as_array()) {
for item in arr {
let (Some(tc_str), Some(cc_str), Some(desc)) = (
item.get("type").and_then(|v| v.as_str()),
item.get("creator").and_then(|v| v.as_str()),
item.get("description").and_then(|v| v.as_str()),
) else {
continue;
};
if !seen.insert((tc_str.to_string(), cc_str.to_string())) {
continue;
}
out.push(TypeCreatorEntry {
type_code: encode_fourcc(tc_str),
creator_code: encode_fourcc(cc_str),
description: desc.to_string(),
});
}
}
if let Some(exts) = parsed.get("extensions").and_then(|v| v.as_object()) {
for (ext, val) in exts {
if ext.starts_with('_') {
continue;
}
let (Some(tc_str), Some(cc_str)) = (
val.get("type").and_then(|v| v.as_str()),
val.get("creator").and_then(|v| v.as_str()),
) else {
continue;
};
if !seen.insert((tc_str.to_string(), cc_str.to_string())) {
continue;
}
let desc = type_desc
.get(tc_str)
.cloned()
.unwrap_or_else(|| format!(".{ext}"));
out.push(TypeCreatorEntry {
type_code: encode_fourcc(tc_str),
creator_code: encode_fourcc(cc_str),
description: desc,
});
}
}
out.sort_by(|a, b| {
a.description
.to_lowercase()
.cmp(&b.description.to_lowercase())
.then_with(|| a.type_code.cmp(&b.type_code))
.then_with(|| a.creator_code.cmp(&b.creator_code))
});
out
})
}
pub fn known_type_creators() -> &'static [TypeCreatorEntry] {
load_type_creator_entries().as_slice()
}
pub fn describe_type_creator(type_code: &[u8; 4], creator_code: &[u8; 4]) -> Option<&'static str> {
load_type_creator_entries()
.iter()
.find(|e| &e.type_code == type_code && &e.creator_code == creator_code)
.map(|e| e.description.as_str())
}
pub fn uses_hfs_type_dictionary(fs_type: &str) -> bool {
fs_type.starts_with("HFS") || fs_type == "MFS" || fs_type == "BeOS OFS"
}
pub fn is_blank_ostype(code: &[u8; 4]) -> bool {
code == &[0u8; 4]
}
pub fn type_creator_for_filename(name: &str) -> Option<([u8; 4], [u8; 4])> {
let ext = name.rsplit('.').next().unwrap_or("");
if ext == name {
return None; }
type_creator_for_extension(ext)
}
pub fn resolve_create_type_creator(
name: &str,
os_type: Option<[u8; 4]>,
os_creator: Option<[u8; 4]>,
type_code: Option<&str>,
creator_code: Option<&str>,
) -> ([u8; 4], [u8; 4]) {
let (dict_t, dict_c) = type_creator_for_filename(name).unwrap_or(([0; 4], [0; 4]));
let resolve = |raw: Option<[u8; 4]>, text: Option<&str>, dict: [u8; 4]| {
raw.filter(|c| !is_blank_ostype(c))
.or_else(|| text.filter(|s| !s.trim().is_empty()).map(encode_fourcc))
.unwrap_or(dict)
};
(
resolve(os_type, type_code, dict_t),
resolve(os_creator, creator_code, dict_c),
)
}
pub fn type_creator_for_extension(ext: &str) -> Option<([u8; 4], [u8; 4])> {
let lower = ext.to_lowercase();
let map = load_extension_map();
for (e, tc, cc) in &map.entries {
if e == &lower {
return Some((*tc, *cc));
}
}
None
}
pub fn encode_fourcc(s: &str) -> [u8; 4] {
let bytes = s.as_bytes();
let mut out = [b' '; 4];
for (i, &b) in bytes.iter().take(4).enumerate() {
out[i] = b;
}
out
}
pub use disk_core::display::decode_ostype;
pub use fs_mac_unicode::collate::{compare_hfs_keys, compare_hfsplus_keys};
pub fn compare_classic_extents_keys(a: &[u8], b: &[u8]) -> Ordering {
if a.len() < 8 || b.len() < 8 {
return a.len().cmp(&b.len());
}
let file_a = BigEndian::read_u32(&a[2..6]);
let file_b = BigEndian::read_u32(&b[2..6]);
match file_a.cmp(&file_b) {
Ordering::Equal => {}
other => return other,
}
match a[1].cmp(&b[1]) {
Ordering::Equal => {}
other => return other,
}
let start_a = BigEndian::read_u16(&a[6..8]);
let start_b = BigEndian::read_u16(&b[6..8]);
start_a.cmp(&start_b)
}
pub const BTREE_MIN_NODE_SIZE: usize = 512;
pub const BTREE_LEAF_NODE: i8 = -1;
pub const BTREE_INDEX_NODE: i8 = 0;
#[allow(dead_code)]
pub const BTREE_HEADER_NODE: i8 = 1;
#[allow(dead_code)]
pub const BTREE_MAP_NODE: i8 = 2;
pub const HFS_CAT_MAX_KEY_LEN: u8 = 0x25;
pub fn normalize_catalog_index_key(key: &[u8]) -> Vec<u8> {
let max = HFS_CAT_MAX_KEY_LEN as usize; let mut result = vec![0u8; 1 + max]; result[0] = HFS_CAT_MAX_KEY_LEN;
if key.len() > 1 {
let copy_len = (key.len() - 1).min(max);
result[1..1 + copy_len].copy_from_slice(&key[1..1 + copy_len]);
}
result
}
pub const KBT_BIG_KEYS_MASK: u32 = 0x0000_0002;
pub const KBT_VARIABLE_INDEX_KEYS_MASK: u32 = 0x0000_0004;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BTreeKeyFormat {
pub big_keys: bool,
pub variable_index_keys: bool,
pub max_key_len: u16,
}
impl BTreeKeyFormat {
pub const CLASSIC_CATALOG: BTreeKeyFormat = BTreeKeyFormat {
big_keys: false,
variable_index_keys: false,
max_key_len: HFS_CAT_MAX_KEY_LEN as u16,
};
pub const HFSPLUS_CATALOG: BTreeKeyFormat = BTreeKeyFormat {
big_keys: true,
variable_index_keys: true,
max_key_len: 516,
};
pub const HFSPLUS_EXTENTS: BTreeKeyFormat = BTreeKeyFormat {
big_keys: true,
variable_index_keys: false,
max_key_len: 10,
};
pub const HFSPLUS_ATTRIBUTES: BTreeKeyFormat = BTreeKeyFormat {
big_keys: true,
variable_index_keys: true,
max_key_len: 264,
};
pub const CLASSIC_EXTENTS: BTreeKeyFormat = BTreeKeyFormat {
big_keys: false,
variable_index_keys: false,
max_key_len: 7,
};
pub fn from_header(attributes: u32, max_key_len: u16) -> Self {
BTreeKeyFormat {
big_keys: attributes & KBT_BIG_KEYS_MASK != 0,
variable_index_keys: attributes & KBT_VARIABLE_INDEX_KEYS_MASK != 0,
max_key_len,
}
}
pub fn header_attributes(&self) -> u32 {
let mut a = 0;
if self.big_keys {
a |= KBT_BIG_KEYS_MASK;
}
if self.variable_index_keys {
a |= KBT_VARIABLE_INDEX_KEYS_MASK;
}
a
}
#[inline]
pub fn len_prefix(&self) -> usize {
if self.big_keys {
2
} else {
1
}
}
#[inline]
pub fn key_len(&self, record: &[u8]) -> usize {
if self.big_keys {
if record.len() < 2 {
0
} else {
BigEndian::read_u16(&record[0..2]) as usize
}
} else if record.is_empty() {
0
} else {
record[0] as usize
}
}
pub fn key_portion(&self, record: &[u8]) -> Vec<u8> {
let end = (self.len_prefix() + self.key_len(record)).min(record.len());
record[..end].to_vec()
}
pub fn make_index_key(&self, first_key: &[u8]) -> Vec<u8> {
if !self.big_keys {
if self.max_key_len == HFS_CAT_MAX_KEY_LEN as u16 {
return normalize_catalog_index_key(first_key);
}
let max = self.max_key_len as usize;
let mut result = vec![0u8; 1 + max];
result[0] = max as u8;
let copy = first_key.len().saturating_sub(1).min(max);
result[1..1 + copy].copy_from_slice(&first_key[1..1 + copy]);
return result;
}
if self.variable_index_keys {
return first_key.to_vec();
}
let max = self.max_key_len as usize;
let copy = self.key_len(first_key).min(max);
let mut result = vec![0u8; 2 + max];
BigEndian::write_u16(&mut result[0..2], max as u16);
if first_key.len() >= 2 + copy {
result[2..2 + copy].copy_from_slice(&first_key[2..2 + copy]);
}
result
}
}
pub struct BTreeHeader {
pub depth: u16,
pub root_node: u32,
pub leaf_records: u32,
pub first_leaf_node: u32,
pub last_leaf_node: u32,
pub node_size: u16,
pub total_nodes: u32,
pub free_nodes: u32,
}
impl BTreeHeader {
pub fn read(catalog_data: &[u8]) -> Self {
let d = &catalog_data[14..];
BTreeHeader {
depth: BigEndian::read_u16(&d[0..2]),
root_node: BigEndian::read_u32(&d[2..6]),
leaf_records: BigEndian::read_u32(&d[6..10]),
first_leaf_node: BigEndian::read_u32(&d[10..14]),
last_leaf_node: BigEndian::read_u32(&d[14..18]),
node_size: BigEndian::read_u16(&d[18..20]),
total_nodes: BigEndian::read_u32(&d[22..26]),
free_nodes: BigEndian::read_u32(&d[26..30]),
}
}
pub fn write(&self, catalog_data: &mut [u8]) {
let d = &mut catalog_data[14..];
BigEndian::write_u16(&mut d[0..2], self.depth);
BigEndian::write_u32(&mut d[2..6], self.root_node);
BigEndian::write_u32(&mut d[6..10], self.leaf_records);
BigEndian::write_u32(&mut d[10..14], self.first_leaf_node);
BigEndian::write_u32(&mut d[14..18], self.last_leaf_node);
BigEndian::write_u32(&mut d[22..26], self.total_nodes);
BigEndian::write_u32(&mut d[26..30], self.free_nodes);
}
}
pub fn btree_node_free_space(node: &[u8], node_size: usize) -> usize {
let num_records = BigEndian::read_u16(&node[10..12]) as usize;
if num_records == 0 {
let data_end = 14;
let table_start = node_size - 2 * (num_records + 1);
if table_start <= data_end + 2 {
return 0;
}
return table_start - data_end - 2;
}
let free_offset_pos = node_size - 2 * (num_records + 1);
let last_rec_end = BigEndian::read_u16(&node[free_offset_pos..free_offset_pos + 2]) as usize;
let table_start = free_offset_pos;
if table_start <= last_rec_end + 2 {
return 0;
}
table_start - last_rec_end - 2
}
fn index_child_ptr(node: &[u8], end: usize) -> u32 {
if end < 4 || end > node.len() {
return 0;
}
BigEndian::read_u32(&node[end - 4..end])
}
pub fn btree_record_range(node: &[u8], node_size: usize, rec_idx: usize) -> (usize, usize) {
let node_size = node_size.min(node.len());
let (Some(offset_pos), Some(next_pos)) = (
node_size.checked_sub(2 * (rec_idx + 1)),
node_size.checked_sub(2 * (rec_idx + 2)),
) else {
return (0, 0);
};
let start = (BigEndian::read_u16(&node[offset_pos..offset_pos + 2]) as usize).min(node_size);
let end = (BigEndian::read_u16(&node[next_pos..next_pos + 2]) as usize).min(node_size);
(start, start.max(end))
}
pub fn walk_leaf_records<T, F>(
catalog_data: &[u8],
first_leaf: u32,
node_size: usize,
mut visit: F,
) -> Option<T>
where
F: FnMut(u32, usize, usize, &[u8]) -> Option<T>,
{
if node_size < BTREE_MIN_NODE_SIZE || catalog_data.len() < node_size {
return None;
}
let mut node_idx = first_leaf;
let mut visited = std::collections::HashSet::new();
while node_idx != 0 {
if !visited.insert(node_idx) {
return None;
}
let offset = (node_idx as usize).checked_mul(node_size)?;
if offset + node_size > catalog_data.len() {
return None;
}
let node = &catalog_data[offset..offset + node_size];
let next = BigEndian::read_u32(&node[0..4]);
let kind = node[8] as i8;
if kind != BTREE_LEAF_NODE {
node_idx = next;
continue;
}
let num_records = BigEndian::read_u16(&node[10..12]) as usize;
for i in 0..num_records {
let (rec_start, rec_end) = btree_record_range(node, node_size, i);
if rec_start >= rec_end || rec_end > node_size {
continue;
}
if let Some(t) = visit(node_idx, i, offset + rec_start, &node[rec_start..rec_end]) {
return Some(t);
}
}
node_idx = next;
}
None
}
pub fn btree_insert_record<F>(
node: &mut [u8],
node_size: usize,
key_record_bytes: &[u8],
compare_fn: &F,
) -> Result<usize, FilesystemError>
where
F: Fn(&[u8], &[u8]) -> Ordering,
{
let num_records = BigEndian::read_u16(&node[10..12]) as usize;
let total_needed = key_record_bytes.len() + 2;
let free = btree_node_free_space(node, node_size);
if free < total_needed {
return Err(FilesystemError::InvalidData("B-tree node full".into()));
}
let mut records: Vec<Vec<u8>> = Vec::with_capacity(num_records + 1);
for i in 0..num_records {
let (rec_start, rec_end) = btree_record_range(node, node_size, i);
if rec_start < rec_end && rec_end <= node_size {
records.push(node[rec_start..rec_end].to_vec());
}
}
let mut insert_pos = records.len();
for (i, rec) in records.iter().enumerate() {
if compare_fn(rec, key_record_bytes) == Ordering::Greater {
insert_pos = i;
break;
}
}
records.insert(insert_pos, key_record_bytes.to_vec());
let mut write_pos = 14usize;
for (i, rec) in records.iter().enumerate() {
node[write_pos..write_pos + rec.len()].copy_from_slice(rec);
let opos = node_size - 2 * (i + 1);
BigEndian::write_u16(&mut node[opos..opos + 2], write_pos as u16);
write_pos += rec.len();
}
let fpos = node_size - 2 * (records.len() + 1);
BigEndian::write_u16(&mut node[fpos..fpos + 2], write_pos as u16);
let offset_table_start = node_size - 2 * (records.len() + 1);
if write_pos < offset_table_start {
node[write_pos..offset_table_start].fill(0);
}
BigEndian::write_u16(&mut node[10..12], records.len() as u16);
Ok(insert_pos)
}
pub fn btree_remove_record(node: &mut [u8], node_size: usize, rec_idx: usize) {
let num_records = BigEndian::read_u16(&node[10..12]) as usize;
if rec_idx >= num_records {
return;
}
let mut records: Vec<Vec<u8>> = Vec::with_capacity(num_records - 1);
for i in 0..num_records {
if i == rec_idx {
continue;
}
let (rec_start, rec_end) = btree_record_range(node, node_size, i);
if rec_start < rec_end && rec_end <= node_size {
records.push(node[rec_start..rec_end].to_vec());
}
}
let mut write_pos = 14usize;
for (i, rec) in records.iter().enumerate() {
node[write_pos..write_pos + rec.len()].copy_from_slice(rec);
let opos = node_size - 2 * (i + 1);
BigEndian::write_u16(&mut node[opos..opos + 2], write_pos as u16);
write_pos += rec.len();
}
let fpos = node_size - 2 * (records.len() + 1);
BigEndian::write_u16(&mut node[fpos..fpos + 2], write_pos as u16);
let offset_table_start = node_size - 2 * (records.len() + 1);
if write_pos < offset_table_start {
node[write_pos..offset_table_start].fill(0);
}
let clear_pos = node_size - 2 * (num_records + 1);
BigEndian::write_u16(&mut node[clear_pos..clear_pos + 2], 0);
BigEndian::write_u16(&mut node[10..12], records.len() as u16);
}
pub fn btree_node_bitmap_range(catalog_data: &[u8], node_size: usize) -> (usize, usize) {
let offset_pos = node_size - 2 * 3; let bitmap_start = BigEndian::read_u16(&catalog_data[offset_pos..offset_pos + 2]) as usize;
let next_pos = node_size - 2 * 4; let bitmap_end = BigEndian::read_u16(&catalog_data[next_pos..next_pos + 2]) as usize;
(bitmap_start, bitmap_end.saturating_sub(bitmap_start))
}
pub fn map_node_bitmap_bytes(node_size: usize) -> usize {
node_size.saturating_sub(18)
}
pub fn map_nodes_required(total_nodes: u32, node_size: usize) -> u32 {
let header_cap = ((node_size.saturating_sub(256)) * 8) as u32;
if total_nodes <= header_cap {
return 0;
}
let per_map = (map_node_bitmap_bytes(node_size) * 8) as u32;
if per_map == 0 {
return 0;
}
(total_nodes - header_cap).div_ceil(per_map)
}
#[derive(Debug, Clone, Copy)]
pub struct BitmapSegment {
pub byte_off: usize,
pub len: usize,
pub base_bit: u32,
}
pub fn btree_bitmap_segments(catalog_data: &[u8], node_size: usize) -> Vec<BitmapSegment> {
let mut segs = Vec::with_capacity(1);
if catalog_data.len() < node_size {
return segs;
}
let (hdr_off, hdr_len) = btree_node_bitmap_range(catalog_data, node_size);
segs.push(BitmapSegment {
byte_off: hdr_off,
len: hdr_len,
base_bit: 0,
});
let mut next_bit = (hdr_len as u32) * 8;
let mut next_node = BigEndian::read_u32(&catalog_data[0..4]);
let mut visited: std::collections::HashSet<u32> = std::collections::HashSet::with_capacity(8);
while next_node != 0 {
if !visited.insert(next_node) {
break;
}
let off = next_node as usize * node_size;
if off + node_size > catalog_data.len() {
break;
}
let kind = catalog_data[off + 8] as i8;
let num_records = BigEndian::read_u16(&catalog_data[off + 10..off + 12]);
if kind != 2 || num_records < 1 {
break;
}
let rec0_pos = off + node_size - 2;
let rec0_off = BigEndian::read_u16(&catalog_data[rec0_pos..rec0_pos + 2]) as usize;
let free_pos = off + node_size - 4;
let free_off = BigEndian::read_u16(&catalog_data[free_pos..free_pos + 2]) as usize;
if rec0_off < 14 || free_off <= rec0_off || free_off > node_size {
break;
}
let len = free_off - rec0_off;
segs.push(BitmapSegment {
byte_off: off + rec0_off,
len,
base_bit: next_bit,
});
next_bit += (len as u32) * 8;
next_node = BigEndian::read_u32(&catalog_data[off..off + 4]);
}
segs
}
fn locate_bit_in_segments(segs: &[BitmapSegment], global_bit: u32) -> Option<(usize, u32)> {
for (i, seg) in segs.iter().enumerate() {
let cap = (seg.len as u32) * 8;
if global_bit >= seg.base_bit && global_bit < seg.base_bit + cap {
return Some((i, global_bit - seg.base_bit));
}
}
None
}
pub fn btree_bitmap_test(catalog_data: &[u8], node_size: usize, bit: u32) -> bool {
let segs = btree_bitmap_segments(catalog_data, node_size);
let Some((idx, local)) = locate_bit_in_segments(&segs, bit) else {
return false;
};
let seg = segs[idx];
let slice = &catalog_data[seg.byte_off..seg.byte_off + seg.len];
bitmap_test_bit_be(slice, local)
}
pub fn btree_alloc_node(
catalog_data: &mut [u8],
node_size: usize,
total_nodes: u32,
) -> Result<u32, FilesystemError> {
let segs = btree_bitmap_segments(catalog_data, node_size);
for seg in &segs {
let bitmap = &catalog_data[seg.byte_off..seg.byte_off + seg.len];
let cap = (seg.len as u32) * 8;
let upper = total_nodes.saturating_sub(seg.base_bit).min(cap);
for local in 0..upper {
if !bitmap_test_bit_be(bitmap, local) {
let global = seg.base_bit + local;
bitmap_set_bit_be(
&mut catalog_data[seg.byte_off..seg.byte_off + seg.len],
local,
);
return Ok(global);
}
}
}
Err(FilesystemError::DiskFull("no free B-tree nodes".into()))
}
pub fn btree_detach_freed_node(data: &mut [u8], node_size: usize, freed: u32) -> u32 {
let mut header = BTreeHeader::read(data);
let max_nodes = (data.len() / node_size) as u32;
let mut freed_count = 0u32;
if header.root_node == freed {
header.root_node = 0;
header.depth = 0;
header.write(data);
return 0;
}
let mut target = freed;
loop {
let mut found = None;
'scan: for idx in 1..max_nodes {
if idx == target || !btree_bitmap_test(data, node_size, idx) {
continue;
}
let off = idx as usize * node_size;
let node = &data[off..off + node_size];
if node[8] as i8 != BTREE_INDEX_NODE {
continue;
}
let n = BigEndian::read_u16(&node[10..12]) as usize;
for r in 0..n {
let (_, end) = btree_record_range(node, node_size, r);
if index_child_ptr(node, end) == target {
found = Some((idx, r));
break 'scan;
}
}
}
let Some((idx, r)) = found else {
break;
};
let off = idx as usize * node_size;
btree_remove_record(&mut data[off..off + node_size], node_size, r);
if BigEndian::read_u16(&data[off + 10..off + 12]) > 0 {
break;
}
btree_free_node(data, node_size, idx);
freed_count += 1;
if header.root_node == idx {
header.root_node = 0;
header.depth = 0;
header.free_nodes += freed_count;
header.write(data);
return freed_count;
}
target = idx;
}
while header.depth > 1 && header.root_node != 0 {
let off = header.root_node as usize * node_size;
let node = &data[off..off + node_size];
if node[8] as i8 != BTREE_INDEX_NODE || BigEndian::read_u16(&node[10..12]) != 1 {
break;
}
let (_, end) = btree_record_range(node, node_size, 0);
let child = index_child_ptr(node, end);
if child == 0 {
break;
}
btree_free_node(data, node_size, header.root_node);
freed_count += 1;
header.root_node = child;
header.depth -= 1;
}
header.free_nodes += freed_count;
header.write(data);
freed_count
}
pub fn btree_find_parent(data: &[u8], node_size: usize, child: u32) -> Option<(u32, usize)> {
let max_nodes = (data.len() / node_size) as u32;
for idx in 1..max_nodes {
if idx == child || !btree_bitmap_test(data, node_size, idx) {
continue;
}
let off = idx as usize * node_size;
let node = &data[off..off + node_size];
if node[8] as i8 != BTREE_INDEX_NODE {
continue;
}
let n = BigEndian::read_u16(&node[10..12]) as usize;
for r in 0..n {
let (_, end) = btree_record_range(node, node_size, r);
if index_child_ptr(node, end) == child {
return Some((idx, r));
}
}
}
None
}
fn btree_parent_chain(data: &[u8], node_size: usize, node: u32) -> Vec<(u32, u32)> {
let mut chain = Vec::new();
let mut cur = node;
while cur != 0 && chain.len() < 64 {
let parent = btree_find_parent(data, node_size, cur).map_or(0, |(p, _)| p);
chain.push((cur, parent));
cur = parent;
}
chain.reverse();
chain
}
pub fn btree_refresh_index_keys<F>(
data: &mut [u8],
node_size: usize,
mut node_idx: u32,
kf: &BTreeKeyFormat,
cmp: &F,
) -> Result<(), FilesystemError>
where
F: Fn(&[u8], &[u8]) -> Ordering,
{
let max_nodes = (data.len() / node_size) as u32;
for _ in 0..64 {
if node_idx == 0 || node_idx >= max_nodes {
return Ok(());
}
let first_key = {
let off = node_idx as usize * node_size;
let node = &data[off..off + node_size];
if BigEndian::read_u16(&node[10..12]) == 0 {
return Ok(());
}
let (s, e) = btree_record_range(node, node_size, 0);
kf.key_portion(&node[s..e])
};
let Some((parent, rec_idx)) = btree_find_parent(data, node_size, node_idx) else {
return Ok(());
};
let mut wanted = kf.make_index_key(&first_key);
let mut ptr = [0u8; 4];
BigEndian::write_u32(&mut ptr, node_idx);
wanted.extend_from_slice(&ptr);
let parent_off = parent as usize * node_size;
let replaced = {
let pnode = &mut data[parent_off..parent_off + node_size];
let (s, e) = btree_record_range(pnode, node_size, rec_idx);
if pnode[s..e] == wanted[..] {
return Ok(());
}
btree_remove_record(pnode, node_size, rec_idx);
btree_insert_record(pnode, node_size, &wanted, cmp).is_ok()
};
if !replaced {
let mut header = BTreeHeader::read(data);
let chain = btree_parent_chain(data, node_size, parent);
btree_insert_into_index(
data,
node_size,
parent,
node_idx,
&first_key,
&mut header,
kf,
cmp,
&chain,
)?;
header.write(data);
}
node_idx = parent;
}
Ok(())
}
pub fn btree_detach_freed_node_and_refresh<F>(
data: &mut [u8],
node_size: usize,
freed: u32,
kf: &BTreeKeyFormat,
cmp: &F,
) -> Result<u32, FilesystemError>
where
F: Fn(&[u8], &[u8]) -> Ordering,
{
let parent = btree_find_parent(data, node_size, freed);
let freed_count = btree_detach_freed_node(data, node_size, freed);
if let Some((parent, 0)) = parent {
if btree_bitmap_test(data, node_size, parent) {
btree_refresh_index_keys(data, node_size, parent, kf, cmp)?;
}
}
Ok(freed_count)
}
pub fn btree_stale_index_keys<F>(
data: &[u8],
node_size: usize,
kf: &BTreeKeyFormat,
cmp: &F,
) -> Vec<(u32, usize, u32, Ordering)>
where
F: Fn(&[u8], &[u8]) -> Ordering,
{
let mut stale = Vec::new();
if node_size < BTREE_MIN_NODE_SIZE || data.len() < node_size {
return stale;
}
let max_nodes = (data.len() / node_size) as u32;
for idx in 1..max_nodes {
if !btree_bitmap_test(data, node_size, idx) {
continue;
}
let off = idx as usize * node_size;
let node = &data[off..off + node_size];
if node[8] as i8 != BTREE_INDEX_NODE {
continue;
}
let n = BigEndian::read_u16(&node[10..12]) as usize;
for r in 0..n {
let (s, e) = btree_record_range(node, node_size, r);
if e < s + 4 || e > node_size {
continue;
}
let child = index_child_ptr(node, e);
if child == 0 || child >= max_nodes || !btree_bitmap_test(data, node_size, child) {
continue;
}
let coff = child as usize * node_size;
let cnode = &data[coff..coff + node_size];
if BigEndian::read_u16(&cnode[10..12]) == 0 {
continue;
}
let (cs, ce) = btree_record_range(cnode, node_size, 0);
let wanted = kf.make_index_key(&kf.key_portion(&cnode[cs..ce]));
let order = cmp(&node[s..e - 4], &wanted);
if order != Ordering::Equal {
stale.push((idx, r, child, order));
}
}
}
stale
}
pub fn btree_retire_empty_leaf<F>(
data: &mut [u8],
node_size: usize,
node_idx: u32,
kf: &BTreeKeyFormat,
cmp: &F,
) -> Result<u32, FilesystemError>
where
F: Fn(&[u8], &[u8]) -> Ordering,
{
let off = node_idx as usize * node_size;
if off + node_size > data.len() || BigEndian::read_u16(&data[off + 10..off + 12]) != 0 {
return Ok(0);
}
let flink = BigEndian::read_u32(&data[off..off + 4]);
let blink = BigEndian::read_u32(&data[off + 4..off + 8]);
if blink != 0 {
let b = blink as usize * node_size;
BigEndian::write_u32(&mut data[b..b + 4], flink);
}
if flink != 0 {
let f = flink as usize * node_size;
BigEndian::write_u32(&mut data[f + 4..f + 8], blink);
}
let mut h = BTreeHeader::read(data);
if h.first_leaf_node == node_idx {
h.first_leaf_node = flink;
}
if h.last_leaf_node == node_idx {
h.last_leaf_node = blink;
}
h.free_nodes += 1;
h.write(data);
btree_free_node(data, node_size, node_idx);
let freed = btree_detach_freed_node_and_refresh(data, node_size, node_idx, kf, cmp)?;
Ok(freed + 1)
}
pub fn btree_lonely_empty_leaves(data: &[u8], node_size: usize) -> Vec<u32> {
let mut lonely = Vec::new();
if node_size < BTREE_MIN_NODE_SIZE || data.len() < node_size {
return lonely;
}
let max_nodes = (data.len() / node_size) as u32;
for idx in 1..max_nodes {
if !btree_bitmap_test(data, node_size, idx) {
continue;
}
let off = idx as usize * node_size;
let node = &data[off..off + node_size];
if node[8] as i8 == BTREE_LEAF_NODE
&& BigEndian::read_u16(&node[10..12]) == 0
&& BigEndian::read_u32(&node[0..4]) == 0
&& BigEndian::read_u32(&node[4..8]) == 0
{
lonely.push(idx);
}
}
lonely
}
pub fn btree_free_node(catalog_data: &mut [u8], node_size: usize, node_idx: u32) {
let segs = btree_bitmap_segments(catalog_data, node_size);
let Some((idx, local)) = locate_bit_in_segments(&segs, node_idx) else {
return;
};
let seg = segs[idx];
bitmap_clear_bit_be(
&mut catalog_data[seg.byte_off..seg.byte_off + seg.len],
local,
);
let off = node_idx as usize * node_size;
if node_idx != 0 && off + node_size <= catalog_data.len() {
catalog_data[off..off + node_size].fill(0);
}
}
pub fn btree_unerased_free_nodes(data: &[u8], node_size: usize) -> Vec<u32> {
let mut dirty = Vec::new();
if node_size < BTREE_MIN_NODE_SIZE || data.len() < node_size {
return dirty;
}
let max_nodes = (data.len() / node_size) as u32;
for idx in 1..max_nodes {
if btree_bitmap_test(data, node_size, idx) {
continue;
}
let off = idx as usize * node_size;
if data[off..off + node_size].iter().any(|&b| b != 0) {
dirty.push(idx);
}
}
dirty
}
pub fn btree_erase_free_nodes(data: &mut [u8], node_size: usize) -> usize {
let dirty = btree_unerased_free_nodes(data, node_size);
for &idx in &dirty {
let off = idx as usize * node_size;
data[off..off + node_size].fill(0);
}
dirty.len()
}
pub fn btree_bitmap_clear(catalog_data: &mut [u8], node_size: usize, node_idx: u32) {
let segs = btree_bitmap_segments(catalog_data, node_size);
let Some((idx, local)) = locate_bit_in_segments(&segs, node_idx) else {
return;
};
let seg = segs[idx];
bitmap_clear_bit_be(
&mut catalog_data[seg.byte_off..seg.byte_off + seg.len],
local,
);
}
pub fn btree_bitmap_set(catalog_data: &mut [u8], node_size: usize, node_idx: u32) {
let segs = btree_bitmap_segments(catalog_data, node_size);
let Some((idx, local)) = locate_bit_in_segments(&segs, node_idx) else {
return;
};
let seg = segs[idx];
bitmap_set_bit_be(
&mut catalog_data[seg.byte_off..seg.byte_off + seg.len],
local,
);
}
pub fn btree_map_chain_tail(buf: &[u8], node_size: usize) -> u32 {
let mut next = BigEndian::read_u32(&buf[0..4]); let mut last = 0u32;
let mut guard = 0;
while next != 0 && guard < 1_000_000 {
last = next;
let off = next as usize * node_size;
if off + 4 > buf.len() {
break;
}
next = BigEndian::read_u32(&buf[off..off + 4]);
guard += 1;
}
last
}
pub fn btree_grow_blocks(
total_nodes: u32,
add_slots: u32,
node_size: usize,
block_size: u32,
) -> u32 {
let node_bytes = node_size as u64;
let cur_blocks = (total_nodes as u64 * node_bytes).div_ceil(block_size as u64);
let new_blocks = ((total_nodes + add_slots) as u64 * node_bytes).div_ceil(block_size as u64);
(new_blocks - cur_blocks) as u32
}
pub fn init_map_node(
catalog_data: &mut [u8],
node_size: usize,
node_idx: u32,
prev_link: u32,
next_link: u32,
) {
let off = node_idx as usize * node_size;
let node = &mut catalog_data[off..off + node_size];
node.fill(0);
BigEndian::write_u32(&mut node[0..4], next_link);
BigEndian::write_u32(&mut node[4..8], prev_link);
node[8] = 2; node[9] = 0;
BigEndian::write_u16(&mut node[10..12], 1);
let bitmap_bytes = map_node_bitmap_bytes(node_size) as u16;
let free_off = 14u16 + bitmap_bytes;
BigEndian::write_u16(&mut node[node_size - 2..node_size], 14);
BigEndian::write_u16(&mut node[node_size - 4..node_size - 2], free_off);
}
pub(crate) fn init_node(
catalog_data: &mut [u8],
node_size: usize,
node_idx: u32,
kind: i8,
height: u8,
) {
let offset = node_idx as usize * node_size;
let node = &mut catalog_data[offset..offset + node_size];
node.fill(0);
node[8] = kind as u8; node[9] = height; BigEndian::write_u16(&mut node[10..12], 0); BigEndian::write_u16(&mut node[node_size - 2..node_size], 14);
}
pub fn btree_split_leaf_with_insert<F>(
catalog_data: &mut [u8],
node_size: usize,
node_idx: u32,
header: &mut BTreeHeader,
new_record: &[u8],
kf: &BTreeKeyFormat,
compare_fn: &F,
) -> Result<Vec<(u32, Vec<u8>)>, FilesystemError>
where
F: Fn(&[u8], &[u8]) -> Ordering,
{
let old_offset = node_idx as usize * node_size;
let old_num = BigEndian::read_u16(&catalog_data[old_offset + 10..old_offset + 12]) as usize;
let old_height = catalog_data[old_offset + 9];
let mut records: Vec<Vec<u8>> = Vec::with_capacity(old_num + 1);
for i in 0..old_num {
let (s, e) = btree_record_range(
&catalog_data[old_offset..old_offset + node_size],
node_size,
i,
);
records.push(catalog_data[old_offset + s..old_offset + e].to_vec());
}
let mut insert_pos = records.len();
for (i, rec) in records.iter().enumerate() {
if compare_fn(rec, new_record) == Ordering::Greater {
insert_pos = i;
break;
}
}
records.insert(insert_pos, new_record.to_vec());
let payload = node_size.saturating_sub(16);
for rec in &records {
if rec.len() + 2 > payload {
return Err(FilesystemError::InvalidData(format!(
"btree_split_leaf_with_insert: record of {} bytes exceeds node payload {}",
rec.len(),
payload
)));
}
}
let mut cuts: Vec<usize> = Vec::new();
let mut cur_bytes: usize = 0;
for (i, rec) in records.iter().enumerate() {
let cost = rec.len() + 2;
if i > 0 && cur_bytes + cost > payload {
cuts.push(i);
cur_bytes = 0;
}
cur_bytes += cost;
}
if cuts.is_empty() {
cuts.push(
(records.len() / 2)
.max(1)
.min(records.len().saturating_sub(1)),
);
}
let is_append = insert_pos == old_num;
if cuts.len() == 1 && !is_append {
let total: usize = records.iter().map(|r| r.len() + 2).sum();
let mut prefix = 0usize;
let mut best_k = cuts[0];
let mut best_diff = usize::MAX;
for k in 1..records.len() {
prefix += records[k - 1].len() + 2;
let right = total - prefix;
if prefix <= payload && right <= payload {
let diff = prefix.abs_diff(total / 2);
if diff < best_diff {
best_diff = diff;
best_k = k;
}
}
}
cuts[0] = best_k;
}
let mut chunks: Vec<Vec<Vec<u8>>> = Vec::with_capacity(cuts.len() + 1);
let mut prev = 0usize;
for &cut in &cuts {
chunks.push(records.drain(0..cut - prev).collect());
prev = cut;
}
chunks.push(records);
let new_count = chunks.len() - 1;
let mut new_indices: Vec<u32> = Vec::with_capacity(new_count);
for _ in 0..new_count {
let idx = btree_alloc_node(catalog_data, node_size, header.total_nodes)?;
header.free_nodes -= 1;
init_node(catalog_data, node_size, idx, BTREE_LEAF_NODE, old_height);
new_indices.push(idx);
}
let write_chunk = |data: &mut [u8], target_idx: u32, chunk: &[Vec<u8>]| {
let off = target_idx as usize * node_size;
let node = &mut data[off..off + node_size];
BigEndian::write_u16(&mut node[10..12], 0);
BigEndian::write_u16(&mut node[node_size - 2..node_size], 14);
let mut write_pos = 14usize;
for (i, rec) in chunk.iter().enumerate() {
node[write_pos..write_pos + rec.len()].copy_from_slice(rec);
let opos = node_size - 2 * (i + 1);
BigEndian::write_u16(&mut node[opos..opos + 2], write_pos as u16);
write_pos += rec.len();
}
let fpos = node_size - 2 * (chunk.len() + 1);
BigEndian::write_u16(&mut node[fpos..fpos + 2], write_pos as u16);
BigEndian::write_u16(&mut node[10..12], chunk.len() as u16);
};
write_chunk(catalog_data, node_idx, &chunks[0]);
for (i, idx) in new_indices.iter().enumerate() {
write_chunk(catalog_data, *idx, &chunks[i + 1]);
}
let old_next = BigEndian::read_u32(&catalog_data[old_offset..old_offset + 4]);
let mut prev_idx = node_idx;
for &idx in &new_indices {
let prev_off = prev_idx as usize * node_size;
let cur_off = idx as usize * node_size;
BigEndian::write_u32(&mut catalog_data[prev_off..prev_off + 4], idx);
BigEndian::write_u32(&mut catalog_data[cur_off + 4..cur_off + 8], prev_idx);
prev_idx = idx;
}
let last_idx = *new_indices.last().expect("at least one new node");
let last_off = last_idx as usize * node_size;
BigEndian::write_u32(&mut catalog_data[last_off..last_off + 4], old_next);
if old_next != 0 {
let next_offset = old_next as usize * node_size;
BigEndian::write_u32(
&mut catalog_data[next_offset + 4..next_offset + 8],
last_idx,
);
}
if header.last_leaf_node == node_idx {
header.last_leaf_node = last_idx;
}
let mut results: Vec<(u32, Vec<u8>)> = Vec::with_capacity(new_count);
for (i, idx) in new_indices.iter().enumerate() {
let split_key = kf.key_portion(&chunks[i + 1][0]);
results.push((*idx, split_key));
}
Ok(results)
}
fn btree_write_node_records(data: &mut [u8], node_size: usize, idx: u32, recs: &[Vec<u8>]) {
let off = idx as usize * node_size;
let node = &mut data[off..off + node_size];
let mut write_pos = 14usize;
for (i, rec) in recs.iter().enumerate() {
node[write_pos..write_pos + rec.len()].copy_from_slice(rec);
let opos = node_size - 2 * (i + 1);
BigEndian::write_u16(&mut node[opos..opos + 2], write_pos as u16);
write_pos += rec.len();
}
let fpos = node_size - 2 * (recs.len() + 1);
BigEndian::write_u16(&mut node[fpos..fpos + 2], write_pos as u16);
if write_pos < fpos {
node[write_pos..fpos].fill(0);
}
BigEndian::write_u16(&mut node[10..12], recs.len() as u16);
}
fn index_node_has_child(data: &[u8], node_size: usize, parent_idx: u32, child: u32) -> bool {
if parent_idx == 0 {
return false;
}
let off = parent_idx as usize * node_size;
if off + node_size > data.len() {
return false;
}
let num = BigEndian::read_u16(&data[off + 10..off + 12]) as usize;
for i in 0..num {
let (s, e) = btree_record_range(&data[off..off + node_size], node_size, i);
if e < s + 4 || e > node_size {
continue;
}
if BigEndian::read_u32(&data[off + e - 4..off + e]) == child {
return true;
}
}
false
}
#[must_use]
fn btree_update_index_separator(
data: &mut [u8],
node_size: usize,
parent_idx: u32,
child: u32,
new_first_key: &[u8],
kf: &BTreeKeyFormat,
) -> bool {
let off = parent_idx as usize * node_size;
if off + node_size > data.len() {
return false;
}
let num = BigEndian::read_u16(&data[off + 10..off + 12]) as usize;
for i in 0..num {
let (s, e) = btree_record_range(&data[off..off + node_size], node_size, i);
if e < s + 4 || e > node_size {
continue;
}
if BigEndian::read_u32(&data[off + e - 4..off + e]) == child {
let new_key = kf.make_index_key(new_first_key);
let key_bytes = e - s - 4;
if new_key.len() != key_bytes {
return false;
}
data[off + s..off + s + key_bytes].copy_from_slice(&new_key);
return true;
}
}
false
}
fn btree_try_rotate_leaf<F>(
data: &mut [u8],
node_size: usize,
leaf_idx: u32,
parent_idx: u32,
new_record: &[u8],
kf: &BTreeKeyFormat,
cmp: &F,
) -> bool
where
F: Fn(&[u8], &[u8]) -> Ordering,
{
let payload = node_size.saturating_sub(16);
let cost = |r: &Vec<u8>| r.len() + 2;
let first_key = |r: &[u8]| -> Vec<u8> { kf.key_portion(r) };
let read_records = |data: &[u8], idx: u32| -> Vec<Vec<u8>> {
let off = idx as usize * node_size;
let num = BigEndian::read_u16(&data[off + 10..off + 12]) as usize;
let mut v = Vec::with_capacity(num);
for i in 0..num {
let (s, e) = btree_record_range(&data[off..off + node_size], node_size, i);
v.push(data[off + s..off + e].to_vec());
}
v
};
let loff = leaf_idx as usize * node_size;
let rsib = BigEndian::read_u32(&data[loff..loff + 4]);
let lsib = BigEndian::read_u32(&data[loff + 4..loff + 8]);
let leaf_recs = read_records(data, leaf_idx);
for (sib_is_right, sib) in [(true, rsib), (false, lsib)] {
if sib == 0 || !index_node_has_child(data, node_size, parent_idx, sib) {
continue;
}
let sib_recs = read_records(data, sib);
let (left_idx, right_idx, left_recs, right_recs) = if sib_is_right {
(leaf_idx, sib, &leaf_recs, &sib_recs)
} else {
(sib, leaf_idx, &sib_recs, &leaf_recs)
};
let Some(left_first) = left_recs.first() else {
continue;
};
let mut pool: Vec<Vec<u8>> = left_recs.iter().chain(right_recs.iter()).cloned().collect();
let mut p = pool.len();
for (i, r) in pool.iter().enumerate() {
if cmp(r, new_record) == Ordering::Greater {
p = i;
break;
}
}
pool.insert(p, new_record.to_vec());
if cmp(&pool[0], left_first) != Ordering::Equal {
continue;
}
let total: usize = pool.iter().map(cost).sum();
if total > 2 * payload {
continue; }
let mut prefix = 0usize;
let mut best_p = 0usize;
let mut best_diff = usize::MAX;
for cut in 1..pool.len() {
prefix += cost(&pool[cut - 1]);
let right = total - prefix;
if prefix <= payload && right <= payload {
let diff = prefix.abs_diff(total / 2);
if diff < best_diff {
best_diff = diff;
best_p = cut;
}
}
}
if best_p == 0 {
continue;
}
let left: Vec<Vec<u8>> = pool[..best_p].to_vec();
let right: Vec<Vec<u8>> = pool[best_p..].to_vec();
let right_first = first_key(&right[0]);
if !btree_update_index_separator(data, node_size, parent_idx, right_idx, &right_first, kf) {
continue;
}
btree_write_node_records(data, node_size, left_idx, &left);
btree_write_node_records(data, node_size, right_idx, &right);
return true;
}
false
}
fn btree_split_index_with_insert<F>(
catalog_data: &mut [u8],
node_size: usize,
node_idx: u32,
header: &mut BTreeHeader,
new_record: &[u8],
kf: &BTreeKeyFormat,
compare_fn: &F,
) -> Result<(u32, Vec<u8>), FilesystemError>
where
F: Fn(&[u8], &[u8]) -> Ordering,
{
let old_offset = node_idx as usize * node_size;
let old_num = BigEndian::read_u16(&catalog_data[old_offset + 10..old_offset + 12]) as usize;
let old_height = catalog_data[old_offset + 9];
let mut records: Vec<Vec<u8>> = Vec::with_capacity(old_num + 1);
for i in 0..old_num {
let (s, e) = btree_record_range(
&catalog_data[old_offset..old_offset + node_size],
node_size,
i,
);
records.push(catalog_data[old_offset + s..old_offset + e].to_vec());
}
let mut insert_pos = records.len();
for (i, rec) in records.iter().enumerate() {
if compare_fn(rec, new_record) == Ordering::Greater {
insert_pos = i;
break;
}
}
records.insert(insert_pos, new_record.to_vec());
let payload = node_size.saturating_sub(16);
let cost = |r: &Vec<u8>| r.len() + 2;
let total: usize = records.iter().map(cost).sum();
let mut cut = {
let mut acc = 0usize;
let mut k = 1usize;
for (i, r) in records.iter().enumerate() {
let c = cost(r);
if i > 0 && acc + c > payload {
break;
}
acc += c;
k = i + 1;
}
k.clamp(1, records.len() - 1)
};
let is_append = insert_pos == old_num;
if !is_append {
let mut prefix = 0usize;
let mut best = cut;
let mut best_diff = usize::MAX;
for k in 1..records.len() {
prefix += cost(&records[k - 1]);
let right = total - prefix;
if prefix <= payload && right <= payload {
let diff = prefix.abs_diff(total / 2);
if diff < best_diff {
best_diff = diff;
best = k;
}
}
}
cut = best;
}
let new_idx = btree_alloc_node(catalog_data, node_size, header.total_nodes)?;
header.free_nodes -= 1;
init_node(
catalog_data,
node_size,
new_idx,
BTREE_INDEX_NODE,
old_height,
);
let left: Vec<Vec<u8>> = records[..cut].to_vec();
let right: Vec<Vec<u8>> = records[cut..].to_vec();
btree_write_node_records(catalog_data, node_size, node_idx, &left);
btree_write_node_records(catalog_data, node_size, new_idx, &right);
let old_next = BigEndian::read_u32(&catalog_data[old_offset..old_offset + 4]);
let new_offset = new_idx as usize * node_size;
BigEndian::write_u32(&mut catalog_data[old_offset..old_offset + 4], new_idx);
BigEndian::write_u32(&mut catalog_data[new_offset + 4..new_offset + 8], node_idx);
BigEndian::write_u32(&mut catalog_data[new_offset..new_offset + 4], old_next);
if old_next != 0 {
let next_off = old_next as usize * node_size;
BigEndian::write_u32(&mut catalog_data[next_off + 4..next_off + 8], new_idx);
}
let split_key = kf.key_portion(&right[0]);
Ok((new_idx, split_key))
}
#[allow(clippy::too_many_arguments)] pub fn btree_insert_into_index<F>(
catalog_data: &mut [u8],
node_size: usize,
index_node_idx: u32,
child_node: u32,
split_key: &[u8],
header: &mut BTreeHeader,
kf: &BTreeKeyFormat,
compare_fn: &F,
parent_chain: &[(u32, u32)], ) -> Result<(), FilesystemError>
where
F: Fn(&[u8], &[u8]) -> Ordering,
{
let mut index_record = kf.make_index_key(split_key);
let mut ptr = [0u8; 4];
BigEndian::write_u32(&mut ptr, child_node);
index_record.extend_from_slice(&ptr);
let offset = index_node_idx as usize * node_size;
let node = &mut catalog_data[offset..offset + node_size];
match btree_insert_record(node, node_size, &index_record, compare_fn) {
Ok(_) => Ok(()),
Err(_) => {
if let Some(&(_, parent_idx)) = parent_chain
.iter()
.find(|&&(nidx, _)| nidx == index_node_idx)
{
if parent_idx != 0
&& btree_try_rotate_leaf(
catalog_data,
node_size,
index_node_idx,
parent_idx,
&index_record,
kf,
compare_fn,
)
{
return Ok(());
}
}
let (new_idx, new_split_key) = btree_split_index_with_insert(
catalog_data,
node_size,
index_node_idx,
header,
&index_record,
kf,
compare_fn,
)?;
if let Some(&(_, parent_idx)) = parent_chain
.iter()
.find(|&&(nidx, _)| nidx == index_node_idx)
{
if parent_idx == 0 && index_node_idx == header.root_node {
btree_grow_root(
catalog_data,
node_size,
header,
index_node_idx,
new_idx,
&new_split_key,
kf,
)?;
} else {
btree_insert_into_index(
catalog_data,
node_size,
parent_idx,
new_idx,
&new_split_key,
header,
kf,
compare_fn,
parent_chain,
)?;
}
} else {
btree_grow_root(
catalog_data,
node_size,
header,
index_node_idx,
new_idx,
&new_split_key,
kf,
)?;
}
Ok(())
}
}
}
pub fn btree_grow_root(
catalog_data: &mut [u8],
node_size: usize,
header: &mut BTreeHeader,
old_root: u32,
new_sibling: u32,
split_key: &[u8],
kf: &BTreeKeyFormat,
) -> Result<(), FilesystemError> {
let new_root = btree_alloc_node(catalog_data, node_size, header.total_nodes)?;
header.free_nodes -= 1;
let old_root_offset = old_root as usize * node_size;
let old_height = catalog_data[old_root_offset + 9];
init_node(
catalog_data,
node_size,
new_root,
BTREE_INDEX_NODE,
old_height + 1,
);
let new_root_offset = new_root as usize * node_size;
let (first_rec_start, first_rec_end) = btree_record_range(
&catalog_data[old_root_offset..old_root_offset + node_size],
node_size,
0,
);
let first_key = kf.key_portion(
&catalog_data[old_root_offset + first_rec_start..old_root_offset + first_rec_end],
);
let mut rec1 = kf.make_index_key(&first_key);
let mut ptr1 = [0u8; 4];
BigEndian::write_u32(&mut ptr1, old_root);
rec1.extend_from_slice(&ptr1);
let mut rec2 = kf.make_index_key(split_key);
let mut ptr2 = [0u8; 4];
BigEndian::write_u32(&mut ptr2, new_sibling);
rec2.extend_from_slice(&ptr2);
let node = &mut catalog_data[new_root_offset..new_root_offset + node_size];
let mut write_pos = 14usize;
node[write_pos..write_pos + rec1.len()].copy_from_slice(&rec1);
BigEndian::write_u16(&mut node[node_size - 2..node_size], 14);
write_pos += rec1.len();
node[write_pos..write_pos + rec2.len()].copy_from_slice(&rec2);
BigEndian::write_u16(&mut node[node_size - 4..node_size - 2], write_pos as u16);
write_pos += rec2.len();
BigEndian::write_u16(&mut node[node_size - 6..node_size - 4], write_pos as u16);
BigEndian::write_u16(&mut node[10..12], 2);
header.root_node = new_root;
header.depth += 1;
Ok(())
}
#[allow(dead_code)]
#[allow(clippy::type_complexity)] pub fn btree_find_record<K, C>(
catalog_data: &[u8],
header: &BTreeHeader,
search_key: &[u8],
key_extract_fn: &K,
compare_fn: &C,
) -> (Option<(u32, usize, usize)>, Vec<(u32, u32)>)
where
K: Fn(&[u8]) -> &[u8],
C: Fn(&[u8], &[u8]) -> Ordering,
{
let node_size = header.node_size as usize;
let mut parent_chain: Vec<(u32, u32)> = Vec::new();
let mut current_node = header.root_node;
let mut parent_node = 0u32;
let mut visited = std::collections::HashSet::new();
loop {
if !visited.insert(current_node) {
return (None, parent_chain);
}
let offset = current_node as usize * node_size;
if offset + node_size > catalog_data.len() {
return (None, parent_chain);
}
let node = &catalog_data[offset..offset + node_size];
let kind = node[8] as i8;
let num_records = BigEndian::read_u16(&node[10..12]) as usize;
parent_chain.push((current_node, parent_node));
if kind == BTREE_LEAF_NODE {
for i in 0..num_records {
let (start, end) = btree_record_range(node, node_size, i);
if start >= end || end > node_size {
continue;
}
let rec_bytes = &node[start..end];
let key = key_extract_fn(rec_bytes);
match compare_fn(key, search_key) {
Ordering::Equal => {
return (Some((current_node, i, offset + start)), parent_chain);
}
Ordering::Greater => return (None, parent_chain),
_ => {}
}
}
return (None, parent_chain);
} else if kind == BTREE_INDEX_NODE {
let mut next_child = 0u32;
for i in 0..num_records {
let (start, end) = btree_record_range(node, node_size, i);
if end < start + 4 || end > node_size {
continue;
}
let rec_bytes = &node[start..end];
let key = key_extract_fn(rec_bytes);
let child_ptr = index_child_ptr(node, end);
match compare_fn(key, search_key) {
Ordering::Greater => {
break;
}
_ => {
next_child = child_ptr;
}
}
}
if next_child == 0 && num_records > 0 {
let (_, end) = btree_record_range(node, node_size, 0);
next_child = index_child_ptr(node, end);
}
parent_node = current_node;
current_node = next_child;
} else {
return (None, parent_chain);
}
}
}
pub fn btree_find_insert_leaf<C>(
catalog_data: &[u8],
header: &BTreeHeader,
search_key: &[u8],
compare_fn: &C,
) -> (u32, Vec<(u32, u32)>)
where
C: Fn(&[u8], &[u8]) -> Ordering,
{
let node_size = header.node_size as usize;
let mut parent_chain: Vec<(u32, u32)> = Vec::new();
let mut current_node = header.root_node;
let mut parent_node = 0u32;
let mut visited = std::collections::HashSet::new();
loop {
if !visited.insert(current_node) {
return (header.first_leaf_node, parent_chain);
}
let offset = current_node as usize * node_size;
if offset + node_size > catalog_data.len() {
return (current_node, parent_chain);
}
let node = &catalog_data[offset..offset + node_size];
let kind = node[8] as i8;
let num_records = BigEndian::read_u16(&node[10..12]) as usize;
parent_chain.push((current_node, parent_node));
if kind == BTREE_LEAF_NODE {
return (current_node, parent_chain);
}
let mut next_child = 0u32;
for i in 0..num_records {
let (start, end) = btree_record_range(node, node_size, i);
if end < start + 4 || end > node_size {
continue;
}
let key_portion = &node[start..end - 4];
let child_ptr = index_child_ptr(node, end);
if compare_fn(key_portion, search_key) != Ordering::Greater {
next_child = child_ptr;
} else {
break;
}
}
if next_child == 0 && num_records > 0 {
let (_, end) = btree_record_range(node, node_size, 0);
next_child = index_child_ptr(node, end);
}
parent_node = current_node;
current_node = next_child;
}
}
pub fn btree_insert_full<F>(
data: &mut [u8],
key_record: &[u8],
kf: &BTreeKeyFormat,
cmp: &F,
) -> Result<(), disk_core::filesystem::FilesystemError>
where
F: Fn(&[u8], &[u8]) -> Ordering,
{
let mut header = BTreeHeader::read(data);
let node_size = header.node_size as usize;
if node_size < BTREE_MIN_NODE_SIZE {
return Err(disk_core::filesystem::FilesystemError::InvalidData(
format!("B-tree has invalid node_size {node_size}"),
));
}
if header.depth == 0 || header.root_node == 0 {
let leaf = btree_alloc_node(data, node_size, header.total_nodes)?;
let off = leaf as usize * node_size;
let node = &mut data[off..off + node_size];
node.fill(0);
node[8] = 0xFF; node[9] = 1; BigEndian::write_u16(&mut node[10..12], 0); BigEndian::write_u16(&mut node[node_size - 2..node_size], 14); header.depth = 1;
header.root_node = leaf;
header.first_leaf_node = leaf;
header.last_leaf_node = leaf;
header.free_nodes = header.free_nodes.saturating_sub(1);
header.write(data);
}
let (leaf_idx, parent_chain) = btree_find_insert_leaf(data, &header, key_record, cmp);
let off = leaf_idx as usize * node_size;
let node = &mut data[off..off + node_size];
match btree_insert_record(node, node_size, key_record, cmp) {
Ok(_) => {
let mut h = BTreeHeader::read(data);
h.leaf_records += 1;
h.write(data);
Ok(())
}
Err(_) => {
let mut h = BTreeHeader::read(data);
if h.depth > 1 {
if let Some(&(_, parent_idx)) =
parent_chain.iter().find(|&&(nidx, _)| nidx == leaf_idx)
{
if parent_idx != 0
&& btree_try_rotate_leaf(
data, node_size, leaf_idx, parent_idx, key_record, kf, cmp,
)
{
h.leaf_records += 1;
h.write(data);
return Ok(());
}
}
}
if h.free_nodes < h.depth as u32 + 1 {
return Err(disk_core::filesystem::FilesystemError::DiskFull(
"no free B-tree nodes".into(),
));
}
let splits = btree_split_leaf_with_insert(
data, node_size, leaf_idx, &mut h, key_record, kf, cmp,
)?;
h.leaf_records += 1;
let first = &splits[0];
if h.depth == 1 {
btree_grow_root(data, node_size, &mut h, leaf_idx, first.0, &first.1, kf)?;
} else if let Some(&(_, parent_idx)) =
parent_chain.iter().find(|&&(nidx, _)| nidx == leaf_idx)
{
btree_insert_into_index(
data,
node_size,
parent_idx,
first.0,
&first.1,
&mut h,
kf,
cmp,
&parent_chain,
)?;
} else {
btree_grow_root(data, node_size, &mut h, leaf_idx, first.0, &first.1, kf)?;
}
for split in &splits[1..] {
let header_now = BTreeHeader::read(data);
let (_, fresh_chain) = btree_find_insert_leaf(data, &header_now, &split.1, cmp);
let parent_for_sep = fresh_chain
.last()
.map(|&(_, p)| p)
.unwrap_or(header_now.root_node);
btree_insert_into_index(
data,
node_size,
parent_for_sep,
split.0,
&split.1,
&mut h,
kf,
cmp,
&fresh_chain,
)?;
}
h.write(data);
Ok(())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_mac_date() {
assert_eq!(format_mac_date(0), None);
assert_eq!(
format_mac_date(MAC_EPOCH_DELTA as u32).as_deref(),
Some("1970-01-01 00:00:00")
);
assert_eq!(
format_mac_date((MAC_EPOCH_DELTA + 946_684_800) as u32).as_deref(),
Some("2000-01-01 00:00:00")
);
}
#[test]
fn test_parse_mac_date_round_trips() {
assert_eq!(parse_mac_date(""), Some(0));
assert_eq!(parse_mac_date("0"), Some(0));
let mac = (MAC_EPOCH_DELTA + 946_684_800) as u32; let s = format_mac_date(mac).unwrap();
assert_eq!(parse_mac_date(&s), Some(mac));
assert_eq!(parse_mac_date("not a date"), None);
}
#[test]
fn test_parse_mac_date_checked_specific_errors() {
assert!(parse_mac_date_checked("1997-13-01 11:09:34")
.unwrap_err()
.contains("month 13"));
assert!(parse_mac_date_checked("1997-02-29 11:09:34")
.unwrap_err()
.contains("day 29 is invalid for 1997-02"));
assert!(parse_mac_date_checked("2000-01-01 25:00:00")
.unwrap_err()
.contains("out of range"));
assert!(parse_mac_date_checked("garbage")
.unwrap_err()
.contains("expected YYYY-MM-DD"));
assert!(parse_mac_date_checked("2000-02-29 12:00:00").is_ok());
}
#[test]
fn test_bitmap_set_clear_test_be() {
let mut data = [0u8; 2];
bitmap_set_bit_be(&mut data, 0);
assert_eq!(data[0], 0b10000000);
assert!(bitmap_test_bit_be(&data, 0));
assert!(!bitmap_test_bit_be(&data, 1));
bitmap_set_bit_be(&mut data, 7);
assert_eq!(data[0], 0b10000001);
bitmap_set_bit_be(&mut data, 8);
assert_eq!(data[1], 0b10000000);
bitmap_clear_bit_be(&mut data, 0);
assert_eq!(data[0], 0b00000001);
assert!(!bitmap_test_bit_be(&data, 0));
}
#[test]
fn test_bitmap_find_clear_bit_be() {
let data = [0b11111110, 0b11111111]; assert_eq!(bitmap_find_clear_bit_be(&data, 16), Some(7));
let full = [0xFF, 0xFF];
assert_eq!(bitmap_find_clear_bit_be(&full, 16), None);
let empty = [0x00, 0x00];
assert_eq!(bitmap_find_clear_bit_be(&empty, 16), Some(0));
}
#[test]
fn test_bitmap_find_clear_run_be() {
let data = [0b11100011, 0b00000000];
assert_eq!(bitmap_find_clear_run_be(&data, 16, 3), Some(3));
assert_eq!(bitmap_find_clear_run_be(&data, 16, 4), Some(8));
assert_eq!(bitmap_find_clear_run_be(&data, 16, 8), Some(8));
assert_eq!(bitmap_find_clear_run_be(&data, 16, 9), None);
}
#[test]
fn test_bitmap_be_roundtrip() {
let mut data = [0u8; 4];
for i in 0..32u32 {
bitmap_set_bit_be(&mut data, i);
}
assert_eq!(data, [0xFF; 4]);
for i in 0..32u32 {
assert!(bitmap_test_bit_be(&data, i));
bitmap_clear_bit_be(&mut data, i);
}
assert_eq!(data, [0x00; 4]);
}
#[test]
fn test_bitmap_single_bit() {
let mut data = [0u8; 1];
bitmap_set_bit_be(&mut data, 3);
assert_eq!(data[0], 0b00010000);
assert!(bitmap_test_bit_be(&data, 3));
assert!(!bitmap_test_bit_be(&data, 2));
assert!(!bitmap_test_bit_be(&data, 4));
}
#[test]
fn test_hfs_now_sanity() {
let now = hfs_now();
assert!(now > 3_786_912_000);
}
#[test]
fn test_type_creator_for_extension_known() {
let (tc, cc) = type_creator_for_extension("txt").unwrap();
assert_eq!(&tc, b"TEXT");
assert_eq!(&cc, b"ttxt");
}
#[test]
fn test_type_creator_for_extension_case_insensitive() {
let (tc, cc) = type_creator_for_extension("TXT").unwrap();
assert_eq!(&tc, b"TEXT");
assert_eq!(&cc, b"ttxt");
}
#[test]
fn test_uses_hfs_type_dictionary() {
for t in ["HFS", "HFS+", "HFSX", "MFS"] {
assert!(uses_hfs_type_dictionary(t), "{t} should use the dictionary");
}
for t in ["ProDOS", "FAT16", "ext4", ""] {
assert!(!uses_hfs_type_dictionary(t), "{t} should not");
}
}
#[test]
fn test_type_creator_for_filename_needs_a_real_extension() {
assert!(type_creator_for_filename("readme.txt").is_some());
assert!(type_creator_for_filename("txt").is_none());
assert!(type_creator_for_filename("README").is_none());
}
#[test]
fn test_resolve_blank_ostype_falls_through_to_dictionary() {
let (t, c) =
resolve_create_type_creator("readme.txt", Some([0; 4]), Some([0; 4]), None, None);
assert_eq!(&t, b"TEXT");
assert_eq!(&c, b"ttxt");
let (t, c) = resolve_create_type_creator("pic.gif", None, None, Some(""), Some(" "));
assert_eq!(&t, b"GIFf");
assert_eq!(&c, b"ogle");
}
#[test]
fn test_resolve_real_ostype_wins_over_dictionary() {
let (t, c) = resolve_create_type_creator(
"poi.txt",
Some(*b"APPL"),
Some([0x50, 0x6f, 0xC4, 0x50]),
None,
None,
);
assert_eq!(&t, b"APPL");
assert_eq!(c, [0x50, 0x6f, 0xC4, 0x50]);
let (t, _) =
resolve_create_type_creator("spc.txt", Some(*b" "), Some(*b" "), None, None);
assert_eq!(&t, b" ");
}
#[test]
fn test_resolve_fills_only_the_missing_half() {
let (t, c) = resolve_create_type_creator("pic.gif", None, Some(*b"MINE"), None, None);
assert_eq!(&t, b"GIFf");
assert_eq!(&c, b"MINE");
}
#[test]
fn test_resolve_unknown_extension_stays_blank() {
let (t, c) = resolve_create_type_creator("thing.xyz123", None, None, None, None);
assert_eq!(t, [0; 4]);
assert_eq!(c, [0; 4]);
}
#[test]
fn test_resolve_text_type_keeps_trailing_space_codes() {
let (t, _) = resolve_create_type_creator("x.unknown", None, None, Some("ZIP "), None);
assert_eq!(&t, b"ZIP ");
}
#[test]
fn test_type_creator_for_extension_unknown() {
assert!(type_creator_for_extension("xyz123").is_none());
}
#[test]
fn test_encode_fourcc() {
assert_eq!(encode_fourcc("TEXT"), *b"TEXT");
assert_eq!(encode_fourcc("AB"), *b"AB ");
assert_eq!(encode_fourcc("SIT!"), *b"SIT!");
}
#[test]
fn test_encode_fourcc_roundtrip() {
let code = encode_fourcc("JPEG");
assert_eq!(&code, b"JPEG");
}
fn make_test_btree(node_size: usize) -> Vec<u8> {
let mut data = vec![0u8; node_size * 4];
data[8] = BTREE_HEADER_NODE as u8; data[9] = 0; BigEndian::write_u16(&mut data[10..12], 3);
BigEndian::write_u16(&mut data[14..16], 1); BigEndian::write_u32(&mut data[16..20], 1); BigEndian::write_u32(&mut data[20..24], 0); BigEndian::write_u32(&mut data[24..28], 1); BigEndian::write_u32(&mut data[28..32], 1); BigEndian::write_u16(&mut data[32..34], node_size as u16); BigEndian::write_u16(&mut data[34..36], 256); BigEndian::write_u32(&mut data[36..40], 4); BigEndian::write_u32(&mut data[40..44], 2);
let header_rec_offset = 14u16;
let user_rec_offset = 120u16; let bitmap_rec_offset = 128u16; let free_offset = 256u16;
BigEndian::write_u16(&mut data[node_size - 2..node_size], header_rec_offset);
BigEndian::write_u16(&mut data[node_size - 4..node_size - 2], user_rec_offset);
BigEndian::write_u16(&mut data[node_size - 6..node_size - 4], bitmap_rec_offset);
BigEndian::write_u16(&mut data[node_size - 8..node_size - 6], free_offset);
data[bitmap_rec_offset as usize] = 0b11000000;
let n1 = node_size;
data[n1 + 8] = BTREE_LEAF_NODE as u8; data[n1 + 9] = 1; BigEndian::write_u16(&mut data[n1 + 10..n1 + 12], 0); BigEndian::write_u16(&mut data[n1 + node_size - 2..n1 + node_size], 14);
data
}
#[test]
fn test_btree_node_free_space_empty_leaf() {
let data = make_test_btree(512);
let node = &data[512..1024]; let free = btree_node_free_space(node, 512);
assert_eq!(free, 494);
}
#[test]
fn test_btree_insert_and_remove() {
let mut data = make_test_btree(512);
let node_size = 512;
let rec1 = vec![0u8; 20]; let rec2 = vec![1u8; 20];
let compare = |a: &[u8], b: &[u8]| a.cmp(b);
let node = &mut data[node_size..node_size * 2];
let pos = btree_insert_record(node, node_size, &rec1, &compare).unwrap();
assert_eq!(pos, 0);
assert_eq!(BigEndian::read_u16(&node[10..12]), 1);
let pos2 = btree_insert_record(node, node_size, &rec2, &compare).unwrap();
assert_eq!(pos2, 1); assert_eq!(BigEndian::read_u16(&node[10..12]), 2);
btree_remove_record(node, node_size, 0);
assert_eq!(BigEndian::read_u16(&node[10..12]), 1);
}
#[test]
fn test_btree_alloc_free_node() {
let mut data = make_test_btree(512);
let node_size = 512;
let idx = btree_alloc_node(&mut data, node_size, 4).unwrap();
assert_eq!(idx, 2);
let idx2 = btree_alloc_node(&mut data, node_size, 4).unwrap();
assert_eq!(idx2, 3);
assert!(btree_alloc_node(&mut data, node_size, 4).is_err());
btree_free_node(&mut data, node_size, 2);
let idx3 = btree_alloc_node(&mut data, node_size, 4).unwrap();
assert_eq!(idx3, 2);
}
#[test]
fn test_btree_header_read_write() {
let mut data = make_test_btree(512);
let header = BTreeHeader::read(&data);
assert_eq!(header.depth, 1);
assert_eq!(header.root_node, 1);
assert_eq!(header.total_nodes, 4);
assert_eq!(header.free_nodes, 2);
let mut header = header;
header.leaf_records = 42;
header.write(&mut data);
let header2 = BTreeHeader::read(&data);
assert_eq!(header2.leaf_records, 42);
}
#[test]
fn test_btree_split_leaf_with_insert() {
let mut data = make_test_btree(512);
let node_size = 512;
let compare = |a: &[u8], b: &[u8]| a.cmp(b);
for i in 0..10u8 {
let mut rec = vec![0u8; 45];
rec[0] = i * 2;
let node = &mut data[node_size..node_size * 2];
btree_insert_record(node, node_size, &rec, &compare).unwrap();
}
let node = &data[node_size..node_size * 2];
assert_eq!(BigEndian::read_u16(&node[10..12]), 10);
let mut new_rec = vec![0u8; 45];
new_rec[0] = 5;
let mut header = BTreeHeader::read(&data);
let splits = btree_split_leaf_with_insert(
&mut data,
node_size,
1,
&mut header,
&new_rec,
&BTreeKeyFormat::CLASSIC_CATALOG,
&compare,
)
.unwrap();
assert_eq!(splits.len(), 1, "30-byte records: 2-way split is enough");
let new_idx = splits[0].0;
assert_eq!(new_idx, 2);
let old_node = &data[node_size..node_size * 2];
let new_node = &data[new_idx as usize * node_size..(new_idx as usize + 1) * node_size];
let old_count = BigEndian::read_u16(&old_node[10..12]);
let new_count = BigEndian::read_u16(&new_node[10..12]);
assert_eq!(old_count + new_count, 11); }
}