# source-code-pro-fonts-verify.awk -- turn one find, one sha256sum, the package's # own file list and one fc-scan into the payload, inventory, packaging-footprint # and fontconfig-derivation 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. Everything below is derived from four # files the .test already produced with four execs. # # INPUTS: # REF the reference manifest, " " -- FIFTEEN rows # SUMS `sha256sum` over the fifteen payload files, " " # INV `find ... -printf '%p %y %s %m %l\n'` over the payload font directory, # /etc/fonts/conf.d, and /usr/share/fonts/ttf (the other font directory) # PKGF /var/lib/dpkg/info/source-code-pro-fonts.list, one path per line # FACES the native fontconfig control, "||..." # SCAN this image's own fc-scan over the fourteen installed faces, same format # # FIFTEEN PATHS, AND THE FIFTEENTH IS THE WHOLE STORY OF THIS PORT. Every font # port on this image before it installed fonts and nothing else; this recipe also # installs /etc/fonts/conf.d/44-source-code-pro-fonts-fontconfig.conf, which is # what moves the `monospace` generic. It is asserted here by DIGEST, at the file # layer, so a fragment that arrived truncated or edited reds one row with a name # that says so -- rather than surfacing three layers down as an fc-match answer # nobody can localise. # # THERE IS NO DOC DIRECTORY IN THIS SUITE, AND THAT IS THE RECIPE. The zip's # LICENSE.txt is never installed: the recipe's do_install writes the .otf files # and the fragment, and FILES:${PN} is an ASSIGNMENT naming ${sysconfdir}/fonts # and ${datadir}/fonts only. So ${PN}-doc is empty and dpkg builds none, which # pkg-no-doc-package in the .test asserts. The licence is not lost -- name IDs # 13/14 inside each face carry the OFL notice and its URL. 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 } BEGIN { S = "source-code-pro-fonts" PROCINFO["sorted_in"] = "@ind_str_asc" TTFDIR = "/usr/share/fonts/truetype" CONFDIR = "/etc/fonts/conf.d" COTDIR = "/usr/share/fonts/ttf" CONF = CONFDIR "/44-source-code-pro-fonts-fontconfig.conf" # The 22 files liberation-fonts (12) and ttf-bitstream-vera (10) install into # COTDIR. This port never writes there; see the cotenant cases below. WANT_COT = 22 NFIELD = 11 # fields on an fcfaces/fc-scan line NFACE = 14 # font faces NREFROW = 15 # manifest rows: the fourteen faces plus the fragment # This package's own fourteen basenames. Everything scoped below is scoped # against THIS list rather than against "not ours" -- the distinction that # keeps the next Debian-layout font from reddening this suite, and the # mistake 99-ttf-inconsolata had to have repaired for it when ttf-gentium # moved in beside it. split("SourceCodePro-Black.otf SourceCodePro-BlackIt.otf SourceCodePro-Bold.otf " \ "SourceCodePro-BoldIt.otf SourceCodePro-ExtraLight.otf SourceCodePro-ExtraLightIt.otf " \ "SourceCodePro-It.otf SourceCodePro-Light.otf SourceCodePro-LightIt.otf " \ "SourceCodePro-Medium.otf SourceCodePro-MediumIt.otf SourceCodePro-Regular.otf " \ "SourceCodePro-Semibold.otf SourceCodePro-SemiboldIt.otf", _o, " ") for (i in _o) OURS[_o[i]] = 1 # THE FOUR RIBBI FACES, WHICH ARE THE ONES WHOSE NAME IDs 16/17 EQUAL 1/2. # fontconfig records both readings and comma-joins them, so these four carry # ONE family value and the other ten carry TWO. See fcq-family-typographic. split("SourceCodePro-Regular.otf SourceCodePro-Bold.otf SourceCodePro-It.otf " \ "SourceCodePro-BoldIt.otf", _r, " ") for (i in _r) RIBBI[_r[i]] = 1 # The twelve faces already in TTFDIR when this port arrived. Named, not # counted: a count would go stale the next time a font lands in this # directory, and naming them is what says WHICH neighbour a displacement # took. ttf-inconsolata (1), ttf-gentium (4), ttf-droid (7). split("Inconsolata.ttf GenAI102.ttf GenAR102.ttf GenI102.ttf GenR102.ttf " \ "DroidSans-Bold.ttf DroidSans.ttf DroidSansMono.ttf DroidSerif-Bold.ttf " \ "DroidSerif-BoldItalic.ttf DroidSerif-Italic.ttf DroidSerif-Regular.ttf", _c, " ") for (i in _c) COTEN[_c[i]] = 1 } FILENAME == REF && /^#/ { next } FILENAME == REF && NF == 3 { WSIZE[$3] = $1; WSUM[$3] = $2; NREF++; next } FILENAME == SUMS && NF >= 2 { p = $2; sub(/^\*/, "", p); GSUM[p] = $1; next } FILENAME == INV && NF >= 4 { ITYPE[$1] = $2; ISIZE[$1] = $3; IMODE[$1] = $4; next } FILENAME == PKGF && NF >= 1 { PKG[$0] = 1; NPKG++; next } FILENAME == FACES && /^#/ { next } FILENAME == FACES && NF >= 1 { split($0, a, "|"); WFC[a[1]] = $0; NFC++; next } FILENAME == SCAN && NF >= 1 { split($0, a, "|"); GFC[a[1]] = $0; next } END { # THE INPUTS WERE REALLY READ. Every case below is a comparison, and a # comparison over two empty tables is vacuously true -- the failure that # reads as a full green. emit("ref-count", NREF == NREFROW, "the manifest carried " NREF " entries, want " NREFROW) emit("faces-count", NFC == NFACE, "the fcfaces control carried " NFC " lines, want " NFACE) # ------------------------------------------------------------ per file # Size, bytes, type and mode in one case per file. %y catches a symlink where # a regular file is meant and %m the mode, neither of which `test -f` sees; # the digest catches a file of the right length that is not the right file. # # MODE 644 IS A REAL ASSERTION HERE, NOT BOILERPLATE. The .otf files are 0644 # inside the zip already, but LICENSE.txt beside them is 0755 -- so the # archive is NOT uniformly moded and it is the recipe's `install -m 0644` # that guarantees these. A do_install that stopped passing -m would show up # on these rows. for (p in WSIZE) { id = "file-" base(p) if (!(p in ITYPE)) { emit(id, 0, p " is absent"); continue } if (ITYPE[p] != "f") { emit(id, 0, p " is type '" ITYPE[p] "', want a regular file"); continue } if (ISIZE[p] != WSIZE[p]) { emit(id, 0, p " is " ISIZE[p] " bytes, want " WSIZE[p]); continue } if (IMODE[p] != "644") { emit(id, 0, p " is mode " IMODE[p] ", want 644"); continue } if (!(p in GSUM)) { emit(id, 0, p " was not digested"); continue } if (GSUM[p] != WSUM[p]) { emit(id, 0, p " sha256 " substr(GSUM[p],1,16) "..., want " substr(WSUM[p],1,16) "..."); continue } emit(id, 1) } # ------------------------------------------------- the payload directory # THE DIRECTORY IS SHARED FOUR WAYS NOW -- Inconsolata, the four Gentium # faces, the seven Droid ones, and these fourteen. So this case is scoped to # OUR OWN basename shape: a suite is entitled to say its own payload is # intact and nothing more, and the co-tenant census belongs to whoever is # counting. # # THE SHAPE IS `SourceCodePro-*.otf`, WHICH IS ALSO WHAT THE zip'S TTF/ AND # WOFF/ DIRECTORIES WOULD NOT MATCH. The recipe's do_install replaces # ttf.inc's `*.tt[cf]` glob with an `*.otf` one; a revision that restored the # inc's version would install fourteen SourceCodePro-*.ttf beside these, and # that is a different basename shape which no case here would see. So this # case is deliberately anchored on the extension too. extra = "" for (p in ITYPE) { if (ITYPE[p] != "f") continue if (p !~ ("^" TTFDIR "/")) continue b = base(p) if (b in OURS) continue if (b ~ /^SourceCodePro.*\.(otf|ttf)$/) extra = extra " " b } emit("ttf-no-extras", extra == "", TTFDIR " also holds SourceCodePro-shaped file(s):" extra) # ...AND THE FRAGMENT DIRECTORY THE SAME WAY. /etc/fonts/conf.d is owned by # the fontconfig package, which fills it with symlinks into conf.avail; this # port adds ONE REGULAR FILE to it. A second fragment of ours -- a recipe # revision, a stale copy from an earlier build -- would silently double the # alias rules, and outranking is decided by filename order, so which one won # would depend on the name. cextra = "" for (p in ITYPE) { if (p !~ ("^" CONFDIR "/")) continue b = base(p) if (p == CONF) continue if (b ~ /source-code-pro/) cextra = cextra " " b } emit("conf-no-extras", cextra == "", CONFDIR " also holds source-code-pro fragment(s):" cextra) # THE FRAGMENT IS A REGULAR FILE, NOT A SYMLINK. Every other entry in # conf.d on this image is a symlink into /usr/share/fontconfig/conf.avail, # placed there by the fontconfig package. This one is installed directly by # the recipe, so `%y` reading `l` here would mean something quite different # had happened -- and file-44-... above would still be green, because it # follows the link. emit("conf-regular-file", (CONF in ITYPE) && ITYPE[CONF] == "f", CONF " is type '" ((CONF in ITYPE) ? ITYPE[CONF] : "absent") "', want a regular file") # ------------------------------------------------------- the co-tenants # THIS PORT MUST ADD TO ITS DIRECTORY, NOT REPLACE IT. The recipe's # do_install is a `find -exec install` into a shared directory, so the # failure this guards is real rather than theoretical: a do_install that # cleaned its target first, or a packaging change that made one package claim # the whole directory, would take three neighbours with it and every file-* # case above would stay green. # # NAMED, NOT COUNTED. All three neighbours assert their own payload from the # other side, so a disagreement between the suites is visible rather than # silent -- and a name says WHICH package was displaced where a total only # says that one was. miss = "" for (b in COTEN) if (!((TTFDIR "/" b) in ITYPE)) miss = miss " " b emit("cotenant-truetype", miss == "", TTFDIR " is missing co-tenant face(s):" miss " -- this port displaced a neighbour") # AND THE OTHER FONT DIRECTORY IS UNTOUCHED. The 22 files liberation-fonts # and ttf-bitstream-vera own live in a directory this port never writes to, # so this is the case that says a fontconfig or packaging change did not move # the fonts that were already here. cot = 0 for (p in ITYPE) if (p ~ ("^" COTDIR "/") && ITYPE[p] == "f") cot++ emit("cotenant-ttfdir", cot == WANT_COT, COTDIR " holds " cot " files, want " WANT_COT " (12 Liberation + 10 Vera)") # ------------------------------------------- the package's real footprint # THE FILE LIST IS THE ONLY PLACE THE PACKAGE'S OWN CLAIM IS VISIBLE. The # file-* cases would look identical if some other package had installed the # same bytes; this says source-code-pro-fonts claims exactly the fifteen # paths the manifest names and nothing else. FILES:${PN} is # `${sysconfdir}/fonts ${datadir}/fonts` -- two DIRECTORY trees, not a file # list -- so a recipe or class that dropped anything else under either would # be swept into this package silently. That is what this case refuses. # # dpkg's list carries the parent directories too, so only the regular-file # entries are compared; the directories are the packaging's business. emit("pkgfiles-loaded", NPKG > 0, "the package file list was empty or absent") pmiss = ""; pextra = "" for (p in WSIZE) if (!(p in PKG)) pmiss = pmiss " " p for (p in PKG) { if (p in WSIZE) continue if ((p in ITYPE) && ITYPE[p] != "f") continue # a directory entry if (!(p in ITYPE)) continue # not walked by our find pextra = pextra " " p } emit("pkgfiles-exact", pmiss == "" && pextra == "", "package claims differ from the manifest -- missing:" pmiss " extra:" pextra) # ------------------------------------- fontconfig's own reading of the faces # FIELD BY FIELD AGAINST THE NATIVE CONTROL, not as one string compare: a # whole-line case would name the file and nothing else, and these fields fail # for different reasons. See the control file's header for what each is. split("file family style slant weight spacing postscriptname fontformat scalable outline charset", FN, " ") for (b in WFC) { if (!(b in GFC)) { emit("fcq-" b, 0, "fc-scan produced no line for " b); continue } nw = split(WFC[b], w, "|"); ng = split(GFC[b], g, "|") if (nw != NFIELD || ng != NFIELD) { emit("fcq-" b, 0, "field count " ng ", control " nw ", want " NFIELD); continue } bad = "" for (i = 2; i <= NFIELD; i++) if (w[i] != g[i]) bad = bad " " FN[i] "='" substr(g[i],1,24) "'(want '" substr(w[i],1,24) "')" emit("fcq-" b, bad == "", b ":" bad) } # THE THREE DERIVATIONS THIS PORT TURNS ON, NAMED SEPARATELY. fcq-* above # already rejects any input these reject, so on their own they would be the # subsumed-guard mistake -- they are here because each localises a DIFFERENT # downstream consequence and a reviewer reading a red needs to know which. # # Keyed on the LITERAL basenames rather than on `for (b in GFC)`: an empty # control table would make the loop body unreachable and silently drop these # cases, which is the vacuous-green shape the count guards above refuse. nsp = 0; nfmt = 0; ntyp = 0; spbad = ""; fmtbad = ""; typbad = "" for (i in _o) { b = _o[i] if (!(b in GFC)) { spbad = spbad " " b "=(no fc-scan line)" fmtbad = fmtbad " " b "=(no fc-scan line)" typbad = typbad " " b "=(no fc-scan line)"; continue } split(GFC[b], g, "|") # SPACING IS 100 ON ALL FOURTEEN, WHICH IS WHAT MAKES THIS PORT ABLE TO # ANSWER `monospace` AT ALL. 99-ttf-droid's equivalent splits 6-and-1 and # 99-ttf-gentium's is unset on all four; this one is the first where # every face carries FC_MONO. It is the precondition for the generic # handover the .test's fc-match-monospace case asserts -- fontconfig # would not put a proportional face at the head of monospace's list # however loudly a fragment preferred it. if (g[6] == "100") nsp++; else spbad = spbad " " b "='" g[6] "'" # fontformat=CFF is the counterpart of 99-ttf-droid's TrueType assertion: # between them the two suites pin both of freetype's outline drivers on # this image from fontconfig's side. if (g[8] == "CFF") nfmt++; else fmtbad = fmtbad " " b "='" g[8] "'" # THE TYPOGRAPHIC-NAME SPLIT, ASSERTED AS THE 10-AND-4 IT IS. fontconfig # comma-joins ID16 then ID1; the four RIBBI faces have them equal and so # carry ONE value, and the other ten carry TWO. A single "all fourteen # have two" case would be WRONG here and a single "all fourteen have one" # equally so -- and getting it wrong in either direction is exactly the # failure that would let ten faces collapse onto one style. nval = split(g[2], _fv, ",") want = (b in RIBBI) ? 1 : 2 if (nval == want) ntyp++ else typbad = typbad " " b "=" nval "(want " want ")" } emit("fcq-spacing-mono", nsp == NFACE, "spacing must be 100 (FC_MONO) on all " NFACE " faces;" spbad) emit("fcq-format-cff", nfmt == NFACE, "fontformat must be CFF on all " NFACE " faces;" fmtbad) emit("fcq-family-typographic", ntyp == NFACE, "family value count must be 2 except on the four RIBBI faces;" typbad) }