| // Copyright 2014-2021 Ulrich Kunitz. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package xz |
| |
| import ( |
| "errors" |
| "fmt" |
| "hash" |
| "io" |
| |
| "github.com/ulikunitz/xz/lzma" |
| ) |
| |
| // WriterConfig describe the parameters for an xz writer. |
| type WriterConfig struct { |
| Properties *lzma.Properties |
| DictCap int |
| BufSize int |
| BlockSize int64 |
| // checksum method: CRC32, CRC64 or SHA256 (default: CRC64) |
| CheckSum byte |
| // Forces NoChecksum (default: false) |
| NoCheckSum bool |
| // match algorithm |
| Matcher lzma.MatchAlgorithm |
| } |
| |
| // fill replaces zero values with default values. |
| func (c *WriterConfig) fill() { |
| if c.Properties == nil { |
| c.Properties = &lzma.Properties{LC: 3, LP: 0, PB: 2} |
| } |
| if c.DictCap == 0 { |
| c.DictCap = 8 * 1024 * 1024 |
| } |
| if c.BufSize == 0 { |
| c.BufSize = 4096 |
| } |
| if c.BlockSize == 0 { |
| c.BlockSize = maxInt64 |
| } |
| if c.CheckSum == 0 { |
| c.CheckSum = CRC64 |
| } |
| if c.NoCheckSum { |
| c.CheckSum = None |
| } |
| } |
| |
| // Verify checks the configuration for errors. Zero values will be |
| // replaced by default values. |
| func (c *WriterConfig) Verify() error { |
| if c == nil { |
| return errors.New("xz: writer configuration is nil") |
| } |
| c.fill() |
| lc := lzma.Writer2Config{ |
| Properties: c.Properties, |
| DictCap: c.DictCap, |
| BufSize: c.BufSize, |
| Matcher: c.Matcher, |
| } |
| if err := lc.Verify(); err != nil { |
| return err |
| } |
| if c.BlockSize <= 0 { |
| return errors.New("xz: block size out of range") |
| } |
| if err := verifyFlags(c.CheckSum); err != nil { |
| return err |
| } |
| return nil |
| } |
| |
| // filters creates the filter list for the given parameters. |
| func (c *WriterConfig) filters() []filter { |
| return []filter{&lzmaFilter{int64(c.DictCap)}} |
| } |
| |
| // maxInt64 defines the maximum 64-bit signed integer. |
| const maxInt64 = 1<<63 - 1 |
| |
| // verifyFilters checks the filter list for the length and the right |
| // sequence of filters. |
| func verifyFilters(f []filter) error { |
| if len(f) == 0 { |
| return errors.New("xz: no filters") |
| } |
| if len(f) > 4 { |
| return errors.New("xz: more than four filters") |
| } |
| for _, g := range f[:len(f)-1] { |
| if g.last() { |
| return errors.New("xz: last filter is not last") |
| } |
| } |
| if !f[len(f)-1].last() { |
| return errors.New("xz: wrong last filter") |
| } |
| return nil |
| } |
| |
| // newFilterWriteCloser converts a filter list into a WriteCloser that |
| // can be used by a blockWriter. |
| func (c *WriterConfig) newFilterWriteCloser(w io.Writer, f []filter) (fw io.WriteCloser, err error) { |
| if err = verifyFilters(f); err != nil { |
| return nil, err |
| } |
| fw = nopWriteCloser(w) |
| for i := len(f) - 1; i >= 0; i-- { |
| fw, err = f[i].writeCloser(fw, c) |
| if err != nil { |
| return nil, err |
| } |
| } |
| return fw, nil |
| } |
| |
| // nopWCloser implements a WriteCloser with a Close method not doing |
| // anything. |
| type nopWCloser struct { |
| io.Writer |
| } |
| |
| // Close returns nil and doesn't do anything else. |
| func (c nopWCloser) Close() error { |
| return nil |
| } |
| |
| // nopWriteCloser converts the Writer into a WriteCloser with a Close |
| // function that does nothing beside returning nil. |
| func nopWriteCloser(w io.Writer) io.WriteCloser { |
| return nopWCloser{w} |
| } |
| |
| // Writer compresses data written to it. It is an io.WriteCloser. |
| type Writer struct { |
| WriterConfig |
| |
| xz io.Writer |
| bw *blockWriter |
| newHash func() hash.Hash |
| h header |
| index []record |
| closed bool |
| } |
| |
| // newBlockWriter creates a new block writer writes the header out. |
| func (w *Writer) newBlockWriter() error { |
| var err error |
| w.bw, err = w.WriterConfig.newBlockWriter(w.xz, w.newHash()) |
| if err != nil { |
| return err |
| } |
| if err = w.bw.writeHeader(w.xz); err != nil { |
| return err |
| } |
| return nil |
| } |
| |
| // closeBlockWriter closes a block writer and records the sizes in the |
| // index. |
| func (w *Writer) closeBlockWriter() error { |
| var err error |
| if err = w.bw.Close(); err != nil { |
| return err |
| } |
| w.index = append(w.index, w.bw.record()) |
| return nil |
| } |
| |
| // NewWriter creates a new xz writer using default parameters. |
| func NewWriter(xz io.Writer) (w *Writer, err error) { |
| return WriterConfig{}.NewWriter(xz) |
| } |
| |
| // NewWriter creates a new Writer using the given configuration parameters. |
| func (c WriterConfig) NewWriter(xz io.Writer) (w *Writer, err error) { |
| if err = c.Verify(); err != nil { |
| return nil, err |
| } |
| w = &Writer{ |
| WriterConfig: c, |
| xz: xz, |
| h: header{c.CheckSum}, |
| index: make([]record, 0, 4), |
| } |
| if w.newHash, err = newHashFunc(c.CheckSum); err != nil { |
| return nil, err |
| } |
| data, err := w.h.MarshalBinary() |
| if err != nil { |
| return nil, fmt.Errorf("w.h.MarshalBinary(): error %w", err) |
| } |
| if _, err = xz.Write(data); err != nil { |
| return nil, err |
| } |
| if err = w.newBlockWriter(); err != nil { |
| return nil, err |
| } |
| return w, nil |
| |
| } |
| |
| // Write compresses the uncompressed data provided. |
| func (w *Writer) Write(p []byte) (n int, err error) { |
| if w.closed { |
| return 0, errClosed |
| } |
| for { |
| k, err := w.bw.Write(p[n:]) |
| n += k |
| if err != errNoSpace { |
| return n, err |
| } |
| if err = w.closeBlockWriter(); err != nil { |
| return n, err |
| } |
| if err = w.newBlockWriter(); err != nil { |
| return n, err |
| } |
| } |
| } |
| |
| // Close closes the writer and adds the footer to the Writer. Close |
| // doesn't close the underlying writer. |
| func (w *Writer) Close() error { |
| if w.closed { |
| return errClosed |
| } |
| w.closed = true |
| var err error |
| if err = w.closeBlockWriter(); err != nil { |
| return err |
| } |
| |
| f := footer{flags: w.h.flags} |
| if f.indexSize, err = writeIndex(w.xz, w.index); err != nil { |
| return err |
| } |
| data, err := f.MarshalBinary() |
| if err != nil { |
| return err |
| } |
| if _, err = w.xz.Write(data); err != nil { |
| return err |
| } |
| return nil |
| } |
| |
| // countingWriter is a writer that counts all data written to it. |
| type countingWriter struct { |
| w io.Writer |
| n int64 |
| } |
| |
| // Write writes data to the countingWriter. |
| func (cw *countingWriter) Write(p []byte) (n int, err error) { |
| n, err = cw.w.Write(p) |
| cw.n += int64(n) |
| if err == nil && cw.n < 0 { |
| return n, errors.New("xz: counter overflow") |
| } |
| return |
| } |
| |
| // blockWriter is writes a single block. |
| type blockWriter struct { |
| cxz countingWriter |
| // mw combines io.WriteCloser w and the hash. |
| mw io.Writer |
| w io.WriteCloser |
| n int64 |
| blockSize int64 |
| closed bool |
| headerLen int |
| |
| filters []filter |
| hash hash.Hash |
| } |
| |
| // newBlockWriter creates a new block writer. |
| func (c *WriterConfig) newBlockWriter(xz io.Writer, hash hash.Hash) (bw *blockWriter, err error) { |
| bw = &blockWriter{ |
| cxz: countingWriter{w: xz}, |
| blockSize: c.BlockSize, |
| filters: c.filters(), |
| hash: hash, |
| } |
| bw.w, err = c.newFilterWriteCloser(&bw.cxz, bw.filters) |
| if err != nil { |
| return nil, err |
| } |
| if bw.hash.Size() != 0 { |
| bw.mw = io.MultiWriter(bw.w, bw.hash) |
| } else { |
| bw.mw = bw.w |
| } |
| return bw, nil |
| } |
| |
| // writeHeader writes the header. If the function is called after Close |
| // the commpressedSize and uncompressedSize fields will be filled. |
| func (bw *blockWriter) writeHeader(w io.Writer) error { |
| h := blockHeader{ |
| compressedSize: -1, |
| uncompressedSize: -1, |
| filters: bw.filters, |
| } |
| if bw.closed { |
| h.compressedSize = bw.compressedSize() |
| h.uncompressedSize = bw.uncompressedSize() |
| } |
| data, err := h.MarshalBinary() |
| if err != nil { |
| return err |
| } |
| if _, err = w.Write(data); err != nil { |
| return err |
| } |
| bw.headerLen = len(data) |
| return nil |
| } |
| |
| // compressed size returns the amount of data written to the underlying |
| // stream. |
| func (bw *blockWriter) compressedSize() int64 { |
| return bw.cxz.n |
| } |
| |
| // uncompressedSize returns the number of data written to the |
| // blockWriter |
| func (bw *blockWriter) uncompressedSize() int64 { |
| return bw.n |
| } |
| |
| // unpaddedSize returns the sum of the header length, the uncompressed |
| // size of the block and the hash size. |
| func (bw *blockWriter) unpaddedSize() int64 { |
| if bw.headerLen <= 0 { |
| panic("xz: block header not written") |
| } |
| n := int64(bw.headerLen) |
| n += bw.compressedSize() |
| n += int64(bw.hash.Size()) |
| return n |
| } |
| |
| // record returns the record for the current stream. Call Close before |
| // calling this method. |
| func (bw *blockWriter) record() record { |
| return record{bw.unpaddedSize(), bw.uncompressedSize()} |
| } |
| |
| var errClosed = errors.New("xz: writer already closed") |
| |
| var errNoSpace = errors.New("xz: no space") |
| |
| // Write writes uncompressed data to the block writer. |
| func (bw *blockWriter) Write(p []byte) (n int, err error) { |
| if bw.closed { |
| return 0, errClosed |
| } |
| |
| t := bw.blockSize - bw.n |
| if int64(len(p)) > t { |
| err = errNoSpace |
| p = p[:t] |
| } |
| |
| var werr error |
| n, werr = bw.mw.Write(p) |
| bw.n += int64(n) |
| if werr != nil { |
| return n, werr |
| } |
| return n, err |
| } |
| |
| // Close closes the writer. |
| func (bw *blockWriter) Close() error { |
| if bw.closed { |
| return errClosed |
| } |
| bw.closed = true |
| if err := bw.w.Close(); err != nil { |
| return err |
| } |
| s := bw.hash.Size() |
| k := padLen(bw.cxz.n) |
| p := make([]byte, k+s) |
| bw.hash.Sum(p[k:k]) |
| if _, err := bw.cxz.w.Write(p); err != nil { |
| return err |
| } |
| return nil |
| } |