gunnar-lfs 1.6.0

Git LFS over pure SSH: the git-lfs 3.0 `git-lfs-transfer` protocol, server side, streaming and without HTTP.
Documentation
//! **Git LFS over pure SSH.** The git-lfs 3.0 `git-lfs-transfer` protocol,
//! server side.
//!
//! No HTTP server, no second endpoint, no second authorisation path. A client
//! execs a third command over the SSH connection it is already authenticated on,
//! and speaks a pkt-line protocol there. The session's grant was decided once,
//! by [`gunnar_ssh::command::parse`](../gunnar_ssh/command/fn.parse.html), from
//! the same key table `git-upload-pack` and `git-receive-pack` use.
//!
//! # What the specification actually says, verified 2026-08-03
//!
//! The LFS design said to confirm the protocol against the git-lfs spec before building to
//! it. That was done against `git-lfs/docs/proposals/ssh_adapter.md`, against the
//! **client implementation** (`git-lfs/ssh/{ssh,connection,protocol}.go`,
//! `git-lfs/tq/ssh.go`, `git-lfs/pktline`), and against the reference server
//! (`charmbracelet/git-lfs-transfer`). That description is correct. Four details
//! it does not carry, all of which change the code:
//!
//! | detail | what it is |
//! |---|---|
//! | the exec line | `git-lfs-transfer <path> <operation>`: **three tokens, path unquoted**, `operation ∈ {upload, download}` |
//! | the grant | therefore **not a constant per verb**: `upload` needs Write, `download` needs Read. See [`Operation`] |
//! | the version | **1.** `version=1` capability, `version 1` command. The design's "v2, not v1" is gunnar's release phase, not the wire |
//! | locking | **optional.** `lock`/`unlock`/`list-lock` only if the server advertises `locking`. gunnar does not; see [`wire::CAPABILITIES`] |
//!
//! # The conversation
//!
//! ```text
//! server → version=1                              capability advertisement, flush
//! client → version 1                              flush
//! server → status 200                             delim, flush
//! client → batch  transfer=ssh hash-algo=sha256   delim, <oid> <size>…, flush
//! server → status 200                             delim, <oid> <size> <action>…, flush
//!            action ∈ upload | download | noop
//!            a `download` batch never answers `noop`; see `Session::batch`
//! client → get-object <oid>                       flush              (download only)
//! server → status 200  size=<n>                   delim, data…, flush
//! client → put-object <oid>  size=<n>             delim, data…, flush  (upload only)
//! server → status 200                             flush
//! client → verify-object <oid>  size=<n>          flush              (upload only)
//! server → status 200                             flush
//! client → quit                                   flush
//! server → status 200                             flush   ← no delim; see `wire`
//! ```
//!
//! # Streaming is the design, not a tuning goal
//!
//! An LFS object is a video, a firmware image, a dataset. Nothing on this path
//! allocates from an object's size: every byte in both directions passes through
//! one [`CHUNK_BYTES`] buffer, and a declared size is used only to *refuse* and
//! to *verify*. `tests/streaming.rs` asserts that against the process's real
//! resident-memory high-water mark while moving a gigabyte, because a test in
//! this tree OOM'd a 499 GB machine by allocating what a header claimed, and
//! this is the code path most likely to repeat it.
//!
//! # Objects are stored raw. Measured.
//!
//! Level-6 DEFLATE over large high-entropy content, which is what LFS content
//! is, was measured at **ratio 1.000, 48.2 MB/s**, against **2027 MB/s** for
//! SHA-1 over the same bytes. Compressing an LFS object is 42× slower than the
//! hash that has to run over it anyway and returns nothing. Nothing here
//! compresses; see [`fs_store`].
//!
//! And the hash cannot be parallelised: a single object's SHA-256 is serial by
//! construction. The win for large objects is I/O, which is why the chunk is a
//! full pkt-line payload and why the `fs` backend hands out a bare `File`.
//!
//! # Verification (measure, never assert)
//!
//! * `tests/interop.rs` drives the **real `git-lfs` binary**. It hard-fails if
//!   `git lfs version` is absent rather than skipping: a self-skipping test that
//!   reports success is the failure this project keeps finding.
//! * `tests/authorisation.rs` watches both directions go red: a `download`
//!   session is refused `put-object`, an `upload` session is refused
//!   `get-object`.
//! * `tests/streaming.rs` asserts bounded RSS while a gigabyte moves.

#![forbid(unsafe_code)]
#![warn(missing_docs)]
#![warn(clippy::all)]
#![doc(
    html_logo_url = "https://gunnar.rs/download/gunnar.webp",
    html_favicon_url = "https://gunnar.rs/download/favicon.svg"
)]

pub mod allowance;
pub mod batch;
pub mod error;
pub mod ledger;
pub mod limits;
pub mod oid;
pub mod operation;
pub mod session;
pub mod store;
pub mod verify;
pub mod wire;

#[cfg(feature = "fs")]
pub mod fs_store;

pub use allowance::Allowance;
pub use batch::{plan_object, Plan};
pub use error::{Error, Result};
pub use ledger::{AllowanceLedger, Mark, NamespaceAllowance};
pub use limits::{Limits, CHUNK_BYTES, DEFAULT_MAX_BATCH_OBJECTS, DEFAULT_MAX_OBJECT_BYTES};
pub use oid::{Oid, OidError};
pub use operation::Operation;
pub use session::{Session, Summary};
pub use store::{DiscardingLfsStore, LfsObject, LfsStore, MemLfsStore, PendingWrite};
pub use verify::VerifyingSink;

#[cfg(feature = "fs")]
pub use fs_store::FsLfsStore;

/// The tracing target every line in this crate carries, so it is filterable with
/// the same `RUST_LOG=gunnar::lfs=info` the rest of the server answers to.
///
/// # Why the string and not `gunnar_core::observe::TARGET_LFS`
///
/// That is where it is *named*, and this is a second spelling of it, which one
/// implementation is normally against. The edge that would remove the second
/// spelling runs the wrong way: this crate has no `gunnar-core` dependency,
/// and pulling one in to reach a `&str` would put the identity table, the ACL
/// and `aws-lc-rs` into its graph. `gunnar-wire`'s `GIT_TARGET` records the
/// same trade.
///
/// The two spellings are held together by a guard where both crates are already
/// linked — `gunnar-cli`'s `tests/observability.rs` — which costs nothing and
/// cannot be forgotten by a build that does not link them.
///
/// ⚠ It was **`"gunnar.lfs"`** until 2026-08-05, with a dot instead of `::`, so
/// `RUST_LOG=gunnar::lfs=…` selected none of these lines and neither did any other
/// documented filter. Nothing was broken about the logging; it was simply
/// unreachable.
pub const LFS_TARGET: &str = "gunnar::lfs";

/// The exec verb a client sends to open one of these sessions.
///
/// Named here rather than spelled as a literal in `gunnar-ssh`'s dispatch table,
/// so the verb and the protocol that answers it cannot drift apart.
pub const TRANSFER_VERB: &str = "git-lfs-transfer";