ref:b37e1e80293bba4105ad46aacf7da643efa162ba

Fix git spec compliance: 30+ findings across objects, packs, and protocol

Audit against the git format specs identified 4 critical, 4 high, 13 medium, and 13 low severity compliance issues. All have been fixed with tests. Critical fixes: - P1: Capability NUL space removed (\0caps not \0 caps) - P2: HEAD advertised first in ref advertisement (spec requirement) - P3: pkt-line max length enforced (65520 bytes/0xFFF0) - O1: GPG signature continuation lines (space-prefixed per spec) High fixes: - PK1: Pack checksum verification on parse - PK2: REF_DELTA resolution via in-memory SHA-to-offset index - P4: Two-phase want/have negotiation with ACK support - P5: Client capability parsing from NUL-separated command line Medium fixes: - O2: Header size validation in decode_raw - O3: Tree sort uses directory_mode? helper - O4: Gitlink (160000) mode canonicalization - O5: Optional tagger in tags - O6: Extra headers preserved in tags - PK3: Pack version 3 support - PK4: Index checksum verification - PK7: SHA sort validation in index - PK8: O(N) fanout computation - P6: Peeled tag refs (^{}) in advertisement - P7: Strip exactly one trailing LF - P9/P10: Per-ref error reporting and result tracking Low fixes: - O7: SHA-1 integrity verification on object read - O8: Tree entry name validation (.git, slash, null byte) - O12-O14: Tag continuation lines and unknown header preservation - P11: Visited set prevents exponential object collection - P12: Empty pack boundary fix (>= 32 not > 32) 352 tests, 0 failures (44 new spec compliance tests added). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
SHA: b37e1e80293bba4105ad46aacf7da643efa162ba
Author: Cole Christensen <cole.christensen@macmillan.com>
Date: 2026-02-11 02:15
Parents: 8a06b8f
19 files changed +1260 -165
Type
lib/ex_git_objectstore/object.ex +25 −4
@@ -86,8 +86,13 @@
@spec decode_raw(binary()) :: {:ok, t()} | {:error, term()}
def decode_raw(raw) do
case parse_header(raw) do
{:ok, type, _size, content} ->
{:ok, type, size, content} ->
# Validate that header size matches actual content length
if byte_size(content) != size do
{:error, {:size_mismatch, expected: size, actual: byte_size(content)}}
else
decode_typed(type, content)
end
decode_typed(type, content)
{:error, _} = err ->
err
@@ -100,7 +105,23 @@
@spec read(Repo.t(), String.t()) :: {:ok, t()} | {:error, term()}
def read(%Repo{} = repo, sha) do
case Repo.storage_call(repo, :get_object, [sha]) do
{:ok, data} ->
# Verify SHA-1 integrity: decompress, hash, and compare to requested SHA
case safe_decompress(data) do
{:ok, raw} ->
actual_sha = :crypto.hash(:sha, raw) |> Base.encode16(case: :lower)
{:ok, data} -> decode(data)
{:error, _} = err -> err
if actual_sha != sha do
{:error, {:sha_mismatch, expected: sha, actual: actual_sha}}
else
decode_raw(raw)
end
{:error, _} = err ->
err
end
{:error, _} = err ->
err
end
end