///| Packfile parsing (deflate + delta)
///|
/// Hard cap on the inflated size of a single pack-encoded object, in
/// bytes. The per-object size header is taken from untrusted pack
/// bytes, so we refuse to call `zlib_decompress_at` if the claimed
/// size is unreasonable — otherwise an attacker can hand us a small
/// pack whose entries claim multi-gigabyte inflated sizes, OOMing
/// the process before any other check fires.
///
/// 2 GiB matches `git`'s effective ceiling on 32-bit size fields.
/// If/when we expose this as `BIT_MAX_OBJECT_SIZE`, the const becomes
/// the default.
const MAX_PACK_OBJECT_SIZE : Int = 0x7fff_ffff
///|
fn check_pack_object_size(size : Int) -> Unit raise @bit.GitError {
if size < 0 || size > MAX_PACK_OBJECT_SIZE {
raise @bit.GitError::PackfileError(
"Pack object size out of range: \{size} (max \{MAX_PACK_OBJECT_SIZE})",
)
}
}
///|
/// Hard cap on the delta chain length for a single pack-encoded
/// object. Matches git's runtime ceiling (50). Resolving a delta
/// chain is iterative here, so depth doesn't blow the stack; the
/// concern is the per-step `apply_delta` cost — at depth 1000+ a
/// hostile pack can drag parsing into the seconds-per-object range.
///
/// OFS_DELTA cycles are impossible by construction (base_offset is
/// strictly less than the current offset, enforced earlier).
/// REF_DELTA cycles are caught by the iterative `progress` flag and
/// leave entries unresolved — but the resolved-chain depth is the
/// remaining bound.
const MAX_DELTA_DEPTH : Int = 50
///|
fn check_delta_depth(depth : Int) -> Unit raise @bit.GitError {
if depth > MAX_DELTA_DEPTH {
raise @bit.GitError::PackfileError(
"Delta chain too deep: \{depth} (max \{MAX_DELTA_DEPTH})",
)
}
}
///|
fn hash_algo_from_size(hash_size : Int) -> @bit.HashAlgorithm {
if hash_size == 32 {
@bit.HashAlgorithm::Sha256
} else {
@bit.HashAlgorithm::Sha1
}
}
///|
/// Parse a packfile into objects.
pub fn parse_packfile(
data : Bytes,
verify_checksum? : Bool = true,
hash_size? : Int = 20,
) -> Array[@bit.PackObject] raise @bit.GitError {
parse_packfile_with_bases(data, [], verify_checksum~, hash_size~)
}
///|
/// Compute the maximum delta chain length in a packfile
pub fn max_delta_chain_length(
data : Bytes,
hash_size? : Int = 20,
) -> Int raise @bit.GitError {
if data.length() < 32 {
raise @bit.GitError::PackfileError("Packfile too short")
}
if !(data[0] == b'P' && data[1] == b'A' && data[2] == b'C' && data[3] == b'K') {
raise @bit.GitError::PackfileError("Invalid packfile magic")
}
let version = read_u32_be(data, 4)
if version != 2 {
raise @bit.GitError::PackfileError(
"Unsupported packfile version: \{version}",
)
}
let count = read_u32_be(data, 8)
let mut offset = 12
let objects : Array[@bit.PackObject] = []
let resolved : Array[Bool] = []
let depths : Array[Int] = []
let pending : Array[PendingDelta] = []
let by_offset : Map[Int, Int] = Map([])
let by_id : Map[@bit.ObjectId, @bit.PackObject] = Map([])
let by_id_depth : Map[@bit.ObjectId, Int] = Map([])
let mut max_depth = 0
for _ in 0.. {
let obj_type = packfile_type_to_object_type(type_id)
let (content, after) = @zlib.zlib_decompress_at(
data,
offset,
max_size=size,
) catch {
e => raise @bit.GitError::PackfileError("Zlib error: \{e}")
}
if content.length() != size {
raise @bit.GitError::PackfileError(
"Object size mismatch: expected=\{size}, got=\{content.length()}",
)
}
(obj_type, content, after)
}
6 => {
let (back_offset, after_ref) = read_ofs_delta_offset(data, offset)
let base_offset = obj_offset - back_offset
if base_offset < 0 {
raise @bit.GitError::PackfileError("Invalid OFS_DELTA base offset")
}
let (delta, after) = @zlib.zlib_decompress_at(
data,
after_ref,
max_size=size,
) catch {
e => raise @bit.GitError::PackfileError("Zlib error: \{e}")
}
if delta.length() != size {
raise @bit.GitError::PackfileError(
"Delta size mismatch: expected=\{size}, got=\{delta.length()}",
)
}
let crc = crc32_range(data, obj_offset, after)
let placeholder = @bit.PackObject::with_metadata(
@bit.ObjectType::Blob,
Bytes::from_array([]),
@bit.ObjectId::zero(),
obj_offset,
crc,
)
objects.push(placeholder)
resolved.push(false)
depths.push(0)
let idx = objects.length() - 1
by_offset[obj_offset] = idx
match by_offset.get(base_offset) {
Some(base_index) =>
if base_index >= 0 &&
base_index < objects.length() &&
resolved[base_index] {
let base = objects[base_index]
let content = apply_delta(base.data, delta)
let obj_id = @bit.hash_object_content_with_algo(
hash_algo_from_size(hash_size),
base.obj_type,
content,
)
let obj = @bit.PackObject::with_metadata(
base.obj_type,
content,
obj_id,
obj_offset,
crc,
)
objects[idx] = obj
resolved[idx] = true
let depth = depths[base_index] + 1
check_delta_depth(depth)
depths[idx] = depth
if depth > max_depth {
max_depth = depth
}
insert_pack_object_by_id(by_id, obj)
by_id_depth[obj_id] = depth
} else {
pending.push({
index: idx,
base: DeltaBaseRef::Ofs(base_index),
delta,
})
}
None =>
raise @bit.GitError::PackfileError(
"Missing base object for OFS_DELTA",
)
}
(@bit.ObjectType::Blob, Bytes::from_array([]), after)
}
7 => {
let (base_oid, after_ref) = read_ref_delta_object_id(
data,
offset,
hash_size~,
)
let (delta, after) = @zlib.zlib_decompress_at(
data,
after_ref,
max_size=size,
) catch {
e => raise @bit.GitError::PackfileError("Zlib error: \{e}")
}
if delta.length() != size {
raise @bit.GitError::PackfileError(
"Delta size mismatch: expected=\{size}, got=\{delta.length()}",
)
}
let crc = crc32_range(data, obj_offset, after)
let placeholder = @bit.PackObject::with_metadata(
@bit.ObjectType::Blob,
Bytes::from_array([]),
@bit.ObjectId::zero(),
obj_offset,
crc,
)
objects.push(placeholder)
resolved.push(false)
depths.push(0)
let idx = objects.length() - 1
by_offset[obj_offset] = idx
match by_id.get(base_oid) {
Some(base) => {
let content = apply_delta(base.data, delta)
let obj_id = @bit.hash_object_content_with_algo(
hash_algo_from_size(hash_size),
base.obj_type,
content,
)
let obj = @bit.PackObject::with_metadata(
base.obj_type,
content,
obj_id,
obj_offset,
crc,
)
objects[idx] = obj
resolved[idx] = true
let base_depth = by_id_depth.get(base_oid).unwrap_or(0)
let depth = base_depth + 1
check_delta_depth(depth)
depths[idx] = depth
if depth > max_depth {
max_depth = depth
}
insert_pack_object_by_id(by_id, obj)
by_id_depth[obj_id] = depth
}
None =>
pending.push({
index: idx,
base: DeltaBaseRef::Ref(base_oid),
delta,
})
}
(@bit.ObjectType::Blob, Bytes::from_array([]), after)
}
_ =>
raise @bit.GitError::PackfileError(
"Unknown packfile object type: \{type_id}",
)
}
offset = after
if type_id == 1 || type_id == 2 || type_id == 3 || type_id == 4 {
let crc = crc32_range(data, obj_offset, after)
let obj_id = @bit.hash_object_content_with_algo(
hash_algo_from_size(hash_size),
obj_type,
content,
)
let obj = @bit.PackObject::with_metadata(
obj_type, content, obj_id, obj_offset, crc,
)
objects.push(obj)
resolved.push(true)
depths.push(0)
by_offset[obj_offset] = objects.length() - 1
insert_pack_object_by_id(by_id, obj)
by_id_depth[obj_id] = 0
}
}
let mut progress = true
let mut pending_list = pending
while pending_list.length() > 0 && progress {
progress = false
let next_pending : Array[PendingDelta] = []
for p in pending_list {
match p.base {
DeltaBaseRef::Ofs(base_index) =>
if base_index >= 0 &&
base_index < objects.length() &&
resolved[base_index] {
let base = objects[base_index]
let content = apply_delta(base.data, p.delta)
let obj_id = @bit.hash_object_content_with_algo(
hash_algo_from_size(hash_size),
base.obj_type,
content,
)
let old = objects[p.index]
let obj = @bit.PackObject::with_metadata(
base.obj_type,
content,
obj_id,
old.offset,
old.crc32,
)
objects[p.index] = obj
resolved[p.index] = true
let depth = depths[base_index] + 1
check_delta_depth(depth)
depths[p.index] = depth
if depth > max_depth {
max_depth = depth
}
insert_pack_object_by_id(by_id, obj)
by_id_depth[obj_id] = depth
progress = true
} else {
next_pending.push(p)
}
DeltaBaseRef::Ref(base_oid) =>
match by_id.get(base_oid) {
Some(base) => {
let content = apply_delta(base.data, p.delta)
let obj_id = @bit.hash_object_content_with_algo(
hash_algo_from_size(hash_size),
base.obj_type,
content,
)
let old = objects[p.index]
let obj = @bit.PackObject::with_metadata(
base.obj_type,
content,
obj_id,
old.offset,
old.crc32,
)
objects[p.index] = obj
resolved[p.index] = true
let base_depth = by_id_depth.get(base_oid).unwrap_or(0)
let depth = base_depth + 1
check_delta_depth(depth)
depths[p.index] = depth
if depth > max_depth {
max_depth = depth
}
insert_pack_object_by_id(by_id, obj)
by_id_depth[obj_id] = depth
progress = true
}
None => next_pending.push(p)
}
}
}
pending_list = next_pending
}
if pending_list.length() > 0 {
raise @bit.GitError::PackfileError("Unresolved delta objects")
}
let trailer_offset = data.length() - hash_size
if offset != trailer_offset {
raise @bit.GitError::PackfileError("Packfile length mismatch")
}
let computed = @bit.hash_prefix(data, trailer_offset, hash_size~)
let trailer = read_trailer_id(data, trailer_offset, hash_size~)
if computed != trailer {
raise @bit.GitError::HashMismatch(computed.to_hex(), trailer.to_hex())
}
max_depth
}
///|
/// Parse a packfile with preloaded base objects (for thin packs).
pub fn parse_packfile_with_bases(
data : Bytes,
bases : Array[@bit.PackObject],
verify_checksum? : Bool = true,
hash_size? : Int = 20,
) -> Array[@bit.PackObject] raise @bit.GitError {
let by_offset : Map[Int, Int] = Map([])
let by_id : Map[@bit.ObjectId, @bit.PackObject] = Map([])
for obj in bases {
let id = @bit.hash_object_content_with_algo(
hash_algo_from_size(hash_size),
obj.obj_type,
obj.data,
)
let base_obj = @bit.PackObject::with_metadata(
obj.obj_type,
obj.data,
id,
obj.offset,
obj.crc32,
)
insert_pack_object_by_id(by_id, base_obj)
}
parse_packfile_internal(data, by_offset, by_id, verify_checksum~, hash_size~)
}
///|
/// Parse a packfile and emit resolved objects (order not guaranteed).
pub fn parse_packfile_stream(
data : Bytes,
emit : (@bit.PackObject) -> Unit,
verify_checksum? : Bool = true,
hash_size? : Int = 20,
) -> Unit raise @bit.GitError {
parse_packfile_stream_with_bases(data, [], emit, verify_checksum~, hash_size~)
}
///|
/// Parse a packfile with base objects and emit resolved objects.
pub fn parse_packfile_stream_with_bases(
data : Bytes,
bases : Array[@bit.PackObject],
emit : (@bit.PackObject) -> Unit,
verify_checksum? : Bool = true,
hash_size? : Int = 20,
) -> Unit raise @bit.GitError {
let by_offset : Map[Int, @bit.PackObject] = Map([])
let by_id : Map[@bit.ObjectId, @bit.PackObject] = Map([])
for obj in bases {
let id = @bit.hash_object_content_with_algo(
hash_algo_from_size(hash_size),
obj.obj_type,
obj.data,
)
let base_obj = @bit.PackObject::with_metadata(
obj.obj_type,
obj.data,
id,
obj.offset,
obj.crc32,
)
insert_pack_object_by_id(by_id, base_obj)
}
parse_packfile_stream_internal(
data,
by_offset,
by_id,
emit,
verify_checksum~,
hash_size~,
)
}
///|
fn insert_pack_object_by_id(
by_id : Map[@bit.ObjectId, @bit.PackObject],
obj : @bit.PackObject,
) -> Unit raise @bit.GitError {
match by_id.get(obj.id) {
Some(existing) =>
if existing.obj_type != obj.obj_type ||
!Bytes::equal(existing.data, obj.data) {
raise @bit.GitError::PackfileError("SHA1 COLLISION FOUND")
}
None => ()
}
by_id[obj.id] = obj
}
///|
priv enum DeltaBaseRef {
Ofs(Int)
Ref(@bit.ObjectId)
}
///|
priv struct PendingDelta {
index : Int
base : DeltaBaseRef
delta : Bytes
}
///|
priv struct PendingDeltaStream {
offset : Int
base : DeltaBaseRef
delta : Bytes
}
///|
fn parse_packfile_internal(
data : Bytes,
by_offset : Map[Int, Int],
by_id : Map[@bit.ObjectId, @bit.PackObject],
verify_checksum? : Bool = true,
hash_size? : Int = 20,
) -> Array[@bit.PackObject] raise @bit.GitError {
if data.length() < 32 {
raise @bit.GitError::PackfileError("Packfile too short")
}
// Magic: "PACK"
if !(data[0] == b'P' && data[1] == b'A' && data[2] == b'C' && data[3] == b'K') {
raise @bit.GitError::PackfileError("Invalid packfile magic")
}
let version = read_u32_be(data, 4)
if version != 2 {
raise @bit.GitError::PackfileError(
"Unsupported packfile version: \{version}",
)
}
let count = read_u32_be(data, 8)
let mut offset = 12
let objects : Array[@bit.PackObject] = Array::new(capacity=count)
let resolved : Array[Bool] = Array::new(capacity=count)
let pending : Array[PendingDelta] = []
for _ in 0.. {
let obj_type = packfile_type_to_object_type(type_id)
let (content, after) = @zlib.zlib_decompress_at(
data,
offset,
max_size=size,
) catch {
e => raise @bit.GitError::PackfileError("Zlib error: \{e}")
}
if content.length() != size {
raise @bit.GitError::PackfileError(
"Object size mismatch: expected=\{size}, got=\{content.length()}",
)
}
(obj_type, content, after)
}
6 => {
let (back_offset, after_ref) = read_ofs_delta_offset(data, offset)
let base_offset = obj_offset - back_offset
if base_offset < 0 {
raise @bit.GitError::PackfileError("Invalid OFS_DELTA base offset")
}
let (delta, after) = @zlib.zlib_decompress_at(
data,
after_ref,
max_size=size,
) catch {
e => raise @bit.GitError::PackfileError("Zlib error: \{e}")
}
if delta.length() != size {
raise @bit.GitError::PackfileError(
"Delta size mismatch: expected=\{size}, got=\{delta.length()}",
)
}
// Compute CRC32 for this delta object
let crc = crc32_range(data, obj_offset, after)
let placeholder = @bit.PackObject::with_metadata(
@bit.ObjectType::Blob,
Bytes::from_array([]),
@bit.ObjectId::zero(),
obj_offset,
crc,
)
objects.push(placeholder)
resolved.push(false)
let idx = objects.length() - 1
by_offset[obj_offset] = idx
match by_offset.get(base_offset) {
Some(base_index) =>
if resolved[base_index] {
let base = objects[base_index]
let content = apply_delta(base.data, delta)
let obj_id = @bit.hash_object_content_with_algo(
hash_algo_from_size(hash_size),
base.obj_type,
content,
)
let obj = @bit.PackObject::with_metadata(
base.obj_type,
content,
obj_id,
obj_offset,
crc,
)
objects[idx] = obj
resolved[idx] = true
insert_pack_object_by_id(by_id, obj)
} else {
pending.push({
index: idx,
base: DeltaBaseRef::Ofs(base_index),
delta,
})
}
None =>
raise @bit.GitError::PackfileError(
"Missing base object for OFS_DELTA",
)
}
(@bit.ObjectType::Blob, Bytes::from_array([]), after)
}
7 => {
let (base_oid, after_ref) = read_ref_delta_object_id(
data,
offset,
hash_size~,
)
let (delta, after) = @zlib.zlib_decompress_at(
data,
after_ref,
max_size=size,
) catch {
e => raise @bit.GitError::PackfileError("Zlib error: \{e}")
}
if delta.length() != size {
raise @bit.GitError::PackfileError(
"Delta size mismatch: expected=\{size}, got=\{delta.length()}",
)
}
// Compute CRC32 for this delta object
let crc = crc32_range(data, obj_offset, after)
let placeholder = @bit.PackObject::with_metadata(
@bit.ObjectType::Blob,
Bytes::from_array([]),
@bit.ObjectId::zero(),
obj_offset,
crc,
)
objects.push(placeholder)
resolved.push(false)
let idx = objects.length() - 1
by_offset[obj_offset] = idx
match by_id.get(base_oid) {
Some(base) => {
let content = apply_delta(base.data, delta)
let obj_id = @bit.hash_object_content_with_algo(
hash_algo_from_size(hash_size),
base.obj_type,
content,
)
let obj = @bit.PackObject::with_metadata(
base.obj_type,
content,
obj_id,
obj_offset,
crc,
)
objects[idx] = obj
resolved[idx] = true
insert_pack_object_by_id(by_id, obj)
}
None =>
pending.push({
index: idx,
base: DeltaBaseRef::Ref(base_oid),
delta,
})
}
(@bit.ObjectType::Blob, Bytes::from_array([]), after)
}
_ =>
raise @bit.GitError::PackfileError(
"Unknown packfile object type: \{type_id}",
)
}
offset = after
if type_id == 1 || type_id == 2 || type_id == 3 || type_id == 4 {
// Compute CRC32 and hash for non-delta objects
let crc = crc32_range(data, obj_offset, after)
let obj_id = @bit.hash_object_content_with_algo(
hash_algo_from_size(hash_size),
obj_type,
content,
)
let obj = @bit.PackObject::with_metadata(
obj_type, content, obj_id, obj_offset, crc,
)
objects.push(obj)
resolved.push(true)
by_offset[obj_offset] = objects.length() - 1
insert_pack_object_by_id(by_id, obj)
}
}
let mut progress = true
let mut pending_list = pending
while pending_list.length() > 0 && progress {
progress = false
let next_pending : Array[PendingDelta] = []
for p in pending_list {
match p.base {
DeltaBaseRef::Ofs(base_index) =>
if base_index >= 0 &&
base_index < objects.length() &&
resolved[base_index] {
let base = objects[base_index]
let content = apply_delta(base.data, p.delta)
let obj_id = @bit.hash_object_content_with_algo(
hash_algo_from_size(hash_size),
base.obj_type,
content,
)
// Preserve offset and crc from placeholder
let old = objects[p.index]
let obj = @bit.PackObject::with_metadata(
base.obj_type,
content,
obj_id,
old.offset,
old.crc32,
)
objects[p.index] = obj
resolved[p.index] = true
insert_pack_object_by_id(by_id, obj)
progress = true
} else {
next_pending.push(p)
}
DeltaBaseRef::Ref(base_oid) =>
match by_id.get(base_oid) {
Some(base) => {
let content = apply_delta(base.data, p.delta)
let obj_id = @bit.hash_object_content_with_algo(
hash_algo_from_size(hash_size),
base.obj_type,
content,
)
// Preserve offset and crc from placeholder
let old = objects[p.index]
let obj = @bit.PackObject::with_metadata(
base.obj_type,
content,
obj_id,
old.offset,
old.crc32,
)
objects[p.index] = obj
resolved[p.index] = true
insert_pack_object_by_id(by_id, obj)
progress = true
}
None => next_pending.push(p)
}
}
}
pending_list = next_pending
}
if pending_list.length() > 0 {
raise @bit.GitError::PackfileError("Unresolved delta objects")
}
let trailer_offset = data.length() - hash_size
if offset != trailer_offset {
raise @bit.GitError::PackfileError("Packfile length mismatch")
}
if verify_checksum {
let computed = @bit.hash_prefix(data, trailer_offset, hash_size~)
let trailer = read_trailer_id(data, trailer_offset, hash_size~)
if computed != trailer {
raise @bit.GitError::HashMismatch(computed.to_hex(), trailer.to_hex())
}
}
objects
}
///|
fn parse_packfile_stream_internal(
data : Bytes,
by_offset : Map[Int, @bit.PackObject],
by_id : Map[@bit.ObjectId, @bit.PackObject],
emit : (@bit.PackObject) -> Unit,
verify_checksum? : Bool = true,
hash_size? : Int = 20,
) -> Unit raise @bit.GitError {
if data.length() < 32 {
raise @bit.GitError::PackfileError("Packfile too short")
}
if !(data[0] == b'P' && data[1] == b'A' && data[2] == b'C' && data[3] == b'K') {
raise @bit.GitError::PackfileError("Invalid packfile magic")
}
let version = read_u32_be(data, 4)
if version != 2 {
raise @bit.GitError::PackfileError(
"Unsupported packfile version: \{version}",
)
}
let count = read_u32_be(data, 8)
let mut offset = 12
let algo = hash_algo_from_size(hash_size)
let pending : Array[PendingDeltaStream] = []
// depth tracking — see MAX_DELTA_DEPTH (parse_packfile_with_bases
// does the same). Stream variant emits objects as they resolve, so
// we keep depth side-tables keyed by offset and id.
let depth_by_offset : Map[Int, Int] = Map([])
let depth_by_id : Map[@bit.ObjectId, Int] = Map([])
for _ in 0.. {
let obj_type = packfile_type_to_object_type(type_id)
let (content, after) = @zlib.zlib_decompress_at(
data,
offset,
max_size=size,
) catch {
e => raise @bit.GitError::PackfileError("Zlib error: \{e}")
}
if content.length() != size {
raise @bit.GitError::PackfileError(
"Object size mismatch: expected=\{size}, got=\{content.length()}",
)
}
let obj_id = @bit.hash_object_content_with_algo(algo, obj_type, content)
let obj = @bit.PackObject::with_metadata(
obj_type, content, obj_id, obj_offset, 0U,
)
by_offset[obj_offset] = obj
insert_pack_object_by_id(by_id, obj)
depth_by_offset[obj_offset] = 0
depth_by_id[obj_id] = 0
emit(obj)
offset = after
}
6 => {
let (back_offset, after_ref) = read_ofs_delta_offset(data, offset)
let base_offset = obj_offset - back_offset
if base_offset < 0 {
raise @bit.GitError::PackfileError("Invalid OFS_DELTA base offset")
}
let (delta, after) = @zlib.zlib_decompress_at(
data,
after_ref,
max_size=size,
) catch {
e => raise @bit.GitError::PackfileError("Zlib error: \{e}")
}
if delta.length() != size {
raise @bit.GitError::PackfileError(
"Delta size mismatch: expected=\{size}, got=\{delta.length()}",
)
}
match by_offset.get(base_offset) {
Some(base) => {
let content = apply_delta(base.data, delta)
let obj_id = @bit.hash_object_content_with_algo(
algo,
base.obj_type,
content,
)
let obj = @bit.PackObject::with_metadata(
base.obj_type,
content,
obj_id,
obj_offset,
0U,
)
by_offset[obj_offset] = obj
insert_pack_object_by_id(by_id, obj)
let depth = depth_by_offset.get(base_offset).unwrap_or(0) + 1
check_delta_depth(depth)
depth_by_offset[obj_offset] = depth
depth_by_id[obj_id] = depth
emit(obj)
}
None =>
pending.push({
offset: obj_offset,
base: DeltaBaseRef::Ofs(base_offset),
delta,
})
}
offset = after
}
7 => {
let (base_oid, after_ref) = read_ref_delta_object_id(
data,
offset,
hash_size~,
)
let (delta, after) = @zlib.zlib_decompress_at(
data,
after_ref,
max_size=size,
) catch {
e => raise @bit.GitError::PackfileError("Zlib error: \{e}")
}
if delta.length() != size {
raise @bit.GitError::PackfileError(
"Delta size mismatch: expected=\{size}, got=\{delta.length()}",
)
}
match by_id.get(base_oid) {
Some(base) => {
let content = apply_delta(base.data, delta)
let obj_id = @bit.hash_object_content_with_algo(
algo,
base.obj_type,
content,
)
let obj = @bit.PackObject::with_metadata(
base.obj_type,
content,
obj_id,
obj_offset,
0U,
)
by_offset[obj_offset] = obj
insert_pack_object_by_id(by_id, obj)
let depth = depth_by_id.get(base_oid).unwrap_or(0) + 1
check_delta_depth(depth)
depth_by_offset[obj_offset] = depth
depth_by_id[obj_id] = depth
emit(obj)
}
None =>
pending.push({
offset: obj_offset,
base: DeltaBaseRef::Ref(base_oid),
delta,
})
}
offset = after
}
_ =>
raise @bit.GitError::PackfileError(
"Unknown packfile object type: \{type_id}",
)
}
}
let mut pending_list = pending
while pending_list.length() > 0 {
let mut progress = false
let next_pending : Array[PendingDeltaStream] = []
for p in pending_list {
match p.base {
Ofs(base_offset) =>
match by_offset.get(base_offset) {
Some(base) => {
let content = apply_delta(base.data, p.delta)
let obj_id = @bit.hash_object_content_with_algo(
algo,
base.obj_type,
content,
)
let obj = @bit.PackObject::with_metadata(
base.obj_type,
content,
obj_id,
p.offset,
0U,
)
by_offset[p.offset] = obj
insert_pack_object_by_id(by_id, obj)
let depth = depth_by_offset.get(base_offset).unwrap_or(0) + 1
check_delta_depth(depth)
depth_by_offset[p.offset] = depth
depth_by_id[obj_id] = depth
emit(obj)
progress = true
}
None => next_pending.push(p)
}
Ref(base_oid) =>
match by_id.get(base_oid) {
Some(base) => {
let content = apply_delta(base.data, p.delta)
let obj_id = @bit.hash_object_content_with_algo(
algo,
base.obj_type,
content,
)
let obj = @bit.PackObject::with_metadata(
base.obj_type,
content,
obj_id,
p.offset,
0U,
)
by_offset[p.offset] = obj
insert_pack_object_by_id(by_id, obj)
let depth = depth_by_id.get(base_oid).unwrap_or(0) + 1
check_delta_depth(depth)
depth_by_offset[p.offset] = depth
depth_by_id[obj_id] = depth
emit(obj)
progress = true
}
None => next_pending.push(p)
}
}
}
if !progress {
break
}
pending_list = next_pending
}
if pending_list.length() > 0 {
raise @bit.GitError::PackfileError("Unresolved delta objects")
}
let trailer_offset = data.length() - hash_size
if offset != trailer_offset {
raise @bit.GitError::PackfileError("Packfile length mismatch")
}
if verify_checksum {
let computed = @bit.hash_prefix(data, trailer_offset, hash_size~)
let trailer = read_trailer_id(data, trailer_offset, hash_size~)
if computed != trailer {
raise @bit.GitError::HashMismatch(computed.to_hex(), trailer.to_hex())
}
}
}
///|
/// Decode packfile object header and return (type_id, size, next_offset).
pub fn decode_type_and_size_at(
data : Bytes,
start : Int,
) -> (Int, Int, Int) raise @bit.GitError {
if start < 0 || start >= data.length() {
raise @bit.GitError::PackfileError("Unexpected end of packfile header")
}
let first = data[start].to_int()
let mut size = first & 0x0f
let type_id = (first >> 4) & 0x07
let mut shift = 4
let mut offset = start + 1
let mut has_more = (first & 0x80) != 0
while has_more {
if offset >= data.length() {
raise @bit.GitError::PackfileError("Unexpected end of packfile size")
}
let b = data[offset].to_int()
offset += 1
size = size | ((b & 0x7f) << shift)
shift += 7
has_more = (b & 0x80) != 0
}
(type_id, size, offset)
}
///|
/// Convert packfile type id to ObjectType.
pub fn packfile_type_to_object_type(
type_id : Int,
) -> @bit.ObjectType raise @bit.GitError {
match type_id {
1 => @bit.ObjectType::Commit
2 => @bit.ObjectType::Tree
3 => @bit.ObjectType::Blob
4 => @bit.ObjectType::Tag
_ =>
raise @bit.GitError::PackfileError(
"Unknown packfile object type: \{type_id}",
)
}
}
///|
pub fn read_ofs_delta_offset(
data : Bytes,
start : Int,
) -> (Int, Int) raise @bit.GitError {
if start >= data.length() {
raise @bit.GitError::PackfileError("Unexpected end of packfile delta")
}
let mut offset = start
let mut c = data[offset].to_int()
offset += 1
let mut val = c & 0x7f
while (c & 0x80) != 0 {
if offset >= data.length() {
raise @bit.GitError::PackfileError("Unexpected end of packfile delta")
}
c = data[offset].to_int()
offset += 1
val = (val + 1) << 7
val = val | (c & 0x7f)
}
(val, offset)
}
///|
fn read_ref_delta_object_id(
data : Bytes,
start : Int,
hash_size? : Int = 20,
) -> (@bit.ObjectId, Int) raise @bit.GitError {
if start + hash_size > data.length() {
raise @bit.GitError::PackfileError("Unexpected end of packfile delta")
}
let bytes : FixedArray[Byte] = FixedArray::make(hash_size, b'\x00')
for i in 0.. (String, Int) raise @bit.GitError {
if start + hash_size > data.length() {
raise @bit.GitError::PackfileError("Unexpected end of packfile delta")
}
let bytes : FixedArray[Byte] = FixedArray::make(hash_size, b'\x00')
for i in 0.. Bytes raise @bit.GitError {
let mut offset = 0
let (base_size, off1) = read_delta_size(delta, offset)
offset = off1
if base_size != base.length() {
raise @bit.GitError::PackfileError("Delta base size mismatch")
}
let (result_size, off2) = read_delta_size(delta, offset)
offset = off2
// Same cap as pack object size: prevents `FixedArray::make` from
// attempting a multi-GB allocation when the delta header claims an
// outsized result (or the varint wraps to a value the caller can't
// service).
if result_size < 0 || result_size > MAX_PACK_OBJECT_SIZE {
raise @bit.GitError::PackfileError(
"Delta result size out of range: \{result_size}",
)
}
let out : FixedArray[Byte] = FixedArray::make(result_size, b'\x00')
let mut out_pos = 0
while offset < delta.length() {
let opcode = delta[offset].to_int()
offset += 1
if (opcode & 0x80) != 0 {
let mut cp_off = 0
let mut cp_size = 0
if (opcode & 0x01) != 0 {
if offset >= delta.length() {
raise @bit.GitError::PackfileError("Delta copy header truncated")
}
cp_off = cp_off | delta[offset].to_int()
offset += 1
}
if (opcode & 0x02) != 0 {
if offset >= delta.length() {
raise @bit.GitError::PackfileError("Delta copy header truncated")
}
cp_off = cp_off | (delta[offset].to_int() << 8)
offset += 1
}
if (opcode & 0x04) != 0 {
if offset >= delta.length() {
raise @bit.GitError::PackfileError("Delta copy header truncated")
}
cp_off = cp_off | (delta[offset].to_int() << 16)
offset += 1
}
if (opcode & 0x08) != 0 {
if offset >= delta.length() {
raise @bit.GitError::PackfileError("Delta copy header truncated")
}
cp_off = cp_off | (delta[offset].to_int() << 24)
offset += 1
}
if (opcode & 0x10) != 0 {
if offset >= delta.length() {
raise @bit.GitError::PackfileError("Delta copy header truncated")
}
cp_size = cp_size | delta[offset].to_int()
offset += 1
}
if (opcode & 0x20) != 0 {
if offset >= delta.length() {
raise @bit.GitError::PackfileError("Delta copy header truncated")
}
cp_size = cp_size | (delta[offset].to_int() << 8)
offset += 1
}
if (opcode & 0x40) != 0 {
if offset >= delta.length() {
raise @bit.GitError::PackfileError("Delta copy header truncated")
}
cp_size = cp_size | (delta[offset].to_int() << 16)
offset += 1
}
if cp_size == 0 {
cp_size = 0x10000
}
// Bounds check guarding against integer overflow on Int32:
// a `cp_off` near 2^31 plus a 7-bit-shifted `cp_size` can wrap
// into a small negative value, slipping past
// `cp_off + cp_size > base.length()`.
let base_len = base.length()
if cp_off < 0 ||
cp_size < 0 ||
cp_off > base_len ||
cp_size > base_len - cp_off {
raise @bit.GitError::PackfileError("Delta copy out of range")
}
// Same defense for the *output* cursor — a malicious delta that
// sums its op sizes past `result_size` would otherwise index
// out of the `out` FixedArray and panic.
if out_pos < 0 || cp_size > result_size - out_pos {
raise @bit.GitError::PackfileError("Delta copy past result size")
}
copy_delta_bytes(base, cp_off, out, out_pos, cp_size)
out_pos += cp_size
} else {
let literal_len = opcode & 0x7f
if literal_len == 0 {
raise @bit.GitError::PackfileError("Invalid delta opcode")
}
if literal_len > delta.length() - offset {
raise @bit.GitError::PackfileError("Delta literal out of range")
}
if literal_len > result_size - out_pos {
raise @bit.GitError::PackfileError("Delta literal past result size")
}
copy_delta_bytes(delta, offset, out, out_pos, literal_len)
out_pos += literal_len
offset += literal_len
}
}
if out_pos != result_size {
raise @bit.GitError::PackfileError("Delta result size mismatch")
}
Bytes::from_array(out)
}
///|
fn read_delta_size(data : Bytes, start : Int) -> (Int, Int) raise @bit.GitError {
let mut offset = start
let mut size = 0
let mut shift = 0
while true {
if offset >= data.length() {
raise @bit.GitError::PackfileError("Unexpected end of delta data")
}
// Cap the shift before it can wrap MoonBit's 32-bit Int. A hostile
// delta that keeps the continuation bit set indefinitely would
// otherwise either truncate to a small `size` (silent corruption)
// or shift into the sign bit (negative size accepted by the
// caller, then handed to `FixedArray::make` — undefined for
// negative len).
if shift >= 32 {
raise @bit.GitError::PackfileError("Delta size varint too large")
}
let b = data[offset].to_int()
offset += 1
size = size | ((b & 0x7f) << shift)
if size < 0 {
raise @bit.GitError::PackfileError("Delta size overflow")
}
if (b & 0x80) == 0 {
break
}
shift = shift + 7
}
(size, offset)
}
///|
fn read_u32_be(data : Bytes, start : Int) -> Int raise @bit.GitError {
if start < 0 || start + 4 > data.length() {
raise @bit.GitError::PackfileError("Unexpected end of packfile header")
}
(data[start].to_int() << 24) |
(data[start + 1].to_int() << 16) |
(data[start + 2].to_int() << 8) |
data[start + 3].to_int()
}
///|
fn read_trailer_id(
data : Bytes,
start : Int,
hash_size? : Int = 20,
) -> @bit.ObjectId {
let bytes : FixedArray[Byte] = FixedArray::make(hash_size, b'\x00')
for i in 0..