// Package backupformat implementiert den versionierten Syncova Backup Container. // // Der Container ist die portable Form eines Backups: eine einzelne, in sich // geschlossene Datei, die alles enthält, was zur Wiederherstellung nötig ist // (PROMPT.md §13, SYNCOVA_ARCHITECTURE.md §8). Er dient der Übertragung an ein // zweites Repository, der Archivierung und dem Austausch zwischen Installationen. // // Aufbau: // // +--------------------------------------------------+ // | Magic "SYNCOVA1" (8 Byte) | // | Formatversion (uint16) | // | Headerlänge (uint32) + Header (JSON) | // +--------------------------------------------------+ // | Abschnitt: Typ, Flags, Länge, Prüfsumme, Inhalt | // | ... beliebig viele Abschnitte ... | // +--------------------------------------------------+ // | Footer: Magic, Manifest-Hash, Gesamt-Prüfsumme, | // | Abschlussvermerk | // +--------------------------------------------------+ // // Zwei Eigenschaften prägen den Entwurf: // // - Der Footer steht am Ende. Ein abgeschnittener Container hat keinen und // wird dadurch zuverlässig als unvollständig erkannt. // - Jeder Abschnitt trägt seine Länge. Eine spätere Programmversion kann // Abschnitte ergänzen, die eine ältere überspringt — sofern sie nicht als // erforderlich gekennzeichnet sind. package backupformat import ( "errors" "fmt" ) // containerMagic kennzeichnet den Beginn eines Syncova-Containers. // // Die Ziffer am Ende gehört zur Kennung und nicht zur Version: sie verhindert, // dass ein Container einer künftigen, grundlegend anderen Formatfamilie // versehentlich als lesbar gilt. var containerMagic = [8]byte{'S', 'Y', 'N', 'C', 'O', 'V', 'A', '1'} // footerMagic kennzeichnet den Beginn des Footers. // // Eine eigene Kennung erlaubt es, den Footer auch dann zu finden, wenn die // Abschnittskette beschädigt ist — eine Notfallanalyse bleibt damit möglich. var footerMagic = [8]byte{'S', 'Y', 'N', 'F', 'O', 'O', 'T', '1'} // Versionen des Containerformats. const ( // FormatVersion ist die Version, die diese Programmversion schreibt. FormatVersion uint16 = 1 // MinimumReadableVersion ist die älteste noch lesbare Version. // // Ältere Container werden abgelehnt statt fehlerhaft gedeutet: ein // falsch interpretiertes Backup ist schlimmer als ein nicht gelesenes. MinimumReadableVersion uint16 = 1 // MaximumReadableVersion ist die neueste lesbare Version. // // Ein neuerer Container wird nicht angetastet, weil diese Programmversion // seine Regeln nicht kennt (PROMPT.md §13). MaximumReadableVersion uint16 = 1 ) // SectionType benennt die Art eines Abschnitts. // // Die Werte sind Teil des Formatvertrags und dürfen niemals neu belegt werden: // ein bestehender Container würde sonst falsch gedeutet. type SectionType uint8 const ( // SectionManifest enthält das Manifest des Backups als JSON. SectionManifest SectionType = 1 // SectionChunkIndex enthält das Verzeichnis aller Chunks mit Länge und Prüfsumme. SectionChunkIndex SectionType = 2 // SectionBlockMap ordnet logische Bereiche der Quelle den Chunks zu. SectionBlockMap SectionType = 3 // SectionDataChunks enthält die eigentlichen Datenblöcke. SectionDataChunks SectionType = 4 // SectionSourceMetadata enthält Angaben zum gesicherten System. SectionSourceMetadata SectionType = 5 // SectionIntegrityInfo enthält ergänzende Integritätsangaben. SectionIntegrityInfo SectionType = 6 ) // String liefert die sprechende Bezeichnung eines Abschnittstyps. // // Sie erscheint in Fehlermeldungen, damit ein Administrator ohne Formattabelle // versteht, welcher Teil betroffen ist. func (sectionType SectionType) String() string { switch sectionType { case SectionManifest: return "Manifest" case SectionChunkIndex: return "Chunk-Verzeichnis" case SectionBlockMap: return "Blockzuordnung" case SectionDataChunks: return "Datenblöcke" case SectionSourceMetadata: return "Quellenangaben" case SectionIntegrityInfo: return "Integritätsangaben" default: return fmt.Sprintf("unbekannter Abschnitt (%d)", uint8(sectionType)) } } // SectionFlags beschreiben Eigenschaften eines Abschnitts. type SectionFlags uint8 const ( // FlagRequired kennzeichnet einen Abschnitt, ohne den das Backup unbrauchbar ist. // // Das ist der Schlüssel zur Erweiterbarkeit: eine spätere Programmversion // darf neue Abschnitte hinzufügen. Kennt eine ältere Version einen // optionalen Abschnitt nicht, überspringt sie ihn. Einen als erforderlich // gekennzeichneten dagegen darf sie nicht übergehen — sie verweigert die // Verarbeitung, statt ein unvollständiges Backup vorzutäuschen. FlagRequired SectionFlags = 1 << 0 // FlagCompressed meldet einen komprimierten Abschnittsinhalt. FlagCompressed SectionFlags = 1 << 1 // FlagEncrypted meldet einen verschlüsselten Abschnittsinhalt. FlagEncrypted SectionFlags = 1 << 2 // FlagDeferredDigest meldet einen Abschnitt ohne Prüfsumme im Kopf. // // Ein Abschnitt, der als Datenstrom entsteht, kennt seine Prüfsumme erst, // wenn er vollständig geschrieben ist — der Kopf steht zu diesem Zeitpunkt // aber längst in der Ausgabe. Ihn nachträglich zu überschreiben verlangte // eine rückspulbare Senke und schlösse damit Netzwerk- und Pipe-Ziele aus. // // Die Unversehrtheit solcher Abschnitte ist doppelt gesichert: über die // Prüfsumme jedes einzelnen Chunks im Chunk-Verzeichnis und über die // Gesamtprüfsumme im Footer. Es entsteht dadurch keine Prüflücke. FlagDeferredDigest SectionFlags = 1 << 3 ) // Has meldet, ob ein Flag gesetzt ist. func (sectionFlags SectionFlags) Has(testedFlag SectionFlags) bool { return sectionFlags&testedFlag != 0 } // Feste Längen des Formats in Byte. const ( // magicLength ist die Länge der Kennung. magicLength = 8 // versionLength ist die Länge der Versionsangabe. versionLength = 2 // headerLengthFieldLength ist die Länge des Längenfelds des Headers. headerLengthFieldLength = 4 // digestLength ist die Länge einer SHA-256-Prüfsumme. digestLength = 32 // sectionHeaderLength ist die Länge eines Abschnittskopfs: // Typ (1) + Flags (1) + Länge (8) + Prüfsumme (32). sectionHeaderLength = 1 + 1 + 8 + digestLength ) // maximumHeaderLength begrenzt die Größe des Headers. // // Ohne Obergrenze könnte eine manipulierte Längenangabe den Arbeitsspeicher // erschöpfen, noch bevor irgendeine Prüfung greift. const maximumHeaderLength = 4 * 1024 * 1024 // maximumSectionLength begrenzt die Größe eines einzelnen Abschnitts. // // Der Wert ist großzügig gewählt, verhindert aber, dass eine verfälschte // Längenangabe zu einer unbegrenzten Speicheranforderung führt. const maximumSectionLength = 64 * 1024 * 1024 * 1024 // Fehler des Containerformats. var ( // ErrNotAContainer meldet Daten, die kein Syncova-Container sind. ErrNotAContainer = errors.New("die daten sind kein syncova backup container") // ErrUnsupportedVersion meldet eine nicht lesbare Formatversion. ErrUnsupportedVersion = errors.New("die formatversion des containers wird von dieser programmversion nicht unterstützt") // ErrContainerTruncated meldet einen abgeschnittenen Container. // // Das ist der häufigste Fall eines abgebrochenen Übertragungsvorgangs und // muss zuverlässig erkannt werden. ErrContainerTruncated = errors.New("der container ist unvollständig: er endet vor dem abschlussvermerk") // ErrSectionCorrupted meldet einen Abschnitt, dessen Inhalt nicht zu seiner Prüfsumme passt. ErrSectionCorrupted = errors.New("ein abschnitt des containers ist beschädigt") // ErrUnknownRequiredSection meldet einen unbekannten, aber erforderlichen Abschnitt. ErrUnknownRequiredSection = errors.New("der container enthält einen erforderlichen abschnitt, den diese programmversion nicht kennt") // ErrMissingSection meldet einen fehlenden Pflichtabschnitt. ErrMissingSection = errors.New("dem container fehlt ein pflichtabschnitt") // ErrIncompleteBackup meldet einen Container ohne gültigen Abschlussvermerk. ErrIncompleteBackup = errors.New("der container trägt keinen gültigen abschlussvermerk und beschreibt kein vollständiges backup") // ErrIntegrityMismatch meldet eine nicht passende Gesamtprüfsumme. ErrIntegrityMismatch = errors.New("die prüfsumme des containers stimmt nicht mit seinem inhalt überein") // ErrChunkNotInContainer meldet einen im Manifest benannten, aber nicht enthaltenen Chunk. ErrChunkNotInContainer = errors.New("ein vom manifest benötigter chunk fehlt im container") // ErrSectionTooLarge meldet eine unplausible Längenangabe. ErrSectionTooLarge = errors.New("die längenangabe eines abschnitts ist unplausibel groß") // ErrWriterClosed meldet die Verwendung eines bereits abgeschlossenen Schreibers. ErrWriterClosed = errors.New("der container wurde bereits abgeschlossen") ) // IsVersionReadable meldet, ob eine Formatversion gelesen werden kann. func IsVersionReadable(formatVersion uint16) bool { return formatVersion >= MinimumReadableVersion && formatVersion <= MaximumReadableVersion } // describeVersionMismatch erklärt eine nicht lesbare Version verständlich. // // Der Unterschied zwischen "zu alt" und "zu neu" ist für den Administrator // entscheidend: im einen Fall hilft ein Konverter, im anderen ein Update. func describeVersionMismatch(formatVersion uint16) error { if formatVersion > MaximumReadableVersion { return fmt.Errorf("%w: der container hat Version %d, diese Programmversion liest bis Version %d. "+ "Bitte Syncova aktualisieren", ErrUnsupportedVersion, formatVersion, MaximumReadableVersion) } return fmt.Errorf("%w: der container hat Version %d, diese Programmversion liest ab Version %d", ErrUnsupportedVersion, formatVersion, MinimumReadableVersion) }