Add lib/litonlylzma
diff --git a/lib/litonlylzma/example_test.go b/lib/litonlylzma/example_test.go new file mode 100644 index 0000000..d2a3b59 --- /dev/null +++ b/lib/litonlylzma/example_test.go
@@ -0,0 +1,45 @@ +// Copyright 2022 The Wuffs Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package litonlylzma_test + +import ( + "fmt" + "log" + + "github.com/google/wuffs/lib/litonlylzma" +) + +func ExampleRoundTrip() { + original := []byte("Hello world.\n") + compressed, err := litonlylzma.FileFormatLZMA.Encode(nil, original) + if err != nil { + log.Fatalf("Encode: %v", err) + } + recovered, _, err := litonlylzma.FileFormatLZMA.Decode(nil, compressed) + if err != nil { + log.Fatalf("Decode: %v", err) + } + for i, c := range compressed { + if i > 0 { + fmt.Printf(" ") + } + fmt.Printf("%02X", c) + } + fmt.Printf("\n%s\n", recovered) + + // Output: + // 5D 00 10 00 00 0D 00 00 00 00 00 00 00 00 24 19 49 86 E7 D5 E5 E2 56 ED 6A E6 0E 81 FE B8 00 00 + // Hello world. +}
diff --git a/lib/litonlylzma/litonlylzma.go b/lib/litonlylzma/litonlylzma.go new file mode 100644 index 0000000..7d4af85 --- /dev/null +++ b/lib/litonlylzma/litonlylzma.go
@@ -0,0 +1,491 @@ +// Copyright 2022 The Wuffs Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// ---------------- + +// Package litonlylzma provides a decoder and encoder for Literal Only LZMA, a +// subset of the LZMA compressed file format. +// +// Subset means that the Encode method's output is a valid LZMA file and can be +// decompressed by the tools available at https://www.7-zip.org/sdk.html (and +// available as /usr/bin/lzma on a Debian system). +// +// Subset also means that this codec's compression ratios are not as good as +// full-LZMA (although compression times are much faster). Moderate compression +// is still achieved, through range coding, but there are no Lempel Ziv +// back-references. The main benefit, compared to full-LZMA, is that this +// implementation is much simpler and hence easier to study. It is only a few +// hundred lines of code. +// +// Example compression numbers on a small English text file (at +// https://github.com/google/wuffs/blob/main/test/data/romeo.txt): +// - romeo.txt is 942 bytes (100%). +// - romeo.txt.litonlylzma is 659 bytes (70%). +// - romeo.txt.xz is 644 bytes (68%). +// - romeo.txt.lzma is 598 bytes (63%). +// - romeo.txt.bz2 is 568 bytes (60%). +// - romeo.txt.zst is 559 bytes (59%). +// - romeo.txt.gz is 558 bytes (59%). +// +// Example compression numbers on a large archive of source code (at +// https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.0.1.tar.xz): +// - linux-5.0.1.tar is 863313920 bytes (100%). +// - linux-5.0.1.tar.litonlylzma is 449726070 bytes (52%). +// - linux-5.0.1.tar.gz is 164575959 bytes (19%). +// - linux-5.0.1.tar.zst is 156959897 bytes (18%). +// - linux-5.0.1.tar.bz2 is 125873134 bytes (15%). +// - linux-5.0.1.tar.xz is 108233572 bytes (13%). +// - linux-5.0.1.tar.lzma is 108216601 bytes (13%). +// +// The various tools (/usr/bin/gzip, /usr/bin/xz, etc) were all ran with their +// default settings, not their "maximum compression" settings. This is why the +// linux-5.0.1.tar.xz file size above differs from what cdn.kernel.org served. +package litonlylzma + +import ( + "errors" +) + +var ( + // ErrUnsupportedLZMAData is potentially returned by Decode. + ErrUnsupportedLZMAData = errors.New("litonlylzma: unsupported LZMA data") + + errInvalidLZMAData = errors.New("litonlylzma: invalid LZMA data") + errUnexpectedEOF = errors.New("litonlylzma: unexpected EOF") + errUnsupportedFileFormat = errors.New("litonlylzma: unsupported FileFormat") +) + +const ( + // LZMA has a few configuration knobs but our subset-of-LZMA hard codes + // these to LZMA's default values: + // https://github.com/jljusten/LZMA-SDK/blob/781863cdf592da3e97420f50de5dac056ad352a5/C/LzmaEnc.h#L19-L21 + // https://github.com/jljusten/LZMA-SDK/blob/781863cdf592da3e97420f50de5dac056ad352a5/DOC/lzma-specification.txt#L43-L45 + lc = 3 // The number of Literal Context bits. + lp = 0 // The number of Literal Position bits. + pb = 2 // The number of Position Bits. + + lpMask = (1 << lp) - 1 + pbMask = (1 << pb) - 1 +) + +// lzmaHeader5 is the first 5 bytes of the LZMA header using the hard-coded +// configuration for the subset-of-LZMA that this package supports. 0x5D +// encodes the (lc, lp, pb) triple and the next four bytes hold the u32le +// dictionary size. 0x1000 is LZMA's minimum dictionary size, although our +// subset-of-LZMA does not use a dictionary (also called a "sliding window"). +const lzmaHeader5 = "\x5D\x00\x10\x00\x00" + +// rangeDecoder and rangeEncoder hold the state for range coding (also known as +// arithmetic coding, roughly speaking). "range" is a keyword in Go, so in this +// implementation, what's traditionally called the "range" fields are called +// "width" instead. +// +// Conceptually, the encoded bitstream holds a high precision number (a binary +// fraction) in the range from zero to one. High precision could mean thousands +// or millions of bits, but when decoding, only 32 of these are 'paged in' at a +// time - this is the rangeDecoder.bits field. The encoder similarly works on +// only a small, fixed number of bits. +// +// When encoding, that high precision *number* isn't fully known until the end +// of the input is reached. *During* the encoding process, what's known is a +// high precision *interval* that gets narrowed down as the input bytes are +// consumed. That interval is represented by the rangeEncoder.low and +// rangeEncoder.width fields (the width is the difference between the +// interval's upper and lower bound). Both low and width are, conceptually, +// 32-bit values - they operate at the same scale - but low is an uint64 +// because of potential overflow. We'll revisit that below. +// +// Width is initialized to 0xFFFF_FFFF and shrinks every time a bit is decoded +// or encoded. Similarly, low can only increase or stay the same on each bit +// step. When width drops below (1 << 24), the rangeDecoder or rangeEncoder +// 'zooms in', left-shifting (with truncation) by 8 the bits, low and width +// fields and reading a src byte or writing a dst byte of compressed data. +// +// Writing a byte may be delayed because the encoding interval could span +// multiple possible digits. To use a decimal fraction analogy, suppose that +// our interval is [0.7138872, 0.7146055] and the low field nominally holds 4 +// digits. We could emit '7' and '1' but we have to hold back on emitting +// either '3' or '4' until we can narrow down the interval further. That '3' +// digit is stashed as the pendingHead field. low is set to 8872, width is set +// to (46055 - 38872) = 7183 and we progress until the width drops below 1000, +// causing a 'zoom in' event. There are three cases: +// - if low is less than 9000 (but starts with an '8'), we can emit the '3' +// with confidence (and then stash '8' in the pendingHead field). +// - if low overflows 9999, we can emit the '4' with confidence (and then stash +// low's 'thousands digit' in the pendingHead field). +// - if low is in 9000 ..= 9999 then we could still be unsure. Keep the '3' +// pending but extend it so that there are two pending digits: '39'. If this +// occurs again: '399', and so on. +// +// In all three cases, 'zooming in' causes us to shift out the most significant +// digit of the low field. This method is therefore named shiftLow here, the +// same name used in other range coding implementations, such as +// https://github.com/jljusten/LZMA-SDK/blob/781863cdf592da3e97420f50de5dac056ad352a5/C/LzmaEnc.c#L572-L602 +// +// The pendingEtc fields combine to hold one or more digits ('0' ..= '9' +// decimal digits in our analogy, 0x00 ..= 0xFF bytes in our actual +// implementation). There are (1 + pendingExtra) digits in total and while the +// head digit might be any value, the extra digits must be all '9' (in the +// decimal analogy) or 0xFF (in our actual implementation). +// +// Some other implementations use the name "cache" instead of "pending", and +// store a "cacheSize" field equivalent to our (1 + pendingExtra). +// +// As above, the low field is conceptually a uint32, but it is a uint64 because +// of this potential overflow. Its high 32 bits are usually zero, rarely one +// and never anything higher. The high 32 bits essentially hold an overflow +// carry bit. If set, it flips "3999etc" to "4000etc". +// +// "Low is conceptually 4 bytes; pendingHead holds another byte" is why the +// shortest LZMA payload is 5 bytes even though the first byte is always 0x00 +// and seems redundant. + +type rangeDecoder struct { + src []byte + bits uint32 + width uint32 +} + +type rangeEncoder struct { + dst []byte + low uint64 + width uint32 + pendingHead uint8 + pendingExtra uint64 +} + +// Precondition: (rEnc.width < (1 << 24)) or we are at the end of our input +// (and are flushing the rangeEncoder). +func (rEnc *rangeEncoder) shiftLow() { + if rEnc.low < 0x0_FF00_0000 { + rEnc.dst = append(rEnc.dst, rEnc.pendingHead+0x00) + for ; rEnc.pendingExtra > 0; rEnc.pendingExtra-- { + rEnc.dst = append(rEnc.dst, 0xFF) + } + rEnc.pendingHead = uint8(rEnc.low >> 24) + rEnc.pendingExtra = 0 + rEnc.low = (rEnc.low << 8) & 0xFFFF_FFFF + + } else if rEnc.low < 0x1_0000_0000 { + rEnc.pendingExtra++ + rEnc.low = (rEnc.low << 8) & 0xFFFF_FFFF + + } else { + rEnc.dst = append(rEnc.dst, rEnc.pendingHead+0x01) + for ; rEnc.pendingExtra > 0; rEnc.pendingExtra-- { + rEnc.dst = append(rEnc.dst, 0x00) + } + rEnc.pendingHead = uint8(rEnc.low >> 24) + rEnc.pendingExtra = 0 + rEnc.low = (rEnc.low << 8) & 0xFFFF_FFFF + } +} + +// prob is a probability (out of 100%) whose uint16 value ranges from 0 to +// (1 << 11) = 2048 inclusive. For example, prob(1024) means a 50% probability +// and prob(384) means a (384 / 2048) = 18.75% probability. +// +// Specifically, it is the probability that the next bit is 0 (instead of 1). +type prob uint16 + +const ( + // probBits is such that prob(1 << probBits) means a 100% probability. + probBits = 11 + minProb = 0 + maxProb = 1 << probBits +) + +func setProbsToOneHalf(p []prob) { + for i := range p { + p[i] = 1 << (probBits - 1) + } +} + +// adaptShift scales how much to adjust "the probability that the next bit is +// 0" after seeing the current bit. The probability is adaptive in that the +// current bit being 0 (or 1) increases (or decreases) the probability that the +// next bit is 0. The larger adaptShift is, the slower the adaptation. +const adaptShift = 5 + +func (p *prob) decodeBit(rDec *rangeDecoder) (bitValue uint32, retErr error) { + threshold := (rDec.width >> probBits) * uint32(*p) + if rDec.bits < threshold { + bitValue = 0 + rDec.width = threshold + *p += (maxProb - *p) >> adaptShift + } else { + bitValue = 1 + rDec.bits -= threshold + rDec.width -= threshold + *p -= (*p - minProb) >> adaptShift + } + + if rDec.width < (1 << 24) { + if len(rDec.src) <= 0 { + return 0, errUnexpectedEOF + } + rDec.bits = rDec.bits<<8 | uint32(rDec.src[0]) + rDec.width <<= 8 + rDec.src = rDec.src[1:] + } + return bitValue, retErr +} + +func (p *prob) encodeBit(rEnc *rangeEncoder, bitValue uint32) { + threshold := (rEnc.width >> probBits) * uint32(*p) + if bitValue == 0 { + rEnc.width = threshold + *p += (maxProb - *p) >> adaptShift + } else { + rEnc.low += uint64(threshold) + rEnc.width -= threshold + *p -= (*p - minProb) >> adaptShift + } + if rEnc.width < (1 << 24) { + rEnc.width <<= 8 + rEnc.shiftLow() + } +} + +// byteProbs hold probabilities for coding 8 bits (1 byte) of data. It is a 256 +// element array such that: +// - The 0th element is unused. +// - The 1st element holds the probability that the 7th bit (the highest, most +// significant bit) is 0. +// - The 2nd element holds the probability that the 6th bit is 0, conditional +// on the high bit being 0. +// - The 3rd element holds the probability that the 6th bit is 0, conditional +// on the high bit being 1. +// - The 4th element holds the probability that the 5th bit is 0, conditional +// on the high two bits being 00. +// - The 5th element holds the probability that the 5th bit is 0, conditional +// on the high two bits being 01. +// - The 6th element holds the probability that the 5th bit is 0, conditional +// on the high two bits being 10. +// - The 7th element holds the probability that the 5th bit is 0, conditional +// on the high two bits being 11. +// - The 8th element holds the probability that the 4th bit is 0, conditional +// on the high three bits being 000. +// - etc +// - The 255th element holds the probability that the 0th bit (the lowest, +// least significant bit) is 0, conditional on the high seven bits being +// 1111111. +// +// Put another way, the 256 elements' value of N, as in "it's a probability for +// the Nth bit", looks like this (when arranged in 8 rows of 32 elements): +// +// u7665555444444443333333333333333 +// 22222222222222222222222222222222 +// 11111111111111111111111111111111 +// 11111111111111111111111111111111 +// 00000000000000000000000000000000 +// 00000000000000000000000000000000 +// 00000000000000000000000000000000 +// 00000000000000000000000000000000 +// +// The 'u' means that the 0th element is unused. +type byteProbs [0x100]prob + +func (p *byteProbs) decodeByte(rDec *rangeDecoder) (byteValue byte, retErr error) { + index := uint32(1) + for index < 0x100 { + if bitValue, err := p[index].decodeBit(rDec); err != nil { + return 0, err + } else { + index = index<<1 | bitValue + } + } + return byte(index), nil +} + +func (p *byteProbs) encodeByte(rEnc *rangeEncoder, byteValue byte) { + b := uint32(byteValue) + index := uint32(1) + for i := 7; i >= 0; i-- { + bitValue := (b >> uint(i)) & 1 + p[index].encodeBit(rEnc, bitValue) + index = index<<1 | bitValue + } +} + +// FileFormat is a compressed file format, either the original LZMA format +// itself or something that builds on or varies that. +// +// Each valid FileFormatXxx value does not distinguish between the full file +// format (as spoken by numerous Xxx software tools) and the subset-of-Xxx +// implemented by this package. Specifically, FileFormatLZMA.Encode will +// produce compressed data that is valid full-LZMA (and is also valid +// subset-of-LZMA). FileFormatLZMA.Decode can decode subset-of-LZMA (but not +// full-LZMA). +// +// This package currently defines only one valid FileFormatXxx value: +// FileFormatLZMA. Future versions of this package could define e.g. +// FileFormatLZMA2 or FileFormatXz such that FileFormatXz.Encode would produce +// valid full-XZ data: a valid XZ file (that could be decoded by official XZ +// tools) that used the LZMA compression filter, but elected not to use the +// full capabilities of that filter (using LZ literals only, not matches). +type FileFormat uint32 + +const ( + FileFormatInvalid = FileFormat(0) + FileFormatLZMA = FileFormat(1) +) + +// Decode converts src from the Literal Only LZMA compressed file format, +// appending the decoding to dst. +// +// f should be FileFormatLZMA but future versions of this package may support +// other file formats. +// +// It is valid to pass a nil dst, like the way it's valid to pass nil to the +// built-in append function. +// +// Decode also returns the suffix of src that was not used during decoding. +// +// It can also return partial results. The appendedDst and remainingSrc return +// values may be non-zero even if retErr is non-zero. +// +// It returns ErrUnsupportedLZMAData if the source bytes look like LZMA +// formatted data that is outside the subset-of-LZMA that this package +// implements. +func (f FileFormat) Decode(dst []byte, src []byte) (appendedDst []byte, remainingSrc []byte, retErr error) { + if f != FileFormatLZMA { + return dst, nil, errUnsupportedFileFormat + } + + // LZMA consists of a 13 byte header and then at least a 5 byte payload + // (range coded data). The 13 byte header is: + // - 1 byte for the (lc, lp, pb) triple with max values (8, 4, 4), + // - 4 bytes u32le dictionary size and + // - 8 bytes i64le uncompressed size. + if (len(src) < 18) || (src[0] >= (9 * 5 * 5)) { + return dst, nil, errInvalidLZMAData + } else if (src[0] != lzmaHeader5[0]) || + (src[1] != lzmaHeader5[1]) || + (src[2] != lzmaHeader5[2]) || + (src[3] != lzmaHeader5[3]) || + (src[4] != lzmaHeader5[4]) || + (src[13] != 0x00) { + return dst, nil, ErrUnsupportedLZMAData + } + + size := uint64(0) + for i := 0; i < 8; i++ { + size |= uint64(src[5+i]) << uint(8*i) + } + if int64(size) < -1 { + return dst, nil, errInvalidLZMAData + } else if int64(size) == -1 { + return dst, nil, ErrUnsupportedLZMAData + } + + rDec := rangeDecoder{ + src: src[18:], + bits: (uint32(src[14]) << 24) | + (uint32(src[15]) << 16) | + (uint32(src[16]) << 8) | + (uint32(src[17]) << 0), + width: 0xFFFF_FFFF, + } + + posProbs := [1 << pb]prob{} + setProbsToOneHalf(posProbs[:]) + + litProbs := [1 << (lc + lp)]byteProbs{} + for ij := range litProbs { + setProbsToOneHalf(litProbs[ij][:]) + } + + pos := uint32(0) + prev := byte(0) + curr := byte(0) + for ; size > 0; size-- { + if bitValue, err := posProbs[pos&pbMask].decodeBit(&rDec); err != nil { + return dst, nil, err + } else if bitValue != 0 { + // A full-LZMA decoder would implement Lempel Ziv matches (or the + // End of Stream marker) here. Our simpler subset-of-LZMA decoder + // only uses literals. + return dst, rDec.src, ErrUnsupportedLZMAData + } + + i := (pos & lpMask) << lc + j := uint32(prev) >> (8 - lc) + if curr, retErr = litProbs[i|j].decodeByte(&rDec); retErr != nil { + return dst, rDec.src, retErr + } + dst = append(dst, curr) + pos++ + prev = curr + } + + return dst, rDec.src, nil +} + +// Encode converts src to the Literal Only LZMA compressed file format, +// appending the encoding to dst. +// +// f should be FileFormatLZMA but future versions of this package may support +// other file formats. +// +// It is valid to pass a nil dst, like the way it's valid to pass nil to the +// built-in append function. +func (f FileFormat) Encode(dst []byte, src []byte) (appendedDst []byte, retErr error) { + if f != FileFormatLZMA { + return nil, errUnsupportedFileFormat + } + + dst = append(dst, lzmaHeader5...) + size := len(src) + for i := 0; i < 8; i++ { + dst = append(dst, byte(size)) + size >>= 8 + } + + rEnc := rangeEncoder{ + dst: dst, + width: 0xFFFF_FFFF, + } + + posProbs := [1 << pb]prob{} + setProbsToOneHalf(posProbs[:]) + + litProbs := [1 << (lc + lp)]byteProbs{} + for ij := range litProbs { + setProbsToOneHalf(litProbs[ij][:]) + } + + pos := uint32(0) + prev := byte(0) + for _, curr := range src { + // A full-LZMA encoder would choose between Lempel Ziv literals and + // matches. Our simpler subset-of-LZMA encoder only uses literals, and + // it is not a streaming encoder (it does not need to emit an End of + // Stream marker), so we always encode a 0 bit here. + posProbs[pos&pbMask].encodeBit(&rEnc, 0) + + i := (pos & lpMask) << lc + j := uint32(prev) >> (8 - lc) + litProbs[i|j].encodeByte(&rEnc, curr) + pos++ + prev = curr + } + + // Flush the rangeEncoder. + for i := 0; i < 5; i++ { + rEnc.shiftLow() + } + + return rEnc.dst, nil +}
diff --git a/lib/litonlylzma/litonlylzma_test.go b/lib/litonlylzma/litonlylzma_test.go new file mode 100644 index 0000000..3c68191 --- /dev/null +++ b/lib/litonlylzma/litonlylzma_test.go
@@ -0,0 +1,64 @@ +// Copyright 2022 The Wuffs Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package litonlylzma + +import ( + "bytes" + "os" + "testing" +) + +const ( + oneThousand0x00s = "oneThousand0x00s" + oneThousand0xFFs = "oneThousand0xFFs" +) + +func testRoundTrip(tt *testing.T, filename string) { + original := []byte(nil) + switch filename { + case oneThousand0x00s: + original = make([]byte, 1000) + case oneThousand0xFFs: + original = make([]byte, 1000) + for i := range original { + original[i] = 0xFF + } + default: + o, err := os.ReadFile("../../test/data/" + filename) + if err != nil { + tt.Fatalf("ReadFile: %v", err) + } + original = o + } + + compressed, err := FileFormatLZMA.Encode(nil, original) + if err != nil { + tt.Fatalf("Encode: %v", err) + } + recovered, _, err := FileFormatLZMA.Decode(nil, compressed) + if err != nil { + tt.Fatalf("Decode: %v", err) + } + if !bytes.Equal(original, recovered) { + tt.Fatalf("round trip produced different bytes") + } +} + +func TestRoundTrip0Bytes(tt *testing.T) { testRoundTrip(tt, "0.bytes") } +func TestRoundTripArchiveTar(tt *testing.T) { testRoundTrip(tt, "archive.tar") } +func TestRoundTripOneThousand0x00s(tt *testing.T) { testRoundTrip(tt, oneThousand0x00s) } +func TestRoundTripOneThousand0xFFs(tt *testing.T) { testRoundTrip(tt, oneThousand0xFFs) } +func TestRoundTripPiTxt(tt *testing.T) { testRoundTrip(tt, "pi.txt") } +func TestRoundTripRomeoTxt(tt *testing.T) { testRoundTrip(tt, "romeo.txt") }
diff --git a/script/litonlylzma.go b/script/litonlylzma.go new file mode 100644 index 0000000..f4f0160 --- /dev/null +++ b/script/litonlylzma.go
@@ -0,0 +1,69 @@ +// Copyright 2022 The Wuffs Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build ignore +// +build ignore + +package main + +// litonlylzma.go converts to and from the Literal Only LZMA compressed file +// format, a subset of the LZMA compressed file format (e.g. as spoken by +// /usr/bin/lzma on a Debian system). +// +// Usage: +// go run litonlylzma.go -encode < foo.txt > foo.txt.litonlylzma +// go run litonlylzma.go -decode < foo.txt.litonlylzma + +import ( + "flag" + "fmt" + "io" + "os" + + "github.com/google/wuffs/lib/litonlylzma" +) + +var ( + decode = flag.Bool("decode", false, "decode from LZMA") + encode = flag.Bool("encode", false, "encode to LZMA") +) + +func main() { + if err := main1(); err != nil { + os.Stderr.WriteString(err.Error() + "\n") + os.Exit(1) + } +} + +func main1() error { + flag.Parse() + if *decode == *encode { + return fmt.Errorf("exactly one of -decode and -encode needed") + } + src, err := io.ReadAll(os.Stdin) + if err != nil { + return err + } + dst, err := []byte(nil), error(nil) + if *decode { + dst, _, err = litonlylzma.FileFormatLZMA.Decode(nil, src) + } else { + dst, err = litonlylzma.FileFormatLZMA.Encode(nil, src) + } + if err != nil { + return err + } + _, err = os.Stdout.Write(dst) + return err +}
diff --git a/test/data/romeo.txt.litonlylzma b/test/data/romeo.txt.litonlylzma new file mode 100644 index 0000000..386950a --- /dev/null +++ b/test/data/romeo.txt.litonlylzma Binary files differ
diff --git a/test/data/romeo.txt.lzma b/test/data/romeo.txt.lzma new file mode 100644 index 0000000..fbeba5e --- /dev/null +++ b/test/data/romeo.txt.lzma Binary files differ
diff --git a/test/data/romeo.txt.xz b/test/data/romeo.txt.xz new file mode 100644 index 0000000..7e3ddb3 --- /dev/null +++ b/test/data/romeo.txt.xz Binary files differ