use std::alloc::{Layout, alloc, dealloc};
use std::ops::Deref;
use std::path::Path;
use std::ptr::NonNull;
use anyhow::{Result, bail, ensure};
use znippy_common::read_reserved_section_bytes;
use znippy_common::GUNNAR_OID_MODULE;
use znippy_zoomies::stree::STree64Mmap;
use crate::object::GitHashKind;
pub const GIT_OID_MAGIC: [u8; 8] = *b"ZNPYGOID";
pub const GIT_OID_VERSION: u32 = 3;
const HEADER_FIXED: usize = 24;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum OidLayout {
#[default]
Compact,
Compact64Alloc,
Aligned64,
}
impl OidLayout {
pub const fn header_len(self) -> usize {
match self {
OidLayout::Compact | OidLayout::Compact64Alloc => 24,
OidLayout::Aligned64 => 64,
}
}
const fn wants_aligned_alloc(self) -> bool {
!matches!(self, OidLayout::Compact)
}
pub const fn name(self) -> &'static str {
match self {
OidLayout::Compact => "compact24",
OidLayout::Compact64Alloc => "compact24+align",
OidLayout::Aligned64 => "aligned64",
}
}
pub const ALL: [OidLayout; 3] =
[OidLayout::Compact, OidLayout::Compact64Alloc, OidLayout::Aligned64];
}
struct Aligned64Bytes {
ptr: NonNull<u8>,
len: usize,
}
unsafe impl Send for Aligned64Bytes {}
unsafe impl Sync for Aligned64Bytes {}
impl Aligned64Bytes {
fn copy_of(src: &[u8]) -> Self {
let len = src.len();
let layout = Layout::from_size_align(len.max(1), 64).expect("64-aligned layout");
let raw = unsafe { alloc(layout) };
let Some(ptr) = NonNull::new(raw) else {
std::alloc::handle_alloc_error(layout);
};
unsafe { std::ptr::copy_nonoverlapping(src.as_ptr(), raw, len) };
Self { ptr, len }
}
}
impl Deref for Aligned64Bytes {
type Target = [u8];
fn deref(&self) -> &[u8] {
unsafe { std::slice::from_raw_parts(self.ptr.as_ptr(), self.len) }
}
}
impl Drop for Aligned64Bytes {
fn drop(&mut self) {
let layout = Layout::from_size_align(self.len.max(1), 64).expect("64-aligned layout");
unsafe { dealloc(self.ptr.as_ptr(), layout) };
}
}
enum SectionBytes {
Plain(Vec<u8>),
Aligned(Aligned64Bytes),
}
impl Deref for SectionBytes {
type Target = [u8];
fn deref(&self) -> &[u8] {
match self {
SectionBytes::Plain(v) => v,
SectionBytes::Aligned(a) => a,
}
}
}
const BATCH_P: usize = 64;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OidEntry {
pub oid: Vec<u8>,
pub lookup_row: u64,
pub ordinal: u32,
}
pub fn key_for_oid(oid: &[u8]) -> i64 {
let mut b = [0u8; 8];
let n = oid.len().min(8);
b[..n].copy_from_slice(&oid[..n]);
(u64::from_be_bytes(b) ^ (1u64 << 63)) as i64
}
pub fn build_section(entries: &[OidEntry], hash: GitHashKind) -> Result<Vec<u8>> {
build_section_with_layout(entries, hash, OidLayout::default())
}
pub fn build_section_with_layout(
entries: &[OidEntry],
hash: GitHashKind,
layout: OidLayout,
) -> Result<Vec<u8>> {
let header_len = layout.header_len();
let oid_len = hash.oid_len();
for e in entries {
ensure!(
e.oid.len() == oid_len,
"oid length {} does not match hash kind {:?}",
e.oid.len(),
hash
);
}
let mut order: Vec<usize> = (0..entries.len()).collect();
order.sort_by(|&a, &b| {
key_for_oid(&entries[a].oid)
.cmp(&key_for_oid(&entries[b].oid))
.then_with(|| entries[a].oid.cmp(&entries[b].oid))
});
let n = entries.len();
let mut out = Vec::with_capacity(header_len + n * (8 + 8 + 4 + oid_len));
out.extend_from_slice(&GIT_OID_MAGIC);
out.extend_from_slice(&GIT_OID_VERSION.to_le_bytes());
out.push(hash.code());
out.push(oid_len as u8);
out.extend_from_slice(&(header_len as u16).to_le_bytes());
out.extend_from_slice(&(n as u64).to_le_bytes());
debug_assert_eq!(out.len(), HEADER_FIXED);
out.resize(header_len, 0);
for &i in &order {
out.extend_from_slice(&key_for_oid(&entries[i].oid).to_le_bytes());
}
for &i in &order {
out.extend_from_slice(&entries[i].lookup_row.to_le_bytes());
}
for &i in &order {
out.extend_from_slice(&entries[i].ordinal.to_le_bytes());
}
for &i in &order {
out.extend_from_slice(&entries[i].oid);
}
Ok(out)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OidHit {
pub entry: usize,
pub lookup_row: u64,
pub ordinal: u32,
}
pub struct GitOidIndex {
bytes: SectionBytes,
header_len: usize,
count: usize,
oid_len: usize,
hash: GitHashKind,
tree: Option<STree64Mmap>,
}
impl GitOidIndex {
pub fn parse(bytes: Vec<u8>) -> Result<Self> {
Self::parse_inner(bytes, None)
}
pub fn parse_as(bytes: Vec<u8>, layout: OidLayout) -> Result<Self> {
Self::parse_inner(bytes, Some(layout))
}
fn parse_inner(bytes: Vec<u8>, want: Option<OidLayout>) -> Result<Self> {
ensure!(bytes.len() >= HEADER_FIXED, "__gunnar_oid__ section truncated");
ensure!(bytes[..8] == GIT_OID_MAGIC, "__gunnar_oid__ bad magic");
let version = u32::from_le_bytes(bytes[8..12].try_into().unwrap());
ensure!(
version == GIT_OID_VERSION,
"__gunnar_oid__ is version {version}, this reader speaks {GIT_OID_VERSION} \
only — v1 sorted its keys on a non-order-preserving key and v2 had no \
header_len, so reading one here would return wrong rows instead of failing"
);
let Some(hash) = GitHashKind::from_code(bytes[12]) else {
bail!("__gunnar_oid__ unknown hash code {}", bytes[12]);
};
let oid_len = bytes[13] as usize;
ensure!(
oid_len == hash.oid_len(),
"__gunnar_oid__ oid_len {oid_len} disagrees with hash {hash:?}"
);
let header_len = u16::from_le_bytes(bytes[14..16].try_into().unwrap()) as usize;
let Some(inferred) = OidLayout::ALL.into_iter().find(|l| l.header_len() == header_len)
else {
bail!(
"__gunnar_oid__ header_len {header_len} is not a layout this reader knows \
(24 or 64)"
);
};
let layout = match want {
Some(w) => {
ensure!(
w.header_len() == header_len,
"__gunnar_oid__ was written with a {header_len}-byte header, cannot be read \
as {} ({} bytes)",
w.name(),
w.header_len()
);
w
}
None => inferred,
};
let count = u64::from_le_bytes(bytes[16..24].try_into().unwrap()) as usize;
let need = header_len
.checked_add(count.checked_mul(8 + 8 + 4 + oid_len).unwrap_or(usize::MAX))
.unwrap_or(usize::MAX);
ensure!(
bytes.len() >= need,
"__gunnar_oid__ declares {count} entries but section is {} bytes (needs {need})",
bytes.len()
);
let bytes = if layout.wants_aligned_alloc() {
SectionBytes::Aligned(Aligned64Bytes::copy_of(&bytes))
} else {
SectionBytes::Plain(bytes)
};
let tree = if count == 0 {
None
} else {
let keys = &bytes[header_len..header_len + count * 8];
Some(STree64Mmap::new_with_stride(keys, count, 8))
};
Ok(Self { bytes, header_len, count, oid_len, hash, tree })
}
pub fn layout(&self) -> OidLayout {
OidLayout::ALL
.into_iter()
.find(|l| l.header_len() == self.header_len && l.wants_aligned_alloc() == self.is_aligned_alloc())
.unwrap_or(OidLayout::Compact)
}
fn is_aligned_alloc(&self) -> bool {
matches!(self.bytes, SectionBytes::Aligned(_))
}
pub fn keyspace_phase(&self) -> usize {
self.keys().as_ptr() as usize % 64
}
pub fn open(archive: &Path) -> Result<Option<Self>> {
match read_reserved_section_bytes(archive, GUNNAR_OID_MODULE)? {
Some(b) => Ok(Some(Self::parse(b)?)),
None => Ok(None),
}
}
pub fn len(&self) -> usize {
self.count
}
pub fn is_empty(&self) -> bool {
self.count == 0
}
pub fn hash_kind(&self) -> GitHashKind {
self.hash
}
fn keys(&self) -> &[u8] {
&self.bytes[self.header_len..self.header_len + self.count * 8]
}
pub fn key_at(&self, i: usize) -> i64 {
let off = self.header_len + i * 8;
i64::from_le_bytes(self.bytes[off..off + 8].try_into().unwrap())
}
pub fn oid_at(&self, i: usize) -> &[u8] {
let base = self.header_len + self.count * (8 + 8 + 4) + i * self.oid_len;
&self.bytes[base..base + self.oid_len]
}
fn row_at(&self, i: usize) -> u64 {
let off = self.header_len + self.count * 8 + i * 8;
u64::from_le_bytes(self.bytes[off..off + 8].try_into().unwrap())
}
fn ordinal_at(&self, i: usize) -> u32 {
let off = self.header_len + self.count * 16 + i * 4;
u32::from_le_bytes(self.bytes[off..off + 4].try_into().unwrap())
}
pub fn candidate_run(&self, key: i64) -> std::ops::Range<usize> {
let Some(tree) = self.tree.as_ref() else { return 0..0 };
let Some(pos) = tree.find_exact(key, self.keys()) else { return 0..0 };
self.expand_run(pos, key)
}
fn expand_run(&self, pos: usize, key: i64) -> std::ops::Range<usize> {
let mut lo = pos;
while lo > 0 && self.key_at(lo - 1) == key {
lo -= 1;
}
let mut hi = pos + 1;
while hi < self.count && self.key_at(hi) == key {
hi += 1;
}
lo..hi
}
pub fn lookup(&self, oid: &[u8]) -> Option<OidHit> {
if oid.len() != self.oid_len {
return None;
}
let tree = self.tree.as_ref()?;
let key = key_for_oid(oid);
let pos = tree.find_exact(key, self.keys())?;
self.verify(pos, key, oid)
}
pub fn lookup_hex(&self, hex_oid: &str) -> Option<OidHit> {
if hex_oid.len() != self.oid_len * 2 {
return None;
}
let raw = hex::decode(hex_oid).ok()?;
self.lookup(&raw)
}
fn verify(&self, pos: usize, key: i64, oid: &[u8]) -> Option<OidHit> {
for i in self.expand_run(pos, key) {
if self.oid_at(i) == oid {
return Some(OidHit {
entry: i,
lookup_row: self.row_at(i),
ordinal: self.ordinal_at(i),
});
}
}
None
}
pub fn lookup_batch(&self, oids: &[&[u8]]) -> Vec<Option<OidHit>> {
let Some(tree) = self.tree.as_ref() else { return vec![None; oids.len()] };
let keys: Vec<i64> = oids.iter().map(|o| key_for_oid(o)).collect();
let raw = tree.lookup_batch_fused::<BATCH_P>(&keys, self.keys());
raw.into_iter()
.zip(oids.iter())
.enumerate()
.map(|(i, (pos, oid))| {
if oid.len() != self.oid_len {
return None;
}
self.verify(pos?, keys[i], oid)
})
.collect()
}
#[cfg(feature = "bench-kernels")]
pub fn lookup_binary_search(&self, oid: &[u8]) -> Option<OidHit> {
if oid.len() != self.oid_len || self.count == 0 {
return None;
}
let key = key_for_oid(oid);
let mut lo = 0usize;
let mut hi = self.count;
while lo < hi {
let mid = lo + (hi - lo) / 2;
if self.key_at(mid) < key { lo = mid + 1 } else { hi = mid }
}
if lo >= self.count || self.key_at(lo) != key {
return None;
}
self.verify(lo, key, oid)
}
pub fn lookup_batch_hex(&self, hex_oids: &[&str]) -> Vec<Option<OidHit>> {
let raw: Vec<Vec<u8>> = hex_oids.iter().map(|h| hex::decode(h).unwrap_or_default()).collect();
let refs: Vec<&[u8]> = raw.iter().map(|v| v.as_slice()).collect();
self.lookup_batch(&refs)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn oid(bytes: &[u8], len: usize) -> Vec<u8> {
let mut v = bytes.to_vec();
v.resize(len, 0);
v
}
fn idx(entries: Vec<OidEntry>, hash: GitHashKind) -> GitOidIndex {
GitOidIndex::parse(build_section(&entries, hash).unwrap()).unwrap()
}
#[test]
fn resolves_every_entry_it_was_built_from() {
let n = 300usize;
let entries: Vec<OidEntry> = (0..n)
.map(|i| {
let mut o = [0u8; 32];
o[..8].copy_from_slice(&(i as u64).wrapping_mul(0x0123_4567_89ab_cdef).to_be_bytes());
o[8] = (i % 251) as u8;
OidEntry { oid: o.to_vec(), lookup_row: (i * 3) as u64, ordinal: i as u32 }
})
.collect();
let index = idx(entries.clone(), GitHashKind::Sha256);
assert_eq!(index.len(), n);
for e in &entries {
let hit = index.lookup(&e.oid).unwrap_or_else(|| panic!("miss for {}", hex::encode(&e.oid)));
assert_eq!(hit.lookup_row, e.lookup_row);
assert_eq!(hit.ordinal, e.ordinal);
}
let mut absent = entries[0].oid.clone();
absent[31] ^= 0xff;
assert!(index.lookup(&absent).is_none());
}
#[test]
fn eight_byte_prefix_collision_is_resolved_by_the_full_oid() {
let prefix = [0xde, 0xad, 0xbe, 0xef, 0x01, 0x02, 0x03, 0x04];
let mut a = oid(&prefix, 32);
let mut b = oid(&prefix, 32);
a[8] = 0xaa;
b[8] = 0xbb;
assert_eq!(key_for_oid(&a), key_for_oid(&b), "test premise: keys must collide");
assert_ne!(a, b);
let mut entries = vec![
OidEntry { oid: a.clone(), lookup_row: 100, ordinal: 7 },
OidEntry { oid: b.clone(), lookup_row: 200, ordinal: 9 },
];
for i in 0..64u64 {
let mut o = [0u8; 32];
o[..8].copy_from_slice(&i.wrapping_mul(0x1111_1111_1111_1111).to_be_bytes());
o[9] = 1;
entries.push(OidEntry { oid: o.to_vec(), lookup_row: 900 + i, ordinal: 100 + i as u32 });
}
let index = idx(entries, GitHashKind::Sha256);
let run = index.candidate_run(key_for_oid(&a));
assert_eq!(run.len(), 2, "expected a 2-entry candidate run, got {run:?}");
assert_eq!(index.key_at(run.start), index.key_at(run.start + 1));
let ha = index.lookup(&a).expect("a must resolve");
let hb = index.lookup(&b).expect("b must resolve");
assert_eq!(ha.lookup_row, 100);
assert_eq!(hb.lookup_row, 200);
assert_eq!(ha.ordinal, 7);
assert_eq!(hb.ordinal, 9);
assert_ne!(ha.lookup_row, hb.lookup_row);
let mut c = oid(&prefix, 32);
c[8] = 0xcc;
assert!(index.lookup(&c).is_none(), "unstored oid on a colliding prefix must miss");
}
#[test]
fn batch_path_agrees_with_the_serial_path_including_on_a_collision() {
let prefix = [0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff];
let mut a = oid(&prefix, 20);
let mut b = oid(&prefix, 20);
a[8] = 1;
b[8] = 2;
let mut entries = vec![
OidEntry { oid: a.clone(), lookup_row: 11, ordinal: 1 },
OidEntry { oid: b.clone(), lookup_row: 22, ordinal: 2 },
];
for i in 0..200u64 {
let mut o = [0u8; 20];
o[..8].copy_from_slice(&(i.wrapping_mul(0x9e37_79b9_7f4a_7c15)).to_be_bytes());
o[10] = (i % 97) as u8;
entries.push(OidEntry { oid: o.to_vec(), lookup_row: 1000 + i, ordinal: 500 + i as u32 });
}
let index = idx(entries.clone(), GitHashKind::Sha1);
let mut queries: Vec<&[u8]> = entries.iter().map(|e| e.oid.as_slice()).collect();
let absent = oid(&[0xab, 0xcd, 0xef, 0x00, 0x11, 0x22, 0x33, 0x44], 20);
queries.push(&absent);
let batched = index.lookup_batch(&queries);
assert_eq!(batched.len(), queries.len());
for (i, q) in queries.iter().enumerate() {
assert_eq!(batched[i], index.lookup(q), "batch/serial disagree at {i}");
}
assert!(batched.last().unwrap().is_none(), "absent oid must miss in the batch path too");
assert_eq!(batched[0].unwrap().lookup_row, 11);
assert_eq!(batched[1].unwrap().lookup_row, 22);
}
#[test]
fn empty_index_is_a_clean_miss_not_a_panic() {
let index = idx(Vec::new(), GitHashKind::Sha256);
assert!(index.is_empty());
assert!(index.lookup(&oid(&[1], 32)).is_none());
assert_eq!(index.lookup_batch(&[&oid(&[1], 32)[..]]), vec![None]);
}
#[test]
fn truncated_or_mislabelled_sections_are_rejected() {
let entries = vec![OidEntry { oid: oid(&[9], 20), lookup_row: 0, ordinal: 0 }];
let good = build_section(&entries, GitHashKind::Sha1).unwrap();
assert!(GitOidIndex::parse(good.clone()).is_ok());
let mut bad_magic = good.clone();
bad_magic[0] = b'X';
assert!(GitOidIndex::parse(bad_magic).is_err());
let mut newer = good.clone();
newer[8..12].copy_from_slice(&(GIT_OID_VERSION + 1).to_le_bytes());
assert!(GitOidIndex::parse(newer).is_err());
assert!(GitOidIndex::parse(good[..HEADER_FIXED + 4].to_vec()).is_err());
assert!(GitOidIndex::parse(Vec::new()).is_err());
let mut bad_header = good.clone();
bad_header[14..16].copy_from_slice(&40u16.to_le_bytes());
let err = match GitOidIndex::parse(bad_header) {
Ok(_) => panic!("a header_len no layout can produce must be refused"),
Err(e) => e.to_string(),
};
assert!(err.contains("header_len 40"), "error must name the width: {err}");
}
#[test]
fn entry_order_is_oid_lexicographic_across_the_sign_boundary() {
let firsts: [u8; 8] = [0x00, 0x7f, 0x80, 0xff, 0x01, 0xfe, 0x81, 0x7e];
let entries: Vec<OidEntry> = firsts
.iter()
.enumerate()
.map(|(i, &f)| {
let mut o = [0u8; 32];
o[0] = f;
o[1] = i as u8;
OidEntry { oid: o.to_vec(), lookup_row: i as u64, ordinal: i as u32 }
})
.collect();
let index = idx(entries.clone(), GitHashKind::Sha256);
let mut want: Vec<Vec<u8>> = entries.iter().map(|e| e.oid.clone()).collect();
want.sort();
for (i, w) in want.iter().enumerate() {
assert_eq!(
index.oid_at(i),
w.as_slice(),
"entry {i} is {} but the {i}-th oid lexicographically is {}",
hex::encode(index.oid_at(i)),
hex::encode(w)
);
}
for i in 1..index.len() {
assert!(
index.key_at(i - 1) < index.key_at(i),
"keys not ascending at {i}: {} then {}",
index.key_at(i - 1),
index.key_at(i)
);
}
for e in &entries {
assert_eq!(index.lookup(&e.oid).unwrap().lookup_row, e.lookup_row);
}
}
#[test]
fn a_v1_section_is_refused_rather_than_misread() {
let entries = vec![OidEntry { oid: oid(&[0x80], 20), lookup_row: 3, ordinal: 0 }];
let mut v1 = build_section(&entries, GitHashKind::Sha1).unwrap();
v1[8..12].copy_from_slice(&1u32.to_le_bytes());
let err = match GitOidIndex::parse(v1) {
Ok(_) => panic!("a v1 section must be refused"),
Err(e) => e.to_string(),
};
assert!(err.contains("version 1"), "error must name the version: {err}");
}
fn spread_entries(n: usize) -> Vec<OidEntry> {
(0..n)
.map(|i| {
let mut o = [0u8; 20];
let mut z = (i as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15);
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
o[..8].copy_from_slice(&(z ^ (z >> 31)).to_be_bytes());
o[8..12].copy_from_slice(&(i as u32).to_be_bytes());
OidEntry { oid: o.to_vec(), lookup_row: (i as u64) * 7 + 1, ordinal: i as u32 }
})
.collect()
}
#[test]
fn each_layout_puts_the_keyspace_where_it_claims() {
let entries = spread_entries(5_000);
let mut phases = Vec::new();
for layout in OidLayout::ALL {
let section = build_section_with_layout(&entries, GitHashKind::Sha1, layout).unwrap();
let index = GitOidIndex::parse_as(section, layout).unwrap();
let phase = index.keyspace_phase();
match layout {
OidLayout::Aligned64 => assert_eq!(
phase, 0,
"aligned64 keyspace must sit at phase 0, got {phase}"
),
OidLayout::Compact64Alloc => assert_eq!(
phase, 24,
"compact24+align keyspace must sit at phase 24 (64-aligned base + 24-byte \
header), got {phase}"
),
OidLayout::Compact => {}
}
assert_eq!(index.layout(), layout);
phases.push(phase);
}
assert_ne!(
phases[0], phases[2],
"compact and aligned64 landed on the same cache-line phase ({}), so there is no \
experiment left to run",
phases[0]
);
assert_ne!(phases[1], phases[2]);
}
#[test]
fn aligned_and_compact_are_the_same_index_byte_for_byte() {
let entries = spread_entries(20_000);
let hash = GitHashKind::Sha1;
let sections: Vec<Vec<u8>> = OidLayout::ALL
.iter()
.map(|&l| build_section_with_layout(&entries, hash, l).unwrap())
.collect();
for (i, l) in OidLayout::ALL.iter().enumerate() {
assert_eq!(
§ions[i][l.header_len()..],
§ions[0][OidLayout::Compact.header_len()..],
"{} moved a payload byte; it is supposed to move only the header",
l.name()
);
}
let mut queries: Vec<Vec<u8>> = Vec::new();
for (i, e) in entries.iter().enumerate() {
queries.push(e.oid.clone());
let mut absent = e.oid.clone();
absent[19] ^= 0x5a;
absent[0] ^= if i % 2 == 0 { 0x80 } else { 0x00 };
queries.push(absent);
}
let refs: Vec<&[u8]> = queries.iter().map(|q| q.as_slice()).collect();
let indices: Vec<GitOidIndex> = sections
.into_iter()
.zip(OidLayout::ALL)
.map(|(s, l)| GitOidIndex::parse_as(s, l).unwrap())
.collect();
let base_serial: Vec<Option<OidHit>> = refs.iter().map(|o| indices[0].lookup(o)).collect();
let base_batch = indices[0].lookup_batch(&refs);
assert_eq!(base_serial, base_batch);
let hits = base_serial.iter().filter(|h| h.is_some()).count();
assert_eq!(hits, entries.len(), "premise: every present oid must resolve");
assert!(base_serial.iter().any(|h| h.is_none()), "premise: some queries must miss");
for (i, l) in OidLayout::ALL.iter().enumerate().skip(1) {
for (q, want) in base_serial.iter().enumerate() {
assert_eq!(
&indices[i].lookup(refs[q]),
want,
"{} disagrees with compact24 on the serial path at {q}",
l.name()
);
}
assert_eq!(
indices[i].lookup_batch(&refs),
base_batch,
"{} disagrees with compact24 on the batch path",
l.name()
);
for e in 0..entries.len() {
assert_eq!(indices[i].key_at(e), indices[0].key_at(e));
assert_eq!(indices[i].oid_at(e), indices[0].oid_at(e));
}
}
}
#[test]
fn build_rejects_an_oid_of_the_wrong_width() {
let entries = vec![OidEntry { oid: oid(&[1], 20), lookup_row: 0, ordinal: 0 }];
assert!(build_section(&entries, GitHashKind::Sha256).is_err());
}
}