git-zero-rustix
Allocation-free no_std rustix adapters for git-zero. The host owns buffers,
object manifests and file descriptors.
client: blocking IPv4 HTTP/1.0 transport.server: IPv4/IPv6 listeners, request parsing and response framing.storage: immutable raw file objects, verified prepacked responses and a selected-pack disk cache.cli: thegit-zerodemonstration binary; enables all adapters and validation.
All features are enabled by default. Disable defaults to select adapters.
The library uses no std or alloc; the CLI uses std for process startup.
Install the native demonstration executable with:
To embed only selected adapters:
[]
= { = "0.1.0", = false, = ["server", "storage"] }
The API reference covers buffers, descriptors, limits, and deadlines. This adapter uses rustix's supported OS interfaces; the transport-independent crates let other hosts supply their own I/O.
See git-zero for usage and limitations.
The server reads Git-Protocol and Content-Type into shared request metadata.
Gzip uploads are decoded into the request storage after the wire bytes; headers,
compressed body and expanded body must all fit. Inflation borrows a fixed 32 KiB
stack history window and validates the gzip checksum and size before routing.
Duplicate framing/selector headers, truncated bodies and unsupported transfer or
content encodings are rejected. Chunked intake requires the explicit APIs
described in Native deadlines and chunked intake.
Header delimiter scanning retains three overlap bytes between reads, so each
possible four-byte delimiter position is examined at most once. Header and body
bytes still share the caller's fixed request storage; fragmentation does not
raise that limit. Metadata uses one UTF-8 check and one field pass, with HTTP
token names, control-free values and space/tab padding. Request lines reject
embedded CR/LF. The http_framing socket tests cover binary bodies, malformed
requests, exact capacity and recovery with reused storage. Scan unit tests prove
the work bound for one-byte fragments through a 64 KiB header.
read_request_with_limits adds inclusive host quotas through RequestLimits:
max_wire_bytes bounds the request line, headers, delimiter and encoded body;
max_body_bytes bounds the decoded body, including identity uploads. None
leaves a policy unset; caller storage always bounds memory. An explicit policy
failure returns LimitExceeded, physical capacity returns BufferTooSmall,
and a tied exhaustion boundary reports the policy failure. Exact limits allow
empty bodies and valid empty gzip members. Header validation precedes known
body-length policy checks. Gzip retains the full physical expansion suffix,
the existing ratio/slack settings and checksum validation. A malformed stream
can fail before a size limit is reached. Every failure invalidates provisional
storage: close the connection and discard it before repository preparation.
read_request retains unset policies and its previous behavior.
respond_stream sends a prepared http::stream::ResponseEncoder with HTTP/1.0
close-delimited framing and caller-owned output staging. It writes all staged
bytes before pulling again, cancels on errors, and requires the host to close the
socket on return. It remains a blocking adapter; keep-alive, TLS and HTTP/1.1
response framing remain future work. Opt-in chunked request intake and
deadline-driven readiness polling are described below.
With storage, cache::DiskCache implements the core selected-pack cache backend.
The host retains a trusted directory descriptor, a fixed resident byte buffer
and a producer nonce unique within that directory. Use it through
git_zero::cache::selected::write after authorization, visible-ref/reachability
checks and graph selection. Each lookup copies one regular file into resident
storage; core validates its exact canonical object set and checksums before
output. The resident capacity bounds disk reads and accepted pack size. Match
it to the request's pack-byte limit. Cache files use the 48-byte selected key
encoded as 96 hexadecimal characters plus .pack; HTTP pack/index publication
uses the actual returned pack checksum separately.
Insertions use exclusively created private staging files, complete writes, file synchronization and a final atomic same-directory rename. Failed insertion preserves published entries. Concurrent producers may repeat work; readers see one complete publication and retain independent resident bytes across replacement or eviction. A sink failure can leave a complete entry for a later retry. Disabled mode bypasses reads/inserts; explicit key eviction and saturating hit/miss/insertion/eviction counters expose backend activity. Hosts supply authorization namespace directories, total disk quotas, cold-miss coordination, stale staging-file cleanup after crashes and directory durability policy.
listen(port) retains IPv4 loopback binding. listen_on(SocketAddr) binds an
explicit numeric IPv4 or IPv6 address, and local_address reports the assigned
address when port zero is used. IPv6 sockets set IPV6_V6ONLY: hosts that need
both families create separate listeners. Accepted sockets retain close-on-exec
and the existing ten-second send/receive timeouts. The CLI accepts a port or a
numeric address, for example git-zero serve '[::1]:8080', and prints the bound
URL. Its existing client transport still accepts IPv4 addresses only.
accept_with_timeouts takes explicit AcceptedSocketTimeouts; its default has
both read and write set to ten seconds, matching accept. None disables the
corresponding child setting, and zero durations are rejected before accepting.
The listener is unchanged, and configuration failure closes the accepted
descriptor. These are per-blocking-call socket timeouts, with OS rounding;
partial operations and retries can extend a whole request or response. They
are not accept or aggregate deadlines. Hosts still own connection admission,
scheduling and cancellation. The allocation-guarded admission socket tests cover
quota precedence, gzip integrity at exact quotas, unread excess bytes, both IP
families, timeout stalls, write backpressure and stream cancellation. The native
references Git host uses explicit limits/timeouts and verifies rejection and
recovery before its existing smart/dumb comparisons in both object formats.
On platforms with rustix flock support, DiskCache::try_lease(key) offers
cooperative cold-miss coordination. Contended locks return None without waiting;
disabled caches also return None. File operations remain synchronous. All
producers in one namespace use the same lease
protocol: authorize/select, try the lease, recheck the cache after acquisition,
then generate only on a remaining miss. The host controls retries and deadlines;
dropping the guard explicitly unlocks and closes its descriptor on cancellation
or failure. Unrelated keys remain independent. Disabled caches create no lock
files. Producers that skip this optional protocol retain atomic complete
publication but may duplicate work.
Lease files are empty regular files named with the same selected key plus
.lock. They persist across pack eviction and producer exits. Hosts reclaim
them only when all users of that namespace are quiescent; replacing a live lock
file would split its identity. Advisory lock scope depends on the filesystem
and mount configuration. Hosts account for lock files in directory quotas and
provide a distributed coordinator when that is required.
The demonstration command git-zero serve ADDRESS --allow-reachable-wants
explicitly grants access to the whole validated visible snapshot closure.
The flag is absent by default; unknown flags fail. The example uses a 1,024
want-occurrence cap in addition to its fixed selection arrays, request storage
and graph-read limit. It is a host configuration option, never an HTTP option.
The bounded upstream-cgi fixture host accepts the same final flag. Production
hosts must authenticate/select their own immutable projection before constructing
a policy-aware selected encoder; neither socket parsing nor a cache grants access.
The selected-cache example integrates the selected HTTP hook with either
caller-owned FixedCache storage or DiskCache. Export a canonical fixture
using tests/server_fixture.py, then run:
It prints an ephemeral loopback clone URL. Add --cache-directory /tmp/private-cache
to use an existing private directory, --port PORT to choose a port, and
--pack-bytes BYTES to set both raw-pack staging and resident cache capacity
(default 1 MiB, maximum 16 MiB). It separately reserves 64 KiB for a request,
raw-pack capacity plus 128 KiB for the framed response, 256 object/ID slots,
64 wants, 512-byte read/parse scratch and a 16 MiB graph-read budget. Fixture
admission retains its 256-object/8-MiB raw-body bounds. Exceeding any budget
fails instead of truncating or falling back.
The example is a single-producer synchronous host. It buffers the entire body before emitting HTTP success; cache failures replace provisional protocol bytes with one error response. It performs request authorization/selection on every hit, and never caches refs, ACKs, shallow updates or sideband framing. Hosts own authentication, namespace selection, process lifetime and disk quotas. Optional producer leases remain a separate host policy. The real-Git test matrix covers memory/disk miss and hit, exact selected closures and true incremental fetch in both formats and v0/v1/v2; reopening the disk backend across A -> B -> A proves retained content reuse. No Worker or async pack storage is implied.
The cache example stays strict unless --allow-reachable-wants explicitly
grants the entire supplied validated visible closure, with a 64-occurrence want
cap. Its buffered responder receives WantOptions on every call; revoking that
grant denies unadvertised wants before consulting even a warmed cache. This
trusted host flag never comes from HTTP input and does not replace authentication
or an authorized immutable repository projection.
Header-specific admission uses read_request_with_policy and RequestPolicy.
HeaderLimits independently bounds complete header bytes (request line and
final delimiter included), field count, name bytes and raw value bytes after the
colon. Leading/trailing whitespace consumes the value budget. Unknown and
repeated ordinary fields consume quotas too. Defaults are unset and preserve
read_request / read_request_with_limits behavior. An incomplete header stops
at the byte cap without consuming the first excess byte. Each field's syntax is
validated before its field quotas; accepted fields still follow the existing
framing/duplicate rules. Exhaustion returns LimitExceeded before body intake,
gzip expansion or repository preparation. Close failed requests and reuse their
storage only after dropping all provisional views.
The CLI accepts these optional inclusive limits, including zero:
Values are decimal byte/count budgets. Duplicate/unknown flags, signs and out-of-range values fail before listening. The address, when present, comes first. These controls leave accepted-socket per-I/O timeouts unchanged and do not establish aggregate request/idle deadlines.
Packed dumb publication is described in the publication guide.
respond_published streams generated packs/indexes;
FilePublication::from_fds verifies pinned complete files and
respond_file_published streams them with independent offsets. Both cancel on
socket/provider failure. The CLI's immutable demonstration snapshot now serves
its generated pack/index alongside loose objects.
Native deadlines and chunked intake
read_request_with_deadlines adds RequestDeadlines { total, idle } to a request
policy. total starts when intake begins and never resets on progress. idle
resets only after a positive read. respond_stream_with_deadlines and
respond_published_with_deadlines apply independent ResponseDeadlines to
streamed/known-length output; successful writes reset idle, while total remains
absolute. Unset defaults preserve the original blocking APIs and their overhead.
The CLI accepts positive, unique --request-deadline-ms=N, --idle-deadline-ms=N,
--response-deadline-ms=N and --response-idle-deadline-ms=N. Deadline exhaustion
returns LimitExceeded (the CLI's existing 413 admission response before output).
A shorter accepted-socket timeout retains Backend. Poll plus per-call
nonblocking I/O preserves descriptor flags and read/write timeout settings.
Intake covers HTTP bytes plus the final parse/inflation check. Response checks bracket provider pulls and EOF, cancelling on expiry without a successful protocol terminator. These calls cannot preempt synchronous provider code. Accept, Git preparation between phases, and total connection lifetime require host policy; the example still serves accepted connections serially.
--chunked-requests explicitly enables bounded HTTP/1.1 transfer decoding.
Library hosts can call read_request_chunked with ChunkedLimits: defaults allow
1024 nonzero chunks and 32 wire bytes per size line. Complete encoded wire remains
resident, while chunk bodies compact in place. Wire quotas count chunk framing;
body quotas count transfer-decoded identity bytes or subsequently inflated gzip
bytes. Gzip expansion uses storage beyond the complete encoded wire.
Duplicate transfer fields and Content-Length/Transfer-Encoding conflicts fail. Unsupported transfer codings, extensions and nonempty trailers are rejected explicitly. HTTP/1.0 chunked requests fail. Retained bytes after a final chunk fail; there is no keep-alive or pipeline processing, and every response closes the connection. Existing entrypoints continue to reject Transfer-Encoding by default.
read_request_from and read_request_from_chunked expose the same parser to a
host-supplied blocking read callback, including TLS streams. The callback owns
peer/certificate validation, TLS records, buffering, deadlines and cancellation;
it fills at most the supplied slice and returns zero only at EOF. Parser errors
invalidate storage and require closing that transport. This is a documented
transport boundary; no TLS provider or authenticated TLS test is bundled here.