Documentation
¶
Overview ¶
Package transfer gives the acoustic link file semantics.
The link layer delivers ordered, verified chunks. This package decides what those chunks mean: a named file, of a known length, with a digest that is checked after the last chunk lands. That final check is the whole basis of - a transfer is complete if and only if the whole-file digest matches - and it is the reason a per-chunk CRC is never treated as proof of anything.
Index ¶
- Constants
- Variables
- func ChooseChunkSize(scheme modulation.Scheme, burstSamples int) int
- func HashFile(path string) ([DigestSize]byte, error)
- func PackMessage(m Message, data []byte) []byte
- type Manifest
- type Message
- type Reader
- type Writer
- func (w *Writer) Abort() error
- func (w *Writer) Close() error
- func (w *Writer) Complete() bool
- func (w *Writer) Finalize() error
- func (w *Writer) Manifest() Manifest
- func (w *Writer) Missing() []int
- func (w *Writer) PartPath() string
- func (w *Writer) Progress() (int, int64)
- func (w *Writer) WriteChunk(index int, data []byte) error
Constants ¶
const ( // DigestSize is SHA-256. BLAKE3 would be faster and is a drop-in upgrade // once a dependency is justified; SHA-256 costs nothing at 50 // bytes per second and is already in the standard library, which for a tool // that may be run on an air-gapped machine is worth more than throughput. DigestSize = 32 // ManifestVersion is the manifest wire format version. ManifestVersion uint8 = 1 // MinChunkSize and MaxChunkSize bound the payload per burst. // // The lower bound keeps framing overhead from dominating: a 16-byte chunk // would spend 32 bytes on a header and CRC. The upper bound is a sanity cap; // the real limit is always what the modulation can carry. MinChunkSize = 32 MaxChunkSize = 4096 // PartSuffix marks an incomplete file. A partial transfer is never written // under its real name, so an interrupted transfer cannot be mistaken for a // complete one by anything - including the user's editor. PartSuffix = ".murmur.part" )
const ( // MessageVersion is the message header version. MessageVersion uint8 = 1 // DigestPrefixSize is how many bytes of the payload's SHA-256 travel with it. // Twelve rather than thirty-two because this digest guards reassembly, not // authenticity: authenticity is the AEAD's job, frame by frame. Ninety-six bits // still makes a coincidence impossible, and the other twenty bytes would have // cost six tenths of a second of sound. DigestPrefixSize = 12 // MessagePrefixSize is the length prefix in front of an encoded header. MessagePrefixSize = 2 )
Messages: the payload unit the link carries. A message is a small header followed by the bytes it describes:
[2 bytes header length][header][payload]
The header travels inside the message rather than as a separate exchange because a second exchange to announce a payload would be another burst of airtime, and the receiver can unpack the header and the payload together. It is deliberately not a Manifest. A Manifest describes a resumable file: name, mode, modification time, chunk size, and a chunk-indexed layout. A message is one thing that either arrives whole or does not arrive at all, and every field it does not need is airtime: on the one profile murmur ships, a byte costs about 32 milliseconds.
A message carries no name. The receiver's output is the payload itself - the whole point is that `murmur decode > go.mod` works - so a file name on the wire would be metadata nothing downstream can act on, and its 64-byte ceiling would have cost two seconds of sound for it.
const DefaultChunkSize = 256
DefaultChunkSize is the fallback when the modulation's capacity is unknown.
Variables ¶
var ( // ErrBadManifest means the manifest is structurally invalid. ErrBadManifest = errors.New("transfer: invalid manifest") // ErrShortManifest means the manifest payload was truncated. ErrShortManifest = errors.New("transfer: manifest too short") // ErrChunkRange means a chunk index is outside the manifest. ErrChunkRange = errors.New("transfer: chunk index out of range") // ErrChunkLength means a chunk's length contradicts the manifest. ErrChunkLength = errors.New("transfer: chunk length contradicts the manifest") // ErrIncomplete means a finalize was attempted with chunks missing. ErrIncomplete = errors.New("transfer: transfer incomplete") // ErrDigestMismatch means the assembled file does not match the manifest. // This is the one error that must never be downgraded to a warning. ErrDigestMismatch = errors.New("transfer: whole-file digest mismatch") )
var ErrBadMessage = errors.New("transfer: malformed message")
ErrBadMessage means a packed message contradicts its own header.
Functions ¶
func ChooseChunkSize ¶
func ChooseChunkSize(scheme modulation.Scheme, burstSamples int) int
ChooseChunkSize returns the largest chunk that fits in one burst. A chunk size larger than a burst's payload capacity would mean every chunk spans multiple bursts, which makes the interleaver and the retransmit unit disagree about what a unit of loss is. Deriving the chunk size from the PHY keeps one chunk equal to one burst, so a lost burst loses exactly one retransmittable unit and nothing else.
func HashFile ¶
func HashFile(path string) ([DigestSize]byte, error)
HashFile returns the SHA-256 digest of a file.
func PackMessage ¶
PackMessage renders a payload and its header as one wire message.
Types ¶
type Manifest ¶
type Manifest struct {
Name string
Size int64
Mode uint32
ModTime time.Time
Digest [DigestSize]byte
ChunkSize int
}
Manifest describes a payload before any of it is sent. It is sent first, and the receiver's acceptance decision is based on it. That ordering is deliberate: everything the user needs in order to decide - name, size, mode - arrives in one small burst, before a single byte of content has been spent.
func BuildManifest ¶
BuildManifest hashes a file and returns its manifest. This is the only place the sender touches the whole payload before starting, and it is what makes the receiver's final verification meaningful: the digest the receiver checks against describes the file as it existed at the moment this function read it.
func DecodeManifest ¶
DecodeManifest parses a manifest.
func (Manifest) ChunkCount ¶
ChunkCount returns how many chunks the payload is divided into.
func (Manifest) ChunkLen ¶
ChunkLen returns the expected length of a chunk. The final chunk is short unless the size divides evenly.
type Message ¶
type Message struct {
// Size is the payload length in bytes.
Size int64
// Digest is the first DigestPrefixSize bytes of the payload's SHA-256.
Digest [DigestPrefixSize]byte
}
Message describes one payload.
func DecodeMessage ¶
DecodeMessage parses a message header.
func UnpackMessage ¶
UnpackMessage splits a received message and checks it against its header. The checks are deliberately paranoid. A receiver that trusted the length alone would hand the caller whatever arrived; the digest is what turns "the frames were delivered in order" into "these are the bytes that were sent".
type Reader ¶
type Reader struct {
// contains filtered or unexported fields
}
Reader reads a manifest's chunks from disk for transmission.
type Writer ¶
type Writer struct {
// contains filtered or unexported fields
}
Writer assembles a file from chunks arriving in any order.
func NewWriter ¶
NewWriter starts a fresh transfer to dst, refusing to overwrite silently if a partial file is already there.
func (*Writer) Abort ¶
Abort removes the partial file. Calling it after Finalize is a no-op, so a deferred Abort cannot delete a completed transfer.
func (*Writer) Close ¶
Close releases the file handle without finalizing. The partial file remains, which is what makes an interrupted transfer distinguishable from a finished one - and what NewWriter refuses to clobber.
func (*Writer) Finalize ¶
Finalize verifies the whole-file digest and atomically installs the file. The order is: verify, then fsync the file, then rename, then fsync the directory. Renaming before verifying would install a file that is known to be wrong; skipping the directory sync would leave a window in which a crash removes the file after the tool has already reported success.
func (*Writer) Progress ¶
Progress returns how many chunks are present contiguously from the start, and how many bytes that represents. Contiguous progress rather than total chunk count is the honest measure: what the user wants to know is how much of the file can be trusted so far, and a file with a hole at byte 100 is worth nothing beyond byte 100.
func (*Writer) WriteChunk ¶
WriteChunk stores one chunk at its manifest-defined offset. Writing at an offset rather than appending is what makes the receive path independent of arrival order. That matters because selective repeat explicitly does not deliver in order - the link layer holds fragments until the holes fill, and the writer must not care either way.