pax_global_header00006660000000000000000000000064151443623470014523gustar00rootroot0000000000000052 comment=1f952f46e89a9abc5dcf320cc904fbf1926cb4eb textandbinarywriter-0.1.0/000077500000000000000000000000001514436234700156325ustar00rootroot00000000000000textandbinarywriter-0.1.0/.github/000077500000000000000000000000001514436234700171725ustar00rootroot00000000000000textandbinarywriter-0.1.0/.github/FUNDING.yml000066400000000000000000000000151514436234700210030ustar00rootroot00000000000000github: [bep]textandbinarywriter-0.1.0/.github/workflows/000077500000000000000000000000001514436234700212275ustar00rootroot00000000000000textandbinarywriter-0.1.0/.github/workflows/test.yml000066400000000000000000000020501514436234700227260ustar00rootroot00000000000000on: push: branches: [main] pull_request: permissions: contents: read name: Test jobs: test: strategy: matrix: go-version: [1.25.x, 1.26.x] platform: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.platform }} steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install Go uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: go-version: ${{ matrix.go-version }} - name: Install staticcheck run: go install honnef.co/go/tools/cmd/staticcheck@latest shell: bash - name: Update PATH run: echo "$(go env GOPATH)/bin" >> $GITHUB_PATH shell: bash - name: Fmt if: matrix.platform == 'ubuntu-latest' run: "diff <(gofmt -d .) <(printf '')" shell: bash - name: Vet run: go vet ./... - name: Staticcheck run: staticcheck ./... - name: Test run: go test -race ./... textandbinarywriter-0.1.0/.gitignore000066400000000000000000000004151514436234700176220ustar00rootroot00000000000000# Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Dependency directories (remove the comment below to include it) # vendor/ textandbinarywriter-0.1.0/LICENSE000066400000000000000000000020651514436234700166420ustar00rootroot00000000000000MIT License Copyright (c) 2022 Bjørn Erik Pedersen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. textandbinarywriter-0.1.0/README.md000066400000000000000000000007201514436234700171100ustar00rootroot00000000000000[![Tests on Linux, MacOS and Windows](https://github.com/bep/textandbinarywriter/workflows/Test/badge.svg)](https://github.com/bep/textandbinarywriter/actions?query=workflow:Test) [![Go Report Card](https://goreportcard.com/badge/github.com/bep/textandbinarywriter)](https://goreportcard.com/report/github.com/bep/textandbinarywriter) [![GoDoc](https://godoc.org/github.com/bep/textandbinarywriter?status.svg)](https://godoc.org/github.com/bep/textandbinarywriter)textandbinarywriter-0.1.0/go.mod000066400000000000000000000004411514436234700167370ustar00rootroot00000000000000module github.com/bep/textandbinarywriter go 1.25 require github.com/frankban/quicktest v1.14.6 require ( github.com/google/go-cmp v0.7.0 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect ) textandbinarywriter-0.1.0/go.sum000066400000000000000000000023021514436234700167620ustar00rootroot00000000000000github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= textandbinarywriter-0.1.0/writer.go000066400000000000000000000101551514436234700174770ustar00rootroot00000000000000// Copyright 2025 Bjørn Erik Pedersen // SPDX-License-Identifier: MIT package textandbinarywriter import ( "bytes" "encoding/binary" "io" ) // BlobMarker is used to identify start of a binary blob. var BlobMarker [8]byte = [8]byte{'T', 'A', 'K', '3', '5', 'E', 'M', '1'} const byteHeaderByteLength = 8 + 4 + 4 // marker + id + size type Writer struct { // Default writer, for text content. textw io.Writer // When we receive a blob header, we switch to this writer. // The header and blob data is written to this writer. binaryw io.Writer // state mode writerMode header [byteHeaderByteLength]byte headerN int // number of header bytes read binaryCurrentWriteSize int64 binaryCurrentWriteCount int64 } type writerMode int const ( modeText writerMode = iota modeHeader modeBinary ) func NewWriter(textw, binaryw io.Writer) *Writer { return &Writer{ textw: textw, binaryw: binaryw, mode: modeText, } } var _ io.Writer = (*Writer)(nil) // Write implements io.Writer. func (w *Writer) Write(p []byte) (n int, err error) { var nn int for len(p) > 0 { switch w.mode { case modeText: idx := bytes.Index(p, BlobMarker[:]) if idx == -1 { // No marker found, write all to textw. nn, err = w.textw.Write(p) n += nn return n, err } if idx > 0 { // Marker found, write up to the marker to textw. nn, err = w.textw.Write(p[:idx]) n += nn if err != nil { return n, err } } p = p[idx:] w.mode = modeHeader w.headerN = 0 case modeHeader: toRead := byteHeaderByteLength - w.headerN canRead := len(p) if canRead >= toRead { copy(w.header[w.headerN:], p[:toRead]) p = p[toRead:] n += toRead _, size, err := ReadBlobHeader(bytes.NewReader(w.header[:])) if err != nil { // Invalid header, reset to text mode and write the buffered header as text. w.mode = modeText nn, err2 := w.textw.Write(w.header[:w.headerN]) n += nn if err2 != nil { return n, err2 } return n, err } w.binaryCurrentWriteSize = int64(size) w.binaryCurrentWriteCount = 0 _, err = w.binaryw.Write(w.header[:]) // n is the bytes read from p, not what's written to the underlying writers. if err != nil { return n, err } w.mode = modeBinary } else { copy(w.header[w.headerN:], p) w.headerN += canRead n += canRead p = nil } case modeBinary: remaining := w.binaryCurrentWriteSize - w.binaryCurrentWriteCount if remaining <= 0 { w.mode = modeText continue } toWrite := min(int64(len(p)), remaining) nn, err = w.binaryw.Write(p[:toWrite]) n += nn if err != nil { return n, err } w.binaryCurrentWriteCount += int64(nn) p = p[nn:] } } return n, nil } func (w *Writer) Close() (err error) { if closer, ok := w.textw.(io.Closer); ok { if err = closer.Close(); err != nil { return } } if closer, ok := w.binaryw.(io.Closer); ok { err = closer.Close() } return } // WriteBlobHeader writes a blob header to w with the given id and size using little-endian encoding. func WriteBlobHeader(w io.Writer, id, size uint32) error { if err := binary.Write(w, binary.LittleEndian, BlobMarker); err != nil { return err } if err := binary.Write(w, binary.LittleEndian, id); err != nil { return err } if err := binary.Write(w, binary.LittleEndian, size); err != nil { return err } return nil } // ReadBlobHeader reads a blob header from r and returns the id and size using little-endian encoding. func ReadBlobHeader(r io.Reader) (id, size uint32, err error) { var marker [8]byte if _, err := io.ReadFull(r, marker[:]); err != nil { return 0, 0, err } if marker != BlobMarker { return 0, 0, io.ErrUnexpectedEOF } return ReadBlobHeaderExcludingMarker(r) } // ReadBlobHeaderExcludingMarker reads a blob header from r excluding the marker using little-endian encoding. func ReadBlobHeaderExcludingMarker(r io.Reader) (id, size uint32, err error) { if err := binary.Read(r, binary.LittleEndian, &id); err != nil { return 0, 0, err } if err := binary.Read(r, binary.LittleEndian, &size); err != nil { return 0, 0, err } return } textandbinarywriter-0.1.0/writer_test.go000066400000000000000000000125241514436234700205400ustar00rootroot00000000000000// Copyright 2025 Bjørn Erik Pedersen // SPDX-License-Identifier: MIT package textandbinarywriter import ( "bytes" "io" "sync" "testing" qt "github.com/frankban/quicktest" ) func TestWriter(t *testing.T) { c := qt.New(t) // A helper to create the binary representation of a blob for the expected output. newExpectedBinary := func(id, size uint32, data []byte) []byte { var buf bytes.Buffer _ = WriteBlobHeader(&buf, id, size) buf.Write(data) return buf.Bytes() } testCases := []struct { name string writes []func(w io.Writer) error expectedText string expectedBinary []byte }{ { name: "text only", writes: []func(w io.Writer) error{ func(w io.Writer) error { _, err := w.Write([]byte("Hello, World!")) return err }, }, expectedText: "Hello, World!", }, { name: "blob only", writes: []func(w io.Writer) error{ func(w io.Writer) error { return WriteBlobHeader(w, 1, 4) }, func(w io.Writer) error { _, err := w.Write([]byte{1, 2, 3, 4}) return err }, }, expectedBinary: newExpectedBinary(1, 4, []byte{1, 2, 3, 4}), }, { name: "text, blob, text", writes: []func(w io.Writer) error{ func(w io.Writer) error { _, err := w.Write([]byte("one")) return err }, func(w io.Writer) error { return WriteBlobHeader(w, 2, 4) }, func(w io.Writer) error { _, err := w.Write([]byte{5, 6, 7, 8}) return err }, func(w io.Writer) error { _, err := w.Write([]byte("two")) return err }, }, expectedText: "onetwo", expectedBinary: newExpectedBinary(2, 4, []byte{5, 6, 7, 8}), }, { name: "split data write", writes: []func(w io.Writer) error{ func(w io.Writer) error { return WriteBlobHeader(w, 4, 4) }, func(w io.Writer) error { _, err := w.Write([]byte{1, 2}) return err }, func(w io.Writer) error { _, err := w.Write([]byte{3, 4}) return err }, }, expectedBinary: newExpectedBinary(4, 4, []byte{1, 2, 3, 4}), }, { name: "multiple blobs", writes: []func(w io.Writer) error{ func(w io.Writer) error { _, err := w.Write([]byte("a")) return err }, func(w io.Writer) error { return WriteBlobHeader(w, 5, 2) }, func(w io.Writer) error { _, err := w.Write([]byte{1, 2}) return err }, func(w io.Writer) error { _, err := w.Write([]byte("b")) return err }, func(w io.Writer) error { return WriteBlobHeader(w, 6, 2) }, func(w io.Writer) error { _, err := w.Write([]byte{3, 4}) return err }, func(w io.Writer) error { _, err := w.Write([]byte("c")) return err }, }, expectedText: "abc", expectedBinary: append(newExpectedBinary(5, 2, []byte{1, 2}), newExpectedBinary(6, 2, []byte{3, 4})...), }, { name: "zero-length blob", writes: []func(w io.Writer) error{ func(w io.Writer) error { _, err := w.Write([]byte("a")) return err }, func(w io.Writer) error { return WriteBlobHeader(w, 7, 0) }, func(w io.Writer) error { _, err := w.Write([]byte("b")) return err }, }, expectedText: "ab", expectedBinary: newExpectedBinary(7, 0, []byte{}), }, { name: "text with partial marker", writes: []func(w io.Writer) error{ func(w io.Writer) error { _, err := w.Write([]byte("Hello TAK35EM World")) return err }, }, expectedText: "Hello TAK35EM World", }, } for _, tc := range testCases { c.Run(tc.name, func(c *qt.C) { textReader, textWriter := io.Pipe() binaryReader, binaryWriter := io.Pipe() w := NewWriter(textWriter, binaryWriter) var textOut, binaryOut bytes.Buffer var wg sync.WaitGroup wg.Add(2) go func() { defer wg.Done() _, err := io.Copy(&textOut, textReader) if err != nil && (err != io.EOF && err != io.ErrClosedPipe) { c.Errorf("error copying text: %v", err) } }() go func() { defer wg.Done() _, err := io.Copy(&binaryOut, binaryReader) if err != nil && (err != io.EOF && err != io.ErrClosedPipe) { c.Errorf("error copying binary: %v", err) } }() go func() { for _, writeFn := range tc.writes { err := writeFn(w) c.Assert(err, qt.IsNil) } textWriter.Close() binaryWriter.Close() }() // Wait for readers to finish. wg.Wait() if len(tc.expectedBinary) == 0 { c.Assert(binaryOut.Len(), qt.Equals, 0) } else { c.Assert(binaryOut.Bytes(), qt.DeepEquals, tc.expectedBinary) } c.Assert(textOut.String(), qt.Equals, tc.expectedText) }) } } func BenchmarkWriter(b *testing.B) { textBuf := bytes.Repeat([]byte("Hello, World!\n"), 1000) blobData := bytes.Repeat([]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, 100) b.ResetTimer() for b.Loop() { var textOut bytes.Buffer var binaryOut bytes.Buffer w := NewWriter(&textOut, &binaryOut) // Write text _, _ = w.Write(textBuf) // Write blob _ = WriteBlobHeader(w, 42, uint32(len(blobData))) _, _ = w.Write(blobData) } } func TestBlobHeaderWriteAndRead(t *testing.T) { c := qt.New(t) var b bytes.Buffer err := WriteBlobHeader(&b, 42, 100) c.Assert(err, qt.IsNil) c.Assert(b.Len(), qt.Equals, 8+4+4) // marker + id + size id, size, err := ReadBlobHeader(&b) c.Assert(err, qt.IsNil) c.Assert(id, qt.Equals, uint32(42)) c.Assert(size, qt.Equals, uint32(100)) }