# ttf-tlwg-verify.awk -- turn one find, one sha256sum, one fc-scan, one charset # digest pass, one fc-list, one fc-list :lang=th, one fc-conflist, thirteen # fc-match calls, three generic fc-match calls and dpkg's own record into the # payload, packaging, fontconfig-derivation, matching and generic-handover cases. # # WHY ONE AWK AND NOT A SHELL LOOP. On this arm a process's memory is not # returned when it exits and /bin/sh is a 16 MB wasm module, so a suite's cost is # dominated by how many programs it starts. 58 faces x several assertions each is # over 300 cases; every one of them below is derived from ten files the .test has # already produced, with no further exec at all. # # INPUTS, in ARGV order: # REF reference manifest, " " -- 58 faces + 16 fragments # SUMS `sha256sum` over the installed payload, " " # INV `find ... -printf '%p %y %s %m %l\n'` over the shared truetype # directory, /etc/fonts/conf.d, and the co-tenant /usr/share/fonts/ttf # FACES native fontconfig control, "||...|" # SCAN this image's own fc-scan, same format minus the charset field # CSSUMS `sha256sum` over one file per face holding that face's charset # LIST `fc-list : file family` # LANGTH `fc-list :lang=th file` # MATCH "\t|" per family # GENERIC "\t" for serif, sans-serif, monospace # CONF `fc-conflist` # DPKG `grep -H .` over /var/lib/dpkg/info/ttf-tlwg.list function emit(id, ok, detail) { if (ok) printf "BST ok %s/%s\n", S, id else printf "BST not ok %s/%s : %s\n", S, id, substr(detail, 1, 100) } function base(p) { sub(/^.*\//, "", p); return p } function cid(b) { gsub(/[^A-Za-z0-9]+/, "-", b); sub(/-+$/, "", b); return b } BEGIN { PROCINFO["sorted_in"] = "@ind_str_asc" TTFDIR = "/usr/share/fonts/truetype" # SHARED -- this port is its fifth tenant CONFDIR = "/etc/fonts/conf.d" # SHARED -- fontconfig's own fragment dir COT1 = "/usr/share/fonts/ttf" # 12 Liberation + 10 Vera; untouched here # Foreign faces whose charset genuinely covers Thai: xorg-minimal-fonts' 6x13 # carries the Thai block. Any face outside this list still reds the two cases. KNOWN_TH["/usr/share/fonts/X11/misc/6x13.pcf.gz"] = 1 # WHAT THE PAYLOAD IS, AS NUMBERS. Read off the built package's own FILES_INFO # (74 entries, PKGSIZE 5,938,462) and cross-checked against the tarball, not # predicted from the recipe's FILES globs. WANT_TTF = 58 WANT_CONF = 16 WANT_TTF_B = 5932464 WANT_CNF_B = 5998 WANT_FAM = 13 WANT_MONO = 8 # faces carrying spacing=100; see the fcfaces header # THE SLANT DISTRIBUTION, PINNED IN CODE RATHER THAN ONLY IN THE CONTROL # TABLE. This collection is the first on this image to ship BOTH italic and # oblique cuts of the same families -- Kinnari-Italic beside Kinnari-Oblique, # and the same for Laksaman and Norasi -- so 100 (FC_SLANT_ITALIC) and 110 # (FC_SLANT_OBLIQUE) both appear, where every sibling font port has seen only # one of them. A mutation that "corrects" 110 to 100 in the control table AND # in the image goes green against the field-by-field fcq- comparison, # because both sides then agree; these three constants are the input only they # cover. Found by exactly that mutation (M25), which was green before they # were added. WANT_SLANT[0] = 27 # roman WANT_SLANT[100] = 6 # italic WANT_SLANT[110] = 25 # oblique # THE CO-TENANTS OF THE SHARED truetype DIRECTORY, AS A FLOOR. 26 files -- # 1 Inconsolata + 4 Gentium + 7 Droid + 14 Source Code Pro -- were there before # this port. The EXACT total belongs to 99-cantarell-fonts' WANT_COT2, which # this change moves 26 -> 84; a second exact copy here would go red on every # neighbouring font port and have to be recomposed by a ticket that did not # move it. What this suite owes is that its own port displaced none of them, # and cotenant-truetype-others says exactly that by counting the files in that # directory whose basename is NOT one of ours. WANT_COT_OTHERS = 26 WANT_COT1 = 22 # 3 regular files (30-liberation-aliases, 44-source-code-pro-fonts-fontconfig, # README) + 21 symlinks into conf.avail. A FLOOR, because fontconfig's own # conf.d set is not this port's to pin. WANT_CONF_OTHERS = 24 # THE GENERICS, AND THE ONE FINDING OF THIS PORT. # # Installing ttf-tlwg TAKES `serif` AND `sans-serif` IMAGE-WIDE, and leaves # `monospace` alone. That is not a guess about fontconfig's precedence rules; # it was measured at fontconfig 2.15.0 against this image's own /etc/fonts and # font set, with and without this package, before a line of this suite was # written. The mechanism is that PREPENDS its family ahead of the # matched generic while APPENDS it after: # # serif 30-liberation-aliases.conf s Liberation Serif, so it # sits AFTER the generic; 64-01-tlwg-kinnari.conf s # Kinnari, which goes in front of it. -> Kinnari # sans-serif same shape, five tlwg fragments, and the FIRST loaded wins # because each later one prepends ahead of the generic and # behind its predecessor. -> Waree # monospace 44-source-code-pro-fonts-fontconfig.conf ALSO s, and # it loads at 44, before the tlwg fragments at 64. Source Code # Pro therefore keeps the generic. -> unchanged # # THE THIRD ROW IS THE ONE WORTH HAVING. Two of the three moved and one did # not, from three fragments of the same shape, so an assertion set that only # pinned the two that moved would leave the interesting case unstated -- and # a future fragment landing between 44 and 64 would take monospace with # nothing going red. # # 99-liberation-fonts and 99-ttf-gentium assert the same three facts from the # other side and are amended in this change, so a disagreement between the # three suites is visible rather than silent. # ttf-dejavu has since landed: 60-latin.conf s DejaVu and loads # before the tlwg fragments at 64, so it takes serif and sans-serif back. WANT_GENERIC["serif"] = "DejaVu Serif" WANT_GENERIC["sans-serif"] = "DejaVu Sans" WANT_GENERIC["monospace"] = "Source Code Pro" NFILE = 0 } FNR == 1 { NFILE++ } /^[ \t]*#/ { next } # ---- 1. REF: the reference manifest -------------------------------------------- NFILE == 1 && NF == 3 { want_size[$3] = $1; want_sum[$3] = $2; nref++ if (index($3, TTFDIR "/") == 1) { ours_ttf[base($3)] = 1; nref_ttf++ } else { ours_conf[base($3)] = 1; nref_conf++ } next } # ---- 2. SUMS: sha256sum over the installed payload ------------------------------ NFILE == 2 && NF >= 2 { p = $2; sub(/^\*/, "", p); got_sum[p] = $1; next } # ---- 3. INV: one find over three directories ------------------------------------ NFILE == 3 && NF >= 4 { p = $1; inv_type[p] = $2; inv_size[p] = $3; inv_mode[p] = $4 d = p; sub(/\/[^\/]*$/, "", d); b = base(p) # THE FRAGMENT DIRECTORY IS COUNTED BY ENTRY, NOT BY TYPE, AND THAT IS THE # WHOLE OF WHY. 21 of the 24 fragments this image had before us are SYMLINKS # into /usr/share/fontconfig/conf.avail -- fontconfig's own packaging -- and # only 3 are regular files. A count restricted to `f`, which is right for the # font directory, sees 3 and reads as 21 missing neighbours. Measured on the # built image: 40 entries, 19 f + 21 l, of which 16 f are ours. if (d == CONFDIR && inv_type[p] != "d") { if (b in ours_conf) { n_conf++; b_conf += inv_size[p] } else if (b ~ /tlwg|laksaman/) conf_extra = conf_extra " " b else n_conf_others++ } if (inv_type[p] != "f") next if (d == TTFDIR) { if (b in ours_ttf) { n_ttf++; b_ttf += inv_size[p] } else if (b ~ /^(Garuda|Kinnari|Laksaman|Loma|Norasi|Purisa|Sawasdee|Tlwg|Umpush|Waree)/) ttf_extra = ttf_extra " " b else n_cot_others++ } if (d == COT1) n_cot1++ next } # ---- 4. FACES: the native fontconfig control ------------------------------------ NFILE == 4 { n = split($0, f, "|"); if (n < 11) next ctl[f[1]] = f[2] "|" f[3] "|" f[4] "|" f[5] "|" f[6] "|" f[7] "|" f[8] "|" f[9] "|" f[10] ctl_cs[f[1]] = f[11] fam_of[f[1]] = f[2] families[f[2]]++ if (f[6] == "100") nctl_mono++ nslant[f[4] + 0]++ key = f[2] "|" f[3] if (key in seen_style) dup_style = dup_style " " f[1] seen_style[key] = f[1] nctl++ next } # ---- 5. SCAN: this image's own fc-scan ------------------------------------------ NFILE == 5 { n = split($0, f, "|"); if (n < 10) next scan[f[1]] = f[2] "|" f[3] "|" f[4] "|" f[5] "|" f[6] "|" f[7] "|" f[8] "|" f[9] "|" f[10] nscan++ next } # ---- 6. CSSUMS: per-face charset digests ---------------------------------------- NFILE == 6 && NF >= 2 { b = base($2); sub(/\.cs$/, "", b); got_cs[b] = $1; next } # ---- 7. LIST: fc-list : file family --------------------------------------------- NFILE == 7 { line = $0; sub(/^[ \t]+/, "", line) p = line; sub(/:.*$/, "", p) fam = line; sub(/^[^:]*:[ \t]*/, "", fam) listed_fam[p] = fam nf = split(fam, ff, ",") for (i = 1; i <= nf; i++) { g = ff[i]; gsub(/^[ \t]+|[ \t]+$/, "", g); if (g != "") seen_family[g] = seen_family[g] " " p } next } # ---- 8. LANGTH: fc-list :lang=th file ------------------------------------------- NFILE == 8 { p = $0; sub(/^[ \t]+/, "", p); sub(/:[ \t]*$/, "", p); sub(/:.*$/, "", p) if (p == "" || (p in KNOWN_TH)) next th[p] = 1; n_th++ if (!(index(p, TTFDIR "/") == 1 && (base(p) in ours_ttf))) th_foreign = th_foreign " " p next } # ---- 9. MATCH: fc-match per family ---------------------------------------------- NFILE == 9 { n = split($0, f, "\t"); if (n >= 2) match_of[f[1]] = f[2]; next } # ---- 10. GENERIC: fc-match per generic ------------------------------------------ NFILE == 10 { n = split($0, f, "\t"); if (n >= 2) generic_of[f[1]] = f[2]; next } # ---- 11. CONF: fc-conflist ------------------------------------------------------ # A `+` line is a file fontconfig LOADED; a `-` line is one merely available under # conf.avail. THE LINE IS `+ : `, MEASURED RATHER THAN # ASSUMED -- fc-conflist 2.15.0 prints # # + /etc/fonts/conf.d/30-liberation-aliases.conf: No description # # so the path is the first whitespace-delimited field AND IT CARRIES A TRAILING # COLON. Two runs of this suite reddened all seventeen conf-loaded cases here: # the first took the whole remainder of the line, the second took the first field # and kept the colon. Neither is visible from the neighbouring suites, which match # their one path INSIDE the line (`case "$l" in "+ "*"$CONF"*`) and so are immune # to both. conf-ran below exists because of this: without it the seventeen reds # read as "the fragments are not loaded" when nothing had been parsed at all. NFILE == 11 { if (conf_first == "") conf_first = substr($0, 1, 90) nconf_lines++ if ($0 !~ /^\+/) next n_plus++ p = $0; sub(/^\+[ \t]*/, "", p); sub(/[ \t].*$/, "", p); sub(/:$/, "", p) if (index(p, CONFDIR "/") == 1 && (base(p) in ours_conf)) { loaded[base(p)] = 1; n_loaded++ } next } # ---- 12. DPKG: grep -H . over ttf-tlwg.list -------------------------------------- # The paths are only COLLECTED here. A .list carries the package's directories as # well as its files, and which directories those are is a fact about the manifest, # not something to spell out in a regex -- so the classification happens in END, # against the directory prefixes the manifest itself implies. NFILE == 12 { i = index($0, ":"); if (i == 0) next path = substr($0, i + 1); gsub(/^[ \t]+|[ \t]+$/, "", path) if (path == "") next sub(/\/$/, "", path); if (path == "" || path == "/.") next # dpkg's root entry dpkg_seen = 1 dpkg_path[path] = 1 next } END { # ------------------------------------------------------------------ payload emit("ref-rows", nref == WANT_TTF + WANT_CONF, "the manifest carries " nref " rows, want " WANT_TTF + WANT_CONF) emit("ref-split", nref_ttf == WANT_TTF && nref_conf == WANT_CONF, "the manifest carries " nref_ttf " faces and " nref_conf " fragments, want " WANT_TTF " and " WANT_CONF) # Every declared file, by type, size, mode and content. The file list is pinned # EXACTLY, which is what the ticket asks for: a partial payload fails here. for (p in want_size) { b = base(p) if (!(p in inv_type)) { emit("payload-" cid(b), 0, p " is absent"); continue } if (inv_type[p] != "f") { emit("payload-" cid(b), 0, p " is type " inv_type[p] ", want f"); continue } if (inv_size[p] != want_size[p]) { emit("payload-" cid(b), 0, p " is " inv_size[p] " bytes, want " want_size[p]); continue } if (inv_mode[p] != "644") { emit("payload-" cid(b), 0, p " is mode " inv_mode[p] ", want 644"); continue } if (!(p in got_sum)) { emit("payload-" cid(b), 0, p " was not digested"); continue } emit("payload-" cid(b), got_sum[p] == want_sum[p], p " sha256 " got_sum[p] ", want " want_sum[p]) } emit("ttf-count", n_ttf == WANT_TTF, TTFDIR " holds " n_ttf " of our faces, want " WANT_TTF) emit("conf-count", n_conf == WANT_CONF, CONFDIR " holds " n_conf " of our fragments, want " WANT_CONF) emit("ttf-total-bytes", b_ttf == WANT_TTF_B, "our faces total " b_ttf " bytes, want " WANT_TTF_B) emit("conf-total-bytes", b_conf == WANT_CNF_B, "our fragments total " b_conf " bytes, want " WANT_CNF_B) # THE OTHER DIRECTION: a file this recipe installed that the manifest does not # declare. Both directories are SHARED, so this is scoped by the thirteen family # prefixes and by the fragment naming rather than by the whole directory -- a # neighbour's new face is not this port's extra file. Without these two, an # extra would only shift a co-tenant count, and the case that moved would name # the wrong thing. emit("ttf-no-extras", ttf_extra == "", TTFDIR " holds tlwg-named faces with no manifest row:" ttf_extra) emit("conf-no-extras", conf_extra == "", CONFDIR " holds tlwg-named fragments with no manifest row:" conf_extra) # ------------------------------------------------------------- the co-tenants # THIS PORT SHARES BOTH ITS DIRECTORIES, so the guard that matters is that it # ADDED to them and displaced nothing. Counting the files that are NOT ours # says that without pinning a total every future font port would have to move. # ASSERTED AS A FLOOR (>=), NOT AN EQUALITY, SINCE BRINTOS-1729. Every one of # these three suites already SAID floor in prose -- the property owed is that # this port DISPLACED no neighbour, and displacement makes the count DROP -- # but all three emitted `==`, which also fires when a neighbouring port ADDS # a face. That is the "rot rather than coverage" this file's own note above # rejects, and it is why ttf-abyssinica (want 26, saw 136) and ttf-hunkyfonts # (want 72, saw 131) were BOTH already red on main before this port existed: # each pinned an exact whole-directory count at its own Epic pin and no later # ticket recomposed it. `>=` is what the two sibling cases in this same file # (cotenant-ttf, cotenant-confd-others) already use, and it needs no future # font port to revisit the constant. Live count of the merged image, measured # 2026-08-31 on the staged rootfs: TTFDIR holds 137 files. emit("cotenant-truetype-others", n_cot_others >= WANT_COT_OTHERS, TTFDIR " holds " n_cot_others " files that are not ours, want at least " WANT_COT_OTHERS \ " (1 Inconsolata + 4 Gentium + 7 Droid + 14 Source Code Pro)") emit("cotenant-ttf", n_cot1 >= WANT_COT1, COT1 " holds " n_cot1 " files, want at least " WANT_COT1 " (12 Liberation + 10 Vera)") emit("cotenant-confd-others", n_conf_others >= WANT_CONF_OTHERS, CONFDIR " holds " n_conf_others " fragments that are not ours, want at least " WANT_CONF_OTHERS) # -------------------------------------------------- fontconfig derivation emit("fcq-rows", nctl == WANT_TTF, "the face control carries " nctl " rows, want " WANT_TTF) emit("fcq-ran", nscan == WANT_TTF, "fc-scan produced " nscan " rows, want " WANT_TTF \ " -- an empty or short parse would otherwise green every comparison below") for (b in ctl) { if (!(b in scan)) { emit("fcq-" cid(b), 0, b " was not scanned by fc-scan"); continue } emit("fcq-" cid(b), scan[b] == ctl[b], b " scans '" scan[b] "', control '" ctl[b] "'") } for (b in ctl) { if (!(b in got_cs)) { emit("fcq-charset-" cid(b), 0, b " produced no charset digest"); continue } emit("fcq-charset-" cid(b), got_cs[b] == ctl_cs[b], b " charset sha256 " got_cs[b] ", control " ctl_cs[b]) } nfam = 0 for (fam in families) nfam++ emit("fcq-families", nfam == WANT_FAM, "the control declares " nfam " families, want " WANT_FAM) # PER FAMILY, FROM fc-list -- i.e. from the cache, not from the control the # loop above already compared field by field. A family losing a face to a # packaging or cache change reds here and nowhere else. for (fam in families) { nlisted = 0 nn = split(seen_family[fam], pp, " ") for (i = 1; i <= nn; i++) if (pp[i] != "") nlisted++ emit("fcq-family-" cid(fam), nlisted == families[fam], "fc-list reports " nlisted " faces under family '" fam "', want " families[fam]) } emit("fcq-styles-unique", dup_style == "", "these faces share a family/style pair with another:" dup_style) emit("fcq-mono-count", nctl_mono == WANT_MONO, nctl_mono " faces carry spacing=100, want " WANT_MONO) for (sl in WANT_SLANT) emit("fcq-slant-" sl, nslant[sl + 0] == WANT_SLANT[sl], nslant[sl + 0] " faces carry slant=" sl ", want " WANT_SLANT[sl] \ " (0 roman, 100 italic, 110 oblique)") # ------------------------------------------------------------------ fc-list # Every installed face is enumerated by fc-list under its own family. This # reads the CACHE, so it is a different path from the fc-scan above. for (b in ctl) { p = TTFDIR "/" b if (!(p in listed_fam)) { emit("fc-list-" cid(b), 0, p " is not enumerated by fc-list"); continue } emit("fc-list-" cid(b), index(listed_fam[p], fam_of[b]) > 0, p " is listed as '" listed_fam[p] "', want family '" fam_of[b] "'") } # A family this release does NOT provide must not be listed. fc-match cannot # express non-resolution -- it always returns a best match -- so the ticket's # clause is narrowed to the property fc-list CAN answer, the same narrowing # BRINTOS-1723, -1724, -1725 and -1726 made. "Tlwg Typist" and "Tlwg Typo" # ARE provided, with a space; the spaceless spellings are not, and they are in # this set precisely because a suite deriving families from basenames would # have asserted them. ABS[1] = "TlwgTypist"; ABS[2] = "TlwgTypo"; ABS[3] = "Tlwg" ABS[4] = "Angsana New"; ABS[5] = "Purisa Light"; ABS[6] = "Umpush Thin" ABS[7] = "Norasi Thai"; ABS[8] = "Tlwg Mono" for (i = 1; i <= 8; i++) { fam = ABS[i] emit("fc-list-absent-" cid(fam), !(fam in seen_family), "fc-list reports '" seen_family[fam] "' under family '" fam "'; this release ships no such family") } # -------------------------------------------------------------- Thai coverage # THE ASSERTION THAT MAKES THE COLLECTION USEFUL RATHER THAN MERELY INSTALLED. # Asked through fontconfig's own language derivation rather than by grepping # the charset for a range: :lang=th is true exactly when the face covers the # Thai orthography fontconfig requires, which is the property the ticket names. emit("lang-th-count", n_th == WANT_TTF, "fc-list :lang=th reports " n_th " faces, want " WANT_TTF) emit("lang-th-only-ours", th_foreign == "", "fc-list :lang=th reports faces this package does not own:" th_foreign) # ------------------------------------------------------------------ fc-match # Each of the thirteen families resolves to a file from THIS package -- the # assertion that the font is usable and not merely present on disk -- and the # face it resolves to covers Thai, which is the ticket's "at least the # default-resolving face" clause. for (fam in families) { if (!(fam in match_of)) { emit("fc-match-" cid(fam), 0, "no fc-match was run for '" fam "'"); continue } got = match_of[fam]; file = got; sub(/\|.*$/, "", file); gotfam = got; sub(/^[^|]*\|/, "", gotfam) if (!(index(file, TTFDIR "/") == 1 && (base(file) in ours_ttf))) { emit("fc-match-" cid(fam), 0, "fc-match '" fam "' => '" got "', want a file this package installs") continue } emit("fc-match-" cid(fam), gotfam == fam, "fc-match '" fam "' resolved to family '" gotfam "', want '" fam "'") emit("lang-th-match-" cid(fam), (file in th), "fc-match '" fam "' resolved to " file ", which fc-list :lang=th does not report") } # ------------------------------------------------------------ the fragments # THE FRAGMENTS ARE LOADED, ASKED DIRECTLY RATHER THAN INFERRED FROM THE # GENERICS BELOW. serif answering Kinnari is consistent with the fragment # being loaded AND with fontconfig having reached the same answer some other # way; only fc-conflist distinguishes them, and if this case reds then the # handover below is an accident rather than a configuration this port controls. # DID THE INSTRUMENT RUN. Without this the seventeen cases below are a # vacuous red whenever fc-conflist prints something this parser does not # recognise, and the report says "the fragments are not loaded" when what # happened is that nothing was read. The detail carries the first line # verbatim, so one run says what the format actually is. emit("conf-ran", n_plus >= 1, "fc-conflist produced " nconf_lines " line(s) and " n_plus " '+' line(s); first line: <" conf_first ">") emit("conf-loaded-count", n_loaded == WANT_CONF, "fc-conflist loads " n_loaded " of our " WANT_CONF " fragments") for (b in ours_conf) emit("conf-loaded-" cid(b), (b in loaded), CONFDIR "/" b " is not in fc-conflist's loaded set") # -------------------------------------------------------------- the generics for (g in WANT_GENERIC) { if (!(g in generic_of)) { emit("generic-" g, 0, "no fc-match was run for '" g "'"); continue } emit("generic-" g, generic_of[g] == WANT_GENERIC[g], "fc-match " g " => '" generic_of[g] "', want '" WANT_GENERIC[g] "' -- see the BEGIN block") } # ------------------------------------------------------------------ ownership # dpkg's own record. FILES:${PN} is "${datadir}/fonts ${sysconfdir}" -- it # claims the WHOLE of /etc, not just the fragment directory -- so the question # this answers is not "did the glob reach another package" (it cannot) but # "what did this package end up owning". pkg-owns-extra is the case that # notices the day something else lands under /etc in this recipe's ${D}. emit("pkg-installed", dpkg_seen == 1, "dpkg has no file list for ttf-tlwg -- the package is not installed") for (p in want_size) { d = p; while (sub(/\/[^\/]*$/, "", d) && d != "") mdir[d] = 1 } own_ok = 0; own_extra = "" for (p in dpkg_path) { if (p in want_size) own_ok++ else if (!(p in mdir)) own_extra = own_extra " " p } emit("pkg-owns-count", own_ok == nref, "dpkg records " own_ok " of the " nref " manifest paths as owned by ttf-tlwg") emit("pkg-owns-extra", own_extra == "", "ttf-tlwg also owns paths outside its manifest:" own_extra) }