package backupformat import ( "bytes" "encoding/binary" "encoding/json" "errors" "io" "testing" "time" ) // buildTestHeader liefert einen vollständigen Container-Header. func buildTestHeader() ContainerHeader { return ContainerHeader{ BackupID: "backup-1", ChainID: "kette-1", SourceID: "quelle-1", SourceType: "filesystem", CreatedAt: time.Date(2026, 8, 10, 12, 0, 0, 0, time.UTC), CreatedByVersion: "test", RepositoryID: "repo-1", } } // writeTestContainer baut einen vollständigen, gültigen Container. func writeTestContainer(testInstance *testing.T) []byte { testInstance.Helper() var containerBuffer bytes.Buffer containerWriter, writerError := NewWriter(&containerBuffer, buildTestHeader()) if writerError != nil { testInstance.Fatalf("der Container konnte nicht begonnen werden: %v", writerError) } if writeError := containerWriter.WriteJSONSection(SectionManifest, FlagRequired, map[string]any{"backup_id": "backup-1", "entries": 3}); writeError != nil { testInstance.Fatalf("das Manifest konnte nicht geschrieben werden: %v", writeError) } if writeError := containerWriter.WriteJSONSection(SectionChunkIndex, FlagRequired, ChunkIndex{ Entries: []ChunkIndexEntry{ {Identifier: "aaaa", ContainerOffset: 0, StoredLength: 5, LogicalLength: 5}, }, }); writeError != nil { testInstance.Fatalf("das Chunk-Verzeichnis konnte nicht geschrieben werden: %v", writeError) } if writeError := containerWriter.WriteSection(SectionDataChunks, FlagRequired, []byte("daten")); writeError != nil { testInstance.Fatalf("die Datenblöcke konnten nicht geschrieben werden: %v", writeError) } if closeError := containerWriter.Close(); closeError != nil { testInstance.Fatalf("der Container konnte nicht abgeschlossen werden: %v", closeError) } return containerBuffer.Bytes() } // readAllSections liest einen Container vollständig. func readAllSections(containerBytes []byte) ([]*Section, *Reader, error) { containerReader, readerError := NewReader(bytes.NewReader(containerBytes)) if readerError != nil { return nil, nil, readerError } var readSections []*Section for { nextSection, sectionError := containerReader.NextSection() if errors.Is(sectionError, io.EOF) { return readSections, containerReader, nil } if sectionError != nil { return readSections, containerReader, sectionError } readSections = append(readSections, nextSection) } } // --------------------------------------------------------------------------- // Serialisieren und Deserialisieren // --------------------------------------------------------------------------- func TestSerializeAndDeserializeRoundTrip(testInstance *testing.T) { containerBytes := writeTestContainer(testInstance) readSections, containerReader, readError := readAllSections(containerBytes) if readError != nil { testInstance.Fatalf("der Container konnte nicht gelesen werden: %v", readError) } if len(readSections) != 3 { testInstance.Fatalf("es sollen 3 Abschnitte gelesen werden, waren %d", len(readSections)) } // Der Header muss unverändert zurückkommen. containerHeader := containerReader.Header() if containerHeader.BackupID != "backup-1" || containerHeader.SourceType != "filesystem" { testInstance.Errorf("der Header weicht ab: %+v", containerHeader) } if !containerHeader.CreatedAt.Equal(time.Date(2026, 8, 10, 12, 0, 0, 0, time.UTC)) { testInstance.Errorf("der Zeitstempel weicht ab: %v", containerHeader.CreatedAt) } // Der Abschlussvermerk muss vorliegen. if !containerReader.IsComplete() { testInstance.Fatal("der Container gilt nicht als vollständig") } if containerReader.Footer().SectionCount != 3 { testInstance.Errorf("der Footer soll 3 Abschnitte nennen, waren %d", containerReader.Footer().SectionCount) } } func TestSectionContentIsPreservedExactly(testInstance *testing.T) { var containerBuffer bytes.Buffer // Ein Inhalt mit allen Bytewerten deckt Kodierungsfehler auf. originalContent := make([]byte, 256) for byteIndex := range originalContent { originalContent[byteIndex] = byte(byteIndex) } containerWriter, _ := NewWriter(&containerBuffer, buildTestHeader()) _ = containerWriter.WriteSection(SectionManifest, FlagRequired, []byte("{}")) _ = containerWriter.WriteSection(SectionDataChunks, FlagRequired, originalContent) _ = containerWriter.Close() readSections, _, readError := readAllSections(containerBuffer.Bytes()) if readError != nil { testInstance.Fatalf("der Container konnte nicht gelesen werden: %v", readError) } if !bytes.Equal(readSections[1].Content, originalContent) { testInstance.Fatal("der Inhalt des Datenabschnitts weicht ab") } } func TestStreamedSectionRoundTrip(testInstance *testing.T) { var containerBuffer bytes.Buffer // Ein Datenbereich, der in mehreren Schritten geschrieben wird. firstBlock := bytes.Repeat([]byte("A"), 4096) secondBlock := bytes.Repeat([]byte("B"), 4096) totalLength := int64(len(firstBlock) + len(secondBlock)) containerWriter, _ := NewWriter(&containerBuffer, buildTestHeader()) _ = containerWriter.WriteSection(SectionManifest, FlagRequired, []byte("{}")) streamWriter, streamError := containerWriter.BeginSectionStream(SectionDataChunks, FlagRequired, totalLength) if streamError != nil { testInstance.Fatalf("der Datenstrom konnte nicht begonnen werden: %v", streamError) } if _, writeError := streamWriter.Write(firstBlock); writeError != nil { testInstance.Fatalf("der erste Block konnte nicht geschrieben werden: %v", writeError) } if _, writeError := streamWriter.Write(secondBlock); writeError != nil { testInstance.Fatalf("der zweite Block konnte nicht geschrieben werden: %v", writeError) } if finishError := streamWriter.Finish(); finishError != nil { testInstance.Fatalf("der Datenstrom konnte nicht abgeschlossen werden: %v", finishError) } if closeError := containerWriter.Close(); closeError != nil { testInstance.Fatalf("der Container konnte nicht abgeschlossen werden: %v", closeError) } readSections, containerReader, readError := readAllSections(containerBuffer.Bytes()) if readError != nil { testInstance.Fatalf("der gestreamte Container konnte nicht gelesen werden: %v", readError) } if !containerReader.IsComplete() { testInstance.Fatal("der gestreamte Container gilt nicht als vollständig") } expectedContent := append(append([]byte{}, firstBlock...), secondBlock...) if !bytes.Equal(readSections[1].Content, expectedContent) { testInstance.Fatal("der gestreamte Inhalt weicht ab") } } func TestStreamedSectionRejectsWrongLength(testInstance *testing.T) { var containerBuffer bytes.Buffer containerWriter, _ := NewWriter(&containerBuffer, buildTestHeader()) _ = containerWriter.WriteSection(SectionManifest, FlagRequired, []byte("{}")) streamWriter, _ := containerWriter.BeginSectionStream(SectionDataChunks, FlagRequired, 100) _, _ = streamWriter.Write(bytes.Repeat([]byte("A"), 50)) // Weniger als angekündigt zu schreiben ergäbe einen unlesbaren Container: // der folgende Abschnittskopf läge an der falschen Stelle. if finishError := streamWriter.Finish(); finishError == nil { testInstance.Fatal("ein zu kurz geschriebener Abschnitt muss abgelehnt werden") } } func TestStreamedSectionRejectsOverlongWrite(testInstance *testing.T) { var containerBuffer bytes.Buffer containerWriter, _ := NewWriter(&containerBuffer, buildTestHeader()) _ = containerWriter.WriteSection(SectionManifest, FlagRequired, []byte("{}")) streamWriter, _ := containerWriter.BeginSectionStream(SectionDataChunks, FlagRequired, 10) // Mehr als angekündigt zu schreiben würde den Container zerstören. if _, writeError := streamWriter.Write(bytes.Repeat([]byte("A"), 20)); writeError == nil { testInstance.Fatal("ein zu langer Schreibvorgang muss abgelehnt werden") } } // --------------------------------------------------------------------------- // Versionskompatibilität // --------------------------------------------------------------------------- func TestReaderRejectsNewerFormatVersion(testInstance *testing.T) { containerBytes := writeTestContainer(testInstance) // Die Version wird auf eine künftige Fassung gesetzt. tamperedContainer := bytes.Clone(containerBytes) binary.BigEndian.PutUint16(tamperedContainer[magicLength:], MaximumReadableVersion+1) // Ein neueres Format darf nicht gedeutet werden: diese Programmversion // kennt seine Regeln nicht. _, readerError := NewReader(bytes.NewReader(tamperedContainer)) if !errors.Is(readerError, ErrUnsupportedVersion) { testInstance.Fatalf("eine neuere Version muss abgelehnt werden, war: %v", readerError) } } func TestVersionMismatchExplainsDirection(testInstance *testing.T) { // Der Unterschied zwischen zu alt und zu neu entscheidet über die Abhilfe. newerVersionError := describeVersionMismatch(MaximumReadableVersion + 1) if !bytes.Contains([]byte(newerVersionError.Error()), []byte("aktualisieren")) { testInstance.Errorf("bei einer neueren Version soll ein Update empfohlen werden: %v", newerVersionError) } olderVersionError := describeVersionMismatch(0) if bytes.Contains([]byte(olderVersionError.Error()), []byte("aktualisieren")) { testInstance.Errorf("bei einer älteren Version hilft kein Update: %v", olderVersionError) } } func TestUnknownOptionalSectionIsSkipped(testInstance *testing.T) { var containerBuffer bytes.Buffer containerWriter, _ := NewWriter(&containerBuffer, buildTestHeader()) _ = containerWriter.WriteSection(SectionManifest, FlagRequired, []byte("{}")) // Ein Abschnittstyp, den diese Programmversion nicht kennt, aber der nicht // als erforderlich gilt - so ergänzt eine spätere Version das Format. const unknownOptionalType SectionType = 200 _ = containerWriter.WriteSection(unknownOptionalType, 0, []byte("zukunftsdaten")) _ = containerWriter.Close() readSections, containerReader, readError := readAllSections(containerBuffer.Bytes()) if readError != nil { testInstance.Fatalf("ein unbekannter optionaler Abschnitt darf das Lesen nicht verhindern: %v", readError) } if len(readSections) != 2 { testInstance.Fatalf("es sollen 2 Abschnitte gelesen werden, waren %d", len(readSections)) } if !containerReader.IsComplete() { testInstance.Error("der Container soll trotz unbekanntem Abschnitt vollständig sein") } } func TestUnknownRequiredSectionIsRejected(testInstance *testing.T) { var containerBuffer bytes.Buffer containerWriter, _ := NewWriter(&containerBuffer, buildTestHeader()) _ = containerWriter.WriteSection(SectionManifest, FlagRequired, []byte("{}")) // Ein unbekannter, aber als erforderlich gekennzeichneter Abschnitt darf // nicht übergangen werden: es fehlte etwas Wesentliches. const unknownRequiredType SectionType = 201 _ = containerWriter.WriteSection(unknownRequiredType, FlagRequired, []byte("wichtig")) _ = containerWriter.Close() _, _, readError := readAllSections(containerBuffer.Bytes()) if !errors.Is(readError, ErrUnknownRequiredSection) { testInstance.Fatalf("ein unbekannter Pflichtabschnitt muss abgelehnt werden, war: %v", readError) } } // --------------------------------------------------------------------------- // Ungültiger Container // --------------------------------------------------------------------------- func TestReaderRejectsForeignData(testInstance *testing.T) { foreignInputs := map[string][]byte{ "leer": {}, "zu kurz": []byte("SYN"), "falsche Kennung": []byte("NICHTSYNCOVA-DATEN-DIE-LAENGER-SIND"), "ZIP-Datei": {0x50, 0x4b, 0x03, 0x04, 0, 0, 0, 0, 0, 0}, } for inputDescription, foreignInput := range foreignInputs { // Ein beliebiges Datenpaket darf nicht als Container durchgehen. if _, readerError := NewReader(bytes.NewReader(foreignInput)); readerError == nil { testInstance.Errorf("%s: die Daten wurden als Container akzeptiert", inputDescription) } } } func TestReaderRejectsUnreadableHeader(testInstance *testing.T) { var containerBuffer bytes.Buffer containerBuffer.Write(containerMagic[:]) versionBytes := make([]byte, versionLength) binary.BigEndian.PutUint16(versionBytes, FormatVersion) containerBuffer.Write(versionBytes) // Eine Längenangabe, der kein gültiges JSON folgt. lengthBytes := make([]byte, headerLengthFieldLength) binary.BigEndian.PutUint32(lengthBytes, 10) containerBuffer.Write(lengthBytes) containerBuffer.Write([]byte("kein-json!")) if _, readerError := NewReader(bytes.NewReader(containerBuffer.Bytes())); !errors.Is(readerError, ErrNotAContainer) { testInstance.Fatalf("ein unlesbarer Header muss abgelehnt werden, war: %v", readerError) } } func TestReaderRejectsImplausibleHeaderLength(testInstance *testing.T) { var containerBuffer bytes.Buffer containerBuffer.Write(containerMagic[:]) versionBytes := make([]byte, versionLength) binary.BigEndian.PutUint16(versionBytes, FormatVersion) containerBuffer.Write(versionBytes) // Eine verfälschte Längenangabe darf keine unbegrenzte Speicheranforderung auslösen. lengthBytes := make([]byte, headerLengthFieldLength) binary.BigEndian.PutUint32(lengthBytes, 4_000_000_000) containerBuffer.Write(lengthBytes) if _, readerError := NewReader(bytes.NewReader(containerBuffer.Bytes())); readerError == nil { testInstance.Fatal("eine unplausible Headerlänge muss abgelehnt werden") } } // --------------------------------------------------------------------------- // Unvollständiges Backup // --------------------------------------------------------------------------- func TestTruncatedContainerIsDetected(testInstance *testing.T) { containerBytes := writeTestContainer(testInstance) // Der Container wird an verschiedenen Stellen abgeschnitten, so wie es ein // abgebrochener Übertragungsvorgang hinterlässt. truncationPoints := map[string]int{ "nach dem Header": 40, "mitten im Abschnitt": len(containerBytes) / 2, "kurz vor dem Footer": len(containerBytes) - 20, } for pointDescription, truncationPoint := range truncationPoints { if truncationPoint <= 0 || truncationPoint >= len(containerBytes) { continue } _, _, readError := readAllSections(containerBytes[:truncationPoint]) // Ein abgeschnittener Container darf niemals als vollständig gelten. if readError == nil { testInstance.Errorf("%s: der abgeschnittene Container wurde akzeptiert", pointDescription) } } } func TestContainerWithoutFooterIsIncomplete(testInstance *testing.T) { var containerBuffer bytes.Buffer // Ein Container, dessen Schreiber vor dem Abschluss abbrach. containerWriter, _ := NewWriter(&containerBuffer, buildTestHeader()) _ = containerWriter.WriteSection(SectionManifest, FlagRequired, []byte("{}")) // Close wird bewusst nicht aufgerufen. _, containerReader, readError := readAllSections(containerBuffer.Bytes()) if readError == nil { testInstance.Fatal("ein Container ohne Footer darf nicht als gültig gelten") } if containerReader != nil && containerReader.IsComplete() { testInstance.Fatal("ein Container ohne Footer darf nicht als vollständig gelten") } } func TestWriterRefusesToCloseWithoutManifest(testInstance *testing.T) { var containerBuffer bytes.Buffer containerWriter, _ := NewWriter(&containerBuffer, buildTestHeader()) _ = containerWriter.WriteSection(SectionDataChunks, FlagRequired, []byte("daten")) // Ohne Manifest beschreibt der Container kein Backup; ihn abzuschließen // hiesse, ein leeres Gebilde als gültig auszugeben. closeError := containerWriter.Close() if !errors.Is(closeError, ErrMissingSection) { testInstance.Fatalf("ein Container ohne Manifest darf nicht abgeschlossen werden, war: %v", closeError) } } func TestFooterWithoutCompletionMarkerIsRejected(testInstance *testing.T) { var containerBuffer bytes.Buffer containerWriter, _ := NewWriter(&containerBuffer, buildTestHeader()) _ = containerWriter.WriteSection(SectionManifest, FlagRequired, []byte("{}")) _ = containerWriter.Close() containerBytes := containerBuffer.Bytes() // Im Footer wird der Abschlussvermerk auf false gesetzt. footerStart := bytes.LastIndex(containerBytes, footerMagic[:]) if footerStart < 0 { testInstance.Fatal("der Footer wurde nicht gefunden") } footerJSONStart := footerStart + magicLength + headerLengthFieldLength var containerFooter ContainerFooter if unmarshalError := json.Unmarshal(containerBytes[footerJSONStart:], &containerFooter); unmarshalError != nil { testInstance.Fatalf("der Footer konnte nicht gelesen werden: %v", unmarshalError) } containerFooter.Complete = false tamperedFooter, _ := json.Marshal(containerFooter) tamperedContainer := bytes.Clone(containerBytes[:footerStart]) tamperedContainer = append(tamperedContainer, footerMagic[:]...) lengthBytes := make([]byte, headerLengthFieldLength) binary.BigEndian.PutUint32(lengthBytes, uint32(len(tamperedFooter))) tamperedContainer = append(tamperedContainer, lengthBytes...) tamperedContainer = append(tamperedContainer, tamperedFooter...) _, _, readError := readAllSections(tamperedContainer) if !errors.Is(readError, ErrIncompleteBackup) { testInstance.Fatalf("ein Container ohne Abschlussvermerk muss abgelehnt werden, war: %v", readError) } } // --------------------------------------------------------------------------- // Beschädigter Inhalt // --------------------------------------------------------------------------- func TestCorruptedSectionIsDetected(testInstance *testing.T) { containerBytes := writeTestContainer(testInstance) // Ein einzelnes verändertes Byte im Manifestbereich. tamperedContainer := bytes.Clone(containerBytes) manifestPosition := bytes.Index(tamperedContainer, []byte(`"backup_id":"backup-1","entries"`)) if manifestPosition < 0 { testInstance.Fatal("der Manifestabschnitt wurde nicht gefunden") } tamperedContainer[manifestPosition+5] ^= 0xff _, _, readError := readAllSections(tamperedContainer) if !errors.Is(readError, ErrSectionCorrupted) { testInstance.Fatalf("ein verfälschter Abschnitt muss erkannt werden, war: %v", readError) } } func TestCorruptedDataSectionIsDetected(testInstance *testing.T) { containerBytes := writeTestContainer(testInstance) tamperedContainer := bytes.Clone(containerBytes) dataPosition := bytes.Index(tamperedContainer, []byte("daten")) if dataPosition < 0 { testInstance.Fatal("der Datenabschnitt wurde nicht gefunden") } tamperedContainer[dataPosition] = 'X' _, _, readError := readAllSections(tamperedContainer) if readError == nil { testInstance.Fatal("ein verfälschter Datenabschnitt muss erkannt werden") } } func TestRemovedSectionIsDetectedByFooter(testInstance *testing.T) { var containerBuffer bytes.Buffer containerWriter, _ := NewWriter(&containerBuffer, buildTestHeader()) _ = containerWriter.WriteSection(SectionManifest, FlagRequired, []byte("{}")) _ = containerWriter.WriteSection(SectionSourceMetadata, 0, []byte("angaben")) _ = containerWriter.Close() containerBytes := containerBuffer.Bytes() // Der optionale Abschnitt wird samt Kopf entfernt - ein Angreifer könnte // so Angaben unterschlagen. sectionStart := bytes.Index(containerBytes, []byte("angaben")) - sectionHeaderLength sectionEnd := bytes.Index(containerBytes, []byte("angaben")) + len("angaben") tamperedContainer := bytes.Clone(containerBytes[:sectionStart]) tamperedContainer = append(tamperedContainer, containerBytes[sectionEnd:]...) // Die Abschnittszahl und die Gesamtprüfsumme im Footer decken das auf. _, _, readError := readAllSections(tamperedContainer) if !errors.Is(readError, ErrIntegrityMismatch) { testInstance.Fatalf("ein entfernter Abschnitt muss erkannt werden, war: %v", readError) } } func TestSwappedSectionWithValidDigestIsDetected(testInstance *testing.T) { // Ein Angreifer tauscht einen Abschnitt samt passender Prüfsumme aus. // Die Gesamtprüfsumme im Footer deckt das dennoch auf. var originalBuffer bytes.Buffer originalWriter, _ := NewWriter(&originalBuffer, buildTestHeader()) _ = originalWriter.WriteSection(SectionManifest, FlagRequired, []byte(`{"echt":true}`)) _ = originalWriter.Close() var forgedBuffer bytes.Buffer forgedWriter, _ := NewWriter(&forgedBuffer, buildTestHeader()) _ = forgedWriter.WriteSection(SectionManifest, FlagRequired, []byte(`{"echt":fals}`)) _ = forgedWriter.Close() originalBytes := originalBuffer.Bytes() forgedBytes := forgedBuffer.Bytes() // Der gefälschte Abschnitt samt Kopf ersetzt den echten, der Footer bleibt. originalFooterStart := bytes.LastIndex(originalBytes, footerMagic[:]) forgedFooterStart := bytes.LastIndex(forgedBytes, footerMagic[:]) tamperedContainer := bytes.Clone(forgedBytes[:forgedFooterStart]) tamperedContainer = append(tamperedContainer, originalBytes[originalFooterStart:]...) _, _, readError := readAllSections(tamperedContainer) if !errors.Is(readError, ErrIntegrityMismatch) { testInstance.Fatalf("ein ausgetauschter Abschnitt muss erkannt werden, war: %v", readError) } } // --------------------------------------------------------------------------- // Chunk-Verzeichnis // --------------------------------------------------------------------------- func TestChunkIndexLookup(testInstance *testing.T) { chunkIndex := &ChunkIndex{ Entries: []ChunkIndexEntry{ {Identifier: "aaa", ContainerOffset: 0, StoredLength: 100, LogicalLength: 100}, {Identifier: "bbb", ContainerOffset: 100, StoredLength: 200, LogicalLength: 200}, }, } foundEntry, wasFound := chunkIndex.FindByIdentifier("bbb") if !wasFound { testInstance.Fatal("der vorhandene Chunk wurde nicht gefunden") } if foundEntry.ContainerOffset != 100 { testInstance.Errorf("die Position soll 100 sein, war %d", foundEntry.ContainerOffset) } // Ein nicht enthaltener Chunk darf nicht gemeldet werden. if _, wasFound := chunkIndex.FindByIdentifier("ccc"); wasFound { testInstance.Error("ein nicht vorhandener Chunk wurde gemeldet") } if chunkIndex.TotalStoredBytes() != 300 { testInstance.Errorf("die Gesamtgröße soll 300 sein, war %d", chunkIndex.TotalStoredBytes()) } } // --------------------------------------------------------------------------- // Metadaten // --------------------------------------------------------------------------- func TestEncryptionMetadataSurvivesRoundTrip(testInstance *testing.T) { var containerBuffer bytes.Buffer headerWithEncryption := buildTestHeader() headerWithEncryption.Encryption = EncryptionMetadata{ Algorithm: "aes-256-gcm", KeyVersion: "v2", KeyDerivation: "hkdf-sha256", } headerWithEncryption.Compression = CompressionMetadata{Algorithm: "zstd", Level: 3} containerWriter, _ := NewWriter(&containerBuffer, headerWithEncryption) _ = containerWriter.WriteSection(SectionManifest, FlagRequired, []byte("{}")) _ = containerWriter.Close() _, containerReader, readError := readAllSections(containerBuffer.Bytes()) if readError != nil { testInstance.Fatalf("der Container konnte nicht gelesen werden: %v", readError) } readHeader := containerReader.Header() // Ohne die Schlüsselversion wäre der Container nach einer Rotation unlesbar. if readHeader.Encryption.KeyVersion != "v2" { testInstance.Errorf("die Schlüsselversion weicht ab: %q", readHeader.Encryption.KeyVersion) } if !readHeader.Encryption.IsEncrypted() { testInstance.Error("der Container soll als verschlüsselt gelten") } if !readHeader.Compression.IsCompressed() { testInstance.Error("der Container soll als komprimiert gelten") } } func TestParentBackupIsPreserved(testInstance *testing.T) { var containerBuffer bytes.Buffer headerWithParent := buildTestHeader() headerWithParent.ParentBackupID = "backup-0" containerWriter, _ := NewWriter(&containerBuffer, headerWithParent) _ = containerWriter.WriteSection(SectionManifest, FlagRequired, []byte("{}")) _ = containerWriter.Close() _, containerReader, _ := readAllSections(containerBuffer.Bytes()) // Ohne diese Angabe liesse sich eine Kette aus einzelnen Containern nicht // wieder zusammensetzen. if containerReader.Header().ParentBackupID != "backup-0" { testInstance.Errorf("das Elternbackup weicht ab: %q", containerReader.Header().ParentBackupID) } } func TestWriterRejectsUseAfterClose(testInstance *testing.T) { var containerBuffer bytes.Buffer containerWriter, _ := NewWriter(&containerBuffer, buildTestHeader()) _ = containerWriter.WriteSection(SectionManifest, FlagRequired, []byte("{}")) _ = containerWriter.Close() // Nach dem Abschluss darf nichts mehr hinzukommen: der Footer ist geschrieben. if writeError := containerWriter.WriteSection(SectionDataChunks, 0, []byte("zu spät")); !errors.Is(writeError, ErrWriterClosed) { testInstance.Fatalf("ein Schreibvorgang nach dem Abschluss muss abgelehnt werden, war: %v", writeError) } if closeError := containerWriter.Close(); !errors.Is(closeError, ErrWriterClosed) { testInstance.Fatalf("ein zweiter Abschluss muss abgelehnt werden, war: %v", closeError) } }