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