Artifact fc493a9c4007352e387bcde14f197a5c918c8319149e295fcdff8fc19459ac96:
- File
internal/xfer/encode.go
— part of check-in
[161d6ab68f]
at
2026-10-13 05:26:26
on branch main
— fix(sync): cover the sync-protocol-01 coverage rows (#394)
Check the remaining partial and uncovered sync rows, and fix the client private-gimme answer, the unversioned-file NAME encoding, the check-in lock table, the uv-hash name form and the uvgimme round limit they exposed. Tracked in #392. (user: [email protected], size: 11693) [annotate] [blame] [check-ins using]
package xfer import ( "bytes" "compress/zlib" "encoding/binary" "fmt" "strings" "github.com/danmestas/go-libfossil/internal/deck" ) // EncodeCard writes the wire-format representation of a Card to w. func EncodeCard(w *bytes.Buffer, c Card) error { if w == nil { panic("xfer.EncodeCard: w must not be nil") } if c == nil { panic("xfer.EncodeCard: c must not be nil") } switch v := c.(type) { case *IGotCard: return encodeIGot(w, v) case *GimmeCard: return encodeGimme(w, v) case *PushCard: return encodePush(w, v) case *PullCard: return encodePull(w, v) case *CookieCard: return encodeCookie(w, v) case *ReqConfigCard: return encodeReqConfig(w, v) case *PrivateCard: return encodePrivate(w) case *CloneCard: return encodeClone(w, v) case *CloneSeqNoCard: return encodeCloneSeqNo(w, v) case *UVGimmeCard: return encodeUVGimme(w, v) case *PragmaCard: return encodePragma(w, v) case *LoginCard: return encodeLogin(w, v) case *CommentCard: return encodeComment(w, v) case *ErrorCard: return encodeError(w, v) case *MessageCard: return encodeMessage(w, v) case *UVIGotCard: return encodeUVIGot(w, v) case *FileCard: return encodeFile(w, v) case *CFileCard: return encodeCFile(w, v) case *ConfigCard: return encodeConfig(w, v) case *UVFileCard: return encodeUVFile(w, v) case *SchemaCard: return encodeSchema(w, v) case *XIGotCard: return encodeXIGot(w, v) case *XGimmeCard: return encodeXGimme(w, v) case *XRowCard: return encodeXRow(w, v) case *XDeleteCard: return encodeXDelete(w, v) case *UnknownCard: return encodeUnknown(w, v) default: return fmt.Errorf("xfer: cannot encode %T", c) } } func encodeIGot(w *bytes.Buffer, c *IGotCard) error { w.WriteString("igot ") w.WriteString(c.UUID) if c.IsPrivate { w.WriteString(" 1") } w.WriteByte('\n') return nil } func encodeGimme(w *bytes.Buffer, c *GimmeCard) error { w.WriteString("gimme ") w.WriteString(c.UUID) w.WriteByte('\n') return nil } // encodePush writes "push <server-code> <project-code>\n". // // Server code first, project code second: canonical emits both cards as // `blob_appendf(&send, "push %s %s\n", zSCode, zPCode)` (src/xfer.c // client_sync) and parses them as `push SERVERCODE PROJECTCODE` on both ends // — the server compares aToken[2] against its own project-code and ignores // the server code, and a cloning client reads the project-code it must adopt // out of aToken[2] of the server's push reply. // // Both codes are required: canonical only honours the card at nToken==3, so a // two-token "push <code>" is silently ignored by a real Fossil server. Callers // with no cached server code send canonical's own placeholder, "x". func encodePush(w *bytes.Buffer, c *PushCard) error { if c.ProjectCode == "" { panic("xfer: encodePush: ProjectCode must not be empty") } if c.ServerCode == "" { panic("xfer: encodePush: ServerCode must not be empty") } w.WriteString("push ") w.WriteString(c.ServerCode) w.WriteByte(' ') w.WriteString(c.ProjectCode) w.WriteByte('\n') return nil } // encodePull writes "pull <server-code> <project-code>\n". // // Same arg order and same nToken==3 requirement as push; see encodePush. func encodePull(w *bytes.Buffer, c *PullCard) error { if c.ProjectCode == "" { panic("xfer: encodePull: ProjectCode must not be empty") } if c.ServerCode == "" { panic("xfer: encodePull: ServerCode must not be empty") } w.WriteString("pull ") w.WriteString(c.ServerCode) w.WriteByte(' ') w.WriteString(c.ProjectCode) w.WriteByte('\n') return nil } func encodeCookie(w *bytes.Buffer, c *CookieCard) error { w.WriteString("cookie ") w.WriteString(c.Value) w.WriteByte('\n') return nil } func encodeReqConfig(w *bytes.Buffer, c *ReqConfigCard) error { w.WriteString("reqconfig ") w.WriteString(c.Name) w.WriteByte('\n') return nil } func encodePrivate(w *bytes.Buffer) error { w.WriteString("private\n") return nil } func encodeClone(w *bytes.Buffer, c *CloneCard) error { if !c.SeqNoIsDecimal && c.Version == 0 && c.SeqNo == 0 { w.WriteString("clone\n") } else { fmt.Fprintf(w, "clone %d %d\n", c.Version, c.SeqNo) } return nil } func encodeCloneSeqNo(w *bytes.Buffer, c *CloneSeqNoCard) error { fmt.Fprintf(w, "clone_seqno %d\n", c.SeqNo) return nil } func encodeUVGimme(w *bytes.Buffer, c *UVGimmeCard) error { w.WriteString("uvgimme ") w.WriteString(deck.FossilEncode(c.Name)) w.WriteByte('\n') return nil } func encodePragma(w *bytes.Buffer, c *PragmaCard) error { w.WriteString("pragma ") w.WriteString(c.Name) for _, v := range c.Values { w.WriteByte(' ') w.WriteString(v) } w.WriteByte('\n') return nil } func encodeLogin(w *bytes.Buffer, c *LoginCard) error { w.WriteString("login ") w.WriteString(deck.FossilEncode(c.User)) w.WriteByte(' ') w.WriteString(c.Nonce) w.WriteByte(' ') w.WriteString(c.Signature) w.WriteByte('\n') return nil } // encodeComment writes "# <text>\n". Text must be a single line: an embedded // newline would make the remainder parse as a card of its own. func encodeComment(w *bytes.Buffer, c *CommentCard) error { if strings.ContainsAny(c.Text, "\n\r") { panic("xfer: encodeComment: Text must not contain a newline") } w.WriteString("# ") w.WriteString(c.Text) w.WriteByte('\n') return nil } func encodeError(w *bytes.Buffer, c *ErrorCard) error { w.WriteString("error ") w.WriteString(deck.FossilEncode(c.Message)) w.WriteByte('\n') return nil } func encodeMessage(w *bytes.Buffer, c *MessageCard) error { w.WriteString("message ") w.WriteString(deck.FossilEncode(c.Message)) w.WriteByte('\n') return nil } // encodeUVIGot writes: uvigot NAME MTIME HASH SIZE. NAME is fossilized, as on // uvfile and uvgimme (draft Appendix B), where fossil(1) sends it raw. func encodeUVIGot(w *bytes.Buffer, c *UVIGotCard) error { fmt.Fprintf(w, "uvigot %s %d %s %d\n", deck.FossilEncode(c.Name), c.MTime, c.Hash, c.Size) return nil } func encodeUnknown(w *bytes.Buffer, c *UnknownCard) error { w.WriteString(c.Command) for _, a := range c.Args { w.WriteByte(' ') w.WriteString(a) } w.WriteByte('\n') return nil } // --- Payload card encoders --- // encodeFile writes: file UUID SIZE \n CONTENT (no trailing \n) // or: file UUID DELTASRC SIZE \n CONTENT (delta variant) func encodeFile(w *bytes.Buffer, c *FileCard) error { w.WriteString("file ") w.WriteString(c.UUID) if c.DeltaSrc != "" { w.WriteByte(' ') w.WriteString(c.DeltaSrc) } fmt.Fprintf(w, " %d\n", len(c.Content)) w.Write(c.Content) // NO trailing newline return nil } // encodeCFile writes: cfile UUID USIZE CSIZE \n ZCONTENT (no trailing \n) // or: cfile UUID DELTASRC USIZE CSIZE \n ZCONTENT (delta variant) // // The ZCONTENT payload is Fossil's on-disk compressed-blob format, produced by // blob_compress() in src/blob.c: a 4-byte big-endian uncompressed-size prefix // followed by the zlib stream. This framing is mandatory for interop (issue // #152): fossil's send_compressed_file transmits a blob.content column // verbatim, and its receiver stores the cfile payload verbatim into // blob.content, then later reads it back through blob_uncompress(), which reads // that 4-byte prefix to size its output before inflating. A bare zlib payload // (no prefix) is stored unreadable, so a real fossil client cloning from us // ends up with zero usable check-ins. CSIZE therefore counts the prefix. // // The prefix value is the size of the bytes being compressed -- len(Content) -- // which for a delta card is the delta payload, not the full artifact. That is // distinct from USIZE. // // §7.2: USIZE is the sender's full reconstructed artifact size, which for a // delta card is not len(Content) -- Content there is the (smaller) delta // payload. A caller that knows the reconstructed size (the clone send path // does, from blob.size) sets c.USize; callers that don't -- every non-delta // caller, where the two quantities coincide -- leave it zero and get the old // len(Content) behaviour. func encodeCFile(w *bytes.Buffer, c *CFileCard) error { // The payload is Fossil's on-disk blob format: [4-byte BE uncompressed // size][zlib stream]. Stored bytes go out verbatim, as fossil's // send_compressed_file sends the blob table's content (§7.2); only a // card without them is compressed here. payload, err := cfilePayload(c) if err != nil { return err } usize := c.USize if usize == 0 { usize = len(c.Content) } w.WriteString("cfile ") w.WriteString(c.UUID) if c.DeltaSrc != "" { w.WriteByte(' ') w.WriteString(c.DeltaSrc) } fmt.Fprintf(w, " %d %d\n", usize, len(payload)) w.Write(payload) // A payload that does not end in LF is followed by one, as fossil pads // it (§5.1); the decoder skips it as an empty line. if len(payload) == 0 || payload[len(payload)-1] != '\n' { w.WriteByte('\n') } return nil } // cfilePayload is a cfile card's payload: its StoredBlob when it has one, // otherwise Content compressed into the on-disk blob format. func cfilePayload(c *CFileCard) ([]byte, error) { if c.StoredBlob != nil { return c.StoredBlob, nil } var payload bytes.Buffer if err := binary.Write(&payload, binary.BigEndian, uint32(len(c.Content))); err != nil { return nil, fmt.Errorf("xfer: cfile size prefix: %w", err) } zw := zlib.NewWriter(&payload) if _, err := zw.Write(c.Content); err != nil { return nil, fmt.Errorf("xfer: cfile zlib write: %w", err) } if err := zw.Close(); err != nil { return nil, fmt.Errorf("xfer: cfile zlib close: %w", err) } return payload.Bytes(), nil } // encodeConfig writes: config NAME SIZE \n CONTENT \n // Note the trailing \n after CONTENT. func encodeConfig(w *bytes.Buffer, c *ConfigCard) error { fmt.Fprintf(w, "config %s %d\n", c.Name, len(c.Content)) w.Write(c.Content) w.WriteByte('\n') // trailing newline return nil } // encodeUVFile writes: uvfile NAME MTIME HASH SIZE FLAGS \n CONTENT // When deleted (0x0001) or content-omitted (0x0004), no payload follows. func encodeUVFile(w *bytes.Buffer, c *UVFileCard) error { fmt.Fprintf(w, "uvfile %s %d %s %d %d\n", deck.FossilEncode(c.Name), c.MTime, c.Hash, c.Size, c.Flags) if !uvFileOmitsContent(c.Flags) { w.Write(c.Content) } // NO trailing newline return nil } func encodeSchema(w *bytes.Buffer, c *SchemaCard) error { fmt.Fprintf(w, "schema %s %d %s %d %d\n", c.Table, c.Version, c.Hash, c.MTime, len(c.Content)) w.Write(c.Content) w.WriteByte('\n') return nil } func encodeXIGot(w *bytes.Buffer, c *XIGotCard) error { if c.Table == "" { panic("encodeXIGot: Table must not be empty") } if c.PKHash == "" { panic("encodeXIGot: PKHash must not be empty") } fmt.Fprintf(w, "xigot %s %s %d\n", c.Table, c.PKHash, c.MTime) return nil } func encodeXGimme(w *bytes.Buffer, c *XGimmeCard) error { if c.Table == "" { panic("encodeXGimme: Table must not be empty") } if c.PKHash == "" { panic("encodeXGimme: PKHash must not be empty") } fmt.Fprintf(w, "xgimme %s %s\n", c.Table, c.PKHash) return nil } func encodeXRow(w *bytes.Buffer, c *XRowCard) error { if c.Table == "" { panic("encodeXRow: Table must not be empty") } if c.PKHash == "" { panic("encodeXRow: PKHash must not be empty") } fmt.Fprintf(w, "xrow %s %s %d %d\n", c.Table, c.PKHash, c.MTime, len(c.Content)) w.Write(c.Content) w.WriteByte('\n') return nil } func encodeXDelete(w *bytes.Buffer, c *XDeleteCard) error { if c.Table == "" { panic("encodeXDelete: Table must not be empty") } if c.PKHash == "" { panic("encodeXDelete: PKHash must not be empty") } fmt.Fprintf(w, "xdelete %s %s %d %d\n", c.Table, c.PKHash, c.MTime, len(c.PKData)) w.Write(c.PKData) w.WriteByte('\n') return nil }