# Development log

What went wrong and what was learned, in the order it happened. Every item was found by running a real tool, not by reading
a specification; the fix is in the code and the check that would catch it again is in `tools/oracle/`.

## Packages (October 18–20)

* **`dpkg -i` does not read PAX headers.** The tar writer used PAX extended headers for long names, which `dpkg-deb --contents`
  accepts, but `dpkg -i` on Ubuntu 22.04 stops with `unsupported PAX tar header type 'x'`. Debs now use GNU `L`/`K` long-name
  entries (what `dpkg-deb` itself writes); the tar package has an option for each style.
* **rpm headers are sorted, aligned and have a region.** The index must be sorted by tag, 2- and 4-byte values aligned in the
  data area, and the "immutable region" trailer must count the region entry itself. A hand-computed byte-for-byte test of a
  three-tag header (in `rpm/header_test.mbt`) is the guard.
* **`rpmbuild` leaves out `PAYLOADCOMPRESSOR` for an uncompressed payload.** Writing `identity` there is not accepted;
  the reader now recognises the payload from its bytes.
* **xz written by `lzma-mbt` was larger than gzip** (265 KB against 230 KB on 757 KB of source). An independent review
  found `moonxz` compressing to 210 KB; the encoder was switched, the decoder stayed. The README table has real numbers and
  says plainly that the pure-MoonBit zstd encoder is weak (about 2.4 times gzip on text).
* **apk checksums cover symbolic links too.** The first apk had no `APK-TOOLS.checksum.SHA1` on links; `apk add` warned that
  packages without embedded checksums will be dropped, and `apk audit` flagged the link. The checksum of a link is the SHA-1 of
  its target text. Found by comparing with a real busybox package.
* **A gzip bomb must stop while decoding.** Unpacking first and checking afterwards runs out of memory; the limit is applied
  per chunk, and for xz and zstd the unpacked size is read from the block and frame headers before decoding.

## Language and toolchain

* **`String::replace` replaces the first match only**, which silently broke XML unescaping and the apk `~` to `_` version
  mapping; `replace_all` is the one to use.
* **String comparison looks at the length first.** `"alpha-1.0.apk" < "beta.apk"` is false because the first is longer, so
  `sort()` is no alphabetical order. Repositories use their own byte-wise comparison (`text_compare`); the same applies to
  `Bytes`.
* **Reserved words** (`member`, `package`, `override`, `final`) kept turning up as variable names; the warning is an error under
  `--deny-warn`.
* **A newer toolchain prints `Int32` in `.mbti`** where an older one printed `Int`; CI regenerates the interface files and
  fails on any difference, so formatting and `moon info` run with the same `moon` as CI.
* **Shell quoting loses backslashes.** Writing Python or MoonBit with `\x00` or `\n` through a here-document silently turned
  them into real NUL bytes and newlines (once into `tools/oracle/run.sh`, which Git then treated as binary). Files with
  escapes are written with the editor tools, and CI scans for NUL bytes.

## Signing (October 21–22)

* **`moonpgp` and `flate` disagree about a function name.** `moonpgp` 0.1.0 needs `flate` 0.8 (`zlib_compress`), this package
  uses 0.9 (`compress`), so its `api` and `compress` packages do not build here. Its low-level packages (`packet`, `pki`,
  `armor`, `primitives`) do not use `flate`, so the `sign` package imports only those and assembles the version 4 signatures
  itself. That also fixed a second problem: `moonpgp` stamps signatures with the clock, and ours must be reproducible.
* **`gpgv` refuses a signature that is older than its key** ("Time conflict"). With `SOURCE_DATE_EPOCH=0` the package build
  time is 1970, so the signature time is the larger of the build time and the key's creation time.
* **An rpm signature covers the main header only.** Tag 268 (RSA) or 267 (EdDSA) in the signature header, signature type
  `0x00`; `rpm -K` then shows `Header V4 RSA/SHA256 Signature ... OK`. The payload is protected by the digest inside the
  header. Checked against `rpmsign`'s own output.
* **`abuild-sign` signs the whole file it is given.** For a package, abuild calls it on `control.tar.gz` only and
  concatenates; for an index it signs the full `APKINDEX.tar.gz`. A first experiment signing a complete `.apk` verified over
  control plus data, which is not what `apk add` checks.
* **`apk` takes a package from the directory of its own architecture.** A `noarch` package listed in `x86_64/APKINDEX.tar.gz`
  is fetched from `noarch/<file>.apk`; found with `strace`, after `package mentioned in index not found`.
* **`repo-add` writes less than documented.** No `%MD5SUM%` any more, the `files` list ends with a single newline, and
  `apk index` writes empty `T:`, `U:` and `L:` lines. The containers job compares our databases with the real tools' output
  byte for byte.

## ELF detection (October 22–23)

* **The ELF library on mooncakes cannot be installed.** `moonbit-community/elf@0.0.1` is published with a legacy `moon.mod.json`,
  which the current `moon` refuses ("is no longer supported"), so the evaluation ended before any code was read. The reader in
  this project is about 300 lines and has no dependency.
* **`DT_STRTAB` is an address, not an offset.** The string table address of the dynamic section has to go through the `PT_LOAD`
  segments to become a file offset, as the dynamic linker does; the nine cross-compiled architectures and `/bin/ls` all pass because
  of that step.
* **Reproducing a download problem found a workflow bug.** The Release of 0.4.0 had a damaged Alpine package: both build jobs
  wrote `linuxpkg-0.4.0-r1.apk` and `merge-multiple` mixed the two files. Python's `zlib` said `invalid stored block lengths` while
  the same input built on four backends was valid, which is what pointed away from the compressor.
* **`apt-get` with a temporary source list.** Installing cross compilers in WSL failed because the configured mirror answered
  403; `apt-get -o Dir::Etc::sourcelist=...` pointing at `archive.ubuntu.com` fixed it without touching the system configuration.

## nFPM configurations (October 22)

* **YAML 1.2 turns `1.10` into 1.1 and `0755` into 755.** nFPM is written in Go, where `version: 1.10` stays the text it was and
  `mode: 0755` is octal. The YAML reader here (YAML 1.2, no raw text for numbers) cannot tell, so the values of a handful of keys
  are quoted before reading. All 38 acceptance configurations of nFPM then read exactly like PyYAML does, except these numbers.
* **The glob of nFPM is not the one in the manual.** `folder/**/*` with the destination `/x/` produced only the files of the sub
  directories in real nFPM: its glob library needs at least one directory for `**/`. The differential run against the real
  binary found it, then a trailing slash on a plain file's `dst` (the file goes *into* that directory), then `config|noreplace`.
* **nFPM writes symbolic links with mode 0** (`l---------` in `dpkg-deb -c` and `rpm -qlv`); the first diff of the symlink
  configuration looked like a linuxpkg bug until `dpkg-deb` of a real dpkg-built package showed 0777.
* **Sorting strings is not alphabetical.** The directory listing of the glob came out in the wrong order because `Array::sort`
  on `String` compares lengths first (the same trap as in the repository indexes); the converter has its own comparison.

## Where the time went

Most of the work was the loop "write it, give it to the official tool, read the complaint". That loop is now a script
(`tools/oracle/run.sh`, about 200 checks) and a container job (six distributions), which is why the later parts of the
project went faster than the earlier ones.
