use super::{CustomFlatUnixFs, Leaf, NamedLeaf, TreeConstructionFailed};
use crate::pb::{UnixFs, UnixFsType};
use crate::Metadata;
use alloc::borrow::Cow;
use ipld_core::cid::{Cid, Version};
use multihash_codetable::Code;
const FANOUT: usize = 256;
const HASH_TYPE_MURMUR3: u64 = 0x22;
const MAX_DEPTH: usize = 8;
pub(super) struct ShardBlock {
pub(super) cid: Cid,
pub(super) block: Vec<u8>,
pub(super) total_size: u64,
}
fn hamt_hash(name: &[u8]) -> [u8; 8] {
let h = murmur3::murmur3_x64_128(&mut std::io::Cursor::new(name), 0)
.expect("hashing an in-memory cursor cannot fail");
(h as u64).to_be_bytes()
}
struct Entry<'a> {
hash: [u8; 8],
leaf: &'a NamedLeaf,
}
pub(super) fn build_sharded(
links: &[Option<NamedLeaf>],
buffer: &mut Vec<u8>,
cid_version: Version,
hasher: Code,
metadata: &Metadata,
) -> Result<(Leaf, Vec<ShardBlock>), TreeConstructionFailed> {
let entries = links
.iter()
.filter_map(|opt| opt.as_ref())
.map(|leaf| Entry {
hash: hamt_hash(leaf.0.as_bytes()),
leaf,
})
.collect::<Vec<_>>();
let mut interior = Vec::new();
let root = build_shard(&entries, 0, cid_version, hasher, &mut interior, metadata)?;
buffer.clear();
buffer.extend_from_slice(&root.block);
Ok((
Leaf {
link: root.cid,
total_size: root.total_size,
},
interior,
))
}
fn build_shard(
entries: &[Entry<'_>],
depth: usize,
cid_version: Version,
hasher: Code,
interior: &mut Vec<ShardBlock>,
metadata: &Metadata,
) -> Result<ShardBlock, TreeConstructionFailed> {
if depth >= MAX_DEPTH {
return Err(TreeConstructionFailed::ShardTooDeep(entries.len()));
}
let mut buckets: Vec<Vec<&Entry<'_>>> = (0..FANOUT).map(|_| Vec::new()).collect();
for entry in entries {
buckets[entry.hash[depth] as usize].push(entry);
}
let mut shard_links = Vec::new();
let mut bitfield = [0u8; FANOUT / 8];
for (index, bucket) in buckets.iter().enumerate() {
if bucket.is_empty() {
continue;
}
let bit = (index % 8) as u32;
bitfield[FANOUT / 8 - 1 - index / 8] |= 1u8 << bit;
let link = if bucket.len() == 1 {
let leaf = bucket[0].leaf;
NamedLeaf(format!("{index:02X}{}", leaf.0), leaf.1, leaf.2)
} else {
let child_entries = bucket
.iter()
.map(|e| Entry {
hash: e.hash,
leaf: e.leaf,
})
.collect::<Vec<_>>();
let child = build_shard(
&child_entries,
depth + 1,
cid_version,
hasher,
interior,
&Metadata::default(),
)?;
let link = NamedLeaf(format!("{index:02X}"), child.cid, child.total_size);
interior.push(child);
link
};
shard_links.push(Some(link));
}
shard_links.sort_by(|a, b| {
let a = a.as_ref().expect("just pushed Some");
let b = b.as_ref().expect("just pushed Some");
a.0.as_bytes().cmp(b.0.as_bytes())
});
let (mode, mtime) = metadata.to_pb();
let start = bitfield
.iter()
.position(|&b| b != 0)
.unwrap_or(bitfield.len() - 1);
let data = UnixFs {
Type: UnixFsType::HAMTShard,
Data: Some(Cow::Owned(bitfield[start..].to_vec())),
fanout: Some(FANOUT as u64),
hashType: Some(HASH_TYPE_MURMUR3),
mode,
mtime,
..Default::default()
};
let (cid, block) = render_shard_node(&shard_links, data, cid_version, hasher)?;
let links_total: u64 = shard_links
.iter()
.map(|l| l.as_ref().expect("just pushed Some").2)
.sum();
Ok(ShardBlock {
cid,
total_size: block.len() as u64 + links_total,
block,
})
}
fn render_shard_node(
links: &[Option<NamedLeaf>],
data: UnixFs<'_>,
cid_version: Version,
hasher: Code,
) -> Result<(Cid, Vec<u8>), TreeConstructionFailed> {
use quick_protobuf::{BytesWriter, MessageWrite, Writer};
let node = CustomFlatUnixFs { links, data };
let size = node.get_size();
let mut block = vec![0u8; size];
let mut writer = Writer::new(BytesWriter::new(&mut block[..]));
node.write_message(&mut writer)
.map_err(TreeConstructionFailed::Protobuf)?;
let cid = crate::pb::make_cid(cid_version, hasher, crate::file::DAG_PB_CODEC, &block);
Ok((cid, block))
}
#[cfg(test)]
mod tests {
use super::super::NamedLeaf;
use super::{build_sharded, hamt_hash};
use crate::dir::MaybeResolved;
use crate::pb::{FlatUnixFs, UnixFsType};
use ipld_core::cid::{Cid, Version};
use multihash::Multihash;
use multihash_codetable::Code;
use sha2::{Digest, Sha256};
use std::collections::HashMap;
fn cid_of(n: u8) -> Cid {
let mh = Multihash::wrap(Code::Sha2_256.into(), &Sha256::digest([n])).unwrap();
Cid::new_v0(mh).unwrap()
}
fn link(name: &str, n: u8) -> Option<NamedLeaf> {
Some(NamedLeaf(name.to_string(), cid_of(n), 1))
}
fn names(parsed: &FlatUnixFs<'_>) -> Vec<String> {
parsed
.links
.iter()
.map(|l| l.Name.as_deref().unwrap().to_string())
.collect()
}
#[test]
fn murmur3_hash_half_and_endianness() {
let h = |s: &str| u64::from_be_bytes(hamt_hash(s.as_bytes()));
assert_eq!(h("a"), 0x85555565f6597889);
assert_eq!(h("b"), 0x7a98a957b1d3d1ee);
assert_eq!(h("file.txt"), 0xb53875c83534fe3e);
assert_eq!(h("0"), 0x2ac9debed546a380);
assert_eq!(h("dir"), 0x2a7c964720718e4f);
}
#[test]
fn shard_without_collision() {
let links = [link("a", 1), link("b", 2), link("file.txt", 3)];
let mut buffer = Vec::new();
let (_leaf, interior) = build_sharded(
&links,
&mut buffer,
Version::V0,
Code::Sha2_256,
&crate::Metadata::default(),
)
.unwrap();
assert!(interior.is_empty());
let parsed = FlatUnixFs::try_parse(&buffer).unwrap();
assert_eq!(parsed.data.Type, UnixFsType::HAMTShard);
assert_eq!(parsed.data.fanout, Some(256));
assert_eq!(parsed.data.hashType, Some(0x22));
assert!(parsed.data.filesize.is_none());
assert!(parsed.data.blocksizes.is_empty());
assert_eq!(names(&parsed), ["7Ab", "85a", "B5file.txt"]);
let bf = parsed.data.Data.as_deref().unwrap();
assert_eq!(bf.len(), 23);
assert_eq!(bf.iter().map(|b| b.count_ones()).sum::<u32>(), 3);
for idx in [0x7Au8, 0x85, 0xB5] {
let i = idx as usize;
assert_ne!(
bf[bf.len() - 1 - i / 8] & (1 << (i % 8)),
0,
"bit for {idx:#x}"
);
}
}
#[test]
fn shard_with_collision_creates_subshard() {
let links = [link("0", 1), link("dir", 2)];
let mut buffer = Vec::new();
let (_leaf, interior) = build_sharded(
&links,
&mut buffer,
Version::V0,
Code::Sha2_256,
&crate::Metadata::default(),
)
.unwrap();
assert_eq!(interior.len(), 1);
let root = FlatUnixFs::try_parse(&buffer).unwrap();
assert_eq!(names(&root), ["2A"]);
assert_eq!(root.links[0].Tsize, Some(interior[0].total_size));
let sub = FlatUnixFs::try_parse(&interior[0].block).unwrap();
assert_eq!(sub.data.Type, UnixFsType::HAMTShard);
assert_eq!(names(&sub), ["7Cdir", "C90"]);
}
#[test]
fn shard_resolves_via_read_side() {
let entries: [(&str, u8); 3] = [("0", 1), ("dir", 2), ("file.txt", 3)];
let links = entries.map(|(n, c)| link(n, c));
let mut buffer = Vec::new();
let (leaf, interior) = build_sharded(
&links,
&mut buffer,
Version::V0,
Code::Sha2_256,
&crate::Metadata::default(),
)
.unwrap();
let mut blocks = HashMap::new();
blocks.insert(leaf.link, buffer);
for s in &interior {
blocks.insert(s.cid, s.block.clone());
}
for (name, n) in entries {
assert_eq!(resolve(&blocks, leaf.link, name), Some(cid_of(n)), "{name}");
}
assert_eq!(resolve(&blocks, leaf.link, "missing"), None);
}
fn resolve(blocks: &HashMap<Cid, Vec<u8>>, root: Cid, name: &str) -> Option<Cid> {
let mut cache = None;
let block = blocks.get(&root).unwrap();
let mut state = crate::dir::resolve(block, name, &mut cache).unwrap();
loop {
match state {
MaybeResolved::Found(cid) => return Some(cid),
MaybeResolved::NotFound => return None,
MaybeResolved::NeedToLoadMore(lookup) => {
let next = *lookup.pending_links().0;
let block = blocks.get(&next).expect("bucket block present");
state = lookup.continue_walk(block, &mut cache).unwrap();
}
}
}
}
}