Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,19 @@ jobs:
( mkdir sfxout && cd sfxout && ../readme.run -q )
cmp README.md sfxout/README.md

- name: Directory archive round-trip
run: |
mkdir -p tree/sub tree/empty
cp README.md tree/a.txt
printf 'nested\n' > tree/sub/b.txt
./squish c tree tree.sq
./squish d tree.sq tree-out
diff -r tree tree-out
test -d tree-out/empty
./squish s tree tree.run
( mkdir sfxdir && cd sfxdir && ../tree.run -q )
diff -r tree sfxdir/tree

windows-cross:
name: cross-compile Windows DLL + CLI
runs-on: ubuntu-latest
Expand Down Expand Up @@ -77,3 +90,14 @@ jobs:
..\readme_sfx.exe -q
cd ..
fc /b README.md sfxout\README.md

- name: Directory archive round-trip
shell: cmd
run: |
mkdir tree\sub
copy README.md tree\a.txt
echo nested> tree\sub\b.txt
squish.exe c tree tree.sq
squish.exe d tree.sq tree-out
fc /b tree\a.txt tree-out\a.txt
fc /b tree\sub\b.txt tree-out\sub\b.txt
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ Per [CONTRIBUTING.md](CONTRIBUTING.md), any change to the model constants in

### Added

- CLI: directory support for `c`, `d`, and `s`. When `input` is a directory,
`squish c` serializes the whole tree (files, subdirectories, empty
directories, and unix permission bits) into a single `SQAR` archive stream
and compresses that; `squish d` detects the archive on decompression and
recreates the tree under `output`. `squish s` packs a directory the same
way into a self-extracting executable. Single-file compression is
unchanged and byte-for-byte compatible with earlier releases (the archive
wrapper is used only for directories). Extraction rejects absolute paths
and `..` traversal, so an archive can never write outside its target
directory; entries are stored in sorted order, so an archive depends only
on the tree, not on filesystem iteration order. See docs/FORMAT.md §12.
- CLI: `s` command — build a self-extracting archive. `squish s input output`
compresses `input` and appends it (plus the original name and a 32-byte
trailer) to a copy of the `squish` CLI used as a stub; the resulting
Expand Down
21 changes: 17 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,25 @@ No dependencies beyond libc/libm.
```sh
./squish c bigfile bigfile.sq # compress
./squish d bigfile.sq restored # decompress (checksum-verified)
./squish c project project.sq # compress a whole directory tree
./squish d project.sq restored-dir # ... recreate it under restored-dir
./squish -t 0 c bigfile bigfile.sq # compress on all cores (multi-block)
./squish -t 0 -b 4 c big big.sq # ... with 4 MiB blocks (more parallel,
# slightly worse ratio)
./squish s report.pdf report.run # make a self-extracting archive
./report.run # ... run it to restore report.pdf
```

`input` may be a file or a **directory**. A directory is packed into a single
archive stream (files, subdirectories, empty directories, and permission bits
preserved) and compressed as one unit, so cross-file redundancy is modeled
too; `d` detects the archive and recreates the tree under `output`.
Extraction refuses absolute paths and `..`, so an archive never writes
outside its target directory, and entries are stored sorted, so the output
depends only on the tree. Single files are compressed exactly as before —
the archive wrapper is used only for directories. See
[docs/FORMAT.md §12](docs/FORMAT.md) for the layout.

`-t N` compresses with N threads (`0` = all cores) by splitting the input
into independently modeled blocks — near-linear speedup, at a small ratio
cost because each block's model starts cold (about 1–2% at the default
Expand All @@ -65,10 +77,11 @@ it) and reads both formats transparently. Budget ~150 MB of model memory
per thread.

The `s` command writes a **self-extracting archive**: `squish s input output`
compresses `input` and produces an executable `output` that carries the
compressed data. Running it — with no `squish` and no `libsquish` installed —
decompresses (and checksum-verifies) the original back to its stored name in
the current directory, or to a path you name:
compresses `input` (a file or a directory tree) and produces an executable
`output` that carries the compressed data. Running it — with no `squish` and
no `libsquish` installed — decompresses (and checksum-verifies) the original
back to its stored name in the current directory, or to a path you name;
a directory archive unpacks the whole tree:

```sh
./squish s report.pdf report.run # build (Linux: chmod +x is applied for you)
Expand Down
7 changes: 6 additions & 1 deletion SQUISH.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,15 @@ ratio for near-linear speedup.

```
make # libsquish.so + squish CLI
./squish c input output.sq # compress
./squish c input output.sq # compress a file (or a directory tree)
./squish d output.sq restored # decompress (checksum-verified)
```

`input` may be a directory: it is serialized into a single `SQAR` archive
stream and compressed as one unit, and `d` recreates the tree under
`restored`. Single files are compressed exactly as before. See
[docs/FORMAT.md](docs/FORMAT.md) §12.

SQUISH is also a library — `squish.h` + `libsquish.so` (or `make dll` for
Windows); see [docs/API.md](docs/API.md) and `examples/`.

Expand Down
52 changes: 52 additions & 0 deletions docs/FORMAT.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,3 +243,55 @@ offset size field
- The trailer is independent of the stream format: changing it does not affect
stream compatibility, and it carries its own version byte (the `1` in the
magic) for future revisions.

## 12. Directory archive "SQAR" (CLI packaging)

Like §11, this section is **not** part of the compressed format. It describes
how the CLI packs a directory tree into a single byte stream so the ordinary
compressor can handle directories without knowing anything about files. The
CLI serializes the tree into an `SQAR` buffer, compresses that buffer as an
§1 stream, and on decompression checks whether the decompressed bytes begin
with the `SQAR` magic: if so it unpacks a tree, otherwise the bytes are a
single file, written verbatim. Single-file compression never uses this
wrapper, so it stays byte-for-byte compatible with readers that predate it.

The buffer is a fixed header followed by a flat, pre-order list of entries;
all integers are little-endian:

```
offset size field
0 8 magic: 'S' 'Q' 'A' 'R' '0' '1' 0x0A 0x1A
8 4 version u32 (currently 1)
12 4 flags u32 (0)
16 8 entry count u64
24 ... entries
```

Each entry:

```
offset size field
0 1 type: 0 = regular file, 1 = directory
1 4 mode u32: unix permission bits (low 9); informational,
synthesized as 0644/0755 by producers without them
5 4 path length P u32 (1 .. 65535)
9 8 data length D u64 (0 for a directory)
17 P path: relative, '/'-separated, UTF-8, no terminator
17 + P D file contents (regular files only; absent for a directory)
```

- Paths are relative to the archived directory and use `/` separators.
Directories are emitted before their contents so empty directories are
preserved. Sibling entries are stored sorted by name, so the archive
depends only on the tree, not on directory iteration order.
- A reader must reject any entry whose path is absolute, contains an empty,
`.`, or `..` component, or contains `\` or `:` — this is what stops an
archive from writing outside the extraction root. It must also verify that
every length field stays within the buffer and that the entries tile it
exactly; a violation is a format error.
- There is no archive-level checksum: the enclosing §1 stream already
carries and verifies one over these exact bytes.
- Because the archive is just the payload of an ordinary stream, `squish s`
(§11) packs a directory with no change to the SFX wrapper — the stub
decompresses the payload and, finding the `SQAR` magic, unpacks a tree
instead of writing one file.
Loading
Loading