@@ -44,6 +44,8 @@
@obj_ofs_delta 6
@obj_ref_delta 7
@base_types [@obj_commit, @obj_tree, @obj_blob, @obj_tag]
@type pack_object :: %{
type: atom(),
data: binary(),
@@ -71,33 +73,29 @@
if count > @max_pack_objects do
{:error, {:pack_too_large, count}}
else
external_resolver = Keyword.get(opts, :external_resolver)
parse_verified(pack_data, count, Keyword.get(opts, :external_resolver))
end
end
# Verify trailing checksum (PK1)
case verify_pack_checksum(pack_data) do
:ok ->
# Strip the trailing 20-byte checksum before parsing entries
def parse(_, _opts), do: {:error, :invalid_pack_header}
entries_len = byte_size(pack_data) - 12 - 20
defp parse_verified(pack_data, count, external_resolver) do
with :ok <- verify_pack_checksum(pack_data),
<<_header::binary-size(12), entries_data::binary-size(entries_len),
_checksum::binary-size(20)>> = pack_data
entries_data = strip_pack_envelope(pack_data),
{:ok, entries, cache} <- parse_entries(entries_data, count, [], %{}, 12) do
resolve_delta_entries(entries, pack_data, cache, external_resolver)
end
end
case parse_entries(entries_data, count, [], %{}, 12) do
{:ok, entries, cache} ->
resolve_delta_entries(entries, pack_data, cache, external_resolver)
defp strip_pack_envelope(pack_data) do
entries_len = byte_size(pack_data) - 12 - 20
{:error, _} = err ->
err
end
<<_header::binary-size(12), entries_data::binary-size(entries_len),
_checksum::binary-size(20)>> = pack_data
{:error, _} = err ->
err
end
end
entries_data
end
def parse(_, _opts), do: {:error, :invalid_pack_header}
@doc """
Verify the trailing SHA-1 checksum of a pack file.
"""
@@ -134,26 +132,14 @@
do_read_object(pack_data, offset, cache, 0, nil)
end
# -- Entry parsing (sequential scan) --
# -- Private --
defp parse_entries(_rest, 0, acc, cache, _abs_offset), do: {:ok, Enum.reverse(acc), cache}
defp parse_entries(rest, remaining, acc, cache, abs_offset) do
case parse_entry_at(rest, cache, 0) do
{:ok, entry, consumed, _entry_cache} ->
case parse_entry_at(rest) do
{:ok, entry, consumed} ->
entry = %{entry | offset: abs_offset}
# Build SHA-to-offset cache for non-delta objects during parsing
cache = maybe_cache_sha(cache, entry, abs_offset)
# so resolve_delta_entries can use it for REF_DELTA lookups
cache =
if entry.type in [:commit, :tree, :blob, :tag] do
type_str = Atom.to_string(entry.type)
raw = "#{type_str} #{byte_size(entry.data)}\0" <> entry.data
sha = :crypto.hash(:sha, raw) |> Base.encode16(case: :lower)
Map.put(cache, {:sha, sha}, abs_offset)
else
cache
end
<<_consumed::binary-size(consumed), new_rest::binary>> = rest
parse_entries(new_rest, remaining - 1, [entry | acc], cache, abs_offset + consumed)
@@ -163,7 +149,16 @@
end
end
defp maybe_cache_sha(cache, entry, abs_offset) do
if entry.type in [:commit, :tree, :blob, :tag] do
sha = compute_object_sha(Atom.to_string(entry.type), entry.data)
Map.put(cache, {:sha, sha}, abs_offset)
# Resolve delta entries in two phases:
else
cache
end
end
# -- Two-phase delta resolution --
#
# Phase 1: Resolve all non-REF_DELTA entries (full objects + OFS_DELTAs).
# OFS_DELTAs can be fully resolved using offset chains within the pack.
@@ -179,215 +174,203 @@
defp resolve_delta_entries(entries, pack_data, cache, external_resolver) do
{ref_deltas, others} = Enum.split_with(entries, fn e -> e.type == :ref_delta end)
# Phase 1: Resolve non-REF_DELTA entries
phase1_result =
Enum.reduce_while(others, {:ok, []}, fn entry, {:ok, acc} ->
if entry.type == :ofs_delta do
case do_read_object(pack_data, entry.offset, cache, 0, nil) do
{:ok, {resolved_type, resolved_data}} ->
resolved = %{type: resolved_type, data: resolved_data, offset: entry.offset}
{:cont, {:ok, [resolved | acc]}}
with {:ok, resolved_others} <- resolve_non_ref_deltas(others, pack_data, cache) do
if ref_deltas == [] do
{:ok, resolved_others}
else
sha_map = build_sha_data_map(resolved_others)
{:error, _} = err ->
{:halt, err}
end
else
{:cont, {:ok, [entry | acc]}}
resolve_ref_deltas(ref_deltas, pack_data, cache, sha_map, external_resolver,
resolved: resolved_others
)
end
end
end
end
end)
case phase1_result do
{:ok, resolved_others} ->
resolved_others = Enum.reverse(resolved_others)
defp resolve_non_ref_deltas(entries, pack_data, cache) do
entries
|> Enum.reduce_while({:ok, []}, &resolve_non_ref_entry(&1, &2, pack_data, cache))
|> finalize_entries()
end
if ref_deltas == [] do
{:ok, resolved_others}
else
# Build SHA→{type, data} map from all resolved objects
sha_map = build_sha_data_map(resolved_others)
defp resolve_non_ref_entry(entry, {:ok, acc}, pack_data, cache) do
if entry.type == :ofs_delta do
case do_read_object(pack_data, entry.offset, cache, 0, nil) do
{:ok, {resolved_type, resolved_data}} ->
{:cont,
{:ok, [%{type: resolved_type, data: resolved_data, offset: entry.offset} | acc]}}
# Phase 2: Resolve REF_DELTA entries using sha_map + external_resolver
resolve_ref_deltas(ref_deltas, pack_data, cache, sha_map, external_resolver,
resolved: resolved_others
)
{:error, _} = err ->
{:halt, err}
end
else
{:cont, {:ok, [entry | acc]}}
end
{:error, _} = err ->
err
end
end
# Build a SHA→{type, data} map from resolved pack entries
defp finalize_entries({:ok, reversed}), do: {:ok, Enum.reverse(reversed)}
defp finalize_entries({:error, _} = err), do: err
defp build_sha_data_map(entries) do
Enum.reduce(entries, %{}, fn entry, acc ->
type_str = Atom.to_string(entry.type)
raw = "#{type_str} #{byte_size(entry.data)}\0" <> entry.data
sha = :crypto.hash(:sha, raw) |> Base.encode16(case: :lower)
sha = compute_object_sha(Atom.to_string(entry.type), entry.data)
Map.put(acc, sha, {entry.type, entry.data})
end)
end
# Multi-pass REF_DELTA resolution: newly resolved REF_DELTAs may be
# Resolve REF_DELTA entries using the SHA data map. Falls back to external
# resolver for thin packs. Multi-pass: newly resolved REF_DELTAs may be
# bases for other REF_DELTAs.
defp resolve_ref_deltas([], _pack_data, _cache, _sha_map, _ext_resolver, resolved: resolved) do
defp resolve_ref_deltas([], _pack_data, _cache, _sha_map, _ext, resolved: resolved) do
{:ok, resolved}
end
defp resolve_ref_deltas(ref_deltas, pack_data, cache, sha_map, ext, resolved: resolved) do
defp resolve_ref_deltas(ref_deltas, pack_data, cache, sha_map, ext_resolver, resolved: resolved) do
{newly_resolved, still_pending} =
Enum.reduce(ref_deltas, {[], []}, fn entry, {done, pending} ->
case resolve_single_ref_delta(entry, pack_data, cache, sha_map, ext_resolver) do
case resolve_single_ref_delta(entry, pack_data, cache, sha_map, ext) do
{:ok, resolved_entry} -> {[resolved_entry | done], pending}
:pending -> {done, [entry | pending]}
{:error, _} = err -> throw(err)
end
end)
if newly_resolved == [] and still_pending != [] do
# No progress — remaining REF_DELTAs can't be resolved
first_pending = hd(still_pending)
# Extract the base SHA from the pack for error reporting
<<_::binary-size(first_pending.offset), _header_data::binary>> = pack_data
handle_ref_delta_pass(newly_resolved, still_pending, pack_data, cache, sha_map, ext, resolved)
catch
{:error, _} = err -> err
end
{:error,
{:ref_delta_base_not_found, "unresolvable REF_DELTA at offset #{first_pending.offset}"}}
else
# Add newly resolved to sha_map for next pass
new_sha_map =
Enum.reduce(newly_resolved, sha_map, fn entry, acc ->
type_str = Atom.to_string(entry.type)
raw = "#{type_str} #{byte_size(entry.data)}\0" <> entry.data
sha = :crypto.hash(:sha, raw) |> Base.encode16(case: :lower)
Map.put(acc, sha, {entry.type, entry.data})
defp handle_ref_delta_pass([], [first | _], _pack_data, _cache, _sha_map, _ext, _resolved) do
{:error, {:ref_delta_base_not_found, "unresolvable REF_DELTA at offset #{first.offset}"}}
end
defp handle_ref_delta_pass(
newly_resolved,
still_pending,
pack_data,
cache,
sha_map,
ext,
end)
resolved
) do
new_sha_map =
Enum.reduce(newly_resolved, sha_map, fn entry, acc ->
sha = compute_object_sha(Atom.to_string(entry.type), entry.data)
Map.put(acc, sha, {entry.type, entry.data})
end)
all_resolved = resolved ++ Enum.reverse(newly_resolved)
all_resolved = resolved ++ Enum.reverse(newly_resolved)
resolve_ref_deltas(still_pending, pack_data, cache, new_sha_map, ext_resolver,
resolved: all_resolved
)
end
catch
{:error, _} = err -> err
resolve_ref_deltas(still_pending, pack_data, cache, new_sha_map, ext, resolved: all_resolved)
end
# Try to resolve a single REF_DELTA entry
defp resolve_single_ref_delta(entry, pack_data, _cache, sha_map, ext_resolver) do
<<_::binary-size(entry.offset), data::binary>> = pack_data
case parse_object_header(data) do
{:ok, @obj_ref_delta, _size, header_len, _rest} ->
data_start = entry.offset + header_len
resolve_ref_delta_at(entry, pack_data, header_len, sha_map, ext_resolver)
<<_::binary-size(data_start), base_sha_bin::binary-size(20), compressed::binary>> =
{:ok, _, _, _, _} ->
{:error, {:unexpected_type_at_ref_delta_offset, entry.offset}}
pack_data
base_sha = Base.encode16(base_sha_bin, case: :lower)
{:error, _} = err ->
throw(err)
end
end
case decompress_data(compressed) do
{:ok, delta_data, _} ->
# Try sha_map first (objects resolved in earlier phases)
case Map.get(sha_map, base_sha) do
{base_type, base_data} ->
defp resolve_ref_delta_at(entry, pack_data, header_len, sha_map, ext_resolver) do
data_start = entry.offset + header_len
case Delta.apply(base_data, delta_data) do
{:ok, result} ->
{:ok, %{type: base_type, data: result, offset: entry.offset}}
{:error, _} = err ->
<<_::binary-size(data_start), base_sha_bin::binary-size(20), compressed::binary>> = pack_data
throw(err)
end
nil ->
# Try external resolver (thin pack support)
if is_function(ext_resolver) do
case ext_resolver.(base_sha) do
{:ok, {base_type, base_data}} ->
case Delta.apply(base_data, delta_data) do
{:ok, result} ->
base_sha = Base.encode16(base_sha_bin, case: :lower)
{:ok, %{type: base_type, data: result, offset: entry.offset}}
case decompress_data(compressed) do
{:error, _} = err ->
throw(err)
end
{:ok, delta_data, _} ->
apply_ref_delta(delta_data, base_sha, entry.offset, sha_map, ext_resolver)
{:error, _} ->
:pending
end
else
:pending
end
end
{:error, _} = err ->
throw(err)
end
end
defp apply_ref_delta(delta_data, base_sha, offset, sha_map, ext_resolver) do
case Map.get(sha_map, base_sha) do
{:error, _} = err ->
throw(err)
end
{base_type, base_data} ->
apply_delta_to_entry(base_type, base_data, delta_data, offset)
nil ->
_ ->
{:error, {:unexpected_type_at_ref_delta_offset, entry.offset}}
try_external_ref_delta(delta_data, base_sha, offset, ext_resolver)
end
end
defp try_external_ref_delta(delta_data, base_sha, offset, ext_resolver)
when is_function(ext_resolver) do
defp parse_entry_at(data, _cache, _base_offset) do
case parse_object_header(data) do
{:ok, type_num, _size, header_len, rest_after_header} ->
case type_num do
t when t in [@obj_commit, @obj_tree, @obj_blob, @obj_tag] ->
case ext_resolver.(base_sha) do
{:ok, {base_type, base_data}} ->
apply_delta_to_entry(base_type, base_data, delta_data, offset)
type = type_num_to_atom(t)
{:error, _} ->
case decompress_data(rest_after_header) do
{:ok, decompressed, compressed_len} ->
entry = %{type: type, data: decompressed, offset: 0}
{:ok, entry, header_len + compressed_len, %{}}
:pending
end
end
{:error, _} = err ->
err
end
defp try_external_ref_delta(_delta_data, _base_sha, _offset, _ext_resolver), do: :pending
defp apply_delta_to_entry(base_type, base_data, delta_data, offset) do
@obj_ofs_delta ->
case parse_ofs_delta_offset(rest_after_header) do
{:ok, _neg_offset, _ofs_len, rest_after_ofs} ->
case Delta.apply(base_data, delta_data) do
{:ok, result} -> {:ok, %{type: base_type, data: result, offset: offset}}
{:error, _} = err -> throw(err)
end
end
case decompress_data(rest_after_ofs) do
{:ok, delta_data, compressed_len} ->
# For sequential parsing, we can't resolve OFS_DELTA without
# knowing the absolute offset. Store as unresolved delta.
entry = %{type: :ofs_delta, data: delta_data, offset: 0}
ofs_len = byte_size(rest_after_header) - byte_size(rest_after_ofs)
{:ok, entry, header_len + ofs_len + compressed_len, %{}}
{:error, _} = err ->
err
end
# -- Single-entry parsing --
defp parse_entry_at(data) do
{:error, _} = err ->
err
end
with {:ok, type_num, _size, header_len, rest_after_header} <- parse_object_header(data) do
parse_entry_body(type_num, header_len, rest_after_header)
end
end
@obj_ref_delta ->
if byte_size(rest_after_header) >= 20 do
defp parse_entry_body(t, header_len, rest) when t in @base_types do
type = type_num_to_atom(t)
<<_base_sha::binary-size(20), rest_after_sha::binary>> = rest_after_header
case decompress_data(rest_after_sha) do
{:ok, delta_data, compressed_len} ->
entry = %{type: :ref_delta, data: delta_data, offset: 0}
{:ok, entry, header_len + 20 + compressed_len, %{}}
case decompress_data(rest) do
{:ok, decompressed, compressed_len} ->
{:ok, %{type: type, data: decompressed, offset: 0}, header_len + compressed_len}
{:error, _} = err ->
err
end
else
{:error, :truncated_ref_delta}
end
{:error, _} = err ->
err
end
end
defp parse_entry_body(@obj_ofs_delta, header_len, rest) do
with {:ok, _neg_offset, ofs_len, rest_after_ofs} <- parse_ofs_delta_offset(rest),
{:ok, delta_data, compressed_len} <- decompress_data(rest_after_ofs) do
{:ok, %{type: :ofs_delta, data: delta_data, offset: 0},
header_len + ofs_len + compressed_len}
end
end
defp parse_entry_body(@obj_ref_delta, header_len, rest) when byte_size(rest) >= 20 do
<<_base_sha::binary-size(20), rest_after_sha::binary>> = rest
_ ->
{:error, {:unknown_object_type, type_num}}
end
case decompress_data(rest_after_sha) do
{:ok, delta_data, compressed_len} ->
{:ok, %{type: :ref_delta, data: delta_data, offset: 0}, header_len + 20 + compressed_len}
{:error, _} = err ->
err
end
end
defp parse_entry_body(@obj_ref_delta, _header_len, _rest), do: {:error, :truncated_ref_delta}
defp parse_entry_body(type_num, _header_len, _rest),
do: {:error, {:unknown_object_type, type_num}}
# -- Recursive object resolution --
defp do_read_object(_pack_data, _offset, _cache, depth, _external_resolver)
when depth > @max_delta_depth do
{:error, :max_delta_depth_exceeded}
@@ -395,103 +378,117 @@
defp do_read_object(pack_data, offset, cache, depth, external_resolver) do
case Map.get(cache, offset) do
{type, data} ->
{:ok, {type, data}}
nil ->
resolve_object(pack_data, offset, cache, depth, external_resolver)
{type, data} -> {:ok, {type, data}}
nil -> resolve_object(pack_data, offset, cache, depth, external_resolver)
end
end
defp resolve_object(pack_data, offset, cache, depth, external_resolver) do
<<_skip::binary-size(offset), data::binary>> = pack_data
with {:ok, type_num, _size, header_len, _rest} <- parse_object_header(data) do
case parse_object_header(data) do
{:ok, type_num, _size, header_len, _rest} ->
data_start = offset + header_len
resolve_object_by_type(
type_num,
pack_data,
offset,
header_len,
cache,
depth,
external_resolver
)
end
end
defp resolve_object_by_type(t, pack_data, offset, header_len, _cache, _depth, _ext)
when t in @base_types do
type = type_num_to_atom(t)
data_start = offset + header_len
<<_::binary-size(data_start), compressed::binary>> = pack_data
case type_num do
t when t in [@obj_commit, @obj_tree, @obj_blob, @obj_tag] ->
type = type_num_to_atom(t)
<<_::binary-size(data_start), compressed::binary>> = pack_data
case decompress_data(compressed) do
case decompress_data(compressed) do
{:ok, decompressed, _} -> {:ok, {type, decompressed}}
{:error, _} = err -> err
end
end
{:ok, decompressed, _} -> {:ok, {type, decompressed}}
{:error, _} = err -> err
end
@obj_ofs_delta ->
<<_::binary-size(data_start), rest::binary>> = pack_data
defp resolve_object_by_type(@obj_ofs_delta, pack_data, offset, header_len, cache, depth, ext) do
data_start = offset + header_len
<<_::binary-size(data_start), rest::binary>> = pack_data
resolve_ofs_delta(rest, pack_data, offset, data_start, cache, depth, ext)
end
case parse_ofs_delta_offset(rest) do
{:ok, neg_offset, ofs_len, _rest_after_ofs} ->
base_offset = offset - neg_offset
defp resolve_object_by_type(@obj_ref_delta, pack_data, offset, header_len, cache, depth, ext) do
data_start = offset + header_len
if base_offset < 0 do
{:error, :invalid_ofs_delta_offset}
else
delta_start = data_start + ofs_len
<<_::binary-size(data_start), base_sha_bin::binary-size(20), compressed::binary>> = pack_data
<<_::binary-size(delta_start), compressed::binary>> = pack_data
with {:ok, delta_data, _} <- decompress_data(compressed),
{:ok, {base_type, base_data}} <-
base_sha = Base.encode16(base_sha_bin, case: :lower)
resolve_ref_delta(compressed, base_sha, pack_data, cache, depth, ext)
end
do_read_object(
pack_data,
base_offset,
cache,
depth + 1,
external_resolver
),
{:ok, result} <- Delta.apply(base_data, delta_data) do
{:ok, {base_type, result}}
end
end
defp resolve_object_by_type(type_num, _pack_data, _offset, _hl, _cache, _depth, _ext) do
{:error, {:unknown_object_type, type_num}}
{:error, _} = err ->
end
err
end
@obj_ref_delta ->
<<_::binary-size(data_start), base_sha_bin::binary-size(20), compressed::binary>> =
pack_data
defp resolve_ofs_delta(rest, pack_data, offset, data_start, cache, depth, ext) do
with {:ok, neg_offset, ofs_len, _} <- parse_ofs_delta_offset(rest) do
base_offset = offset - neg_offset
base_sha = Base.encode16(base_sha_bin, case: :lower)
if base_offset < 0 do
{:error, :invalid_ofs_delta_offset}
else
delta_start = data_start + ofs_len
<<_::binary-size(delta_start), compressed::binary>> = pack_data
apply_delta_from_pack(compressed, pack_data, base_offset, cache, depth, ext)
end
end
end
defp apply_delta_from_pack(compressed, pack_data, base_offset, cache, depth, ext) do
with {:ok, delta_data, _} <- decompress_data(compressed),
with {:ok, delta_data, _} <- decompress_data(compressed) do
case find_sha_offset(pack_data, base_sha, cache) do
{:ok, base_offset} ->
with {:ok, {base_type, base_data}} <-
do_read_object(
pack_data,
base_offset,
cache,
depth + 1,
external_resolver
),
{:ok, result} <- Delta.apply(base_data, delta_data) do
{:ok, {base_type, result}}
end
{:ok, {base_type, base_data}} <-
do_read_object(pack_data, base_offset, cache, depth + 1, ext),
{:ok, result} <- Delta.apply(base_data, delta_data) do
{:ok, {base_type, result}}
end
end
{:error, {:ref_delta_base_not_found, _}} when is_function(external_resolver) ->
with {:ok, {base_type, base_data}} <- external_resolver.(base_sha),
{:ok, result} <- Delta.apply(base_data, delta_data) do
{:ok, {base_type, result}}
end
defp resolve_ref_delta(compressed, base_sha, pack_data, cache, depth, ext) do
with {:ok, delta_data, _} <- decompress_data(compressed) do
resolve_ref_delta_base(delta_data, base_sha, pack_data, cache, depth, ext)
end
end
{:error, _} = err ->
err
end
end
defp resolve_ref_delta_base(delta_data, base_sha, pack_data, cache, depth, ext) do
case find_sha_offset(pack_data, base_sha, cache) do
{:ok, base_offset} ->
apply_delta_from_pack_with_data(delta_data, pack_data, base_offset, cache, depth, ext)
_ ->
{:error, {:unknown_object_type, type_num}}
end
{:error, {:ref_delta_base_not_found, _}} when is_function(ext) ->
apply_delta_from_external(delta_data, base_sha, ext)
{:error, _} = err ->
err
end
end
defp apply_delta_from_pack_with_data(delta_data, pack_data, base_offset, cache, depth, ext) do
with {:ok, {base_type, base_data}} <-
do_read_object(pack_data, base_offset, cache, depth + 1, ext),
{:ok, result} <- Delta.apply(base_data, delta_data) do
{:ok, {base_type, result}}
end
end
defp apply_delta_from_external(delta_data, base_sha, external_resolver) do
with {:ok, {base_type, base_data}} <- external_resolver.(base_sha),
{:ok, result} <- Delta.apply(base_data, delta_data) do
{:ok, {base_type, result}}
end
end
# -- Object header parsing --
@doc """
Parse the variable-length object header.
Returns `{:ok, type_num, uncompressed_size, header_bytes_consumed, rest}`.
@@ -563,8 +560,8 @@
defp parse_ofs_continuation(<<>>, _offset, _consumed), do: {:error, :truncated_ofs_offset}
# -- Decompression --
# Decompress data with size limit and find the exact compressed length.
# Uses safeInflate for streaming decompression with early abort on oversize.
defp decompress_data(compressed) do
decompress_data(compressed, @max_decompressed_size)
end
@@ -634,11 +631,6 @@
defp probe_compressed_length(_compressed, _expected_size, low, _high), do: low
# Try to decompress a prefix of the compressed data using safeInflate (size-limited).
# Returns {:ok, decompressed_size} only if the prefix forms a complete zlib stream
# (verified by inflateEnd which checks the Adler32 checksum).
# On OTP 27+, safeInflate returns {:finished, _} even for incomplete streams,
# so inflateEnd is essential for correctness.
defp try_decompress_prefix(compressed, len) do
<<prefix::binary-size(len), _rest::binary>> = compressed
z = :zlib.open()
@@ -655,8 +647,6 @@
end
end
# Decompress data using safeInflate, returning total decompressed size.
# Raises on oversize (caught by try_decompress_prefix's rescue).
defp inflate_prefix_size(z, data, max_size, total) do
case :zlib.safeInflate(z, data) do
{:continue, chunk} ->
@@ -671,31 +661,30 @@
end
end
# -- Type helpers --
defp type_num_to_atom(@obj_commit), do: :commit
defp type_num_to_atom(@obj_tree), do: :tree
defp type_num_to_atom(@obj_blob), do: :blob
defp type_num_to_atom(@obj_tag), do: :tag
# Build an in-memory SHA-to-offset index by scanning pack entries.
# For non-delta objects, compute the SHA from type+content.
# This enables REF_DELTA resolution without a .idx file.
# -- SHA index --
defp find_sha_offset(pack_data, sha, cache) do
# Check cache first (caller may have provided from .idx)
case Map.get(cache, {:sha, sha}) do
offset when is_integer(offset) ->
{:ok, offset}
nil ->
# Build index by scanning the pack
case build_sha_index(pack_data) do
{:ok, index} ->
case Map.get(index, sha) do
nil -> {:error, {:ref_delta_base_not_found, sha}}
lookup_sha_in_pack(pack_data, sha)
end
end
offset -> {:ok, offset}
end
defp lookup_sha_in_pack(pack_data, sha) do
with {:ok, index} <- build_sha_index(pack_data) do
case Map.get(index, sha) do
{:error, _} = err ->
err
end
nil -> {:error, {:ref_delta_base_not_found, sha}}
offset -> {:ok, offset}
end
end
end
@@ -716,72 +705,67 @@
defp scan_entries_for_index(_data, 0, _offset, index), do: {:ok, index}
defp scan_entries_for_index(data, remaining, offset, index) do
with {:ok, type_num, _size, header_len, rest_after_header} <- parse_object_header(data) do
scan_entry_by_type(type_num, data, header_len, rest_after_header, remaining, offset, index)
end
end
case parse_object_header(data) do
{:ok, type_num, _size, header_len, rest_after_header} ->
case type_num do
t when t in [@obj_commit, @obj_tree, @obj_blob, @obj_tag] ->
type_str = Atom.to_string(type_num_to_atom(t))
case decompress_data(rest_after_header) do
{:ok, content, compressed_len} ->
# Compute SHA: hash("type size\0content")
raw = "#{type_str} #{byte_size(content)}\0" <> content
sha = :crypto.hash(:sha, raw) |> Base.encode16(case: :lower)
consumed = header_len + compressed_len
<<_::binary-size(consumed), next_data::binary>> = data
defp scan_entry_by_type(t, data, header_len, rest, remaining, offset, index)
when t in @base_types do
type_str = Atom.to_string(type_num_to_atom(t))
case decompress_data(rest) do
{:ok, content, compressed_len} ->
sha = compute_object_sha(type_str, content)
consumed = header_len + compressed_len
<<_::binary-size(consumed), next_data::binary>> = data
scan_entries_for_index(
next_data,
remaining - 1,
offset + consumed,
Map.put(index, sha, offset)
)
scan_entries_for_index(
next_data,
remaining - 1,
offset + consumed,
Map.put(index, sha, offset)
{:error, _} = err ->
err
)
end
@obj_ofs_delta ->
case parse_ofs_delta_offset(rest_after_header) do
{:error, _} = err ->
err
end
end
{:ok, _neg_offset, ofs_len, rest_after_ofs} ->
case decompress_data(rest_after_ofs) do
{:ok, _delta_data, compressed_len} ->
consumed = header_len + ofs_len + compressed_len
<<_::binary-size(consumed), next_data::binary>> = data
# We don't index delta objects (they need resolution first)
scan_entries_for_index(next_data, remaining - 1, offset + consumed, index)
{:error, _} = err ->
err
end
defp scan_entry_by_type(@obj_ofs_delta, data, header_len, rest, remaining, offset, index) do
with {:ok, _neg_offset, ofs_len, rest_after_ofs} <- parse_ofs_delta_offset(rest),
{:ok, _delta_data, compressed_len} <- decompress_data(rest_after_ofs) do
consumed = header_len + ofs_len + compressed_len
<<_::binary-size(consumed), next_data::binary>> = data
scan_entries_for_index(next_data, remaining - 1, offset + consumed, index)
end
end
defp scan_entry_by_type(@obj_ref_delta, _data, _hl, rest, _rem, _off, _idx)
when byte_size(rest) < 20 do
{:error, _} = err ->
err
end
@obj_ref_delta ->
if byte_size(rest_after_header) >= 20 do
<<_base_sha::binary-size(20), rest_after_sha::binary>> = rest_after_header
case decompress_data(rest_after_sha) do
{:ok, _delta_data, compressed_len} ->
consumed = header_len + 20 + compressed_len
<<_::binary-size(consumed), next_data::binary>> = data
scan_entries_for_index(next_data, remaining - 1, offset + consumed, index)
{:error, :truncated_ref_delta}
end
{:error, _} = err ->
err
end
else
{:error, :truncated_ref_delta}
end
defp scan_entry_by_type(@obj_ref_delta, data, header_len, rest, remaining, offset, index) do
<<_base_sha::binary-size(20), rest_after_sha::binary>> = rest
_ ->
{:error, {:unknown_object_type, type_num}}
end
case decompress_data(rest_after_sha) do
{:ok, _delta_data, compressed_len} ->
consumed = header_len + 20 + compressed_len
<<_::binary-size(consumed), next_data::binary>> = data
scan_entries_for_index(next_data, remaining - 1, offset + consumed, index)
{:error, _} = err ->
err
end
end
defp scan_entry_by_type(type_num, _data, _hl, _rest, _rem, _off, _idx) do
{:error, {:unknown_object_type, type_num}}
end
defp compute_object_sha(type_str, content) do
raw = "#{type_str} #{byte_size(content)}\0" <> content
:crypto.hash(:sha, raw) |> Base.encode16(case: :lower)
end
end