Petter Reinholdtsen

Entries from November 2023.

«Når «på» blir «pÃ¥»: Et reservoar av tegn sett fra depotet» i tidsskriftet Aksess
15th November 2023

For noen uker siden skrev en kamerat og meg en artikkel om tegnsett i arkivtidsskriftet Aksess både på web og i papirutgave nr. 3 2023. Her er det som nettopp ble publisert.

Når «på» blir «pÃ¥»: Et reservoar av tegn sett fra depotet

av Thomas Sødring og Petter Reinholdtsen

De færreste av oss tenker over hva som skjer dypere i datamaskinen mens vi sitter der og skriver noe på tastaturet. Når du trykker på tasten «Å», så vises bokstaven Å. Men noen ganger blir det feil. Hvorfor det – og hva er viktig å være klar over i arkivsammenheng?

Dersom bokstaver tolkes forskjellig mellom systemer, blir det fort rot, dette kalles mojibake blant kjennere, etter det japanske uttrykket for tegnomforming. Det er en lang historie her som tidvis har vært preget av rot. Noen husker kanskje tilbake til en tid der bokstavene æ, ø og å ofte var ødelagt i e-poster – et klassisk eksempel på tegnsettproblemstilling.

«Nå» og «før»

Tid er et skjult problem for depot fordi vi danner dokumentasjon i en kontekst som er preget av å være «nå». Vår forståelse av verden og bruken av teknologi er utgangspunktet for denne konteksten. Tenk selv hvordan verden har utviklet seg de siste 20 årene, hva samfunnet er opptatt av, og hvordan vi bruker teknologi i hverdagen. Tid er et skjult problem fordi når vi trekker dokumentasjon ut av systemer og deponerer for langtidsbevaring, er konteksten til materialet «nå», men verden går videre. Ettersom teknologien og måten vi bruker den på, utvikler seg, blir «nå» til «før», og dokumentasjonen befinner seg snart i en «før»-kontekst.

Dette med «før» og «nå» i forhold til dokumentasjonens kontekst er noe vi er veldig lite bevisste på, men det er en problemstilling depotarkivene eier og forvalter. En av disse utfordringene er hvorfor «Ø» ikke nødvendigvis er det samme som «Ø», og hvorfor det i det hele tatt gir mening å si noe sånt. Vi snakker her om noe som heter tegnsett, som er en avtalt måte å representere bokstaver, tall og andre symboler på slik at vi på en feilfri måte kan utveksle tekst mellom datasystemer.

Tegnsettproblemstillingen er satt sammen av fire fasetter; repertoar, representasjon, koding og uttegning.

Repertoarer

Repertoar er en samling med tegn og symboler som kan representeres. Tenk norsk alfabet eller japanske piktogrammer, men også matematiske og elektroniske symboler. Bokstaven «stor a» kan være en oppføring i et slikt repertoar. For å kunne brukes i en datamaskin trenger hver oppføring i et slikt repertoar en representasjon, hvilket i datamaskinsammenheng betyr at det tilordnes et tall. Tallet kan lagres på ulike vis i en eller flere kodingsformater. For eksempel kan en skrive tallet ti som både 10, X og A, i henholdsvis titallssystemet, romertallssystemet og sekstentallssystemet.

Hvis en skal kunne lese inn filer og vite hvilket tall og hvilken representasjon og instans i et repertoar det er snakk om, så må en vite hvordan tallet er kodet. Sist, men ikke minst, for å kunne bruke symbolet til noe må det kunne være kjent hvordan det skal se ut eller tegnes på ark. Det finnes utallige skrifttyper med norske bokstaver, alle litt forskjellige, og skal en kunne tegne en stor A på skjermen, så må datamaskinen vite hva den skal tegne. Skrifttyper inneholder informasjon om hvordan ulike tall skal tegnes. De inneholder ikke alltid alle symbolene som er brukt i en tekst, hvilket gjør at ikke alle forståtte tegn vil kunne vises på skjerm eller ark.

Hver av disse fasettene må være avklart for å kunne ta vare på og vise frem tekst med en datamaskin. Kombinasjon av repertoar, representasjon og koding er det en kaller et tegnsett. Kombinasjonen av representasjon og uttegning kalles en skrifttype. De fleste skrifttyper har også informasjon om repertoar, men det finnes skrifttyper som kun kobler mellom tallkode og uttegning, uten å fortelle noe om hvordan tallkodene egentlig skal tolkes.

Fra ASCII til ISO-8859

Vi begynner historien med ASCII (American Standard Code for Information Interchange) som har en historie som spores tilbake til 1963. Utgangspunktet til ASCII var at det kunne kode opp til 128 forskjellige symboler i vanlig bruk i USA. De visuelle symbolene i ASCII er de små og store bokstavene (a til z og A til Z), tall (0 til 9) og tegnsettingssymboler (for eksempel semikolon, komma og punktum). ASCII har også noen usynlige symboler som ble brukt for bl.a. kommunikasjon. Før ASCII var det for eksempel teleks-tegnsett med plass til bare 32 tegn og EBCDIC med plass til 256 tegn, alle med en helt annen rekkefølge på symbolene enn ASCII, men de har vært lite brukt de siste femti årene. Et eksempel på noen utvalgte symboler i repertoaret til ASCII vises i tabell 1.

Tabell 1. Eksempel på utvalgte symboler hentet fra ASCII-tegnsettet. Kolonnen «Binær» viser symbolets verdi i totallssystemet (1 og 0 tall), mens kolonnen «Desimal» viser symbolets verdi i titallssystemet.
Grafisk Binær Desimal
A 1000001 65
M 1001101 77
Z 1011010 90
a 1100001 97
m 1101101 109
z 1111010 122
0 0110000 48
9 0111001 58
; 0111011 59

Det opprinnelige ASCII-tegnsettet ble også omtalt som ASCII-7 og brukte 7 bits (0 og 1) for å representere symboler. Datamaskiner er ofte konfigurert til å jobbe med enheter der bits er gruppert som 4 eller 8 bits . Det lå en mulighet i å ta i bruk bit åtte. En slik endring ville gjøre det mulig for datamaskiner å øke antall symboler de kunne representere, noe som ga en økning fra 128 forskjellige symboler til 256 forskjellige symboler. Det ble åpnet for å innlemme de nordiske bokstavene sammen med ASCII, og dette ble etter hvert standardisert som ISO-8859-1. Tabell 2 viser deler av ISO-8859-1 som støtter de norske bokstavene.

Det sier seg selv at muligheten til å representere inntil 256 symboler ikke holder når vi snakker om en global verden, og det ble gjort et standardiseringsløp som tok utgangspunkt i ASCII-7 med en utvidelse til å bruke den åttende biten for ulike språkgrupper. Denne standarden heter ISO-8859 og er inndelt i opptil 16 varianter, altså fra ISO-8859-1 til ISO-8859-16.

Tabell 2. Koding av de norske symbolene slik de er definert i ISO-8859-1 tegnsettet.
Grafisk Binær Desimal
Æ 11000110 198
Ø 11011000 216
Å 11000101 197
æ 11100110 230
ø 11111000 248
å 11100101 229

Norske tegn er definert i ISO-8859-1, som også omtales som Latin 1, de fleste samiske tegn er definert i ISO-8859-4 (Latin 4) mens tilgang til €-symbolet kom med ISO-8859-15 (Latin 9). ISO-8859-15 er en revisjon av ISO-8859-1 som fjerner noen lite brukte symboler og erstatter bokstaver som er mer brukt, og introduserer €-symbolet. Det er viktig å merke at alle ISO-8859-variantene har overlapp med ASCII-7, noe som ga samvirke med de engelskspråklige landene som ikke trengte å gjøre noe. Det innebærer også at de første 128 verdiene i ISO-8859-variantene representerer de samme symbolene. Det er først når du kommer til tolkningen av de resterende 128 verdiene med nummer 128 til 255, at det oppsto tolkningsutfordringer mellom ISO-8859-variantene.

ISO-8859-verdenen fungerte godt så lenge tegnsettet som ble brukt når innhold ble skapt, også ble brukt når innhold ble gjengitt og du ikke trengte å kombinere innhold fra forskjellige tegnsett i samme dokument. Utfordringen med bruken av ISO-8859-variantene ble raskt tydelig i en mer globalisert verden med utveksling av tekst på tvers av landegrenser der tekstlig innhold i dokumenter, e-poster og websider kunne bli skrevet med ett tegnsett og gjengitt med et annet tegnsett.

Tabell 3. Viser tolkning av verdiene som er tilegnet de norske symbolene i ISO-8859-1 i de andre ISO 8859-variatene. Merk ISO-8859-12 ikke finnes da arbeidet ble avsluttet.[1]
Binærverdi 1 2 3 4 5 6 7 8 9 10 11 13 14 15 16
11000110 Æ Ć Ĉ Æ Ц ئ Ζ Æ Æ Ę Æ Æ Æ
11011000 Ø Ř Ĝ Ø и ظ Ψ Ø Ø Ų Ø Ø Ű
11000101 Å Ĺ Ċ Å Х إ Ε Å Å Å Å Å Ć
11100110 æ ć ĉ æ ц ن ζ ז æ æ ę æ æ v
11111000 ø ř ĝ ø ј ψ ר ø ø ų ø ø ű
11100101 å ĺ ċ å х م ε ו å å å å å ć

Denne problemstillingen er illustrert i tabell 3, der vi ser verdiene tilegnet de norske symbolene i ISO-8859-1 i kolonne «1». I de øvrige kolonnene ser vi hvilket symbol verdien får i de andre ISO-8859-variantene. Tar vi utgangspunkt i tabell 3, kan vi se at ordet lærlingspørsmål gjengitt med ISO-8859-2 (kolonne 2) blir lćrlingspřrsmĺl, mens det blir lζrlingspψrsmεl med ISO- 8859-7 (kolonne 7). Med ISO-8859-2 blir «æ» til «ć», «ø» til «ř» og «å» til «ĺ». I ISO-8859-7 blir «æ» til «ζ», «ø» til «ψ», mens «å» blir «ε».

Det er egentlig ingen utfordring med dette så lenge du vet hvilket tegnsett innholdet ditt er representert med, og det ikke har skjedd omforminger som du ikke er klar over. Det er det siste som er problematisk, spesielt de datasystemene som har vært i bruk de siste 20 årene, som ikke har noe innebygd funksjonalitet for å forvalte tegnsettproblematikken. Et godt eksempel på dette er Microsoft-tegnsettet Windows-1252, som ble forvekslet som 100 % kompatibel med ISO-8859-1, men hadde byttet ut plassene fra 127 til 159. Historisk vil det finnes en del variasjon i hvilket tegnsett som har vært i bruk, og hvor vellykket konvertering mellom tegnsett har vært.

Unicode som løsning

Tegnsettforvirring ble etter hvert et irritasjonsmoment og samvirkeproblem. Ofte fikk man en e-post der æøå var erstattet av rare symboler fordi e-posten hadde vært innom et eller annet datasystem som ikke brukte samme tegnsett.

For å løse dette samvirkeproblemet for tegnsett ble det startet et arbeid og en ny standard så dagens lys etter hvert. Denne standarden fikk navnet Unicode (ISO/ IEC 10646) og skulle resultere i et tegnsett som alle skulle være enige om. Unicode er et repertoar og en representasjon, dvs. navngivning og tilordning av tallverdi til alle symboler i bruk i verden i dag. Oppføringer i Unicode skrives gjerne U+XXXX der XXXX er tallkoden i sekstentallssystemet som oppføringen har i Unicode-katalogen. Her finner vi tegn brukt av både levende og døde språk, konstruerte språk, tekniske symboler, morsomme tegninger (såkalte emojier) og tegn ingen vet hva betyr eller skal brukes til. Et morsomt eksempel er i nettartikkelen: U+237C ⍼ RIGHT ANGLE WITH DOWNWARDS ZIGZAG ARROW, av Jonathan Chan.[2]

Sammen med Unicode kom det tre måter å kode disse tallene på; UTF-8, UTF-16 og UTF-32. Av datatekniske årsaker er UTF-8 mye brukt, spesielt når det gjelder utveksling av tekst over Internett, mens UTF-16 er brukt en del til tekstfiler lagret på Windows. En utfordring med Unicode og UTF-variantene er at disse gir flere måter å kode samme symbol på med en kombinasjonsmekanisme. Dette kan gi utfordringer ved søk, hvis en skal søke etter et ord som har ett eller flere symboler som kan skrives på ulikt vis, så er det ikke sikkert at søkesystemet vil finne alle forekomster. For eksempel kan bokstaven U+00F8 «Latin Small Letter O with Stroke» kodes som den tradisjonelle norske tegnet ø, men også som o kombinert med skråstrek U+0338. Begge deler er gyldig bruk av Unicode, selv om det er tradisjon for å foretrekke å «normalisere» kombinasjoner som enkelttegn der det er mulig, nettopp for å forenkle søk.

Bare Unicode fremover

Forvaltningens bruk av tegnsett er regulert i Forskrift om IT-standarder i offentlig forvaltning[3]. Her står det: «Ved all utveksling av informasjon mellom forvaltningsorganer og fra forvaltningsorgan til innbyggere og næringsliv skal tegnsettstandarden ISO/IEC 10646 representert ved UTF8 benyttes.» Det er forskjellige bruksområder til UTF-8, UTF-16 og UTF-32, men UTF-8 er kodingen vi kjenner mest til. Det er flere grunner at UTF-8 «vant» konkurransen til å bli den utvalgte. Den kanskje viktigste er at UTF-8 er fullt samvirkende med ASCII-7, slik at den engelskspråklige delen av verden kunne rulle ut UTF-8 uten å merke noe forskjell. En tekstfil med kun ASCII-tekst vil være identisk på disken hvis den lagres som UTF-8 og ASCII. UTF-16 og UTF-32 byr på noen optimaliseringer som gjør dem relevant for spesifikke problemområder, men for det meste vil vi aldri oppleve disse standardene på nært hold i hverdagen. Det er uansett kun bruken av UTF-8 som er lovregulert i Norge.

Det er ikke slik at hele verden bruker ISO/IEC 10646 og UTF-8. Kina har egne standarder for tegnsett, mye brukt er GB 18030, som er Unicode med en annen koding enn UTF-8, mens Taiwan og andre asiatiske land gjerne bruker Big5 eller andre tegnsett.

UTF-8 er dominerende i Norge, men det er tidsperioder der forskjellige datasystemer utvekslet data i henhold til ISO-8859-1, ISO-8859-15, Windows-1252, Codepage 865 og ISO-646-60 / Codepage 1016 mens overgangen til UTF-8 pågikk. Det er ikke slik at et datasystem enkelt kan tvinges til å bruke et tegnsett, da det er flere lag i et datasystem som må settes opp til å bruke riktig tegnsett, og tegnsettproblemet fort oppstår når det er et eller annet i datasystemet som bruker feil tegnsett.

Et klassisk eksempel på problemet er en utveksling av tekst mellom to systemer der teksten i utgangspunktet er kodet i UTF-8, men går gjennom noe som er ISO-8859-1 underveis. Dette kan vises med at ordet «på» i et slik scenario ender opp som «pÃ¥». Det er mulig å spore dette tilbake til verdiene symbolene er tilordnet i tegnsettene. «på» blir til «pÃ¥» fordi «å» i UTF-8 er representert med U+C3AF, og dersom vi ser på hva disse verdiene representerer, ser vi at sekstentallssystemverdien C3 er 1100 0011 i totallssystemet og symbolet med dette tallet i ISO-8859-1 er Ã.

Vi ser det samme med sekstentallssystemverdien A5, som er 1010 0101 i totallssystemet, og tilsvarende symbol i ISO-8859-1 er ¥. Slik mojibake kan lett skje hvis «på» i utgangspunktet var representert med UTF-8, men ble behandlet med et system som bruker ISO-8859-1. Det er ingen automatikk i å fange opp slike ødeleggelser mens tekstlig innhold utveksles mellom datasystemer.

En utfordring for depotarkivene er at bruken av tegnsett ikke alltid har vært regulert, og at det kan finnes flere dokumentasjonssamlinger som er opprettet med varierende tegnsett før gjeldende forskrift inntraff – uten at det er mulig å avlede fra filene hvilket tegnsett som ble brukt. Et eksempel på dette er €-symbolet, som kom først etter at ISO-8859-1 var tatt i bruk. Det kan bli en utfordring for et depotarkiv, men så lenge det er kjent hvilket tegnsett var i bruk, så bør det gå bra. Riksarkivarens forskrift[4] formaliserer dette ved å kreve følgende:

§ 5-11. Tegnsett i arkivuttrekk

  1. Arkivuttrekk og medfølgende struktur- og innholdsbeskrivelser skal overføres som ren tekst i ukryptert form, og benytte godkjent tegnsett.
  2. Godkjente tegnsett er:
    1. Unicode UTF-8
      (ISO/IEC 10646-1:2000 Annex D)
    2. ISO 8859-1:1998, Latin 1
    3. ISO 8859-4:1998, Latin 4 for samiske tegn.
  3. Andre tegnsett aksepteres bare etter avtale med Arkivverket.

Ditt ansvar

På mange måter burde ikke tegnsett være et problem i 2023, men sånn er det nok ikke. Land som har oppgradert til UTF-8 som primærtegnsett for utveksling av tekstlig innhold, begrenser problematikken betraktelig, men globalt sett så er tegnsettutfordringen ikke løst fordi ikke alle er enige om å bruke samme tegnsett. Det kan være geopolitiske eller kulturelle hensyn som ligger til grunn for dette.

Det er uansett verdt å merke at selv om bruken av UTF-8 skulle bli 100% utbredt, så er det et historisk perspektiv (ASCII-7, ISO-8859-variantene, UTF-8) her som gjør tegnsett til et problemområde arkivarene må forstå og håndtere. Som danningsarkivar har du et ansvar for å vite hvilket tegnsett systemene og databasene dere forvalter, er i samsvar med. Det er noe IT-avdelingen din eller programvareleverandørene enkelt skal kunne svare på, og svaret skal være UTF-8 for alle nye systemer.


1. Tegnsettkilde https://en.wikipedia.org/wiki/ISO/IEC_8859

2. https://ionathan.ch/2022/04/09/angzarr.html

3. https://lovdata.no/dokument/SF/forskrift/2013-04-05-959/%C2%A78#%C2%A78

4. https://lovdata.no/forskrift/2017-12-19-2286/§5-11

For øvrig burde varsleren Edward Snowden få politisk asyl i Norge.

Som vanlig, hvis du bruker Bitcoin og ønsker å vise din støtte til det jeg driver med, setter jeg pris på om du sender Bitcoin-donasjoner til min adresse 15oWEoG9dUPovwmUL9KWAnYRtNJEkP1u1b. Merk, betaling med bitcoin er ikke anonymt. :)

Tags: noark5, norsk, standard.
New and improved sqlcipher in Debian for accessing Signal database
12th November 2023

For a while now I wanted to have direct access to the Signal database of messages and channels of my Desktop edition of Signal. I prefer the enforced end to end encryption of Signal these days for my communication with friends and family, to increase the level of safety and privacy as well as raising the cost of the mass surveillance government and non-government entities practice these days. In August I came across a nice recipe on how to use sqlcipher to extract statistics from the Signal database explaining how to do this. Unfortunately this did not work with the version of sqlcipher in Debian. The sqlcipher package is a "fork" of the sqlite package with added support for encrypted databases. Sadly the current Debian maintainer announced more than three years ago that he did not have time to maintain sqlcipher, so it seemed unlikely to be upgraded by the maintainer. I was reluctant to take on the job myself, as I have very limited experience maintaining shared libraries in Debian. After waiting and hoping for a few months, I gave up the last week, and set out to update the package. In the process I orphaned it to make it more obvious for the next person looking at it that the package need proper maintenance.

The version in Debian was around five years old, and quite a lot of changes had taken place upstream into the Debian maintenance git repository. After spending a few days importing the new upstream versions, realising that upstream did not care much for SONAME versioning as I saw library symbols being both added and removed with minor version number changes to the project, I concluded that I had to do a SONAME bump of the library package to avoid surprising the reverse dependencies. I even added a simple autopkgtest script to ensure the package work as intended. Dug deep into the hole of learning shared library maintenance, I set out a few days ago to upload the new version to Debian experimental to see what the quality assurance framework in Debian had to say about the result. The feedback told me the pacakge was not too shabby, and yesterday I uploaded the latest version to Debian unstable. It should enter testing today or tomorrow, perhaps delayed by a small library transition.

Armed with a new version of sqlcipher, I can now have a look at the SQL database in ~/.config/Signal/sql/db.sqlite. First, one need to fetch the encryption key from the Signal configuration using this simple JSON extraction command:

/usr/bin/jq -r '."key"' ~/.config/Signal/config.json

Assuming the result from that command is 'secretkey', which is a hexadecimal number representing the key used to encrypt the database. Next, one can now connect to the database and inject the encryption key for access via SQL to fetch information from the database. Here is an example dumping the database structure:

% sqlcipher ~/.config/Signal/sql/db.sqlite
sqlite> PRAGMA key = "x'secretkey'";
sqlite> .schema
CREATE TABLE sqlite_stat1(tbl,idx,stat);
CREATE TABLE conversations(
      id STRING PRIMARY KEY ASC,
      json TEXT,

      active_at INTEGER,
      type STRING,
      members TEXT,
      name TEXT,
      profileName TEXT
    , profileFamilyName TEXT, profileFullName TEXT, e164 TEXT, serviceId TEXT, groupId TEXT, profileLastFetchedAt INTEGER);
CREATE TABLE identityKeys(
      id STRING PRIMARY KEY ASC,
      json TEXT
    );
CREATE TABLE items(
      id STRING PRIMARY KEY ASC,
      json TEXT
    );
CREATE TABLE sessions(
      id TEXT PRIMARY KEY,
      conversationId TEXT,
      json TEXT
    , ourServiceId STRING, serviceId STRING);
CREATE TABLE attachment_downloads(
    id STRING primary key,
    timestamp INTEGER,
    pending INTEGER,
    json TEXT
  );
CREATE TABLE sticker_packs(
    id TEXT PRIMARY KEY,
    key TEXT NOT NULL,

    author STRING,
    coverStickerId INTEGER,
    createdAt INTEGER,
    downloadAttempts INTEGER,
    installedAt INTEGER,
    lastUsed INTEGER,
    status STRING,
    stickerCount INTEGER,
    title STRING
  , attemptedStatus STRING, position INTEGER DEFAULT 0 NOT NULL, storageID STRING, storageVersion INTEGER, storageUnknownFields BLOB, storageNeedsSync
      INTEGER DEFAULT 0 NOT NULL);
CREATE TABLE stickers(
    id INTEGER NOT NULL,
    packId TEXT NOT NULL,

    emoji STRING,
    height INTEGER,
    isCoverOnly INTEGER,
    lastUsed INTEGER,
    path STRING,
    width INTEGER,

    PRIMARY KEY (id, packId),
    CONSTRAINT stickers_fk
      FOREIGN KEY (packId)
      REFERENCES sticker_packs(id)
      ON DELETE CASCADE
  );
CREATE TABLE sticker_references(
    messageId STRING,
    packId TEXT,
    CONSTRAINT sticker_references_fk
      FOREIGN KEY(packId)
      REFERENCES sticker_packs(id)
      ON DELETE CASCADE
  );
CREATE TABLE emojis(
    shortName TEXT PRIMARY KEY,
    lastUsage INTEGER
  );
CREATE TABLE messages(
        rowid INTEGER PRIMARY KEY ASC,
        id STRING UNIQUE,
        json TEXT,
        readStatus INTEGER,
        expires_at INTEGER,
        sent_at INTEGER,
        schemaVersion INTEGER,
        conversationId STRING,
        received_at INTEGER,
        source STRING,
        hasAttachments INTEGER,
        hasFileAttachments INTEGER,
        hasVisualMediaAttachments INTEGER,
        expireTimer INTEGER,
        expirationStartTimestamp INTEGER,
        type STRING,
        body TEXT,
        messageTimer INTEGER,
        messageTimerStart INTEGER,
        messageTimerExpiresAt INTEGER,
        isErased INTEGER,
        isViewOnce INTEGER,
        sourceServiceId TEXT, serverGuid STRING NULL, sourceDevice INTEGER, storyId STRING, isStory INTEGER
        GENERATED ALWAYS AS (type IS 'story'), isChangeCreatedByUs INTEGER NOT NULL DEFAULT 0, isTimerChangeFromSync INTEGER
        GENERATED ALWAYS AS (
          json_extract(json, '$.expirationTimerUpdate.fromSync') IS 1
        ), seenStatus NUMBER default 0, storyDistributionListId STRING, expiresAt INT
        GENERATED ALWAYS
        AS (ifnull(
          expirationStartTimestamp + (expireTimer * 1000),
          9007199254740991
        )), shouldAffectActivity INTEGER
        GENERATED ALWAYS AS (
          type IS NULL
          OR
          type NOT IN (
            'change-number-notification',
            'contact-removed-notification',
            'conversation-merge',
            'group-v1-migration',
            'keychange',
            'message-history-unsynced',
            'profile-change',
            'story',
            'universal-timer-notification',
            'verified-change'
          )
        ), shouldAffectPreview INTEGER
        GENERATED ALWAYS AS (
          type IS NULL
          OR
          type NOT IN (
            'change-number-notification',
            'contact-removed-notification',
            'conversation-merge',
            'group-v1-migration',
            'keychange',
            'message-history-unsynced',
            'profile-change',
            'story',
            'universal-timer-notification',
            'verified-change'
          )
        ), isUserInitiatedMessage INTEGER
        GENERATED ALWAYS AS (
          type IS NULL
          OR
          type NOT IN (
            'change-number-notification',
            'contact-removed-notification',
            'conversation-merge',
            'group-v1-migration',
            'group-v2-change',
            'keychange',
            'message-history-unsynced',
            'profile-change',
            'story',
            'universal-timer-notification',
            'verified-change'
          )
        ), mentionsMe INTEGER NOT NULL DEFAULT 0, isGroupLeaveEvent INTEGER
        GENERATED ALWAYS AS (
          type IS 'group-v2-change' AND
          json_array_length(json_extract(json, '$.groupV2Change.details')) IS 1 AND
          json_extract(json, '$.groupV2Change.details[0].type') IS 'member-remove' AND
          json_extract(json, '$.groupV2Change.from') IS NOT NULL AND
          json_extract(json, '$.groupV2Change.from') IS json_extract(json, '$.groupV2Change.details[0].aci')
        ), isGroupLeaveEventFromOther INTEGER
        GENERATED ALWAYS AS (
          isGroupLeaveEvent IS 1
          AND
          isChangeCreatedByUs IS 0
        ), callId TEXT
        GENERATED ALWAYS AS (
          json_extract(json, '$.callId')
        ));
CREATE TABLE sqlite_stat4(tbl,idx,neq,nlt,ndlt,sample);
CREATE TABLE jobs(
        id TEXT PRIMARY KEY,
        queueType TEXT STRING NOT NULL,
        timestamp INTEGER NOT NULL,
        data STRING TEXT
      );
CREATE TABLE reactions(
        conversationId STRING,
        emoji STRING,
        fromId STRING,
        messageReceivedAt INTEGER,
        targetAuthorAci STRING,
        targetTimestamp INTEGER,
        unread INTEGER
      , messageId STRING);
CREATE TABLE senderKeys(
        id TEXT PRIMARY KEY NOT NULL,
        senderId TEXT NOT NULL,
        distributionId TEXT NOT NULL,
        data BLOB NOT NULL,
        lastUpdatedDate NUMBER NOT NULL
      );
CREATE TABLE unprocessed(
        id STRING PRIMARY KEY ASC,
        timestamp INTEGER,
        version INTEGER,
        attempts INTEGER,
        envelope TEXT,
        decrypted TEXT,
        source TEXT,
        serverTimestamp INTEGER,
        sourceServiceId STRING
      , serverGuid STRING NULL, sourceDevice INTEGER, receivedAtCounter INTEGER, urgent INTEGER, story INTEGER);
CREATE TABLE sendLogPayloads(
        id INTEGER PRIMARY KEY ASC,

        timestamp INTEGER NOT NULL,
        contentHint INTEGER NOT NULL,
        proto BLOB NOT NULL
      , urgent INTEGER, hasPniSignatureMessage INTEGER DEFAULT 0 NOT NULL);
CREATE TABLE sendLogRecipients(
        payloadId INTEGER NOT NULL,

        recipientServiceId STRING NOT NULL,
        deviceId INTEGER NOT NULL,

        PRIMARY KEY (payloadId, recipientServiceId, deviceId),

        CONSTRAINT sendLogRecipientsForeignKey
          FOREIGN KEY (payloadId)
          REFERENCES sendLogPayloads(id)
          ON DELETE CASCADE
      );
CREATE TABLE sendLogMessageIds(
        payloadId INTEGER NOT NULL,

        messageId STRING NOT NULL,

        PRIMARY KEY (payloadId, messageId),

        CONSTRAINT sendLogMessageIdsForeignKey
          FOREIGN KEY (payloadId)
          REFERENCES sendLogPayloads(id)
          ON DELETE CASCADE
      );
CREATE TABLE preKeys(
        id STRING PRIMARY KEY ASC,
        json TEXT
      , ourServiceId NUMBER
        GENERATED ALWAYS AS (json_extract(json, '$.ourServiceId')));
CREATE TABLE signedPreKeys(
        id STRING PRIMARY KEY ASC,
        json TEXT
      , ourServiceId NUMBER
        GENERATED ALWAYS AS (json_extract(json, '$.ourServiceId')));
CREATE TABLE badges(
        id TEXT PRIMARY KEY,
        category TEXT NOT NULL,
        name TEXT NOT NULL,
        descriptionTemplate TEXT NOT NULL
      );
CREATE TABLE badgeImageFiles(
        badgeId TEXT REFERENCES badges(id)
          ON DELETE CASCADE
          ON UPDATE CASCADE,
        'order' INTEGER NOT NULL,
        url TEXT NOT NULL,
        localPath TEXT,
        theme TEXT NOT NULL
      );
CREATE TABLE storyReads (
        authorId STRING NOT NULL,
        conversationId STRING NOT NULL,
        storyId STRING NOT NULL,
        storyReadDate NUMBER NOT NULL,

        PRIMARY KEY (authorId, storyId)
      );
CREATE TABLE storyDistributions(
        id STRING PRIMARY KEY NOT NULL,
        name TEXT,

        senderKeyInfoJson STRING
      , deletedAtTimestamp INTEGER, allowsReplies INTEGER, isBlockList INTEGER, storageID STRING, storageVersion INTEGER, storageUnknownFields BLOB, storageNeedsSync INTEGER);
CREATE TABLE storyDistributionMembers(
        listId STRING NOT NULL REFERENCES storyDistributions(id)
          ON DELETE CASCADE
          ON UPDATE CASCADE,
        serviceId STRING NOT NULL,

        PRIMARY KEY (listId, serviceId)
      );
CREATE TABLE uninstalled_sticker_packs (
        id STRING NOT NULL PRIMARY KEY,
        uninstalledAt NUMBER NOT NULL,
        storageID STRING,
        storageVersion NUMBER,
        storageUnknownFields BLOB,
        storageNeedsSync INTEGER NOT NULL
      );
CREATE TABLE groupCallRingCancellations(
        ringId INTEGER PRIMARY KEY,
        createdAt INTEGER NOT NULL
      );
CREATE TABLE IF NOT EXISTS 'messages_fts_data'(id INTEGER PRIMARY KEY, block BLOB);
CREATE TABLE IF NOT EXISTS 'messages_fts_idx'(segid, term, pgno, PRIMARY KEY(segid, term)) WITHOUT ROWID;
CREATE TABLE IF NOT EXISTS 'messages_fts_content'(id INTEGER PRIMARY KEY, c0);
CREATE TABLE IF NOT EXISTS 'messages_fts_docsize'(id INTEGER PRIMARY KEY, sz BLOB);
CREATE TABLE IF NOT EXISTS 'messages_fts_config'(k PRIMARY KEY, v) WITHOUT ROWID;
CREATE TABLE edited_messages(
        messageId STRING REFERENCES messages(id)
          ON DELETE CASCADE,
        sentAt INTEGER,
        readStatus INTEGER
      , conversationId STRING);
CREATE TABLE mentions (
        messageId REFERENCES messages(id) ON DELETE CASCADE,
        mentionAci STRING,
        start INTEGER,
        length INTEGER
      );
CREATE TABLE kyberPreKeys(
        id STRING PRIMARY KEY NOT NULL,
        json TEXT NOT NULL, ourServiceId NUMBER
        GENERATED ALWAYS AS (json_extract(json, '$.ourServiceId')));
CREATE TABLE callsHistory (
        callId TEXT PRIMARY KEY,
        peerId TEXT NOT NULL, -- conversation id (legacy) | uuid | groupId | roomId
        ringerId TEXT DEFAULT NULL, -- ringer uuid
        mode TEXT NOT NULL, -- enum "Direct" | "Group"
        type TEXT NOT NULL, -- enum "Audio" | "Video" | "Group"
        direction TEXT NOT NULL, -- enum "Incoming" | "Outgoing
        -- Direct: enum "Pending" | "Missed" | "Accepted" | "Deleted"
        -- Group: enum "GenericGroupCall" | "OutgoingRing" | "Ringing" | "Joined" | "Missed" | "Declined" | "Accepted" | "Deleted"
        status TEXT NOT NULL,
        timestamp INTEGER NOT NULL,
        UNIQUE (callId, peerId) ON CONFLICT FAIL
      );
[ dropped all indexes to save space in this blog post ]
CREATE TRIGGER messages_on_view_once_update AFTER UPDATE ON messages
      WHEN
        new.body IS NOT NULL AND new.isViewOnce = 1
      BEGIN
        DELETE FROM messages_fts WHERE rowid = old.rowid;
      END;
CREATE TRIGGER messages_on_insert AFTER INSERT ON messages
      WHEN new.isViewOnce IS NOT 1 AND new.storyId IS NULL
      BEGIN
        INSERT INTO messages_fts
          (rowid, body)
        VALUES
          (new.rowid, new.body);
      END;
CREATE TRIGGER messages_on_delete AFTER DELETE ON messages BEGIN
        DELETE FROM messages_fts WHERE rowid = old.rowid;
        DELETE FROM sendLogPayloads WHERE id IN (
          SELECT payloadId FROM sendLogMessageIds
          WHERE messageId = old.id
        );
        DELETE FROM reactions WHERE rowid IN (
          SELECT rowid FROM reactions
          WHERE messageId = old.id
        );
        DELETE FROM storyReads WHERE storyId = old.storyId;
      END;
CREATE VIRTUAL TABLE messages_fts USING fts5(
        body,
        tokenize = 'signal_tokenizer'
      );
CREATE TRIGGER messages_on_update AFTER UPDATE ON messages
      WHEN
        (new.body IS NULL OR old.body IS NOT new.body) AND
         new.isViewOnce IS NOT 1 AND new.storyId IS NULL
      BEGIN
        DELETE FROM messages_fts WHERE rowid = old.rowid;
        INSERT INTO messages_fts
          (rowid, body)
        VALUES
          (new.rowid, new.body);
      END;
CREATE TRIGGER messages_on_insert_insert_mentions AFTER INSERT ON messages
      BEGIN
        INSERT INTO mentions (messageId, mentionAci, start, length)
        
    SELECT messages.id, bodyRanges.value ->> 'mentionAci' as mentionAci,
      bodyRanges.value ->> 'start' as start,
      bodyRanges.value ->> 'length' as length
    FROM messages, json_each(messages.json ->> 'bodyRanges') as bodyRanges
    WHERE bodyRanges.value ->> 'mentionAci' IS NOT NULL
  
        AND messages.id = new.id;
      END;
CREATE TRIGGER messages_on_update_update_mentions AFTER UPDATE ON messages
      BEGIN
        DELETE FROM mentions WHERE messageId = new.id;
        INSERT INTO mentions (messageId, mentionAci, start, length)
        
    SELECT messages.id, bodyRanges.value ->> 'mentionAci' as mentionAci,
      bodyRanges.value ->> 'start' as start,
      bodyRanges.value ->> 'length' as length
    FROM messages, json_each(messages.json ->> 'bodyRanges') as bodyRanges
    WHERE bodyRanges.value ->> 'mentionAci' IS NOT NULL
  
        AND messages.id = new.id;
      END;
sqlite>

Finally I have the tool needed to inspect and process Signal messages that I need, without using the vendor provided client. Now on to transforming it to a more useful format.

As usual, if you use Bitcoin and want to show your support of my activities, please send Bitcoin donations to my address 15oWEoG9dUPovwmUL9KWAnYRtNJEkP1u1b.

Tags: debian, english, sikkerhet, surveillance.
New chrpath release 0.17
10th November 2023

The chrpath package provide a simple command line tool to remove or modify the rpath or runpath of compiled ELF program. It is almost 10 years since I updated the code base, but I stumbled over the tool today, and decided it was time to move the code base from Subversion to git and find a new home for it, as the previous one (Debian Alioth) has been shut down. I decided to go with Codeberg this time, as it is my git service of choice these days, did a quick and dirty migration to git and updated the code with a few patches I found in the Debian bug tracker. These are the release notes:

New in 0.17 released 2023-11-10:

The latest edition is tagged and available from https://codeberg.org/pere/chrpath.

As usual, if you use Bitcoin and want to show your support of my activities, please send Bitcoin donations to my address 15oWEoG9dUPovwmUL9KWAnYRtNJEkP1u1b.

Tags: chrpath, debian, english.
Test framework for DocBook processors / formatters
5th November 2023

All the books I have published so far has been using DocBook somewhere in the process. For the first book, the source format was DocBook, while for every later book it was an intermediate format used as the stepping stone to be able to present the same manuscript in several formats, on paper, as ebook in ePub format, as a HTML page and as a PDF file either for paper production or for Internet consumption. This is made possible with a wide variety of free software tools with DocBook support in Debian. The source format of later books have been docx via rst, Markdown, Filemaker and Asciidoc, and for all of these I was able to generate a suitable DocBook file for further processing using pandoc, a2x and asciidoctor, as well as rendering using xmlto, dbtoepub, dblatex, docbook-xsl and fop.

Most of the books I have published are translated books, with English as the source language. The use of po4a to handle translations using the gettext PO format has been a blessing, but publishing translated books had triggered the need to ensure the DocBook tools handle relevant languages correctly. For every new language I have published, I had to submit patches dblatex, dbtoepub and docbook-xsl fixing incorrect language and country specific issues in the framework themselves. Typically this has been missing keywords like 'figure' or sort ordering of index entries. After a while it became tiresome to only discover issues like this by accident, and I decided to write a DocBook "test framework" exercising various features of DocBook and allowing me to see all features exercised for a given language. It consist of a set of DocBook files, a version 4 book, a version 5 book, a v4 book set, a v4 selection of problematic tables, one v4 testing sidefloat and finally one v4 testing a book of articles. The DocBook files are accompanied with a set of build rules for building PDF using dblatex and docbook-xsl/fop, HTML using xmlto or docbook-xsl and epub using dbtoepub. The result is a set of files visualizing footnotes, indexes, table of content list, figures, formulas and other DocBook features, allowing for a quick review on the completeness of the given locale settings. To build with a different language setting, all one need to do is edit the lang= value in the .xml file to pick a different ISO 639 code value and run 'make'.

The test framework source code is available from Codeberg, and a generated set of presentations of the various examples is available as Codeberg static web pages at https://pere.codeberg.page/docbook-example/. Using this test framework I have been able to discover and report several bugs and missing features in various tools, and got a lot of them fixed. For example I got Northern Sami keywords added to both docbook-xsl and dblatex, fixed several typos in Norwegian bokmål and Norwegian Nynorsk, support for non-ascii title IDs added to pandoc, Norwegian index sorting support fixed in xindy and initial Norwegian Bokmål support added to dblatex. Some issues still remains, though. Default index sorting rules are still broken in several tools, so the Norwegian letters æ, ø and å are more often than not sorted properly in the book index.

The test framework recently received some more polish, as part of publishing my latest book. This book contained a lot of fairly complex tables, which exposed bugs in some of the tools. This made me add a new test file with various tables, as well as spend some time to brush up the build rules. My goal is for the test framework to exercise all DocBook features to make it easier to see which features work with different processors, and hopefully get them all to support the full set of DocBook features. Feel free to send patches to extend the test set, and test it with your favorite DocBook processor. Please visit these two URLs to learn more:

If you want to learn more on Docbook and translations, I recommend having a look at the the DocBook web site, the DoCookBook site and my earlier blog post on how the Skolelinux project process and translate documentation, a talk I gave earlier this year on how to translate and publish books using free software (Norwegian only).

As usual, if you use Bitcoin and want to show your support of my activities, please send Bitcoin donations to my address 15oWEoG9dUPovwmUL9KWAnYRtNJEkP1u1b.

Tags: debian, docbook, english.

RSS Feed

Created by Chronicle v4.6