// SPDX-FileCopyrightText: 2024 Shun Sakai // // SPDX-License-Identifier: Apache-2.0 OR MIT package lzip import ( "bytes" "encoding/binary" "hash/crc32" "io" "github.com/ulikunitz/xz/lzma" ) // Writer is an [io.WriteCloser] that can be written to retrieve a lzip-format // compressed file from data. type Writer struct { w io.Writer compressor *lzma.Writer buf bytes.Buffer header *header wroteHeader bool trailer closed bool } // WriterOptions configures [Writer]. type WriterOptions struct { // DictSize sets the dictionary size. DictSize uint32 } func newWriterOptions() *WriterOptions { opt := &WriterOptions{DefaultDictSize} return opt } // Verify checks if [WriterOptions] is valid. func (o *WriterOptions) Verify() error { switch dictSize := o.DictSize; { case dictSize < MinDictSize: return &DictSizeTooSmallError{dictSize} case dictSize > MaxDictSize: return &DictSizeTooLargeError{dictSize} } return nil } // NewWriter creates a new [Writer] writing the given writer. // // This uses the default parameters. func NewWriter(w io.Writer) *Writer { opt := newWriterOptions() z, err := NewWriterOptions(w, opt) if err != nil { panic(err) } return z } // NewWriterOptions creates a new [Writer] writing the given writer. // // This uses the given [WriterOptions]. func NewWriterOptions(w io.Writer, opt *WriterOptions) (*Writer, error) { if err := opt.Verify(); err != nil { return nil, err } z := &Writer{w: w} compressor, err := lzma.WriterConfig{DictCap: int(opt.DictSize)}.NewWriter(&z.buf) if err != nil { return nil, err } z.compressor = compressor header := newHeader(opt.DictSize) z.header = header return z, nil } // Write compresses the given uncompressed data. func (z *Writer) Write(p []byte) (int, error) { if !z.wroteHeader { z.wroteHeader = true var header [headerSize]byte copy(header[:magicSize], z.header.magic[:]) header[4] = byte(z.header.version) header[5] = z.header.dictSize if _, err := z.w.Write(header[:]); err != nil { return 0, err } } n, err := z.compressor.Write(p) if err != nil { return n, err } z.crc = crc32.Update(z.crc, crc32.IEEETable, p) z.dataSize += uint64(len(p)) return n, nil } // Close closes the [Writer] and writing the lzip trailer. It does not close // the underlying [io.Writer]. func (z *Writer) Close() error { if z.closed { return nil } z.closed = true if err := z.compressor.Close(); err != nil { return err } cb := z.buf.Bytes()[lzma.HeaderLen:] if _, err := z.w.Write(cb); err != nil { return err } var trailer [trailerSize]byte binary.LittleEndian.PutUint32(trailer[:4], z.crc) binary.LittleEndian.PutUint64(trailer[4:12], z.dataSize) binary.LittleEndian.PutUint64(trailer[12:], headerSize+uint64(len(cb))+trailerSize) if memberSize := binary.LittleEndian.Uint64(trailer[12:]); memberSize > MaxMemberSize { return &MemberSizeTooLargeError{memberSize} } _, err := z.w.Write(trailer[:]) return err }