package backupformat import ( "crypto/sha256" "encoding/binary" "encoding/hex" "encoding/json" "fmt" "hash" "io" "time" ) // Writer schreibt einen Backup Container. // // Der Schreibvorgang läuft als Datenstrom: der Container entsteht Abschnitt für // Abschnitt, ohne dass das Backup jemals vollständig im Arbeitsspeicher liegt // (PROMPT.md §80). Erst der abschließende Footer macht ihn gültig. type Writer struct { // outputWriter ist die Senke des Containers. outputWriter io.Writer // contentDigest berechnet die Gesamtprüfsumme über Header und Abschnitte mit. contentDigest hash.Hash // bytesWritten zählt die geschriebenen Byte ohne den Footer. bytesWritten int64 // sectionCount zählt die geschriebenen Abschnitte. sectionCount int // manifestHash ist die Prüfsumme des Manifestabschnitts. manifestHash string // writtenSectionTypes hält fest, welche Abschnitte bereits vorliegen. writtenSectionTypes map[SectionType]bool // isClosed meldet, ob der Container bereits abgeschlossen ist. isClosed bool // timeSource liefert die aktuelle Zeit und ist in Tests ersetzbar. timeSource func() time.Time } // NewWriter beginnt einen Container und schreibt dessen Header. // // Der Header wird sofort ausgegeben, damit ein Leser schon während des // Schreibens Version und Kennung erkennen kann. func NewWriter(outputWriter io.Writer, containerHeader ContainerHeader) (*Writer, error) { containerWriter := &Writer{ outputWriter: outputWriter, contentDigest: sha256.New(), writtenSectionTypes: make(map[SectionType]bool), timeSource: time.Now, } if writeError := containerWriter.writeContainerHeader(containerHeader); writeError != nil { return nil, writeError } return containerWriter, nil } // writeContainerHeader schreibt Kennung, Version und Header. func (containerWriter *Writer) writeContainerHeader(containerHeader ContainerHeader) error { encodedHeader, marshalError := json.Marshal(containerHeader) if marshalError != nil { return fmt.Errorf("der container-header konnte nicht erzeugt werden: %w", marshalError) } if len(encodedHeader) > maximumHeaderLength { return fmt.Errorf("der container-header ist mit %d byte zu groß", len(encodedHeader)) } // Kennung if writeError := containerWriter.writeAndDigest(containerMagic[:]); writeError != nil { return writeError } // Version, durchgehend als Big Endian - die Byte-Reihenfolge ist Teil des // Formats und darf nicht von der Rechnerarchitektur abhängen. versionBytes := make([]byte, versionLength) binary.BigEndian.PutUint16(versionBytes, FormatVersion) if writeError := containerWriter.writeAndDigest(versionBytes); writeError != nil { return writeError } // Headerlänge headerLengthBytes := make([]byte, headerLengthFieldLength) binary.BigEndian.PutUint32(headerLengthBytes, uint32(len(encodedHeader))) if writeError := containerWriter.writeAndDigest(headerLengthBytes); writeError != nil { return writeError } return containerWriter.writeAndDigest(encodedHeader) } // WriteSection schreibt einen Abschnitt mit vorliegendem Inhalt. // // Für den Datenbereich, der beliebig groß werden kann, steht stattdessen // WriteSectionStream bereit. func (containerWriter *Writer) WriteSection(sectionType SectionType, sectionFlags SectionFlags, sectionContent []byte) error { if containerWriter.isClosed { return ErrWriterClosed } sectionDigest := sha256.Sum256(sectionContent) if writeError := containerWriter.writeSectionHeader(sectionType, sectionFlags, int64(len(sectionContent)), sectionDigest[:]); writeError != nil { return writeError } if writeError := containerWriter.writeAndDigest(sectionContent); writeError != nil { return writeError } // Die Prüfsumme des Manifests wandert in den Footer: sie ist die Kennung // des Backup-Inhalts und wird beim Lesen gegengeprüft. if sectionType == SectionManifest { containerWriter.manifestHash = hex.EncodeToString(sectionDigest[:]) } containerWriter.sectionCount++ containerWriter.writtenSectionTypes[sectionType] = true return nil } // WriteJSONSection schreibt einen Abschnitt mit JSON-Inhalt. func (containerWriter *Writer) WriteJSONSection(sectionType SectionType, sectionFlags SectionFlags, sectionValue any) error { encodedValue, marshalError := json.Marshal(sectionValue) if marshalError != nil { return fmt.Errorf("der abschnitt %s konnte nicht erzeugt werden: %w", sectionType, marshalError) } return containerWriter.WriteSection(sectionType, sectionFlags, encodedValue) } // SectionStreamWriter nimmt den Inhalt eines Abschnitts als Datenstrom auf. type SectionStreamWriter struct { // containerWriter ist der zugehörige Container. containerWriter *Writer // sectionDigest berechnet die Prüfsumme des Abschnitts mit. sectionDigest hash.Hash // declaredLength ist die angekündigte Länge des Abschnitts. declaredLength int64 // writtenLength ist die bisher geschriebene Länge. writtenLength int64 // sectionType ist die Art des Abschnitts. sectionType SectionType // isFinished meldet, ob der Abschnitt bereits abgeschlossen wurde. isFinished bool } // BeginSectionStream beginnt einen Abschnitt, dessen Inhalt als Datenstrom folgt. // // Die Länge muss vorab feststehen, weil sie im Abschnittskopf steht — nur so // kann ein Leser einen unbekannten Abschnitt überspringen, ohne ihn zu deuten. // Beim Export aus einem Repository ist sie aus dem Chunk-Verzeichnis bekannt. // // Der Abschnitt trägt FlagDeferredDigest: seine Prüfsumme steht zum Zeitpunkt // des Kopfes noch nicht fest. Der Weg arbeitet deshalb auf jeder Senke und // setzt kein Rückspulen voraus (PROMPT.md §80). func (containerWriter *Writer) BeginSectionStream(sectionType SectionType, sectionFlags SectionFlags, declaredLength int64) (*SectionStreamWriter, error) { if containerWriter.isClosed { return nil, ErrWriterClosed } if declaredLength < 0 || declaredLength > maximumSectionLength { return nil, fmt.Errorf("%w: %d byte", ErrSectionTooLarge, declaredLength) } // Die Prüfsumme im Kopf bleibt leer und wird beim Lesen nicht herangezogen. emptyDigest := make([]byte, digestLength) if writeError := containerWriter.writeSectionHeader(sectionType, sectionFlags|FlagDeferredDigest, declaredLength, emptyDigest); writeError != nil { return nil, writeError } return &SectionStreamWriter{ containerWriter: containerWriter, sectionDigest: sha256.New(), declaredLength: declaredLength, sectionType: sectionType, }, nil } // Write nimmt Daten des Abschnitts auf. func (streamWriter *SectionStreamWriter) Write(sectionData []byte) (int, error) { if streamWriter.isFinished { return 0, fmt.Errorf("der abschnitt %s wurde bereits abgeschlossen", streamWriter.sectionType) } // Mehr als angekündigt zu schreiben würde den Container unlesbar machen: // der folgende Abschnittskopf läge dann an der falschen Stelle. if streamWriter.writtenLength+int64(len(sectionData)) > streamWriter.declaredLength { return 0, fmt.Errorf("der abschnitt %s würde seine angekündigte länge von %d byte überschreiten", streamWriter.sectionType, streamWriter.declaredLength) } bytesWritten, writeError := streamWriter.containerWriter.outputWriter.Write(sectionData) if bytesWritten > 0 { // Der Fehler von Write kann bei einem Hash nicht auftreten. _, _ = streamWriter.sectionDigest.Write(sectionData[:bytesWritten]) _, _ = streamWriter.containerWriter.contentDigest.Write(sectionData[:bytesWritten]) streamWriter.writtenLength += int64(bytesWritten) streamWriter.containerWriter.bytesWritten += int64(bytesWritten) } if writeError != nil { return bytesWritten, fmt.Errorf("der abschnitt %s konnte nicht geschrieben werden: %w", streamWriter.sectionType, writeError) } return bytesWritten, nil } // Finish schließt den Abschnitt ab. // // Die gebildete Prüfsumme wandert nicht in den bereits geschriebenen Kopf, // sondern wird vom Aufrufer im Chunk-Verzeichnis abgelegt. Sie ist über // Digest() abrufbar. func (streamWriter *SectionStreamWriter) Finish() error { if streamWriter.isFinished { return nil } // Weniger als angekündigt zu schreiben ergäbe einen Abschnitt, dessen // Längenangabe nicht zum Inhalt passt — der folgende Abschnittskopf läge // dann an der falschen Stelle und der Container wäre unlesbar. if streamWriter.writtenLength != streamWriter.declaredLength { return fmt.Errorf("der abschnitt %s wurde mit %d von %d angekündigten byte geschrieben", streamWriter.sectionType, streamWriter.writtenLength, streamWriter.declaredLength) } streamWriter.containerWriter.sectionCount++ streamWriter.containerWriter.writtenSectionTypes[streamWriter.sectionType] = true streamWriter.isFinished = true return nil } // Digest liefert die Prüfsumme des geschriebenen Abschnittsinhalts. func (streamWriter *SectionStreamWriter) Digest() string { return hex.EncodeToString(streamWriter.sectionDigest.Sum(nil)) } // writeSectionHeader schreibt den Kopf eines Abschnitts. func (containerWriter *Writer) writeSectionHeader(sectionType SectionType, sectionFlags SectionFlags, sectionLength int64, sectionDigest []byte) error { if sectionLength < 0 || sectionLength > maximumSectionLength { return fmt.Errorf("%w: %d byte", ErrSectionTooLarge, sectionLength) } headerBuffer := make([]byte, 0, sectionHeaderLength) headerBuffer = append(headerBuffer, byte(sectionType), byte(sectionFlags)) lengthBytes := make([]byte, 8) binary.BigEndian.PutUint64(lengthBytes, uint64(sectionLength)) headerBuffer = append(headerBuffer, lengthBytes...) headerBuffer = append(headerBuffer, sectionDigest...) return containerWriter.writeAndDigest(headerBuffer) } // writeAndDigest schreibt Daten und führt die Gesamtprüfsumme mit. func (containerWriter *Writer) writeAndDigest(outputData []byte) error { bytesWritten, writeError := containerWriter.outputWriter.Write(outputData) if bytesWritten > 0 { _, _ = containerWriter.contentDigest.Write(outputData[:bytesWritten]) containerWriter.bytesWritten += int64(bytesWritten) } if writeError != nil { return fmt.Errorf("der container konnte nicht geschrieben werden: %w", writeError) } return nil } // Close schreibt den Footer und schließt den Container ab. // // Erst danach ist der Container gültig. Ein Abbruch vor diesem Aufruf // hinterlässt eine Datei ohne Abschlussvermerk, die beim Lesen zuverlässig als // unvollständig erkannt wird. func (containerWriter *Writer) Close() error { if containerWriter.isClosed { return ErrWriterClosed } // Ein Container ohne Manifest beschreibt kein Backup. Ihn abzuschließen // hiesse, ein leeres Gebilde als gültig auszugeben (PROMPT.md §138). if !containerWriter.writtenSectionTypes[SectionManifest] { return fmt.Errorf("%w: es fehlt der Abschnitt %s", ErrMissingSection, SectionManifest) } containerFooter := ContainerFooter{ ManifestHash: containerWriter.manifestHash, ContentHash: hex.EncodeToString(containerWriter.contentDigest.Sum(nil)), SectionCount: containerWriter.sectionCount, TotalBytes: containerWriter.bytesWritten, CompletedAt: containerWriter.timeSource().UTC(), Complete: true, } encodedFooter, marshalError := json.Marshal(containerFooter) if marshalError != nil { return fmt.Errorf("der container-footer konnte nicht erzeugt werden: %w", marshalError) } // Der Footer wird nicht in die Gesamtprüfsumme aufgenommen: er enthält sie. if _, writeError := containerWriter.outputWriter.Write(footerMagic[:]); writeError != nil { return fmt.Errorf("der container-footer konnte nicht geschrieben werden: %w", writeError) } footerLengthBytes := make([]byte, headerLengthFieldLength) binary.BigEndian.PutUint32(footerLengthBytes, uint32(len(encodedFooter))) if _, writeError := containerWriter.outputWriter.Write(footerLengthBytes); writeError != nil { return fmt.Errorf("der container-footer konnte nicht geschrieben werden: %w", writeError) } if _, writeError := containerWriter.outputWriter.Write(encodedFooter); writeError != nil { return fmt.Errorf("der container-footer konnte nicht geschrieben werden: %w", writeError) } containerWriter.isClosed = true return nil } // BytesWritten liefert die bisher geschriebene Menge ohne den Footer. func (containerWriter *Writer) BytesWritten() int64 { return containerWriter.bytesWritten }