Die Standardbibliothek
Jedes Modul der Standardbibliothek, in der Reihenfolge seines Namens, mit dem Kopf seiner Schnittstelle: wofür es da ist, was es garantiert, und ein Beispiel. Die Module namens Bindings binden eine C-Bibliothek und sind unsicher; welches sichere Modul auf welchem steht, steht in die unsicheren Module.
Bindings.Account
Bindet das Nachschlagen eines Kontos in der Passwortdatenbank und den effektiven Benutzer des Prozesses, aus der Laufzeit.
- Es erreicht C mit @embed und ist deshalb ein unsicheres Modul.
- Standard.Account ist das sichere Modul darauf und dasjenige, das ein Programm importiert.
- Ein Name, der länger als 255 Bytes ist oder ein Nullbyte enthält, wird nicht nachgeschlagen.
let user: Int64 := accountNumber("_notes", false);
Bindings.Arena
Bindet die Arena der Laufzeit, die Speicher mit mmap nimmt und in Stücken ausgibt.
- Es erreicht C mit @embed und macht aus einer Adresse eine Referenz, ist also ein unsicheres Modul.
- Standard.Arena ist das sichere Modul darauf und dasjenige, das ein Programm importiert.
- Jede Funktion hier ist die gleichnamige von Standard.Arena.
let made: Option[Arena] := createArena(65536);
let got: Option[Span![Int32, R]] := arenaSpan(r, 100);
Bindings.Argon2
Bindet libargon2, die Referenzimplementierung von Argon2, über Bindings.C.Argon2.
- Es reicht Adressen an C weiter und ist deshalb ein unsicheres Modul.
- Standard.Password ist das sichere Modul darauf.
- Es berechnet nur Argon2id der Version 0x13.
- Eine abgelehnte Eingabe oder ein Fehler der Bibliothek ergibt None, und nichts bricht ab.
let tag: Option[String] := argon2id(asSpan(&password), asSpan(&salt), 3, 65536, 4, 32);
Bindings.Array
Bindet das Lesen und Schreiben von Zahlen in Little-Endian-Bytes, mit @embed in einfachem C.
- Es erreicht C mit @embed und ist deshalb ein unsicheres Modul.
- Standard.Array ist das sichere Modul darauf und dasjenige, das ein Programm importiert.
- Jeder Aufruf braucht die ganze Zahl innerhalb des Spans; das sichere Modul prüft das, bevor es aufruft.
- Die Byte-Reihenfolge ist auf jedem Host Little Endian, und ein Float behält seine Bits, ein NaN eingeschlossen.
let x: Float64 := loadFloat64(bytes, 8);
Bindings.Bcrypt
Bindet die Prüfung eines bcrypt-Hashes über die eigene Bibliothek des Systems, durch eine Funktion der Laufzeit.
- Die Prüfung ist crypt_checkpass auf OpenBSD, crypt_rn von libxcrypt auf Linux und crypt_r auf FreeBSD.
- Auf macOS hat die C-Bibliothek kein bcrypt, und jede Prüfung antwortet -1.
- Es erreicht C mit @embed und ist deshalb ein unsicheres Modul.
- Standard.Password ist das sichere Modul darauf.
- Es prüft einen bcrypt-Hash und erzeugt nie einen.
let verdict: Int32 := bcryptCheck(asSpan(&password), asSpan(&stored));
Bindings.Bits
Bindet das Lesen der Bits einer Zahl als Zahl eines anderen Typs derselben Breite, in C.
- Es erreicht C mit @embed und ist deshalb ein unsicheres Modul.
- Standard.Bits ist das sichere Modul darauf und dasjenige, das ein Programm importiert.
- Jedes Bitmuster bleibt erhalten: nichts wird gerundet, und ein NaN behält seine Nutzlast.
let bits: Nat64 := float64Bits(1.0);
Bindings.BuildInfo
Bindet das, was der Compiler über seinen Build in das C des Programms schreibt.
- Es erreicht C mit @embed und ist deshalb ein unsicheres Modul.
- Standard.BuildInfo ist das sichere Modul darauf und dasjenige, das ein Programm importiert.
- Die Texte sind C-Strings aus den Daten des Programms, ihre Spans leben also so lange wie das Programm.
let hash: Span[Nat8, Static] := commitText();
Bindings.Context
Bindet, was Standard.Context vom Kernel und von der Laufzeit braucht: Pipes und eine Kette, auf der gewartet wird.
- Die Kette der lesenden Enden liegt in der Laufzeit, und ein Kontext wartet darauf mit poll(2).
- Beide Enden einer neuen Pipe sind close-on-exec.
- Es reicht Adressen an C weiter und ist deshalb ein unsicheres Modul.
- Standard.Context ist das sichere Modul darauf und dasjenige, das ein Programm importiert.
let root: Node := emptyNode();
let ready: Int32 := nodeReady(&root, 0);
releaseNode(root);
Bindings.Database
Bindet nichts nach außen: Es hält den Wert eines Treibers in einer Box, deren Typ den Treiber nicht nennt.
- Es bindet nichts nach außen, wandelt aber eine Box in den Typ um, den sein Aufrufer nennt, und ist deshalb ein unsicheres Modul.
- Standard.Database ist das sichere Modul darauf, und seine Treiber legen nur offen, was sie gelöscht haben.
- abandon kehrt nicht zurück.
let box: Erased := erase(connection);
Bindings.DuckDB
Bindet libduckdb, den Release-Build von DuckDB, über die Deklarationen von Bindings.C.Duckdb.
- Es reicht Handles per Wert und per Adresse an C weiter und bildet Spans über den Speicher eines Data Chunks.
- Es ist deshalb ein unsicheres Modul.
- Standard.DuckDB ist das sichere Modul darauf, dasjenige, das ein Programm importiert, und dort dokumentiert.
let opened: Either[DuckError, Connection] := connect("/var/db/stats.duckdb", false);
let changed: Either[DuckError, Nat64] := execute(&!db, "DELETE FROM hits", noValues());
Bindings.Entropy
Bindet getentropy, den Systemaufruf für ein paar Bytes sicherer Zufallszahlen, für die Schlüssel von Hash-Maps.
- Es ruft C mit @embed auf und ist deshalb ein unsicheres Modul.
- Standard.HashMap ist das sichere Modul darauf.
- Ein fehlgeschlagener Aufruf ergibt 0, was ein Aufrufer von einer seltenen echten 0 unterscheidet, indem er zweimal fragt.
let seed: Nat64 := entropyWord();
Bindings.File
Bindet die Datei-, Verzeichnis-, Prozess-, Umgebungs- und Socket-Aufrufe der C-Bibliothek, über die Laufzeit.
- Es gibt dem Kernel die Adresse der Bytes eines Strings und ist deshalb ein unsicheres Modul.
- Es hält einen Verzeichnisstrom als Zeiger.
- Standard.File ist das sichere Modul darauf und dasjenige, das ein Programm importiert.
- Ein Pfad ist hier eine Kopie mit einem Nullbyte dahinter und keinem darin, wie Standard.File ihn macht.
- Ein Pfad, der leer ist, nicht auf einem Nullbyte endet oder vor seinem Ende eines enthält, wird abgelehnt.
- Eine Anzahl reicht nie über den String hinaus, den sie benennt.
let number: Int32 := openRead(&path);
let read: Int64 := readInto(number, &!buffer, 0, 4096);
let closed: Bool := closeNumber(number);
Bindings.Libcrypto
Bindet libcrypto über die Deklarationen in Bindings.C.Libcrypto.
- Die Bibliothek ist die von OpenSSL auf Linux und die von LibreSSL auf OpenBSD.
- Es reicht Adressen an C weiter und ist deshalb ein unsicheres Modul.
- Standard.Libcrypto ist das sichere Modul darauf und dasjenige, das ein Programm importiert.
- Jede Funktion hier ist die gleichnamige von Standard.Libcrypto.
let digest: Option[String] := sha256(asSpan(&message));
let sealed: Option[String] := sealAes256Gcm(asSpan(&key), asSpan(&nonce), asSpan(&text), "");
Bindings.MariaDB
Bindet libmariadb, MariaDB Connector/C, über die Deklarationen von Bindings.C.Mariadb.
- Es füllt die Binding-Strukturen vorbereiteter Anweisungen in C und bildet Spans über C-Speicher.
- Es ist deshalb ein unsicheres Modul.
- Standard.MariaDB ist das sichere Modul darauf, dasjenige, das ein Programm importiert, und dort dokumentiert.
let opened: Either[MariaError, Connection] := connect("db.example.org", 3306, "shop", "app",
asSpan(&password), "/etc/ssl/cert.pem", false);
let changed: Either[MariaError, Nat64] := execute(&!db, "DELETE FROM carts", noValues());
Bindings.MlDsa
Bindet ML-DSA nach FIPS 204 aus dem libcrux-Code, den der Compiler mitführt, das Backend von Standard.MlDsa.
- Es ruft C mit @embed auf und ist deshalb ein unsicheres Modul.
- Standard.MlDsa ist das sichere Modul darauf und dasjenige, das ein Programm importiert.
- Dasselbe portable C läuft auf jedem System, ohne eine Bibliothek zu linken.
- Der libcrux-Code ist eine Übergangslösung: Sobald LibreSSL ML-DSA nativ anbietet, zieht dieses Backend nach libcrypto um, und der libcrux-Code wird entfernt.
- Jede Länge wird hier geprüft, bevor C ein Byte liest; eine falsche ergibt None oder false.
- Eine Stufe ist 44, 65 oder 87; jede andere ergibt None, false oder 0.
- In einem Task mit zu wenig verbleibendem Stack ergibt ein Aufruf None oder false, statt über die Schutzseiten hinaus zu laufen.
let made: Option[KeyBytes] := keypair(44, asSpan(&seed));
Bindings.Pcre
Bindet die 8-Bit-Bibliothek von PCRE2, libpcre2-8, über die Deklarationen in Bindings.C.Pcre2.
- Es reicht Adressen an C weiter und ist deshalb ein unsicheres Modul.
- Standard.Pcre ist das sichere Modul darauf und dasjenige, das ein Programm importiert.
- Es übersetzt ein Muster und führt eine Suche aus; jede Suche, die ein Programm sieht, baut darauf auf.
- Standard.PcreJit, das andere sichere Modul darauf, fragt zusätzlich den JIT von PCRE2 nach Maschinencode.
let compiled: Either[String, Code] := compileCode("[a-z]+", 1000000);
let found: Either[Int32, Option[Buffer[Index]]] := search(®ex, asSpan(&text), 0);
Bindings.Platform
Bindet die Namen des Systems und der Maschine, für die das Programm übersetzt wurde, und die seines Hosts, aus der Laufzeit.
- Es erreicht C mit @embed und ist deshalb ein unsicheres Modul.
- Standard.Platform ist das sichere Modul darauf und dasjenige, das ein Programm importiert.
- Das System und die Maschine stehen beim Übersetzen des Programms fest, aus den Makros des C-Compilers.
- Der Hostname wird beim ersten Mal einmal beim Kernel erfragt und behalten.
let system: Span[Nat8, Static] := systemText();
Bindings.Poller
Bindet den Poller der Laufzeit, der auf viele Deskriptoren zugleich wartet.
- Er wartet mit epoll auf Linux und mit kqueue auf OpenBSD, FreeBSD und macOS.
- Es erreicht C mit @embed und hält das Handle der Laufzeit als Zeiger, ist also ein unsicheres Modul.
- Standard.Poller ist das sichere Modul darauf und dasjenige, das ein Programm importiert.
let added: Bool := addDescriptor(&!handle, listener, 1);
let ready: Int32 := waitHandle(&!handle, 1000);
let first: Int32 := eventDescriptorAt(&handle, 0);
Bindings.Postgres
Bindet libpq, die Client-Bibliothek von PostgreSQL, über die Deklarationen von Bindings.C.Libpq.
- Es reicht Adressen und Arrays von Werten an C weiter und bildet Spans über ein Ergebnis: ein unsicheres Modul.
- Standard.Postgres ist das sichere Modul darauf, dasjenige, das ein Programm importiert und liest.
- Eine Verbindung läuft über TCP mit TLS, sslmode verify-full, gegen die angegebenen CA-Zertifikate.
- Alle Werte und alle Spalten reisen in der Binärform des Servers.
- Ein Fehler ist ein PgError: der SQLSTATE zur Basis 36 gelesen, oder ein negativer Code dieses Moduls.
- Ein Fehler beim Start wird im Cursor gehalten und von finish gemeldet.
let values: Values := withInt64(noValues(), 42);
let changed: Either[PgError, Nat64] := execute(&!db, "DELETE FROM cart WHERE id = $1", values);
Bindings.Resolve
Bindet den Resolver der C-Bibliothek, getaddrinfo, für Standard.Resolve.
- Es erreicht C mit @embed und ist deshalb ein unsicheres Modul; Standard.Resolve ist das sichere Modul darauf.
- Eine Funktion der Laufzeit kopiert jede Adresse aus der Antwort, bevor die Antwort freigegeben wird.
- Eine Antwort ergibt höchstens maximumAddresses Adressen; die übrigen des Resolvers fallen weg.
- Beide Familien kommen zurück, jede Adresse so, wie der Resolver sie gab; Standard.Resolve faltet eine gemappte zusammen.
- Ein Fehler ist eine Zahl: -1 keine Adresse für den Namen, -2 keine Antwort, -3 alles andere.
let found: Either[Int32, String] := resolveRecords("example.org");
Bindings.Resource
Bindet die Ressourcenaufrufe der Laufzeit: Limits, Verbrauch und die Uhr seit dem Start.
- Es erreicht C mit @embed und ist deshalb ein unsicheres Modul; Standard.Resource ist das sichere Modul darauf.
- Die Aufrufe sind getrlimit(2), setrlimit(2), getrusage(2) und die Uhr seit dem Systemstart.
- Eine Ressource ist ein Index: 0 das Datensegment, 1 offene Dateien, 2 die Größe einer Core-Datei.
- Kein Limit ist die größte Nat64.
- Eine Ablehnung des Kernels ergibt None oder false; nichts bricht ab.
- CPU-Zeit wird in Mikrosekunden gezählt, die größte residente Menge in Bytes.
let files: Option[LimitPair] := limitOf(1);
let raised: Bool := setLimitOf(1, 4096, 4096);
Bindings.Sandbox
Bindet die Aufrufe, die den Prozess einsperren, über die Funktionen der Laufzeit.
- Auf OpenBSD sind die Aufrufe pledge und unveil; auf Linux no new privileges, Landlock und seccomp.
- Auf macOS ist der Aufruf seatbelt, der das Programm unter einem Seatbelt-Profil neu startet.
- Auf FreeBSD ist der Aufruf capsicum, der die Pfade öffnet und den Capability-Modus betritt.
- Es gibt dem Kernel die Adresse der Bytes eines Strings und ist deshalb ein unsicheres Modul.
- Standard.Sandbox ist das sichere Modul darauf und dasjenige, das ein Programm importiert.
- Ein hier übergebener String endet auf einem Nullbyte und enthält kein weiteres, wie Standard.Sandbox ihn macht.
- Jede Funktion prüft das selbst und lehnt einen String ab, der es nicht erfüllt, ohne Aufruf.
- Eine Antwort ist eine Zahl: in der Regel 1 erledigt, 0 abgelehnt, -1 wenn die Plattform keinen solchen Mechanismus hat.
if platform() = 1 then
let pledged: Int32 := pledge(&promises);
end if;
Bindings.Secret
Bindet das Lesen eines Geheimnisses vom Terminal oder einem Deskriptor, in Speicher, der gelöscht wird, bevor er zurückgegeben wird.
- Es erreicht C mit @embed und ist deshalb ein unsicheres Modul.
- Standard.Secret ist das sichere Modul darauf und dasjenige, das ein Programm importiert.
- Der Puffer fasst secretCapacity Bytes; er wird bei jedem Fehler und bei seiner Zerstörung gelöscht.
case allocateRaw() of
when Some(value as raw: RawSecret) do
destroyRaw(raw);
when None do
skip;
end case;
Bindings.Serial
Bindet einen Zähler, den der ganze Prozess teilt, für Zahlen, die sich nie wiederholen dürfen.
- Es hält den Zähler in C mit @embed und ist deshalb ein unsicheres Modul.
- Standard.Graph ist das sichere Modul darauf.
- Der Zähler hält, wenn Tasks auf mehreren Threads ihn zugleich aufrufen.
- Nach der größten Nat64 läuft der Zähler auf 0 über, was der Aufrufer ablehnen muss.
let serial: Nat64 := nextSerial();
Bindings.Shared
Bindet die Mutexe der Laufzeit und die atomaren Operationen des C-Compilers.
- Der Wert, den ein Mutex schützt, liegt im Rohspeicher neben der Sperre, es ist also ein unsicheres Modul.
- Dieser Wert wird über eine Lesereferenz heraus- und wieder hineinbewegt.
- Ein Atomic ist ein Wort von 64 Bit.
- Standard.Shared ist das sichere Modul darauf und dasjenige, das ein Programm importiert.
- Jede Deklaration hier ist die gleichnamige von Standard.Shared.
let { value: Nat64, key: Key } := acquire(&mutex);
release(&mutex, value + 1, key);
Bindings.Signal
Bindet die Signalbehandlung der Laufzeit, mit alarm(2), getpid(2) und kill(2).
- Es erreicht C mit @embed und ist deshalb ein unsicheres Modul; Standard.Signal ist das sichere Modul darauf.
- Der Handler zählt Signale und schreibt ein Byte in eine Pipe der Laufzeit.
- Eine Art ist ein Index: 0 hangup, 1 interrupt, 2 terminate, 3 user1, 4 user2.
- Eine Überwachung ist jeweils aktiv; ein zweites watchMask schlägt fehl und installiert nichts.
- Die Zähler laufen von einer Überwachung zur nächsten weiter.
if watchMask(7) then
let woken: Bool := waitPipe(1000);
let stops: Nat32 := arrivals(2);
end if;
Bindings.SqlCipher
Bindet libsqlcipher, die C-Bibliothek von SQLCipher, über die Deklarationen in Bindings.C.SqlCipher.
- Es reicht Adressen an C weiter und bildet Spans über den Speicher von SQLite und ist deshalb ein unsicheres Modul.
- Standard.SqlCipher ist das sichere Modul darauf und dasjenige, das ein Programm importiert.
- Alles außer dem Schlüssel wird aus Bindings.Sqlite erzeugt.
- Jede Funktion ist die gleichnamige von Standard.SqlCipher, auf einer Verbindung ohne Modus.
case openWriter("app.db", makeKey(secret)) of
when Right(right as db: Connection) do
closeDatabase(db);
when Left(left as error: SqlError) do
printLn(describe(error));
end case;
Bindings.Sqlite
Bindet libsqlite3, die C-Bibliothek von SQLite, über die Deklarationen in Bindings.C.Sqlite3.
- Es reicht Adressen an C weiter und bildet Spans über den Speicher von SQLite und ist deshalb ein unsicheres Modul.
- Standard.Sqlite ist das sichere Modul darauf und dasjenige, das ein Programm importiert.
- Jede Funktion ist die gleichnamige von Standard.Sqlite, auf einer Verbindung ohne Modus.
case openWriter("app.db") of
when Right(right as db: Connection) do
closeDatabase(db);
when Left(left as error: SqlError) do
printLn(describe(error));
end case;
Bindings.SqliteReplica
Bindet libsqlite3 über die Deklarationen in Bindings.C.Sqlite3, für Anweisungen, deren Text erst beim Ausführen bekannt ist.
- Es reicht Adressen an C weiter und ist deshalb ein unsicheres Modul.
- Standard.SqliteReplica ist das sichere Modul darauf und dasjenige, das ein Programm importiert.
- Eine Anweisung wird einzeln vorbereitet und nach ihrem Lauf finalisiert; nichts wird zwischengespeichert.
- prepareOne lehnt Text ab, der mehr als eine Anweisung enthält oder keine.
- Die Sicherung kopiert eine ganze Datenbank von einer Verbindung in eine Datei oder aus einer Datei.
- Ein Fehler kommt als erweiterter Ergebniscode von SQLite zurück, oder als dessen Worte.
let opened: Either[String, Database] := openDatabase("data.db");
Bindings.SystemLog
Bindet das Systemlog, openlog(3), syslog(3) und closelog(3), über die Laufzeit.
- Es erreicht C mit @embed und ist deshalb ein unsicheres Modul; Standard.Log ist das sichere Modul darauf.
- Der Name wird auf 63 Bytes gekürzt; eine unbekannte Facility zählt als user, eine unbekannte Priorität als error.
- Text wird so gesendet, wie er ist, nie als Format gelesen, und endet an einem Nullbyte, falls er eines enthält.
- Erneutes Öffnen schließt die zuvor hergestellte Verbindung.
openSystemLog("shop", 1);
writeSystemLog(1, "started");
closeSystemLog();
Bindings.Task
Bindet die Fibers, Scopes und Kanäle der Laufzeit, die Tasks auf Threads ausführen.
- Das Argument und das Ergebnis eines Tasks liegen zwischen spawn und join im Rohspeicher: ein unsicheres Modul.
- Standard.Task ist das sichere Modul darauf und dasjenige, das ein Programm importiert.
- Der Compiler prüft, was einem Task übergeben wird, an den Aufrufen von Standard.Task, nicht hier.
- Jede Deklaration hier ist die gleichnamige von Standard.Task.
let started: Either[Nat64, Task[Nat64]] := spawn(&!scope, double, 21);
Bindings.Terminal
Bindet die Standardströme von libc, stdin, stdout und stderr, über die Laufzeit.
- Es erreicht C mit @embed und importiert C-Funktionen und ist deshalb ein unsicheres Modul.
- Standard.IO.Terminal ist das sichere Modul darauf und dasjenige, das ein Programm importiert.
- Bytes gehen einzeln durch die Ströme von libc; writeStderrText schreibt mit write(2).
- Ein Terminal wird durch die Nummer seines Deskriptors benannt.
if isTerminalNumber(0) then
let quiet: Bool := setEcho(0, false);
end if;
Bindings.Testing
Bindet, was Standard.Testing von der Laufzeit erfragt: ob das Programm unter valgrind läuft.
- Es erreicht C mit @embed und ist deshalb ein unsicheres Modul.
- Standard.Testing ist das sichere Modul darauf und dasjenige, das ein Test importiert.
- Nur ein Build von hilde test --valgrind fragt valgrind; jeder andere Build antwortet false.
let underValgrind: Bool := runningUnderValgrind();
Bindings.Time
Bindet die Uhren von libc über die Laufzeit, die clock_gettime aufruft.
- Es erreicht C mit @embed und ist deshalb ein unsicheres Modul.
- Standard.Time ist das sichere Modul darauf und dasjenige, das ein Programm importiert.
- Die Wanduhr ergibt null, wenn sie nicht gelesen werden konnte.
- Die monotone Uhr ergibt in einem Build ohne sie eine negative Zahl; hasMonotonic sagt, welcher Fall vorliegt.
if hasMonotonic() then
let now: Int64 := monotonicReading();
end if;
Bindings.Tls
Bindet libtls, die TLS-Bibliothek von OpenBSD und, als libretls über OpenSSL, von Debian.
- Es reicht Adressen über die Deklarationen in Bindings.C.Tls an C weiter und ist deshalb ein unsicheres Modul.
- Standard.Tls ist das sichere Modul darauf und dasjenige, das ein Programm importiert.
- Jede Funktion hier ist die gleichnamige von Standard.Tls, außer connectSocket und connectSocketAs.
- connectSocket ist die Hälfte von Standard.Tls.connectOver, die auf der Nummer des Sockets arbeitet.
- Ein Socket, der an connectSocket oder acceptSocket übergeben wird, bleibt offen, wenn der Client geschlossen wird.
- Ein Fehler kommt als String mit der Meldung zurück.
let client: Either[String, Client] := connect("example.org", "443", "/etc/ssl/cert.pem");
Bindings.Watch
Bindet das Beobachten von Dateien in der Laufzeit: inotify auf Linux, kqueue auf den BSDs.
- Es erreicht C mit @embed und gibt ihm die Adresse der Bytes eines Pfads: ein unsicheres Modul.
- Standard.Watch ist das sichere Modul darauf.
- Ein Pfad endet auf einem Nullbyte und enthält kein weiteres; watchAdd lehnt einen ab, der das nicht erfüllt.
- Watcher und Watches sind Zahlen; eine negative Zahl ist ein Fehler.
- watchWait ergibt -2, wenn die Zeit ablief, und -1 bei einem Fehler.
let watcher: Int32 := watchOpen();
let watch: Int32 := watchAdd(watcher, &path, true);
let changed: Int32 := watchWait(watcher, 5000);
Bindings.X509
Bindet die X.509- und Schlüsselfunktionen von libcrypto, die Standard.X509 braucht.
- Jeder Aufruf ist ein C-Ausdruck auf den OpenSSL-Headern des Systems, gehalten an seinen wirklichen Prototyp.
- Es reicht Adressen an C weiter und ist deshalb ein unsicheres Modul.
- Standard.X509 ist das sichere Modul darauf und dasjenige, das ein Programm importiert.
- Jede Funktion ist die von Standard.X509, die ihr Docstring nennt, auf den rohen Zahlen der Arten.
- rsaBits nimmt nur die drei RSA-Arten; jede andere Art hält das Programm an.
- Arten: 1 P-256, 2 P-384, 3 Ed25519, 4 bis 6 RSA mit 2048, 3072 und 4096 Bit, 7 P-521.
- Die Arten 8 bis 10 sind ML-DSA-44, -65 und -87; 0 steht für keine Art.
- Key und Cert sind linear: dropKey und dropCert geben sie frei.
let key: Option[Key] := newKey(1);
Standard.About
Der volle Versionstext und der Infotext eines Programms, aus seinem Build und seiner Plattform.
- Eine Zeile je Feld, der Name in den ersten zwölf Spalten und der Wert ab Spalte 13.
- Die Felder sind version, commit, built, compiler und system, dann zuletzt das Copyright.
- fullVersionWith fügt vor dem Copyright eine Zeile library für jede gelinkte C-Bibliothek hinzu, die das Programm nennt.
- info fügt hinzu, was das laufende Programm sieht: Binary, Host und die Zeit, gelesen beim Aufruf.
- Der Commit trägt (dirty), wenn der Arbeitsbaum Änderungen enthielt; keinen, wenn der Build keinen Commit hatte.
- Die Build-Zeit ist UTC, in der Form 2026-10-17T19:30:00Z.
- Ein Programm, das dieses Modul importiert, trägt Standard.BuildInfo mit sich; eines, das es nicht importiert, trägt keines von beiden.
setFullVersion(&!command, fullVersion("1.2.0", "2026 Example Corp"));
Standard.Account
Die Konten des Systems: die Nummern eines Kontos zu seinem Namen, und als wer der Prozess läuft.
- Ein Konto wird in der Passwortdatenbank nachgeschlagen, wie getpwnam(3) es tut; nichts wird verändert.
- Ein Name, der länger als 255 Bytes ist oder ein Nullbyte enthält, ist kein Konto.
case lookupAccount("_notes") of
when AccountFound(numbers as n: AccountNumbers) do
useOwner(n.user, n.group);
when NoAccount do
refuse();
when LookupFailed do
refuse();
end case;
Standard.Acme
Ein Client des ACME-Protokolls nach RFC 8555, mit dem eine CA Zertifikate für DNS-Namen ausstellt.
- Jede Anfrage ist ein JWS, mit dem Konto-Schlüssel signiert, einem P-256-Schlüssel, und damit ES256.
- Jede Anfrage trägt eine frische Nonce des Servers und die URL, an die sie geht.
- Anfragen gehen über Standard.HttpClient per HTTPS, geprüft gegen die Zertifikate in caFile.
- Eine Antwort badNonce wird einmal mit der mitgebrachten Nonce wiederholt, wie es die RFC verlangt.
- Jedes andere Problem, das der Server meldet, kommt als Left mit Typ und Detail zurück.
- Die Schritte sind getrennte Funktionen: zwischen ihnen weist der Aufrufer die Kontrolle über jeden Namen nach.
let opened: Either[AcmeError, Account] := openAccount(directory, key, "", caFile, limits);
let order: Either[AcmeError, Order] := newOrder(&!account, "example.org,www.example.org");
let challenge: Either[AcmeError, Challenge] := challengeOf(&!account, authorization, "dns-01");
let record: String := dnsChallengeValue(&!account, asSpan(&token));
let ready: Either[AcmeError, Unit] := respond(&!account, challengeUrl);
Standard.Archive
Archive in tar, tar mit gzip und zip, ganz gelesen und im Speicher geschrieben.
- Das Lesen bleibt innerhalb eines Budget: so viele Einträge, Bytes in einem Eintrag und Bytes insgesamt.
- Das Budget wird zuerst an den Headern geprüft und noch einmal, während ein komprimierter Eintrag entpackt wird.
- Das Lesen prüft die Header, aber nicht die Namen, die so aufgelistet werden, wie das Archiv sie hält.
- Das Extrahieren beurteilt zuerst jeden Namen: absolut, leer, ein Nullbyte oder ein Teil .. wird abgelehnt.
- Ein Name, unter dem im Ziel bereits ein symbolischer Link liegt, wird ebenfalls abgelehnt.
- Ein symbolischer Link wird nur angelegt, wenn sein Ziel, von seinem eigenen Verzeichnis aus gesehen, innen bleibt.
- Ein Hardlink wird nie extrahiert, und nichts, was existiert, wird überschrieben.
- Modi werden auf 0755 für ein Verzeichnis und 0644 für eine Datei gekappt, was immer das Archiv sagt.
case readArchive(asSpan(&bytes), TarGz(), defaultBudget()) of
when Right(right as archive: Archive) do
let unpacked: Either[Failure, Unit] := extractArchive(&archive, "/var/www/site");
destroyArchive(archive);
when Left(left as failure: Failure) do
report(failure);
end case;
Standard.Arena
Ein Bump-Allokator hinter einem linearen Handle, für den Speicher einer Arbeitseinheit wie einer Anfrage.
- Nur Code, der eine Arena um Speicher bittet, allokiert aus ihr; sonst nichts.
- Eine Referenz oder ein Span, den sie ausgibt, trägt die Region ihrer Ausleihe und kann sie nicht überleben.
- Die Arena kann nicht zurückgesetzt oder freigegeben werden, solange sie ausgeliehen ist.
- Die Arena wächst nie: eine Anforderung über ihre Kapazität hinaus ergibt None.
- Sie hält nur Free-Werte, weil resetArena alles auf einmal freigibt.
- resetArena und freeArena überschreiben, was benutzt wurde.
case createArena(65536) of
when Some(value as made: Arena) do
var arena: Arena := made;
borrow r: &![Arena, R] := &!arena do
let got: Option[Span![Int32, R]] := arenaSpan(r, 100);
end borrow;
arena := resetArena(arena);
freeArena(arena);
when None do
abort("No memory for the arena.");
end case;
Standard.Arguments
Das Lesen der Kommandozeile eines Programms gegen ein deklariertes Kommando, seine Optionen und Unterkommandos.
- Jede Deklaration gibt einen Schlüssel zurück, ein falsch geschriebener Name ist also einer, den der Compiler nicht kennt.
- Nichts auf der Kommandozeile hält das Programm an: jeder Fehler kommt als ArgumentError zurück.
- Ein Fehler nennt das Argument, bei dem er gefunden wurde, und bei einem fast richtigen Namen den gemeinten Namen.
- Die Syntax ist POSIX mit langen Optionen; Optionen und Positionsargumente dürfen gemischt werden.
- -h, --help sowie -V und --version, sobald eine Version gesetzt ist, sind eingebaut, ebenso help und version als Wörter; --full nach einer Version fragt den vollen Text ab.
- -v -s -c -e und --verbose --sandbox --config --env --pepper --socket --json sind für ihre eine Bedeutung reserviert.
- Hilfe, Verwendung, eine mdoc-Manpage und Vervollständigung für bash, zsh und fish kommen aus den Deklarationen.
- Optionsnamen und Wörter sind ASCII; ein Textwert muss UTF-8 sein, und ein Pfad wird als seine Bytes genommen.
- In der Hilfe und in Meldungen wird ein Byte, das kein UTF-8 ist, als \xNN gezeigt.
var command: Command := makeCommand("fetch", "Fetches a file.");
let verbose: FlagKey := addVerbose(&!command, "Say more.");
let output: PathKey := addPath(&!command, option("output", 0, "Where to write."));
let arguments: Buffer[String] := processArguments();
let parsed: Either[ArgumentError, Outcome] := parse(&command, &arguments);
Standard.Array
Arrays von Zahlen mit Form, Strides und Elementtyp, und Views, die ihre Bytes teilen.
- Ein Array besitzt seine Bytes und ist linear; destroy gibt sie zurück. Eine View leiht sie nur aus.
- Der Rang ist 1 bis maxRank, jede Ausdehnung mindestens 1, und die Elementzahl höchstens maxElements.
- Slicing, Auswählen, Vertauschen von Achsen und Broadcasting erzeugen eine neue View und kopieren kein Element.
- Das Broadcasting folgt den Regeln von NumPy: Achsen werden vom Ende her ausgerichtet, und eine Ausdehnung von 1 wird gedehnt.
- Die Elementtypen sind Nat8, Int32, Int64, Float32, Float64 und Bool, benannt durch die Codes der Typfunktionen.
- Eine falsche Form, ein falscher Elementtyp oder Index ist ein requires, das scheitert, und das Programm hält dort an.
- Ein Element wird in Zeit linear im Rang gefunden.
var a: Array := zeros(float64Type(), shape2(2, 3));
setFloat64(&!a, 4, 2.5);
let n: Index := size(&a);
destroy(a);
Standard.ArrayWire
Die Binärform eines besitzenden Arrays: ein Header mit Magic, Version, Elementtyp, Rang und Form, dann die Elemente.
- Alle Zahlen sind Little Endian: acht Bytes für eine Ausdehnung, die Breite des Typs für ein Element.
- Der Decoder prüft Magic, Version, Elementtyp, Rang, jede Ausdehnung und die Länge, bevor er ein Element nimmt.
- Ein Wert, der abgeschnitten, zu lang, widersprüchlich oder außerhalb des Bereichs ist, wird mit dem Grund abgelehnt, und dafür wird nichts allokiert.
- Das Dekodieren braucht Zeit linear in der Länge, und das Kodieren schreibt genau encodedSize Bytes.
- Nur ein gepacktes Array wird geschrieben; eine View vorher kopieren.
let written: Index := encode(&a, &!out);
case decode(received) of
when Right(right as b: Array) do
destroy(b);
when Left(left as e: DecodeError) do
skip;
end case;
Standard.ArrayCompute
Arithmetik über Arrays: elementweise Operationen mit Broadcasting, Reduktionen über das ganze Array und entlang einer Achse, und das Zusammenführen von Teilergebnissen.
- add, subtract, multiply und divide verbinden zwei Views nach den Regeln von NumPy und ergeben ein neues Array der gedehnten Form; negate und absolute bilden eine View ab.
- Die Elementtypen sind Int32, Int64, Float32 und Float64; beide Operanden tragen denselben Typ, und nichts wird heraufgestuft.
- Ein Ganzzahlüberlauf und eine Division durch null halten das Programm an, wie es die Operatoren der Sprache tun.
- sum, product, minimum, maximum und mean ergeben ein Array aus einem Element; mit angehängtem Axis reduzieren sie entlang einer Achse und ergeben ein Array ohne diese Achse.
- Die Elemente werden in der zeilenweisen Reihenfolge der View besucht, und minimum und maximum nehmen ein Element nur, wenn es kleiner oder größer vergleicht, die Regel des Sperli-Protokolls.
- combine führt zwei Arrays von Teilergebnissen unter dem Reduktionscode des Protokoll-Headers zusammen, 1 bis 4; ganze Zahlen laufen an ihrer Breite über, und nichts wird gebroadcastet.
var a: Array := zeros(float64Type(), shape2(2, 3));
setFloat64(&!a, 4, 2.5);
let total: Array := sum(view(&a));
destroy(total);
destroy(a);
Standard.AtomicFile
Eine Datei so schreiben, dass jeder, der sie öffnet, den alten oder den neuen Inhalt findet, nie einen Teil davon.
- Der Inhalt geht in eine temporäre Datei daneben, wird synchronisiert und über die alte Datei umbenannt.
- Das Verzeichnis wird nach dem Umbenennen synchronisiert, damit das Umbenennen einen Absturz übersteht.
- Eine ersetzte Datei behält Eigentümer und Gruppe; lassen sie sich nicht setzen, wird nichts ersetzt.
- Die Datei erhält genau den angegebenen Modus; die umask ändert ihn nicht.
- Nur eine gewöhnliche Datei wird ersetzt; ein symbolischer Link wird weder verfolgt noch überschrieben.
- Eine Sicherung ist ein Hardlink auf die alte Datei, benannt mit .bak, und behält so Eigentümer, Modus und Zeiten.
- Das Verzeichnis muss für eine Sicherung Hardlinks erlauben.
let mode: Nat32 := 420;
let written: Either[AtomicFailure, Bool] := writeWithBackup("app.conf", asSpan(&text), mode);
let change: Either[AtomicFailure, Change] := writeIfChanged("app.conf", asSpan(&text), mode);
Standard.Base32
Base32 nach RFC 4648, Abschnitt 6, die Form, in der die Geheimnisse von Einmalpasswörtern geschrieben werden.
- Das Alphabet ist A bis Z und 2 bis 7; fünf Bytes werden zu acht Zeichen.
- Die aufgefüllte Form endet mit = bis zu einem Vielfachen von acht Zeichen; die nicht aufgefüllte hat keines.
- Das Dekodieren nimmt Groß- und Kleinbuchstaben gleich, da Menschen diese Geheimnisse eintippen.
- Ein Byte außerhalb des Alphabets, eine Länge, die keine Kodierung hat, oder falsch gesetztes Padding gibt None.
- Übrige Bits am Ende, die nicht null sind, geben ebenfalls None; bei schlechter Eingabe bricht nichts ab.
let secret: String := encodeBase32Unpadded(asSpan(&key));
let back: Option[String] := decodeBase32(asSpan(&secret));
Standard.Base64
Base64 nach RFC 4648, Abschnitt 4, mit oder ohne Padding.
- Das Alphabet ist A bis Z, a bis z, 0 bis 9, Plus und Schrägstrich; drei Bytes werden zu vier Zeichen.
- Die aufgefüllte Form endet mit einem oder zwei = , sodass ihre Länge ein Vielfaches von vier ist.
- Die nicht aufgefüllte Form lässt sie weg, wie es die Zeichenketten von Passwort-Hashes tun.
- Das Dekodieren lehnt ab, statt zu raten: schlechte Eingabe gibt None, und nichts bricht ab.
let text: String := encode(asSpan(&bytes));
let back: Option[String] := decode(asSpan(&text));
let salt: Option[String] := decodeUnpadded("c2FsdHNhbHQ");
Standard.Bits
Liest die Bits einer Zahl als vorzeichenlose Zahl derselben Breite und zurück, für Formate, die Zahlen als Bytes speichern.
- Jedes Bitmuster bleibt erhalten: nichts wird gerundet, und ein NaN behält seine Nutzlast.
- Eine aus ihren Bits zurückgelesene Zahl ist dieselbe Zahl.
- Die Bytes einer Zahl, niedrigstes zuerst, sind die Bytes ihrer Bits.
let one: Float64 := float64FromBits(float64Bits(1.0));
Standard.Bounded
Eine Typklasse für den kleinsten und den größten Wert eines Typs.
- minBound und maxBound nehmen nichts und können nicht fehlschlagen.
- Instanzen: Bool, Nat8 bis Nat64, Int8 bis Int64 und Index.
- Für Bool ist der kleinste false und der größte true.
- Für einen natürlichen Typ ist der kleinste 0.
let top: Nat32 := maxBound();
let bottom: Int64 := minBound();
Standard.Box
Ein Wert auf dem Heap, im Besitz eines linearen Handles.
- Eine Box ist die Art, wie ein rekursiver Typ auf sich selbst verweist, und ein Muster in einem case sieht durch sie hindurch.
- Eine Box ist linear: unbox gibt sie zurück und liefert den Inhalt.
- Eine fehlgeschlagene Allokation gibt den Wert als Left zurück, sodass nichts verloren geht.
case makeBox(7) of
when Right(right as box: Box[Nat64]) do
let seven: Nat64 := unbox(box);
when Left(left as value: Nat64) do
skip;
end case;
Standard.Buffer
Ein linearer, wachsender Array auf dem Heap.
- Jeder Index und jeder Span wird geprüft; einer außerhalb des Bereichs hält das Programm mit einer Meldung an.
- Das Wachsen bricht ab, wenn der Speicher nicht zu haben ist; die sicheren Hersteller geben stattdessen None.
- Die Leser, die kopieren, nehmen Free-Elemente; nthRef und swapNth funktionieren auch für lineare.
- Ein Buffer ist linear: destroyEmpty oder destroyFree gibt ihn zurück.
- insertFront kostet Zeit linear in der Länge des Buffers.
var prices: Buffer[Nat64] := allocateEmpty();
insertBack(&!prices, 250);
let first: Nat64 := nth(&prices, 0);
destroyFree(prices);
Standard.BuildInfo
Was ein Programm über seinen eigenen Build weiß: Commit, Zustand des Arbeitsbaums, Zeit, Compiler und eigene Werte.
- Der Compiler füllt es nur für ein Programm, das dieses Modul importiert; git läuft nur dann.
- Der Commit besteht aus 40 oder 64 Hexziffern oder ist leer, wenn der Build nicht in einem git-Arbeitsbaum lief oder mit --no-build-vcs.
- Die Zeit ist SOURCE_DATE_EPOCH, wenn es gesetzt war, sodass ein Build zweimal dieselben Bytes liefern kann.
- Werte kommen aus --build-value=name=value: Namen von 1 bis 64 Bytes, Texte von höchstens 1024 Bytes.
- Jeder Text lebt so lange wie das Programm; keine Funktion allokiert.
printLn(commit());
case value("channel") of
when Some(value as channel: Span[Nat8, Static]) do
printLn(channel);
when None do
printLn("no channel");
end case;
Standard.Cache
Ein Cache für Werte unter Schlüsseln, mit einer Lebensdauer je Eintrag und Grenzen für Anzahl und Gesamtgröße.
- Die Zeit kommt vom Aufrufer in monotonen Nanosekunden; eine Lebensdauer von 0 läuft nie ab.
- Ist er voll, geht der am längsten nicht benutzte Eintrag zuerst, bei jedem Lauf auf dieselbe Weise gewählt.
- Ein put in einen vollen Cache kostet Zeit linear in der Zahl der Einträge.
- Jeder Eintrag, der den Cache verlässt, kommt als Paar aus Schlüssel und Wert zum Aufrufer zurück.
- Schlüssel sind Free, und die Leser kopieren, also nehmen sie nur Free-Werte.
- Ein Füllvorgang, der vor einem clear oder einem flush begann, speichert nichts.
var cache: Cache[Nat64, Nat64] := makeCache(1000, 1048576, 60000000000);
let price: Option[Nat64] := getWith(&!cache, item, nowNanos(), lookUpPrice);
Standard.Calendar
Daten und Tageszeiten im gregorianischen Kalender, Zeitzonen aus Zonendateien und RFC-3339-Text.
- Ein Instant zählt Sekunden und Nanosekunden seit 1970-01-01T00:00:00Z, ohne Schaltsekunden.
- Ein Civil ist, was eine Uhr zeigt: ein Datum vom Jahr 0 bis 9999 und eine Tageszeit, ohne Zone.
- Eine Zone wird aus einer Datei im Format von RFC 8536, Version 2 oder später, höchstens eine MiB, gelesen.
- Nach dem letzten Übergang, den eine Zonendatei aufführt, gibt die Regel ihrer Fußzeile den Versatz.
- Außerhalb der Jahre 0 bis 9999 geben die Funktionen None; eine Sekunde 60 wird nie gelesen.
- Standard.Time liest die Uhren; dieses Modul rechnet nur und liest Zonendateien.
let zone: Either[ZoneFailure, Zone] := loadZone("Europe/Berlin");
let stamp: Option[Stamp] := parseRfc3339("2026-09-27T01:10:00+02:00");
Standard.Cbor
CBOR, wie RFC 8949 es definiert, streng in ein Dokument gelesen und Element für Element durchsucht.
- Ein Element wird beim Lesen von seinem ersten bis zu seinem letzten Byte geprüft, und das Dokument bleibt erhalten.
- Nur der kürzeste Kopf einer Zahl oder einer Länge wird genommen, wie es die deterministische Kodierung verlangt.
- Unbestimmte Längen, Tags und einfache Werte außer false, true, null und undefined werden abgelehnt.
- Verschachtelung tiefer als mostDepth und eine Map mit mehr als mostEntries Paaren werden abgelehnt.
- Ein doppelter Schlüssel in einer Map wird abgelehnt, verglichen über seine kodierten Bytes.
- Textzeichenketten werden auf UTF-8 geprüft.
- Das Lesen lehnt ab, statt zu raten, und eine Ablehnung nennt den Fehler und den Byte-Offset.
- Nichts hier bricht bei schlechter Eingabe ab; Zeit linear in der Länge mal mostEntries.
case readCbor(asSpan(&coseKey)) of
when Right(right as key: Cbor) do
let top: CborItem := rootOf(&key);
let entries: Option[Index] := countOf(&key, top);
destroyCbor(key);
when Left(left as refused: CborError) do
printLn(refused.offset);
end case;
Standard.CompositeSig
Signiert und verifiziert mit dem Komposit aus ML-DSA-44 und Ed25519 des IETF-Entwurfs für Kompositsignaturen.
- Die signierte Nachricht ist M' = Präfix, Label, die Länge des Kontexts, der Kontext und der SHA-512 der Nachricht.
- Die Signatur ist die von ML-DSA-44 über M' mit dem Label als Kontext, dann die von Ed25519 über M'.
- verify gilt nur, wenn beide gelten: dass ein Verfahren allein bricht, fälscht keine Signatur.
- Das Label benennt den Gebrauch; openSshLabel ist das von OpenSSHs ssh-mldsa44-ed25519.
- Ein Schlüssel besteht aus zwei Seeds von 32 Bytes, ML-DSA zuerst, wie OpenSSH ihn hält; der öffentliche Schlüssel sind die beiden öffentlichen Schlüssel.
let signed: Option[String] := sign(&key, openSshLabel(), asSpan(&message), "");
Standard.Context
Abbruch und Fristen, die eine ganze Kette von Aufrufen und Tasks erreichen.
- Ein Context ist beendet, wenn sein Canceller oder der eines Contexts über ihm abgebrochen wurde.
- Er ist ebenso beendet, wenn seine Frist oder die eines Contexts über ihm abgelaufen ist.
- Ein Context wird per Lese-Referenz nach unten gereicht, auch in die Tasks eines ausleihenden Scopes.
- Ein Abbruch wird sofort gesehen, von jedem Thread aus.
- Eine Kette aus n Ebenen hält höchstens 2n Deskriptoren und ist höchstens maxDepth tief.
- Context und Canceller sind linear: jeder Canceller wird abgebrochen, jeder Context zerstört.
let root: Context := background();
case withTimeout(&root, 5000) of
when Some(value as made: Derived) do
let { context: Context, canceller: Canceller } := made;
let budget: Nat32 := remainingMilliseconds(&context, 1000);
cancel(canceller);
destroyContext(context);
when None do
skip;
end case;
destroyContext(root);
Standard.Cookie
HTTP-Cookies nach RFC 6265: ein Cookie aus einem Cookie-Header gelesen, ein Set-Cookie-Wert geschrieben.
- Ein hier geschriebener Cookie hat kein Domain-Attribut, sodass nur der Host, der ihn gesetzt hat, ihn zurückbekommt.
- strictOptions gibt Secure, HttpOnly, SameSite=Strict und kein Ablaufdatum.
- Ein Name, Wert oder Pfad, der den Header beenden oder ein Attribut beginnen könnte, wird abgelehnt, nicht maskiert.
- Ein Wert außerhalb des druckbaren ASCII muss vom Aufrufer zuerst kodiert werden, als Hexadezimal oder Base64.
- Jede Ablehnung kommt als None zurück.
let header: Option[String] := setCookie("session", token, "/", strictOptions());
let sent: Option[String] := cookieValue(asSpan(&cookieHeader), "session");
Standard.Cose
COSE-Schlüssel nach RFC 9052 und RFC 9053, umgewandelt in die öffentlichen DER-Schlüssel, mit denen Standard.X509 verifiziert.
- Ein Schlüssel mit kty 2 und crv 1, 2 oder 3 ist NIST P-256, P-384 oder P-521; x und y werden genommen, ein komprimierter Punkt wird abgelehnt.
- Ein Schlüssel mit kty 1 und crv 6 ist Ed25519; ein Schlüssel mit kty 3 ist RSA mit n und e, von mindestens 2048 Bit.
- Nennt der Schlüssel alg, muss es das sein, was X509.verifyMessage prüft: -7, -35 und -36 für die Kurven, -8 für Ed25519, -257 für RSA.
- Jeder andere Schlüssel, eine Koordinate falscher Länge und Bytes, die keine CBOR-Map sind, geben None.
- Nichts hier bricht bei schlechter Eingabe ab.
case publicKeyFromCose(asSpan(&credentialKey)) of
when Some(value as der: String) do
let good: Bool := verifyMessage(asSpan(&der), asSpan(&signed), asSpan(&signature));
destroyString(der);
when None do
printLn("not a key");
end case;
Standard.Csrf
Schutz gegen Cross-Site-Request-Forgery: ein an die Sitzung gebundenes Token und Origin-Header.
- Das Token ist der HMAC-SHA256 des Sitzungstokens unter einem Schlüssel des Servers und wird nie gespeichert.
- Das Token einer anderen Sitzung ist nichts wert, und ein leeres Sitzungstoken ist nie gültig.
- Ein Schlüssel kürzer als csrfKeyBytes gibt None von csrfToken und false von csrfValid.
- csrfValid vergleicht in derselben Zeit, welche Bytes auch immer vorliegen.
- sameOriginRequest ist eine zweite Verteidigungslinie, aus den Headern Sec-Fetch-Site und Origin.
let token: Option[String] := csrfToken(serverKey, sessionToken);
if changingMethod(verb) and not csrfValid(serverKey, sessionToken, submitted) then
refuse();
end if;
Standard.Csv
Tabellen in CSV, wie RFC 4180 sie beschreibt, gelesen und geschrieben.
- Ein in Anführungszeichen gesetztes Feld darf das Trennzeichen, ein verdoppeltes Anführungszeichen und Zeilenumbrüche enthalten; ein einfaches nicht.
- Zeilen enden mit CR LF oder LF, die letzte auch mit keinem von beiden; eine leere Zeile ist keine Zeile.
- Das Lesen lehnt ab, statt zu raten, mit der Zeile und dem Byte, wo der Fehler gefunden wurde.
- Jede Zeile hat so viele Felder wie die erste; Text, der kein UTF-8 ist, wird abgelehnt.
- Eine Byte Order Mark am Anfang wird übersprungen; bei schlechter Eingabe bricht nichts ab.
- Das Schreiben setzt ein Feld nur dann in Anführungszeichen, wenn es muss, und beendet jede Zeile mit CR LF.
case readCsv(text) of
when Right(right as table: CsvTable) do
let rows: Index := rowCount(&table);
destroyCsv(table);
when Left(left as error: CsvError) do
report(error.line, error.offset);
end case;
Standard.Database
Eine Schnittstelle über SQLite, SQLCipher, PostgreSQL und MariaDB, das Backend beim Öffnen gewählt.
- Ein Backend ist nur im Programm, wenn sein Treibermodul importiert wird: Standard.Database.Sqlite, Standard.Database.Postgres oder Standard.Database.MariaDB.
- Der Code, der liest und schreibt, bleibt derselbe, welches Backend auch geöffnet wurde.
- Werte gehen an die Fragezeichen einer Anweisung und werden nie Teil ihres Textes.
- Für PostgreSQL wird jedes Fragezeichen außerhalb von Zeichenketten, Namen in Anführungszeichen und Kommentaren zu $1, $2, ...
- Werte sind Int64, Float64, Bool, Text, Bytes, Null und Zeitstempel in Mikrosekunden seit 1970 UTC.
- In SQLite wird ein Bool als die Ganzzahl 0 oder 1 gespeichert und ein Zeitstempel als seine Ganzzahl.
- Database[M] ist linear und muss geschlossen werden; execute nimmt nur eine Database[Writing].
- Eine Transaction endet mit commit oder rollback, ein Cursor mit finish; der Text einer Spalte lebt für seine Zeile.
- isUniqueViolation, isConstraintViolation und isBusy ordnen einen DbError auf jedem Backend gleich ein.
case openSqlite("app.db") of
when Right(right as opened: Database[Writing]) do
var db: Database[Writing] := opened;
let values: Values := withText(withInt64(noValues(), 7), "Ada");
case execute(&!db, "INSERT INTO people (id, name) VALUES (?, ?)", values) of
...
end case;
closeDatabase(db);
...
end case;
Standard.Database.MariaDB
Der MariaDB-Treiber von Standard.Database: Ein Programm, das ihn importiert, kann eine MariaDB-Verbindung öffnen.
- Er bindet libmariadb, und nur ein Programm, das ihn importiert, tut das.
- Die Fragezeichen einer Anweisung werden so gesendet, wie sie stehen.
case openMariaDB("db.example", 3306, "app", "app", password, "/etc/ssl/ca.pem") of
when Right(right as opened: Database[Writing]) do
...
end case;
Standard.Database.Postgres
Der PostgreSQL-Treiber von Standard.Database: Ein Programm, das ihn importiert, kann eine PostgreSQL-Verbindung öffnen.
- Er bindet libpq, und nur ein Programm, das ihn importiert, tut das.
- Die Fragezeichen einer Anweisung werden vor dem Senden zu $1, $2, ...
case openPostgres("db.example", 5432, "app", "app", password, "/etc/ssl/ca.pem") of
when Right(right as opened: Database[Writing]) do
...
end case;
Standard.Database.Sqlite
Der SQLite- und SQLCipher-Treiber von Standard.Database: Ein Programm, das ihn importiert, kann diese Dateien öffnen.
- Er bindet libsqlcipher, und nur ein Programm, das ihn importiert, tut das.
- SQLite ist SQLCipher ohne Schlüssel geöffnet, was SQLites eigenes Dateiformat schreibt.
case openSqlite("app.db") of
when Right(right as opened: Database[Writing]) do
...
end case;
Standard.DatabaseUsers
Ein Speicher für Standard.Users in einer Datenbank von Standard.Database, auf jedem Backend, das sie hat.
- Die Benutzer liegen in der Tabelle hilde_users, die fehlgeschlagenen Versuche in hilde_user_failures.
- openDatabaseUsers legt die beiden Tabellen und ihren Index an, wenn sie fehlen, in den Typen des Backends.
- Der Speicher hält die Datenbank; closeDatabaseUsers gibt sie zurück.
case openDatabaseUsers(db) of
when Right(right as opened: DatabaseUsers) do
var users: DatabaseUsers := opened;
let added: Either[UserError, Bool] := addUser(&!users, "alice", asSpan(&password));
closeDatabase(closeDatabaseUsers(users));
when Left(left as failed: DatabaseUsersFailed) do
let { db: Database[Writing], error: UserError } := failed;
closeDatabase(db);
end case;
Standard.Decimal
Dezimalzahlen für Geld: exakt, mit höchstens 18 Ziffern und höchstens 18 davon nach dem Komma.
- Eine Zahl ist ein Vorzeichen, ganze Einheiten und eine Skala, die Ziffern nach dem Komma: 12.34 ist 1234 bei Skala 2.
- Addieren und Subtrahieren sind exakt, ebenso das Multiplizieren, solange das Produkt in 18 Ziffern passt.
- Das Dividieren wird auf die Skala gerundet, die der Aufrufer nennt; 0.1 plus 0.2 ist 0.3.
- Es wird kaufmännisch gerundet, also von null weg: 2.345 ist 2.35 bei Skala 2, und -2.345 ist -2.35.
- Nichts bricht ab: Überlauf, Division durch null und ein Text, der keine Zahl ist, geben None.
case parseDecimal("19.99") of
when Some(value as price: Decimal) do
let total: Option[Decimal] := multiplyDecimal(price, quantity);
when None do
skip;
end case;
Standard.Doctor
Prüft, dass die Dateien, Verzeichnisse, der Socket und das Konto, die ein Programm deklariert, so vorhanden sind, wie sie sein sollen, und legt an, was fehlt.
- Eine Zeile je Prüfung, ok oder missing, und für ein fehlendes der Befehl, der es auf diesem System richtet.
- Eine Datei oder ein Verzeichnis muss die Art, den Modus von Standard.Paths und den Eigentümer haben: das Dienstkonto, oder bei einem Benutzerwerkzeug den laufenden Benutzer.
- Ein Socket muss eine Verbindung annehmen; es wird nichts hineingeschrieben.
- Die Befehle eines Dienstes beginnen mit doas auf OpenBSD und mit sudo sonst, wenn der Prozess nicht der Superuser ist.
- Pfade in den Befehlen werden für die Shell in Anführungszeichen gesetzt, wenn sie mehr als Buchstaben, Ziffern und / . _ - + : @ % , = enthalten.
- runCheckup ändert nichts; runInit legt nur an, was fehlt, überschreibt nichts und führt seine Befehle ohne Shell aus.
- Für einen Dienst mit Socket gibt init die Blöcke FastCGIserver und UrlToolkit aus, die eine Hiawatha-Site an ihn übergeben.
var checkup: Checkup := makeCheckup("notes", Service());
expectPath(&!checkup, DataDirectory(), "/var/notes");
var out: StringBuilder := makeStringBuilder();
let missing: Index := runCheckup(&checkup, false, &!out);
destroyCheckup(checkup);
Standard.DuckDB
DuckDB, die Datenbank für Analyse, die innerhalb des Programms über einer eigenen Datei läuft.
- Jedes Öffnen schaltet das Lesen und Schreiben anderer Dateien und das Laden oder Abrufen von Erweiterungen ab.
- Die Konfiguration wird beim Öffnen gesperrt, sodass keine Anweisung diese wieder einschaltet.
- Jeweils ein Programm öffnet eine Datei zum Schreiben, und kein Leser öffnet sie, solange ein Schreiber sie hat.
- Werte gehen an die Fragezeichen einer Anweisung und werden nie Teil ihres Textes.
- Null ist eine Option; ein Zeitstempel ist in Mikrosekunden seit 1970-01-01 UTC.
- Jeder Spaltenleser nimmt nur seine eigenen Typen; jeder andere Typ hält das Programm an.
- Ein doppelter Schlüssel und ein Konflikt zwischen zwei Schreibern haben eigene Codes.
case openWriter("facts.duckdb") of
when Right(right as opened: Connection[Writing]) do
var db: Connection[Writing] := opened;
let values: Values := withText(withInt64(noValues(), 7), "Ada");
case execute(&!db, "INSERT INTO people VALUES (?, ?)", values) of
...
end case;
closeDatabase(db);
...
end case;
Standard.Equality
Typklassen für partielle und totale Äquivalenzrelationen.
- PartialEquality ist symmetrisch und transitiv, muss aber nicht reflexiv sein.
- Equality ist eine totale Äquivalenz: reflexiv, symmetrisch und transitiv.
- Beide vergleichen über Lese-Referenzen, sodass keine ihre Argumente verbraucht.
- Der Compiler prüft diese Gesetze nicht; einer Instanz wird vertraut, sie einzuhalten.
- Dieses Modul gibt beide Instanzen für Unit, wo zwei beliebige Werte gleich sind.
let same: Bool := equal(&left, &right);
Standard.Failure
Ein Fehlschlag für das Left eines Either: seine Operation, seine Art, seine Meldung und der Fehlschlag dahinter.
- Er ist für die Grenze eines Moduls oder eines Programms gedacht, wo ein Aufrufer nach der Art entscheidet.
- Jede Operation auf dem Weg nach außen kann einen Fehlschlag unter ihrem eigenen Namen umhüllen, mit oder ohne Art.
- Die Art einer Kette ist die erste, die nicht Unclassified ist, von der äußersten bis zur ersten Ursache.
- Eine Art wird auf einen HTTP-Status abgebildet, sodass ein Server damit eine Anfrage beantworten kann.
- Ein Failure ist linear: destroyFailure gibt die ganze Kette zurück.
- Das Umhüllen hält das Programm an, wenn kein Speicher mehr da ist, wie es eine Zeichenkette tut.
let failure: Failure := makeFailure("file.write", Unclassified(), "disk full");
let outer: Failure := wrapFailure("store.save", failure);
let line: String := describeFailure(&outer);
destroyFailure(outer);
Standard.Fcgi
Das FastCGI-Record-Protokoll von der antwortenden Seite: Records aus Bytes gelesen und zu Bytes gebaut.
- Dieses Modul ist das Format und nichts sonst: kein Socket und keine Schleife.
- Der Code, der den Deskriptor besitzt, reicht Bytes an es und nimmt Bytes von ihm.
- Jede Länge, die auf der Leitung ankommt, wird gegen die Bytes geprüft, die wirklich da sind.
- Ein Record, der nicht in das passt, was übrig ist, wird mit None abgelehnt, nie über seinen Puffer hinaus gelesen.
- Ein geschriebener Record hält weniger als 65536 Bytes Inhalt; einen längeren aufzuteilen ist Sache des Aufrufers.
case readHeader(bytes, 0) of
when Some(value as header: RecordHeader) do
let content: Option[String] := readContent(bytes, 0, header);
let next: Index := recordLength(header);
when None do
skip;
end case;
Standard.FcgiServer
Ein FastCGI-Responder: die Schleife, die die Anfragen eines Webservers über einen Handler bedient.
- Der Handler bildet einen Request auf die CGI-Ausgabe ab: Header-Zeilen, eine leere Zeile und den Rumpf.
- Jede Verbindung wird von einem eigenen Task bedient, höchstens workers davon gleichzeitig.
- Dieser Task hat 512 KiB Stack, für einen Handler, der C-Bibliotheken aufruft.
- Eine Verbindung über workers hinaus wartet, bis eine davon fertig ist.
- Eine gehaltene Verbindung bedient eine Anfrage nach der anderen; eine zweite gleichzeitige wird abgelehnt.
- Parameter, Rumpf und die Zeit, die eine Anfrage zum Eintreffen haben darf, werden an die Grenzen gehalten.
- Eine Anfrage über einer Grenze wird mit 431 oder 413 beantwortet und ihre Verbindung geschlossen.
- Eine späte Anfrage oder eine, die nicht geparst wird, wird ohne Antwort geschlossen.
- Mit serveUnixWith hat jeder laufende Task allein einen der angegebenen Zustände.
function hello(request: Request): String is
destroyRequest(request);
return cgiResponse(200, "text/plain", "hello");
end;
let accepted: Option[Index] := serveUnix(&listener, hello, defaultLimits(), &!signals);
Standard.File
Dateien, Pipes, Verzeichnisse, Kindprozesse, die Umgebung und Sockets von Unix und TCP.
- Ein FileDescriptor ist linear: Er kann nicht kopiert werden und nicht fallengelassen werden, ohne ihn zu schließen.
- Jeder Pfad wird geprüft, bevor er den Kernel erreicht; ein Pfad mit einem Nullbyte wird abgelehnt.
- Jeder Deskriptor wird mit close-on-exec geöffnet.
- Ein Kindprozess führt eine Binärdatei aus, die über ihren Pfad angegeben wird; PATH wird nicht durchsucht.
- In einem Kind wird zuerst jeder Deskriptor über den drei Standard-Deskriptoren geschlossen.
- Ein Umgebungswert länger als environmentLimit wird abgelehnt, nicht gekürzt.
- secureDelete überschreibt einmal; auf Flash-Speicher und Copy-on-Write-Dateisystemen können alte Blöcke überleben.
case openRead("notes.txt") of
when Some(value as opened: FileDescriptor) do
var file: FileDescriptor := opened;
let text: Option[String] := readAll(&!file);
let closed: Bool := closeFile(file);
when None do
skip;
end case;
Standard.FileHelpers
Helfer über Standard.File zum Kopieren, Messen, Benennen und Entfernen von Dateien und Bäumen.
- Nichts hier folgt einem symbolischen Link: Ein Link in einem kopierten Baum wird als Link kopiert.
- Ein Link in einem gemessenen Baum zählt nichts, und ein Link, wo eine Datei erwartet wird, wird abgelehnt.
- Eine Kopie ersetzt nie, was an ihrem Ziel liegt; sie schlägt stattdessen fehl.
- Ein Baum tiefer als 64 Verzeichnisse wird abgelehnt, statt durchlaufen zu werden.
- Ein Fehlschlag ist ein Failure, dessen Operation die Funktion nennt und dessen Meldung den Pfad nennt.
- Seine Art ist NotFound für eine fehlende Quelle, Invalid für ein schlechtes Argument, sonst Unclassified.
case copyDirectory("site", "site-backup") of
when Right(right as done: Unit) do
skip;
when Left(left as failure: Failure) do
let line: String := describeFailure(&failure);
destroyFailure(failure);
end case;
Standard.Graph
Ein gerichteter Graph, dessen Knoten und Kanten je einen Wert tragen.
- Eine NodeId oder EdgeId eines entfernten Knotens oder einer entfernten Kante oder eines anderen Graphen bekommt None, false oder Left.
- Eine solche Nummer erreicht nie einen anderen Knoten oder eine andere Kante.
- Werte dürfen linear sein, und nichts wird fallengelassen: Was den Graphen verlässt, kommt zum Aufrufer zurück.
- Ein Graph wird nur leer mit destroyEmpty oder mit Free-Werten mit destroyFree zerstört.
- Jeder Durchlauf und jede Suche hat eine feste Reihenfolge, bei jedem Lauf und auf jeder Plattform gleich.
- Die Instanzen von Weight und von TotalOrder für NodeId und EdgeId kommen mit jedem Import von hier.
var graph: Graph[Nat32, Nat64] := makeGraph();
let from: NodeId := addNode(&!graph, 1);
let to: NodeId := addNode(&!graph, 2);
let added: Either[Nat64, EdgeId] := addEdge(&!graph, from, to, 5);
let reached: Bool := hasPath(&graph, from, to);
Standard.Grapheme
Graphemcluster, die Zeichen, die ein Leser sieht, nach den Regeln von Unicodes Standard Annex 29.
- Die Regeln sind die von Unicode Version 17.0.0.
- Ein e mit kombiniertem Akut ist ein Cluster, ebenso eine Flagge aus zwei regionalen Indikatoren.
- Emoji, verbunden durch Zero Width Joiner, eine Hangul-Silbe in Teilen und eine Devanagari-Ligatur sind je eines.
- CR und LF zusammen sind ein Cluster.
- Text ist UTF-8; ein Byte, das kein wohlgeformtes UTF-8 beginnt, beendet den Cluster davor.
let clusters: Option[Index] := countClusters(asSpan(&name));
let firstEnd: Option[Index] := clusterEnd(asSpan(&name), 0);
Standard.Gzip
Kompression in den Formaten von gzip: DEFLATE nach RFC 1951, roh oder in einem gzip-Member nach RFC 1952.
- In Hilde geschrieben; es bindet keine Bibliothek.
- Es dekomprimiert jeden gültigen Strom: gespeicherte Blöcke und Blöcke mit festen und dynamischen Codes.
- Es komprimiert mit festen Codes oder gespeicherten Blöcken und findet Wiederholungen höchstens 32 KiB zurück.
- Ein Strom von zlib auf seiner Standardstufe ist kleiner als das, was es schreibt.
- Das Dekomprimieren hält an einer Grenze an, die der Aufrufer nennt, da wenige Bytes Gigabytes beschreiben können.
- Jede Länge, Distanz und jeder Code wird vor der Nutzung geprüft; ein Strom, der eine Regel verletzt, wird abgelehnt.
let packed: String := gzip(asSpan(&page));
case gunzip(asSpan(&packed), 1048576) of
when Right(right as unpacked: String) do
skip;
when Left(left as failure: GzipFailure) do
skip;
end case;
Standard.Hash
Schlüsselbasiertes Hashing mit SipHash und die Typklasse Hashable, die eine Hash-Map von ihren Schlüsseln braucht.
- SipHash nimmt einen Schlüssel von 128 Bit, als zwei Nat64, und eine Nachricht und gibt 64 Bit.
- Ohne den Schlüssel kann ein Angreifer nicht viele Schlüssel einer Map wählen, die auf einen Slot fallen.
- SipHash-1-3 ist die Variante für Hash-Maps: eine Runde je Wort und drei am Ende.
- SipHash-2-4 ist langsamer und gedacht, wo die Ausgabe das Programm verlässt.
- Zwei Werte, die unter der Ordnung ihres Typs gleich sind, müssen unter demselben Schlüssel denselben Hash geben.
- Die Ganzzahltypen, Index und String sind hier Hashable; sipHash13 und sipHash24 schlagen nie fehl.
let tag: Nat64 := sipHash24(key0, key1, asSpan(&message));
let slot: Nat64 := hashOf(&name, key0, key1);
Standard.HashMap
Eine Hash-Map, deren Schlüssel mit SipHash-1-3 unter einem eigenen Schlüssel gehasht werden.
- makeHashMap schlüsselt eine Map aus der sicheren Zufallsquelle, sodass niemand von außen Abfragen langsam machen kann.
- makeHashMapKeyed nimmt den Schlüssel vom Aufrufer, für eine Map, die sich bei jedem Lauf gleich verhält.
- keyAt und valueAt erreichen jeden Eintrag über seine Position, von 0 bis zur Größe minus eins.
- Das Entfernen eines Eintrags schiebt den letzten an seinen Platz; Standard.Map hält seine Einträge sortiert.
- Ein Schlüsseltyp ist Hashable und hat eine TotalOrder, deren equal entscheidet, ob zwei Schlüssel derselbe sind.
- Eine Instanz wird nur durch einen Import aus Standard.Hash oder Standard.Order gesehen.
var ages: HashMap[Nat64, Nat32] := makeHashMap();
let replaced: Option[Pair[Nat64, Nat32]] := put(&!ages, id, 36);
let age: Option[Nat32] := get(&ages, &id);
destroyFree(ages);
Standard.Html
HTML nach dem Tokenizer des HTML-Standards in Tokens gelesen, sein Text herausgezogen oder bereinigt.
- Ein Dokument ist die Liste seiner Tokens: Text, Start- und End-Tags, Kommentare und Doctypes.
- Tag- und Attributnamen stehen in Kleinbuchstaben; Zeichenreferenzen werden in UTF-8 dekodiert.
- Der Inhalt von script, style, textarea und title ist ein Text-Token: Ein Tag in einem script ist keines.
- sanitize behält nur, was eine Richtlinie erlaubt, und wirft Elemente für Code, Frames und Formulare ganz weg.
- Anderer Inhalt eines nicht erlaubten Elements bleibt, als maskierter Text.
- Eine URL bleibt nur mit dem Schema http, https oder mailto oder ohne eines.
- sanitize schließt jedes Element in der Reihenfolge, in der es geöffnet wurde, und hält höchstens 100 gleichzeitig offen.
- Die Ausgabe von sanitize ist wohlgeformt und maskiert, was die Eingabe auch war.
let clean: String := sanitize(asSpan(&comment), basicPolicy());
let plain: String := textOf(asSpan(&page));
Standard.Htmx
Die Serverseite von htmx: seine Anfragen unterschieden, seine Antwort-Header geschrieben, sein Skript ausgeliefert.
- Eine Anfrage ist die von htmx, wenn ihr Header HX-Request die vier Bytes true ist und kein anderer Wert.
- Eine Antwort, die von HX-Request abhängt, sagt das mit Vary: HX-Request.
- HX-Redirect nimmt einen Feldwert und HX-Trigger einen Ereignisnamen; alles andere hält das Programm an.
- Die Header-Zeilen kommen in der Form heraus, die FcgiServer.cgiResponseWith nimmt.
- Das Skript wird aus einer Datei von höchstens scriptLimit Bytes gelesen; eine fehlende oder größere Datei gibt 404 oder 500.
if isHtmxRequest(asSpan(&hxRequest)) then
let line: String := redirectHeader("/done");
let answer: String := htmlResponse(200, asSpan(&line), "");
end if;
Standard.Http
Fragen an einen HTTP-Header-Block, beantwortet aus dem Text des Blocks, wie er steht.
- Ein Block sind Zeilen, getrennt durch CRLF oder ein einzelnes LF, die an der ersten leeren Zeile oder am Ende enden.
- Jede Frage nimmt Zeit linear in der Länge des Blocks und behält nichts davon.
- Header-Namen werden ohne Rücksicht auf Groß- und Kleinschreibung verglichen, wie RFC 9110 sie definiert.
- Eine Zeile ohne Doppelpunkt hat weder Name noch Wert.
- Ein Header-Wert, der eine Liste ist, wird Element für Element gelesen, mit erlaubtem Leerraum um jedes Element.
let length: Option[String] := headerValue(asSpan(&headers), "Content-Length");
let upgrade: Bool := valueHasToken(asSpan(&connection), "upgrade");
Standard.HttpClient
Ein HTTPS-Client: eine Anfrage, eine Antwort, dann wird die Verbindung geschlossen.
- Nur https-URLs werden gelesen.
- Das Zertifikat des Servers wird gegen die Zertifikate in einer Datei und den Hostnamen verifiziert.
- Jede Anfrage hat ein Zeitlimit zum Verbinden, eines für den ganzen Austausch und ein Größenlimit.
- Standardmäßig wird ein Host abgelehnt, der eine private oder reservierte Adresse ist oder auf eine solche auflöst.
- Weiterleitungen werden nicht verfolgt: Der Aufrufer sieht den Status und den Location-Header.
- Die Zeit zum Auflösen des Namens ist nicht begrenzt, wie Standard.Resolve sagt.
let answer: Either[Failure, Response] :=
request(Get(), "https://example.org/status", "", "", "/etc/ssl/cert.pem", defaultLimits());
Standard.I18n
Übersetzte Texte aus einem Katalog, der Teil des Programms ist, für Templates und für Code.
- Ein Schlüssel ist ein Fall einer Union, eine Sprache eine Funktion mit einem case darüber: Ein unbekannter und ein fehlender Schlüssel lassen sich nicht übersetzen.
- Die Sprache wird bei jedem Aufruf über ihren Code erfragt; nichts wird gespeichert, und mehrere Tasks dürfen gleichzeitig übersetzen.
- Ein Code, den keine Sprache hat, gibt die Standardsprache des Katalogs.
- Ein Text hält Platzhalter {name}, ersetzt durch die Argumente; {{ ist eine geschweifte Klammer, und ein Platzhalter ohne Argument bleibt, wie er geschrieben ist.
- Pluralformen nach den sechs Pluralkategorien von Unicode; englishPlural und germanPlural sind gegeben, ein Katalog darf eine eigene Regel mitbringen.
- Die Richtung und das BCP-47-Tag einer Locale werden allein aus ihrem Code gelesen, nur lesend, für die Attribute lang und dir einer Seite.
var arguments: Arguments := makeArguments();
addText(&!arguments, "name", "Ada");
let greeting: String := translate(messages(), "de", Greeting(), None(), &arguments);
destroyArguments(arguments);
Standard.IO
Die Capability für den Zugang zum Terminal und die Typklassen für Byte-Ströme.
- Eine TerminalCapability ist linear und stammt nur aus der Root-Capability.
- Standard.IO.Terminal nimmt sie entgegen, um die Standardausgabe, die Fehlerausgabe und die Eingabe herauszugeben.
- ByteOutputStream schreibt jeweils ein Byte in einen Strom.
- ByteInputStream liest jeweils ein Byte als Option, sodass ein Strom enden kann.
var terminal: TerminalCapability := acquireTerminal(&!root);
var output: StandardOutput := acquireOutput(&!terminal);
writeByte(&!output, 10);
releaseOutput(output);
releaseTerminal(terminal);
Standard.IO.Terminal
Die Standardausgabe, die Standardfehlerausgabe und die Standardeingabe eines Programms.
- Jeder Strom ist linear, wird über eine TerminalCapability erworben und nach Gebrauch freigegeben.
- readLine liest bis zu einem Wagenrücklauf, einem Zeilenvorschub oder dem Ende der Eingabe.
- readHiddenLine liest ein Passwort ohne Echo; ein Signal, während das Echo aus ist, lässt es ausgeschaltet.
- terminalWidth gibt 0 zurück, wenn die Breite unbekannt ist; das ist kein schmales Terminal.
- writeErrorText und drainInput geben false zurück, wenn der Kernel die Anfrage abgelehnt hat.
var terminal: TerminalCapability := acquireTerminal(&!root);
var input: StandardInput := acquireInput(&!terminal);
let line: String := readLine(&!input);
releaseInput(input);
releaseTerminal(terminal);
Standard.Ini
Konfigurationsdateien in der INI-Form: Zeilen der Art name = wert, gruppiert unter [section]-Köpfen.
- Namen vor dem ersten Kopf gehören zum Abschnitt mit dem leeren Namen.
- Eine Zeile, deren erstes Byte nach dem Leerraum # oder ; ist, ist ein Kommentar.
- Ein Wert, der mit einem Backslash endet, geht in der nächsten Zeile weiter.
- Namen und Werte verlieren den Leerraum um sich herum; ein # innerhalb eines Werts gehört zu ihm.
- Derselbe Name zweimal in einem Abschnitt: der spätere zählt.
- Nichts wird abgelehnt: Eine Zeile ohne bekannte Form wird übersprungen und von skippedLines gezählt.
- Ein lokaler Wert hat Vorrang vor einem Standardwert, auch wenn er leer ist; das schaltet den Standardwert ab.
- Ein Schalter ist an für yes und true in beliebiger Groß- und Kleinschreibung, und aus für alles andere.
let file: IniFile := readIni(asSpan(&text));
let defaults: IniFile := readIni(asSpan(&shipped));
let port: Option[String] := iniValueWith(&file, &defaults, "server", "port");
let verbose: Bool := iniSwitch(&file, "log", "verbose");
destroyIni(defaults);
destroyIni(file);
Standard.Ip
IP-Adressen, IPv4 und IPv6: parsen, ausgeben, maskieren, abgleichen und anonymisieren.
- Eine Adresse ist ein Free-Wert: Sie wird kopiert, verglichen und liegt in einem Record wie eine Zahl.
- Nichts hier berührt das Netz: keine Abfrage, kein Socket und keine Namensauflösung.
- Das Parsen ist strikt und gibt None für alles außerhalb der Formen zurück, die der jeweilige Parser nennt.
- Text kommt in der Form heraus, die RFC 5952 für eine IPv6-Adresse verlangt, und für IPv4 als Dotted Quad.
- Adressen verschiedener Familien sind nie gleich und nie im selben Subnetz.
- Anonymisieren kürzt eine Adresse auf ihr /24 oder /48; einen Weg zurück gibt es nicht.
case parseIp(asSpan(&peer)) of
when Some(value as address: IpAddress) do
let inside: Bool := inSubnet(address, office, 24);
let logged: String := ipToString(anonymise(address));
when None do
printLn("not an address");
end case;
Standard.Json
JSON nach der Definition von RFC 8259, streng in ein Dokument gelesen und über einen Writer geschrieben.
- Ein Text wird beim Lesen vom ersten bis zum letzten Byte geprüft, und das Dokument bleibt erhalten.
- Ein Wert wird über einen JSON Pointer nach RFC 6901 gefunden; einer, der ins Leere führt, gibt None zurück.
- Ein Cursor durchläuft die Kinder eines Arrays oder Objekts in Zeit linear in seinem Text.
- Das Lesen lehnt ab, statt zu raten, und eine Ablehnung nennt den Fehler und den Byte-Offset.
- Ein Name doppelt in einem Objekt, ein einzelnes Surrogat und eine Verschachtelung tiefer als mostDepth werden abgelehnt.
- Nichts hier bricht bei fehlerhafter Eingabe ab.
- Zahlen bleiben als der Text erhalten, in dem sie geschrieben wurden; integerAt liest eine innerhalb von Int64.
- Ein Writer setzt Kommas, Doppelpunkte und Anführungszeichen und maskiert, was ein String braucht.
- Ein Aufruf eines Writers in falscher Reihenfolge verdirbt ihn, und finishJson gibt dann None zurück.
var writer: JsonWriter := makeJsonWriter();
beginObject(&!writer);
writeName(&!writer, "id");
writeInteger(&!writer, 42);
endObject(&!writer);
let text: Option[String] := finishJson(writer);
case readJson(asSpan(&body)) of
when Right(right as document: Json) do
let id: Option[Int64] := integerAt(&document, "/id");
destroyJson(document);
when Left(left as refused: JsonError) do
printLn(refused.offset);
end case;
Standard.Jws
JSON Web Signatures in der kompakten Form von RFC 7515, JSON Web Keys und OpenID-Connect-Claims.
- Öffentliche Schlüssel werden aus JSON Web Keys nach RFC 7517 und RFC 7518 gelesen.
- Der Algorithmus ergibt sich aus der Art des Schlüssels und aus nichts sonst.
- ES256, ES384 und ES512 für P-256, P-384 und P-521, EdDSA für Ed25519, RS256 für RSA.
- ML-DSA-44, ML-DSA-65 und ML-DSA-87 folgen dem Entwurf der JOSE-Arbeitsgruppe.
- Ein Token, dessen Header einen anderen Algorithmus nennt als den des Schlüssels, wird abgelehnt, und none gilt nie.
- ECDSA-Signaturen sind die beiden Zahlen nebeneinander, wie RFC 7518 es verlangt, nicht DER.
case verifyWithJwks(asSpan(&token), asSpan(&keySet)) of
when Some(value as payload: String) do
let problem: Option[String] :=
claimsProblem(asSpan(&payload), issuer, clientId, asSpan(&nonce), now, 60);
when None do
printLn("token refused");
end case;
Standard.Libcrypto
Authentifizierte Verschlüsselung, Schlüsselvereinbarung, Signaturen, Hashes und sichere Zufallsbytes.
- Jede Funktion ruft die libcrypto des Systems auf.
- Schlüssel der Chiffren und Kurven haben 32 Bytes und Nonces 12, bei XChaCha20 24; ein HMAC-Schlüssel darf jede Länge haben.
- Eine versiegelte Nachricht ist der Chiffretext gefolgt von einem 16-Byte-Tag.
- Eine falsche Eingabelänge oder ein Fehler der Bibliothek gibt None oder false zurück, ohne zu sagen, welcher.
- Eine Nonce darf mit einem Schlüssel nie zweimal verwendet werden, und der Aufrufer zählt sie.
- Das Öffnen einer Nachricht, deren Tag nicht passt, gibt None zurück und nichts von der Nachricht.
- Ein Code aus einem HMAC wird mit Standard.Text.constantTimeEqual geprüft, nie mit einem einfachen Vergleich.
case randomBytes(12) of
when Some(value as nonce: String) do
let sealed: Option[String] :=
sealChaCha20Poly1305(asSpan(&key), asSpan(&nonce), asSpan(&message), "");
destroyString(nonce);
when None do
printLn("no random bytes");
end case;
Standard.Log
Log-Einträge mit einer Stufe, einer Nachricht und benannten Feldern, je eine Zeile.
- Eine Zeile geht an die Standardfehlerausgabe, in eine Datei oder in das Systemlog.
- Eine Zeile ist Text, time=... level=... msg=... und Felder der Form name=wert, oder ein JSON-Objekt.
- Die Zeit ist UTC mit Millisekunden.
- Ein Textwert wird in Anführungszeichen gesetzt, wenn er leer ist oder ein Leerzeichen, ein Gleichheitszeichen, ein Anführungszeichen oder einen Backslash enthält.
- In Nachricht, Namen und Werten wird ein Tabulator oder ein Zeilenvorschub zu einem Leerzeichen.
- Andere C0- und C1-Steuerzeichen, das Löschzeichen und fehlerhaftes UTF-8 werden zu einem Punkt.
- Ein Byte eines Namens außerhalb von Buchstaben, Ziffern und _ . - wird zu _.
- Eine Zeile wird erst geschrieben, wenn sie vollständig ist; eine Logdatei wird zum Anhängen mit Modus 0640 geöffnet.
var log: Log := logToError(acquireError(&!terminal), Info(), TextLines());
let entry: Entry := addInteger(makeEntry(Warning(), "slow request"), "millis", millis);
let written: Bool := writeEntry(&!log, entry);
let closed: Bool := closeLog(log);
Standard.Map
Eine Map von Schlüsseln auf Werte, geordnet nach der TotalOrder der Schlüssel aus Standard.Order.
- Eine Suche braucht O(log n) Vergleiche; put und remove brauchen Zeit O(n) in der Zahl der Einträge.
- Einträge kommen in der Reihenfolge der Schlüssel, auf jedem Lauf und jeder Plattform gleich.
- Schlüssel und Werte dürfen linear sein, und was die Map verlässt, geht an den Aufrufer zurück.
- Ein put über einen Schlüssel, den die Map hält, gibt den alten Schlüssel und den alten Wert zurück.
- Eine Map wird nur zerstört, wenn sie leer ist oder wenn beide Typen Free sind.
- Wer String- oder Integer-Schlüssel verwendet, importiert aus Standard.Order, sonst scheitert der Aufruf mit H3061.
var stock: Map[Nat64, Nat64] := makeMap();
let old: Option[Pair[Nat64, Nat64]] := put(&!stock, item, 12);
let count: Option[Nat64] := get(&stock, &item);
destroyFree(stock);
Standard.MariaDB
Ein Client für MariaDB über die libmariadb des Systems, in der Form von Standard.Postgres.
- Dieselben Typen und Funktionen wie Standard.Postgres, sodass Standard.Database es als Backend hält.
- Jede Verbindung läuft über erzwungenes TLS, das Zertifikat wird gegen caFile geprüft, den Namen eingeschlossen.
- Ein Statement wird vorbereitet; seine Werte werden getrennt von seinem Text an seine Fragezeichen gebunden.
- Ein Zeitstempel sind Mikrosekunden seit 1970-01-01 UTC, gesendet als UTC-Datum und -Uhrzeit der Jahre 0 bis 9999.
- Spalten werden so gelesen, wie libmariadb sie umwandelt, und eine Umwandlung, die es ablehnt, hält das Programm an.
- Ein Fehler trägt die MariaDB-Fehlernummer oder einen der negativen Codes dieses Moduls.
- Eine Migration, die auf halbem Weg scheitert, wird nicht rückgängig gemacht.
case openWriter("db.local", 3306, "shop", "app", asSpan(&password), "ca.pem") of
when Right(right as db: Connection[Writing]) do
let values: Values := withText(withInt64(noValues(), 7), "apple");
let changed: Either[MariaError, Nat64] :=
execute(&!db, "insert into stock (id, name) values (?, ?)", values);
closeDatabase(db);
when Left(left as failed: MariaError) do
printLn(describe(failed));
end case;
Standard.MlDsa
Signiert und verifiziert mit ML-DSA nach FIPS 204 auf den Stufen 44, 65 und 87.
- Ein Schlüssel wird aus einem Seed von 32 Bytes erzeugt, sodass derselbe Seed immer denselben Schlüssel ergibt.
- Das Signieren ist die reine Form von FIPS 204 mit einem Kontext von höchstens 255 Bytes, abgesichert mit 32 Zufallsbytes.
- Das Verhalten ist auf jedem System gleich: Das Backend bringt sein eigenes portables C und SHAKE mit.
- Der Seed und der expandierte geheime Schlüssel werden gelöscht, wenn der Schlüssel zerstört wird.
- verify gibt false für jede Länge zurück, die nicht passt; es bricht nie ab.
- In einem Task braucht ein Aufruf 256 KiB Stack-Rest, 576 KiB zum Signieren auf Stufe 87; mit weniger gibt er None oder false zurück.
let key: Option[MlDsaKey] := generateKeyPair(Level44());
Standard.Normalization
Die vier Normalisierungsformen von Unicode für UTF-8-Text, NFD, NFC, NFKD und NFKC.
- Die Daten stammen aus der Unicode Character Database 17.0.0.
- NFD und NFC erhalten, was ein Text bedeutet, und unterscheiden sich nur darin, wie seine Akzente geschrieben werden.
- NFKD und NFKC falten zusätzlich Kompatibilitätsvarianten, etwa Ligaturen, in ihre einfachen Formen.
- Zwei Texte, die gleich vergleichen sollen, vergleichen nach derselben Form Byte für Byte gleich.
- Text, der kein wohlgeformtes UTF-8 ist, wird mit None abgelehnt, nicht repariert.
case toNfc(asSpan(&typed)) of
when Some(value as normal: String) do
printLn(asSpan(&normal));
destroyString(normal);
when None do
printLn("not UTF-8");
end case;
Standard.Order
Typklassen für Halb- und Totalordnungen, mit den Totalordnungen von Integers und Strings.
- Jeder Integer-Typ und Standard.String hat hier eine TotalOrder, für Sortierungen und geordnete Maps.
- Die Ordnung von String geht Byte für Byte, vorzeichenlos; ein Präfix steht vor dem längeren String.
- Diese Ordnung kennt weder Locale noch Groß- und Kleinschreibung; für gültiges UTF-8 ist sie die Ordnung der Codepoints.
- Ein Modul, das String- oder Integer-Schlüssel ordnet, importiert etwas von hier, um die Instanzen zu sehen.
case compare(&left, &right) of
when LessThan do
printLn("before");
when Equal do
printLn("same");
when GreaterThan do
printLn("after");
end case;
Standard.Otp
Einmalpasswörter, HOTP nach RFC 4226 und TOTP nach RFC 6238, wie Authenticator-Apps sie erzeugen.
- Ein Code ist der HMAC eines Zählers unter dem gemeinsamen Geheimnis, gekürzt auf 6 bis 8 Dezimalstellen.
- Bei TOTP ist der Zähler die Zahl der Schritte seit 1970, in der Regel zu 30 Sekunden.
- SHA-1 ist, was die Apps verwenden; SHA-256 und SHA-512 sind, was der RFC hinzufügt.
- verifyTotp nimmt auch Codes von window Schritten davor und danach an und vergleicht in konstanter Zeit.
- Ein einmal angenommener Code muss vom Aufrufer gemerkt werden, damit er nicht zweimal angenommen wird.
let accepted: Bool :=
verifyTotp(asSpan(&secret), asSpan(&code), nowSeconds, 30, 6, OtpSha1(), 1);
Standard.Password
Passwörter, als Argon2id-Hashes in der PHC-String-Form gespeichert und dagegen geprüft.
- Ein Hash liest sich $argon2id$v=19$m=65536,t=3,p=4$, dann Salt, $ und Tag in Base64 ohne Padding.
- Ein neuer Hash nimmt 65536 Kibibyte, 3 Durchgänge, 4 Lanes, ein zufälliges Salt von 16 Bytes und ein Tag von 32 Bytes.
- Kein Aufrufer wählt schwächere Werte.
- Ein gespeicherter Hash wird mit den Werten geprüft, die er nennt, innerhalb von Grenzen, die dieses Modul setzt.
- Die Tags werden in konstanter Zeit verglichen.
- Ein defekter gespeicherter String ist eine Antwort, Unreadable, nie ein Abbruch.
- Ein alter bcrypt-Hash, markiert mit 2a, 2b oder 2y, wird über die Bibliothek des Systems geprüft, nie erzeugt.
- Ein passender bcrypt-Hash gibt MatchesStoreAgain zurück, und der Aufrufer speichert einen neuen Hash.
- Ein Hash mit Pepper liest sich $argon2id-hmac$v=19$m=65536,t=3,p=4$, dann Salt, $ und der HMAC-SHA256 des Tags unter dem Pepper.
- Ein Pepper hat mindestens 32 Bytes und wird getrennt von den gespeicherten Hashes aufbewahrt.
let stored: Option[String] := hashPassword(asSpan(&typed));
let check: PasswordCheck := checkPassword(asSpan(&typed), asSpan(&saved));
Standard.Path
Einen Pfad zerlegen und zusammensetzen, als Text.
- Keine Funktion fragt den Kernel: Jede Antwort betrifft den String, nicht das, was dort liegt.
- Ein Pfad ist ein String; einen eigenen Typ dafür gibt es nicht.
- Das Trennzeichen ist / und kein anderes.
- Nichts normalisiert: Das Entfernen von .. aus einem Pfad mit einem symbolischen Link ändert, wohin er führt.
- Ein aus Eingaben zusammengesetzter Pfad wird zuerst mit hasParentComponent geprüft und bei true abgelehnt.
if hasParentComponent(asSpan(&upload)) then
printLn("refused");
else
let target: String := joinPath("/var/spool/in", asSpan(&upload));
let suffix: Option[String] := extension(asSpan(&target));
end if;
Standard.Paths
Wo ein Dienst oder ein Benutzerwerkzeug auf jedem System seine Konfiguration, Geheimnisse, Daten, Logs und seinen Socket ablegt.
- Ein Dienst folgt hier hier(7) auf OpenBSD und FreeBSD und dem Filesystem Hierarchy Standard auf Linux; ein Benutzerwerkzeug den Basisverzeichnissen von freedesktop.org oder ~/Library auf macOS.
- Ein App-Name besteht aus 1 bis 31 Bytes aus Kleinbuchstaben, Ziffern und Bindestrichen und beginnt mit einem Buchstaben; sein Konto ist _ und der Name.
- Ein Pfad wird gewählt aus dem Flag, dann der Variable APP_ENV, APP_PEPPER, APP_CONFIG oder APP_SOCKET, dann der Konfiguration, dann dem Standardwert.
- Eine Basisverzeichnis-Variable, die keinen absoluten Pfad enthält, wird ignoriert, wie es die Spezifikation verlangt.
- Die Modi sind 0600 für die Env-Datei und den Pepper, 0640 für die Konfiguration, 0660 für den Socket, 0750 für Verzeichnisse.
- Auf dem Datenträger wird nichts angelegt; das Programm legt an, was es braucht, mit den hier genannten Modi.
case choosePath("notes", Service(), PepperFile(), flag, configured) of
when Some(value as path: String) do
usePepper(asSpan(&path));
when None do
refuse();
end case;
Standard.Pcre
Reguläre Ausdrücke über PCRE2, mit dessen Syntax und dessen Engine.
- Die Namen sind die von Standard.Regex, aber eine Suche kann hier scheitern und gibt ein Either zurück.
- Left, wenn das Match-Limit erreicht wurde, wenn eine Suche über 512 MiB brauchte oder PCRE2 gescheitert ist.
- Positionen sind Byte-Offsets.
- Eine Suche verändert ein kompiliertes Muster nicht.
- Für Text von außerhalb des Programms Standard.Regex verwenden, das in linearer Zeit läuft.
case compile("([a-z]+)@([a-z.]+)", 100000) of
when Right(right as regex: Regex) do
let hits: Either[String, Buffer[Hit]] := find_all(®ex, asSpan(&text));
destroyRegex(regex);
when Left(left as message: String) do
printLn(asSpan(&message));
end case;
Standard.PcreJit
Muster von Standard.Pcre, vom JIT von PCRE2 zu Maschinencode kompiliert.
- Dieses Modul zu importieren heißt, dem Programm zu sagen, dass es zur Laufzeit Maschinencode erzeugt.
- Standard.Pcre allein erzeugt nie Maschinencode.
- Ein hier kompiliertes Muster ist ein Regex von Standard.Pcre und liefert bei der Suche dieselben Ergebnisse.
- Wenn der JIT keinen Maschinencode liefert, sucht der Interpreter von PCRE2, und fallBack nennt den Grund.
- Das PCRE2 von Hilde erzeugt Maschinencode, ohne dass eine Seite gleichzeitig schreibbar und ausführbar ist.
case compile("[0-9]{4}-[0-9]{2}-[0-9]{2}", 100000) of
when Right(right as compiled: Compiled) do
let { regex: Regex, fallBack: Option[String] } := compiled;
let found: Either[String, Bool] := match(®ex, asSpan(&line));
destroyRegex(regex);
when Left(left as message: String) do
printLn(asSpan(&message));
end case;
Standard.Platform
Das System und die Maschine, für die das Programm kompiliert wurde, und der Host, auf dem es läuft.
- Das System ist linux, openbsd, freebsd, macos, wasi oder unknown.
- Die Maschine ist x86_64, aarch64, i386, arm, riscv64, ppc64, wasm32 oder unknown.
- Das System und die Maschine stehen fest, wenn das Programm kompiliert wird.
- Der Hostname wird einmal beim Kernel erfragt, beim ersten Mal, höchstens 255 Bytes, unknown, wenn es keinen gibt.
print(systemName());
print("/");
printLn(machineName());
Standard.Png
PNG-Bilder mit 8 Bit pro Kanal, geschrieben und gelesen.
- Ein Bild ist Breite, Höhe, Farbart und Pixel, Zeile für Zeile von oben.
- Ein Pixel ist ein Byte Grau, drei für Rot, Grün und Blau, oder vier mit Alpha dahinter.
- encodePng schreibt einen IDAT-Chunk und keinen Filter, sodass dasselbe Bild dieselben Bytes ergibt.
- decodePng liest Bittiefe 8, Grau, RGB oder RGBA, nicht interlaced, mit jedem der fünf Filter.
- Die CRC jedes Chunks wird geprüft; Ancillary-Chunks werden übersprungen.
- Ein kritischer Chunk, den es nicht kennt, etwa eine Palette, ist Unsupported statt ignoriert.
- Der Aufrufer begrenzt die Pixel vor dem Dekomprimieren, sodass eine kleine Datei kein riesiges Bild erzeugen kann.
- Ein Bild ist Linear und geht über destroyImage zurück.
case decodePng(asSpan(&data), 4194304) of
when Right(right as image: Image) do
let file: String := encodePng(&image);
destroyImage(image);
when Left(left as error: PngError) do
reportBadImage(error);
end case;
Standard.Poller
Auf viele Deskriptoren gleichzeitig warten.
- Das Backend ist epoll auf Linux, kqueue auf OpenBSD, FreeBSD und macOS, anderswo keines.
- Ohne Backend gibt createPoller None zurück; backendAvailable sagt es, bevor etwas aufgebaut wird.
- Ein Deskriptor, der lesbar und schreibbar ist, kommt als ein Ereignis mit beiden Flags zurück.
- Ein Deskriptor wird nur für das gemeldet, wofür er geöffnet wurde.
- Das Leseende einer Pipe wird nur auf OpenBSD als schreibbar gemeldet, das beide Enden in beide Richtungen öffnet.
- Pipes, Sockets und Zeichengeräte können beobachtet werden; epoll lehnt eine reguläre Datei ab.
- Der Poller besitzt nur seinen eigenen Deskriptor; die Deskriptoren, die er beobachtet, bleiben beim Aufrufer.
case createPoller() of
when Some(value as made: Poller) do
var poller: Poller := made;
let watched: Bool := watchFile(&!poller, &socket, interestRead);
let ready: Option[Index] := waitFor(&!poller, 1000);
let first: Option[Event] := nthEvent(&poller, 0);
destroyPoller(poller);
when None do
useBlockingReads();
end case;
Standard.Postgres
PostgreSQL über die libpq des Systems, mit den Typen und der Form von Standard.Sqlite.
- Jede Verbindung läuft über TLS mit sslmode verify-full; es gibt keine Möglichkeit, eine ohne es zu öffnen.
- Ein Wert wird nie Teil des Statement-Texts: Er geht getrennt an den Server, als $1, $2.
- Werte und Spalten reisen in der Binärform ihres Typs; ein Text, der wie SQL aussieht, bleibt Text.
- Ein Spaltenleser des falschen Typs hält das Programm an: Das ist ein Fehler in ihm, nicht in den Daten.
- Eine Spalte, die null sein darf, wird mit dem OrNone-Leser gelesen.
- Eine lesende Verbindung macht jede Transaktion schreibgeschützt, sodass ein Schreibzugriff auch auf dem Server scheitert.
- Das ganze Ergebnis kommt mit dem Statement an; ein Span aus einer Spalte ist für seine Zeile gültig.
- Connection, Transaction, Values und Cursor sind Linear und müssen beendet werden.
case openWriter(host, 5432, "app", "app", asSpan(&password), caFile) of
when Right(right as made: Connection[Writing]) do
var db: Connection[Writing] := made;
let values: Values := withText(withInt64(noValues(), 42), "Ada");
let insert: Span[Nat8, Static] := "INSERT INTO people (id, name) VALUES ($1, $2)";
let changed: Either[PgError, Nat64] := execute(&!db, insert, values);
closeDatabase(db);
when Left(left as error: PgError) do
logLine(describe(error));
end case;
Standard.Qr
QR-Codes nach ISO/IEC 18004, Modell 2, aus Text in Hilde erzeugt.
- Der Text ist ein Segment: numerisch, wenn nur Ziffern, sonst alphanumerisch, wenn möglich, sonst Bytes.
- Von den 40 Versionen wird die kleinste genommen, die den Text auf der verlangten Stufe fasst.
- Von den acht Masken wird die mit der niedrigsten Strafe genommen, bei Gleichstand die erste.
- So ergibt derselbe Text auf derselben Stufe auf jedem Lauf denselben Code.
- Ein Code ist ein Quadrat aus dunklen und hellen Modulen, ohne die Ruhezone.
- qrToImage zeichnet einen Code als Bild von Standard.Png und fügt die Ruhezone hinzu.
- Ein Code ist Linear und geht über destroyQr zurück.
case encodeQr("https://example.org/ticket/4711", QrMedium()) of
when Some(value as code: QrCode) do
let image: Option[Image] := qrToImage(&code, 8, 4);
destroyQr(code);
showImage(image);
when None do
reportTooLong();
end case;
Standard.Raft
Raft-Konsens als eigene Zustandsmaschine: Nachrichten und Ticks gehen hinein, Nachrichten und festgeschriebene Einträge kommen heraus.
- Kein Netz, keine Platte und keine Uhr: Der Aufrufer sendet die Nachrichten, speichert, was überleben muss, und tickt.
- Höchstens ein Leader je Term; ein einmal festgeschriebener Eintrag bleibt im Log jedes späteren Leaders.
- Ein Eintrag ist festgeschrieben, wenn ein Quorum der Mitgliederzahl ihn hat und er vom Term des Leaders ist.
- quorum ist Sache des Aufrufers; majority, mehr als die Hälfte, verhindert, dass zwei Quoren einander verfehlen.
- Jeweils nur eine Änderung der Mitglieder, ein Server hinzugefügt oder entfernt, wirksam, sobald sie im Log steht.
- Ein Follower, der zu weit zurückliegt, erhält statt der Einträge einen Snapshot der Zustandsmaschine.
- Die Zustandsmaschine ist jeder Typ mit einer Instanz von StateMachine.
var node: Node := newNode(Config(id => 1, electionTicks => 10, heartbeatTicks => 2, seed => 7, quorum => majority), members);
tick(&!node);
let outgoing: Buffer[Message] := takeMessages(&!node);
Standard.RaftTcp
Die Nachrichten von Standard.Raft über TCP: jede als Bytes hinter einer Länge von vier Bytes.
- Jede Zahl wird mit dem höchstwertigen Byte zuerst in fester Breite geschrieben.
- Ein Frame enthält höchstens frameLimit Bytes; eine längere Länge wird abgelehnt, bevor etwas gelesen wird.
- decodeMessage nimmt nur Bytes an, die genau eine wohlgeformte Nachricht sind und nichts danach.
- Jede gelesene Anzahl und Länge wird gegen die tatsächlich vorhandenen Bytes geprüft.
- Die Verbindungen sind die von Standard.File: Der Aufrufer lauscht, verbindet, akzeptiert und schließt.
let sent: Bool := sendMessage(&!socket, &message);
let next: Option[Message] := receiveMessage(&!socket);
Standard.Regex
Reguläre Ausdrücke in linearer Zeit.
- Syntax wie RE2; für alles, was es ablehnt oder anders macht, gibt es einen Test.
- Die Zeit ist für jedes Muster linear in der Länge der Eingabe.
- Rückverweise und Lookaround werden abgelehnt, mit einer Meldung, die Standard.Pcre nennt.
- Ein Muster, das compile als String-Literal übergeben wird, wird geprüft, wenn das Programm kompiliert wird.
- Ein kompilierter Regex ändert sich nie: Tasks dürfen ohne Sperre gleichzeitig mit einem suchen.
- Positionen sind Byte-Offsets; Treffer beginnen und enden an UTF-8-Grenzen.
- Ein Punkt und eine Klasse treffen einen Codepoint, nicht ein Byte.
case compile("^[0-9]+-[0-9]+$") of
when Right(right as regex: Regex) do
let isRange: Bool := match(®ex, asSpan(&text));
destroyRegex(regex);
when Left(left as message: String) do
reportPattern(message);
end case;
Standard.Resolve
Hostnamen, über die Hosts-Datei und DNS zu Adressen aufgelöst, so wie das System sie einrichtet.
- Eine literale Adresse kommt unverändert zurück, ohne jemanden zu fragen.
- Ebenso eine IPv4-Adresse in ihrer abgebildeten IPv6-Form, ::ffff: und vier Gruppen.
- Alles andere muss ein Hostname nach RFC 1123 sein, bevor es dem Resolver übergeben wird.
- Eine Antwort enthält jede Adresse einmal, in der Reihenfolge, in der der Resolver sie lieferte, höchstens 64.
- Die Records der C-Laufzeit werden hier gelesen: ihre Länge geprüft, eine abgebildete IPv4-Adresse zurückgefaltet.
- Der Aufruf wartet, bis der Resolver antwortet; ein Task, der auflöst, behält währenddessen seinen Worker.
case resolveHost("mail.example.org", AnyFamily()) of
when Right(right as addresses: Buffer[IpAddress]) do
connectToFirst(addresses);
when Left(left as failure: ResolveFailure) do
reportFailure(failure);
end case;
Standard.Resource
Die Ressourcenlimits des Prozesses, was er verbraucht hat, und wie lange das System läuft.
- Ein Limit hat einen Soft-Wert, den der Kernel durchsetzt, und einen Hard-Wert.
- Der Prozess darf den Soft-Wert bis zum Hard-Wert anheben.
- Das Senken eines Hard-Limits lässt sich ohne Privileg nicht rückgängig machen.
- Kein Limit wird auf jeder Plattform als unlimited geschrieben, das größte Nat64.
- Ein Aufruf, den der Kernel ablehnt, gibt None oder false zurück.
let cores: Bool := disableCoreDumps();
case readLimit(OpenFiles()) of
when Some(value as limit: Limit) do
let wide: Limit := Limit(soft => limit.hard, hard => limit.hard);
let raised: Bool := setLimit(OpenFiles(), wide);
when None do
skip;
end case;
Standard.Sandbox
Beschränkt den laufenden Prozess auf das, was eine Policy erlaubt, und sagt, was ihn wirklich beschränkt.
- Eine Policy nennt Promises in den Worten von pledge und Pfade in den Berechtigungsbuchstaben von unveil.
- Auf OpenBSD werden die Pfade unveiled, die Sicht gesperrt und die Promises gepledged, mit execpromises.
- Eine Liste von Pfaden, für die unveil keinen Platz hat, wendet gar nichts an.
- Mit allowFolding werden Geschwister auf ihren Elternpfad gefaltet und erneut versucht; das erweitert die Sicht.
- Auf Linux wird no new privileges gesetzt und die Pfade werden zu einem Landlock-Regelsatz.
- Auf Linux lehnt ein seccomp-Filter die Aufrufe ab, die ein beschränktes Programm nicht braucht.
- Auf Linux werden die Promises geprüft und nicht durchgesetzt.
- Auf macOS wird das Programm unter einem Seatbelt-Profil der Pfade neu gestartet, ab seiner ersten Zeile.
- Auf macOS läuft alles vor applyPolicy zweimal, beim zweiten Mal beschränkt: applyPolicy zuerst aufrufen.
- Auf macOS erlauben die Promises Netz, fork und exec nur als Ganzes; der Rest wird nicht durchgesetzt.
- Auf FreeBSD werden die Pfade mit ihren Rechten geöffnet und der Prozess tritt in den Capability-Modus ein.
- Auf FreeBSD braucht das Promises ohne Netz oder exec; mit anderen oder ohne wird nichts angewendet.
- Auf FreeBSD ist ein Pfad, den die Laufzeit nicht selbst öffnet, etwa die Datei einer Datenbank, nicht erreichbar.
- Auf Android, auf jeder anderen Plattform und in einer Bibliothek wird nichts angewendet: nicht verfügbar.
- Der Bericht nennt jeden Mechanismus, der gewirkt hat, und keinen anderen.
- Nichts hier hält das Programm an: Was ein unbeschränkter Lauf bedeutet, entscheidet das Programm.
let promised: Policy := allowPromises(makePolicy(), "stdio rpath inet");
let policy: Policy := allowPath(promised, "/var/www", "r");
let report: Report := applyPolicy(policy);
printLine(describeReport(report));
Standard.Search
Volltextsuche in einer Datenbank von Standard.Database, mit dem FTS5-Modul von SQLite.
- Der Index ist die virtuelle Tabelle hilde_search: ein Schlüssel, ein Titel und ein Text je Dokument.
- Nur die Backends SQLite und SQLCipher haben FTS5; prepareSearch lehnt die anderen ab.
- FTS5 muss in diese Bibliothek eingebaut sein; ohne es scheitert prepareSearch mit SearchStorageFailed.
- Der Tokenizer unicode61 findet Wörter ohne Groß- und Kleinschreibung und Diakritika: cafe findet das Wort mit Akzent.
- Treffer werden nach bm25 gerankt, ein Wort im Titel zählt zehnmal so viel wie im Text, der beste zuerst.
- Ein Snippet wird für HTML maskiert, die gefundenen Wörter zwischen zwei Markern, die der Aufrufer wählt.
- Eingetippter Text erreicht FTS5 nie als Query: Seine Wörter werden zu Strings in Anführungszeichen, die alle gefunden werden müssen.
- So kann keine Eingabe eine Spalte nennen, nach Wörtern in der Nähe zueinander fragen, einen Operator verwenden oder nicht parsen.
- Ein Wort, das auf einen Stern endet, findet die Wörter, die damit beginnen.
- Es werden höchstens 16 Wörter von höchstens je 64 Bytes genommen; der Rest wird verworfen.
- Ein Schlüssel ist der Name eines Dokuments beim Aufrufer, nicht leer; die Tabelle hilde_search_keys gibt jedem Schlüssel seine Zeile.
- So finden das Einfügen und das Entfernen eines Dokuments seine Zeile über einen Index, in Zeit logarithmisch in den Dokumenten.
- Beide Tabellen werden nur über dieses Modul geschrieben: Eine daran vorbei geschriebene Zeile hat keinen Schlüssel, den das Modul kennt.
case search(&!db, asSpan(&typed), 10, 0, "<mark>", "</mark>") of
when Right(right as hits: Buffer[SearchHit]) do
renderHits(&hits);
destroyHits(hits);
when Left(left as error: SearchError) do
showNoResults(error);
end case;
Standard.Secret
Passwörter und andere Geheimnisse, ohne Echo vom Terminal gelesen und in Speicher gehalten, der gelöscht wird, wenn sie gehen.
- readSecret liest vom Terminal des Prozesses, nie von der Standardeingabe oder aus einem Argument.
- Das Echo ist aus, während die Zeile getippt wird, und an seiner Stelle wird nichts gezeigt.
- Ein Signal, das das Programm beendet oder anhält, während das Echo aus ist, findet es zuvor wieder eingeschaltet.
- Ohne Terminal ist die Antwort sofort NoTerminal; nichts wartet.
- readSecretFromInput liest die Standardeingabe, für ein Flag wie --password-stdin, das das Programm benennt.
- Ein Secret ist linear; destroySecret löscht seinen gesamten Speicher, und es gibt keinen anderen Weg, es loszuwerden.
- Ein Secret enthält höchstens 1024 Bytes; eine längere Zeile ist TooLong und wird gelöscht.
case readNewSecret("New password: ", "Again: ") of
when Right(right as secret: Secret) do
store(secretBytes(&secret));
destroySecret(secret);
when Left(left as error: SecretError) do
refuse(error);
end case;
Standard.Semver
Release-Versionen der Form vX.Y.Z, wie Git-Tags sie tragen, gelesen, geschrieben und geordnet.
- Eine Version ist ein kleines v und drei Dezimalzahlen, durch Punkte verbunden, etwa v1.12.0; nichts darf folgen.
- Eine Zahl hat keine führende Null, außer der Zahl 0 selbst, und höchstens neun Ziffern.
- Die Ordnung geht nach Major, dann Minor, dann Patch, jeweils als Zahl.
- Das Lesen lehnt ab, statt zu raten: Alles andere gibt None zurück, und nichts bricht bei fehlerhafter Eingabe ab.
- Eine Änderung der Major-Zahl ist ein Bruch; innerhalb eines Majors bleibt eine größere Version kompatibel.
case parseVersion("v1.4.2") of
when Some(value as version: Version) do
let text: String := formatVersion(version);
printLn(asSpan(&text));
Standard.Session
Serverseitige Sessions in einer Datenbank von Standard.Database, mit einem Leerlauf- und einem absoluten Zeitlimit.
- Eine Session ist eine Zeile der Tabelle hilde_sessions: Digest ihres Tokens, Zeiten von Erzeugung und Nutzung, Daten.
- Die Tabelle funktioniert auf jedem Backend: SQLite, SQLCipher, PostgreSQL und MariaDB.
- Ein Token sind 32 sichere Zufallsbytes als 64 hexadezimale Ziffern, dem Aufrufer einmal übergeben.
- Nur der SHA-256 eines Tokens wird gespeichert, sodass eine Kopie der Datenbank keine Session öffnet.
- Eine Session, die länger als das Leerlauflimit ungenutzt war oder älter als das absolute ist, endet beim Laden.
- rotateSession beim Login gibt ein neues Token, sodass eines, das ein Angreifer vorher gesetzt hat, danach nichts öffnet.
- Jede Zeit sind Sekunden, die der Aufrufer übergibt, sodass ein Test die Uhr verschieben kann; unter 0 wird abgelehnt.
- Ein Token in einem Cookie wird mit Standard.Cookie zurückgelesen.
let limits: SessionLimits := defaultSessionLimits();
case loadSession(&!db, asSpan(&cookieToken), limits, now) of
when Right(right as found: Option[String]) do
serveWith(found);
when Left(left as error: SessionError) do
reportFailure(error);
end case;
Standard.Setup
Die Unterbefehle setup und user eines Programms: sein Vault für Geheimnisse, seine Completion und sein Handbuch, und seine Benutzer.
- setup hat init, set, get, delete, list, rotate-key, rotate-pepper, completion und manual; user hat add, del, passwd und list.
- Ein Geheimnis oder Passwort wird ohne Echo vom Terminal gelesen, ein neues zweimal; von der Standardeingabe nur mit --password-stdin; nie aus einem Argument.
- Jedes gelesene Geheimnis wird gelöscht, wenn es nicht mehr gebraucht wird.
- runSetup und runUser geben den Exit-Status zurück, 0 oder 1, und schreiben, was schiefging, nach errors.
- Nur ein Programm, das dieses Modul importiert, trägt den Vault und seine Kryptografie.
let setup: SetupKeys := addSetup(&!root);
case parse(&root, &arguments) of
when Right(right as outcome: Outcome) do
useOutcome(outcome);
when Left(left as error: ArgumentError) do
useError(error);
end case;
Standard.Sftp
SFTP Version 3 über die Pipes eines Kindprozesses.
- Das Kind spricht SFTP auf seiner Standardeingabe und -ausgabe: ssh -s für einen Host oder sftp-server.
- Hier gibt es keine Kryptografie und kein SSH: OpenSSH hält die Verbindung, ihre Schlüssel und die Host-Prüfungen.
- Jeder Aufruf wartet auf seine Antwort und prüft, dass Typ und Request-ID die erfragten sind.
- Eine Antwort, die nicht parst, oder ein verschwundener Server ist ein negativer Code und verdirbt die Session.
- Eine verdorbene Session beantwortet jeden späteren Aufruf gleich, ohne etwas zu schreiben.
- Der Aufrufer gibt startSession die Millisekunden, die ein ganzer Austausch dauern darf; es gibt keinen Standardwert.
- Ein Austausch, der nicht rechtzeitig fertig ist, endet als timedOut, auch bei einem Server, der ein Byte nach dem anderen sendet.
- Eine Session nach Zeitüberschreitung ist verdorben und ihr Kind bekommt SIGTERM, sodass endSession nicht wartet.
- Dateien bewegen sich in Stücken von 32768 Bytes; eine Antwort über 256 KiB wird abgelehnt, statt angelegt zu werden.
case startSession(command, 30000) of
when Some(value as opened: Session) do
var session: Session := opened;
let content: Either[SftpError, String] := readFile(&!session, "/srv/data/report.csv");
let status: ProcessStatus := endSession(session);
store(content);
when None do
reportNoServer();
end case;
Standard.Shared
Zustand, den Tasks gemeinsam nutzen: ein Mutex, der einen Wert schützt, und ein atomares Wort mit 64 Bit.
- Tasks erreichen ein gemeinsames Objekt über eine lesende Referenz, die ein Verleih-Scope von Standard.Task bereitstellt.
- Ein Mutex hält einen Wert beliebigen Typs; acquire holt ihn mit einem Schlüssel heraus, release nimmt beides zurück.
- Der Schlüssel ist linear: Ein Task, der erworben hat, muss ihn zurückgeben, und nur der eigene Mutex nimmt ihn an.
- Ein Task, der auf einen Mutex wartet, lässt seinen Thread andere Tasks ausführen.
- Ein Mutex, der nie zurückgegeben wird, beendet das Programm mit der Deadlock-Meldung von Standard.Task.
- Ein atomares Wort wird jeweils in einem Schritt gelesen und geändert.
- Jeder Task sieht die Änderungen eines atomaren Worts in derselben Reihenfolge.
let { value as count: Int64, key: Key } := acquire(counter);
release(counter, count + 1, key);
let total: Int64 := atomicAdd(requests, 1);
Standard.SharedCache
Ein Standard.Cache, den Tasks gemeinsam nutzen, mit einer Befüllung je Schlüssel.
- Verfehlen mehrere Tasks gleichzeitig denselben Schlüssel, führt einer die Befüllung aus, und die anderen warten auf ihren Wert.
- Tasks erreichen ihn über eine lesende Referenz, die ein Verleih-Scope von Standard.Task bereitstellt.
- Während eine Befüllung läuft, bleiben andere Schlüssel nutzbar.
- Ein Task, der auf die Befüllung seines Schlüssels wartet, lässt in der Zwischenzeit die anderen lauffähigen Tasks laufen.
- Ein clear oder flush während einer Befüllung hält deren Wert fern, und der nächste Fehlgriff befüllt erneut.
- Eine Befüllung, die keinen Wert liefert, speichert nichts, und der nächste Task, der danebengreift, befüllt selbst.
- Grenzen, Lebensdauern und die Reihenfolge, in der Einträge entfallen, sind die von Standard.Cache.
case makeSharedCache(1000, 1048576, 60000000000) of
when Some(value as cache: SharedCache[Nat64, Nat64]) do
servePrices(cache);
when None do
reportNoMemory();
end case;
let price: Option[Nat64] := getWith(prices, item, nowNanos(), lookUpPrice);
Standard.Signal
Signale als Ereignisse, eine Zeitgrenze für das Herunterfahren und PID-Dateien.
- Ein Programm, das ein Signal beobachtet, wird davon nicht beendet; es fragt mit takeSignal danach oder wartet mit waitSignal.
- Beim Eintreffen eines Signals läuft kein Code des Programms; es sieht das Signal erst, wenn es fragt.
- Ein Signal, das nicht beobachtet wird, behält seine Standardaktion.
- Ein Prozess hat jeweils eine Beobachtung; stopWatching gibt den Signalen ihre Standardaktion zurück.
- Nach limitShutdown beendet der Kernel einen Prozess, dessen Aufräumen länger als die Grenze dauert.
- Hangup sendet die Rotation von Logdateien; die Antwort darauf ist Standard.Log.reopenLog.
let wanted: Wanted := Wanted(hangup => true, interrupt => true, terminate => true,
user1 => false, user2 => false);
case watchSignals(wanted) of
when Some(value as watch: Signals) do
var signals: Signals := watch;
while not shutdownRequested(&!signals) do
let woken: Bool := waitSignal(&!signals, 1000);
end while;
let left: Nat32 := limitShutdown(10);
let restored: Bool := stopWatching(signals);
when None do
reportNoWatch();
end case;
Standard.Signify
Schlüssel und Signaturen in den Dateien von signify(1) aus OpenBSD: Ed25519, base64, eine Kommentarzeile.
- Eine Datei besteht aus einer Zeile untrusted comment: samt Kommentar und dann einer Zeile base64; die Schlüsselzeile allein wird ebenfalls angenommen.
- Ein öffentlicher Schlüssel hat 42 Bytes, ein geheimer Schlüssel 104 und eine Signatur 74, jeweils beginnend mit dem Algorithmus Ed.
- Nur ein geheimer Schlüssel ohne Passphrase wird gelesen oder geschrieben, wie ihn signify -G -n erzeugt; ein anderer wird abgelehnt.
- Die Prüfsumme eines geheimen Schlüssels wird geprüft, bevor er signiert, und eine Signatur nennt die Schlüsselnummer, die sie braucht.
- Eine Signatur gilt über die Bytes der Nachricht, abgetrennt, wie signify -S sie ohne -e schreibt.
- Das Lesen lehnt ab, statt zu raten: Eine fehlerhafte Datei ergibt None oder false, und bei schlechter Eingabe bricht nichts ab.
case generateKeys("demo") of
when Some(value as keys: KeyPair) do
let { publicKey: String, secretKey: String } := keys;
case signMessage(asSpan(&secretKey), "text", "verify with demo.pub") of
when Some(value as signature: String) do
let good: Bool := verifyMessage(asSpan(&publicKey), "text", asSpan(&signature));
Standard.Smtp
Mail über ein Relay versenden, im Dialog nach RFC 5321 über TLS, mit einfachen Nachrichten nach RFC 5322.
- TLS ist Pflicht: vom ersten Byte an, wie RFC 8314 es für Port 465 vorsieht, oder per STARTTLS.
- Ein Relay, das STARTTLS nicht anbietet, wird abgelehnt, sodass niemand zwischen beiden das Angebot entfernen kann.
- Klartext gibt es nur für ein Relay auf einer Loopback-Adresse, und nie mit Anmeldung.
- Die Anmeldung ist AUTH PLAIN und wird nur über TLS gesendet.
- Eine Zeitgrenze gilt für den ganzen Dialog.
- Adressen werden geprüft, bevor etwas gesendet wird: Nichts darin kann eine Zeile oder einen Befehl beenden.
- Adressen mit Bytes jenseits von ASCII werden abgelehnt.
- Ein Fehler trägt, was das Relay zuletzt gesagt hat, nie das Passwort.
case makeRelay("mail.example.org", 587, StartTls(), "/etc/ssl/cert.pem", 30000) of
when Some(value as relay: Relay) do
let sent: Either[MailFailure, Unit] := sendMail(&relay, "app", asSpan(&password),
&envelope, asSpan(&message));
reportMail(sent);
destroyRelay(relay);
when None do
reportBadHost();
end case;
Standard.Sort
Sortieren und Suchen in einem Buffer, dessen Elemente eine totale Ordnung nach Standard.Order.TotalOrder haben.
- sort braucht in jedem Fall O(n log n) Vergleiche und O(n) zusätzlichen Speicher.
- sort ist stabil: Gleiche Elemente behalten ihre Reihenfolge, das Ergebnis hängt also allein von der Eingabe ab.
- sort verschiebt Elemente und kopiert sie nie, sortiert also lineare ebenso wie freie Elemente.
- binarySearch braucht O(log n) Vergleiche und prüft nicht, ob der Buffer sortiert ist.
- Ein Modul, das Strings oder Ganzzahlen sortiert, importiert TotalOrder aus Standard.Order, sonst H3061.
let sorted: Buffer[String] := sort(names);
case binarySearch(&sorted, &wanted) of
when Found(position: Index) do
showAt(position);
when Missing(insertAt: Index) do
offerToAdd(insertAt);
end case;
Standard.Sperli
Ein Job aus Rängen, die einander Nachrichten über TLS 1.3 senden, mit der Semantik des Nachrichtenaustauschs und ohne dessen Schnittstelle.
- Jede Verbindung ist TLS 1.3 mit einem Zertifikat auf beiden Seiten von der CA des Jobs; es gibt keinen Klartextmodus.
- Das Subjekt eines Zertifikats muss der Job und der Rang der Gegenstelle sein, und der Host darf keiner sein, den hostRefused von Standard.SperliWire ablehnt.
- Jede Gegenstelle zeigt ein vom Launcher signiertes Join-Token, das geprüft wird, bevor eine Nachricht angenommen wird.
- Eine Nachricht wird über den Rang, von dem sie kommt, und ihr Tag zugeordnet, in der Reihenfolge des Sendens.
- send verbraucht den Buffer; ein offenes Senden oder Empfangen ist ein linearer Wert, der beendet werden muss.
- barrier, bcast, reduce, allreduce, gather und scatter sind auf jedem Rang dieselben Aufrufe in derselben Reihenfolge.
- Eine Reduktion läuft über die Ränge in aufsteigender Reihenfolge, sodass ein Ergebnis nicht vom Timing abhängt.
- checkpoint schreibt den Zustand jedes Rangs auf die Platte, mit einer COMMIT-Datei, die der Launcher-Schlüssel signiert, und resume liest ihn nach einem Neustart zurück.
- Ein Checkpoint wird abgelehnt, solange ein Senden oder Empfangen offen ist, und ein Zustand, dessen Typ keine Instanz von Checkpointable hat, wird nicht übersetzt.
- Jede Ablehnung ist ein Code von Standard.SperliError; die Frames sind die von Standard.SperliWire und Standard.SperliCollective.
let started: Either[Int32, Comm] := init(job);
let sent: Int32 := send(&!comm, 1, 7, message);
let arrived: Int32 := barrier(&!comm);
Standard.SperliCkpt
Die Dateien und Frame-Nutzdaten eines Sperli-Checkpoints: Rang-Dateien, signierte COMMIT-Dateien und ihr Ort auf der Platte.
- Eine Rang-Datei hält einen Header von 80 Bytes und den Zustand; der Header nennt Job, Epoche, Rang, Größe und das SHA-256 des Zustands.
- Eine COMMIT-Datei listet das SHA-256 jeder Rang-Datei und ist mit Standard.CompositeSig signiert; ohne gültige Signatur zählt eine Epoche nicht.
- Jede Datei wird unter einem temporären Namen geschrieben, synchronisiert und umbenannt, sodass ein Leser eine ganze Datei findet oder keine.
- Der Pfad einer Datei besteht aus dem Verzeichnis, dem Job als Hexadezimalziffern, der Epoche und dem Rang.
- Ein Zustand geht über die Typklasse Checkpointable hinein und heraus.
- Jede Ablehnung ist ein Code von Standard.SperliError.
let file: Option[String] := rankFile(job, 1, 0, 2, asSpan(&state));
let verdict: Int32 := checkCommit(asSpan(&commit), publicKey, job, 1, 2);
Standard.SperliCollective
Die Bytes der kollektiven Frames von Sperli und die Reduktion zweier Zahlen-Arrays.
- Die Nutzdaten eines kollektiven Frames sind ein Header von 16 Bytes und die Daten, niederwertigstes Byte zuerst.
- checkCollective vergleicht Nutzdaten mit dem Aufruf, den sie beantworten sollen: Operation, Typ, Reduktionsoperation, Phase, Wurzel und Sequenz.
- combine reduziert zwei Arrays Element für Element; Ganzzahlen laufen an ihrer Breite über, und kein Aufruf löst eine Falle aus.
- Float-Zahlen werden nach den Regeln der Maschine addiert und multipliziert; min und max nehmen den zweiten Operanden nur, wenn er kleiner beziehungsweise größer ist.
- Logische Operationen lesen eine Ganzzahl als wahr, wenn sie nicht 0 ist, und liefern 0 oder 1; bitweise Operationen arbeiten auf den Bits.
- Die Zeit von combine ist linear in der Länge der Arrays.
let payload: String := collectivePayload(bcastOperation(), bytesType(), noReduction(), resultPhase(), 0, 1, "data");
let verdict: Int32 := checkCollective(asSpan(&payload), bcastOperation(), bytesType(), noReduction(), resultPhase(), 0, 1);
Standard.SperliError
Die Codes, mit denen ein Sperli-Job eine Gegenstelle, einen Frame oder ein Token ablehnt, und ihre Worte.
- Ein Code ist eine Zahl ab 1601, geschrieben H1601, und benennt eine Art von Ablehnung.
- Null ist keine Ablehnung.
- Ein Code wird nie einer anderen Ablehnung gegeben.
- describe liefert für jeden Code einen kurzen Satz und für jede andere Zahl den leeren.
let why: Span[Nat8, Static] := describe(badToken());
Standard.SperliExport
Die Aufrufe von Standard.Sperli in der Form, die eine C-Bibliothek exportieren kann: eine Session, die das Comm und das letzte Ergebnis hält, und Codes statt Either.
- Jeder Aufruf ist eine Session und reine Zahlen oder Bytes; nichts anderes geht hinüber.
- Ein Aufruf, der mit Daten antwortet, hält sie in der Session, bis takeResult sie weitergibt.
- Jeder Aufruf antwortet mit 0 oder einem Code von Standard.SperliError; notReady, resultPending und resultMissing kommen hinzu.
- Eine Session, deren Join fehlschlug, hält den Code des Fehlers und kein Comm.
- Die Aufrufe prüfen nur, was Standard.Sperli per Vertrag ablehnen würde; die C-Bibliothek prüft das Übrige vorher.
let session: Session := openSession(text);
let sent: Int32 := sessionSend(&!session, 1, 7, message);
Standard.SperliLaunch
Der Launcher eines Sperli-Jobs: liest die Hostliste, erzeugt die Schlüssel und Zertifikate des Jobs und startet die Ränge.
- Eine Hostliste hat einen Host je Zeile, mit optionalem Port; eine Zeile, die mit einer Raute beginnt, ist ein Kommentar.
- Ein Host, den hostRefused von Standard.SperliWire ablehnt, wird auch dann abgelehnt, wenn er aufgeführt ist; eine Deny-Datei fügt nur Namen hinzu.
- Für jeden Job werden eine CA und ein Schlüsselpaar von Standard.CompositeSig erzeugt, für jeden Rang ein Zertifikat und ein Token.
- Ein Rang auf dieser Maschine wird direkt gestartet, jeder andere Rang über ssh auf Port 62222.
- Ein Rang erfährt seinen Job auf seiner Standardeingabe, als eine Zeile aus Hexadezimalziffern.
- Die Ausgabe der Ränge wird nach ihrem Ende Rang für Rang angezeigt.
- Mit einem Checkpoint-Verzeichnis wird der Job nach einem Fehler erneut gestartet, höchstens restartMax-mal, und ein verlorener Rang geht an den nächsten unbenutzten Host der Liste.
let hosts: Either[Int32, HostList] := parseHostList(text, "");
Standard.SperliWire
Die Bytes von Sperli, der Cluster-Schicht: Frame-Header, Hello-Nutzdaten und Join-Tokens.
- Jede Zahl wird niederwertigstes Byte zuerst in fester Breite geschrieben.
- Ein Header hat 52 Bytes und die Nutzdaten höchstens maxPayload Bytes; die Länge wird geprüft, bevor Nutzdaten gelesen werden.
- checkHeader liefert 0 für einen Frame, der anzunehmen ist, und einen Code von Standard.SperliError für einen Frame, der abzulehnen ist.
- Ein Frame einer älteren Inkarnation liefert staleIncarnation: Der Aufrufer liest seine Nutzdaten und verwirft sie.
- Nur die Checkpoint-Frames tragen eine Epoche; sie tragen kein Tag.
- Ein Join-Token hält Job, Rang, Größe, Inkarnation, Host und Ablauf, signiert mit Standard.CompositeSig.
- verifyJoinToken prüft zuerst die Signatur, dann Job, Rang, Größe, Inkarnation und Host, dann den Ablauf.
let header: String := frameHeader(dataKind(), 5, job, 1, 0, 0, 1, 7);
let verdict: Int32 := checkHeader(asSpan(&header), job, 1, 1, 0);
Standard.SqlCipher
SQLCipher über die libsqlcipher des Systems: Standard.Sqlite mit verschlüsselter Datenbankdatei.
- Alles außer dem Schlüssel wird aus Standard.Sqlite erzeugt und behält dessen Garantien.
- Ein Programm importiert dieses Modul oder Standard.Sqlite; eines, das beide importiert, wird nicht übersetzt (H2044).
- Der Schlüssel ist ein linearer Key, der beim Öffnen verbraucht und gelöscht wird, sobald SQLCipher ihn hält.
- Ein falscher Schlüssel ist ein Left des Öffnens selbst; ein leerer Schlüssel wird abgelehnt.
- openPlainWriter und openPlainReader öffnen eine Datei in SQLites eigenem Format und setzen keinen Schlüssel.
case openWriter("app.db", makeKey(secret)) of
when Right(right as db: Connection[Writing]) do
closeDatabase(db);
when Left(left as error: SqlError) do
printLn(describe(error));
end case;
Standard.Sqlite
SQLite über die libsqlite3 des Systems, mit jeder Abfrage zur Übersetzungszeit gegen das Schema geprüft.
- @query und @execute werden beim Übersetzen des Programms gegen das Schema der Migrationen geprüft.
- SQL außerhalb der angenommenen Formen wird nicht übersetzt, statt ungeprüft zu laufen.
- Die Dateien migrations/1-.sql, 2-.sql und so fort sind das Schema; migrate wendet die neuen an.
- migrate führt jede Datei in einer eigenen Transaktion aus und zählt in pragma user_version.
- Jede Tabelle ist strict, sodass SQLite einen Wert eines Typs ablehnt, den der Compiler nicht angenommen hat.
- Schreiben braucht eine Connection[Writing]; ein Schreibzugriff über eine Connection[Reading] wird nicht übersetzt.
- Im WAL-Modus arbeiten eine schreibende und mehrere lesende Verbindungen nebeneinander, jede in einem eigenen Task.
- Connection, Transaction und Cursor sind linear; eine Transaktion endet nur durch commit oder rollback.
- Text- und Blob-Spalten sind Spans in den Speicher von SQLite, gültig für eine Zeile; copyText kopiert.
- Vorbereitete Statements werden je Verbindung gehalten, nach dem Text der Abfrage geschlüsselt, bis sie schließt.
- Die Funktionen unterhalb von Cursor führen, von Hand mit eigenem SQL aufgerufen, dieses ungeprüft aus.
@query(&!db, "SELECT id, name FROM users WHERE age > ?", age)
for row: User[R] do
printLn(row.name);
else error do
printLn(describe(error));
end for;
Standard.SqliteReplica
SQLite, über Standard.Raft repliziert: Jeder Schreibzugriff ist ein Eintrag des Logs, den jeder Knoten in derselben Reihenfolge anwendet.
- Ein Schreibzugriff ist ein Statement, dessen Werte alle gebundene Parameter sind; ein Literal in Anführungszeichen darin wird abgelehnt.
- random, randomblob, die aktuelle Zeit und das aktuelle Datum sowie eine Zeitfunktion mit now werden abgelehnt.
- attach, detach, pragma, vacuum und die Statements von Transaktionen werden abgelehnt; jeder Schreibzugriff ist seine eigene Transaktion.
- Ein Schreibzugriff wird gegen die Datenbank des Leaders geprüft, bevor er ins Log kommt, soweit diese aktuell ist.
- Das Ergebnis eines Schreibzugriffs, seine geänderten Zeilen oder SQLites Code, wird für die letzten resultLimit Einträge aufbewahrt.
- Ein lokales Lesen sieht, was der Knoten angewendet hat; ein linearisierbares Lesen wartet auf eine Barriere der Amtszeit des Leaders.
- Ein Snapshot ist die Datenbank, kopiert mit dem Online-Backup von SQLite.
case proposeWrite(&!node, &store, "INSERT INTO kv (k, v) VALUES (?, ?)", &values) of
when Right(right as index: Nat64) do
skip;
when Left(left as refusal: Nat8) do
skip;
end case;
Standard.SqliteUsers
Ein Store für Standard.Users in einer SQLite-Datenbank von Standard.Sqlite.
- Die Benutzer liegen in der Tabelle hilde_users, die fehlgeschlagenen Versuche in hilde_user_failures.
- openSqliteUsers legt die beiden Tabellen und ihren Index an, wenn sie fehlen.
- Der Store hält die Verbindung; closeSqliteUsers gibt sie zurück.
case openSqliteUsers(db) of
when Right(right as opened: SqliteUsers) do
var users: SqliteUsers := opened;
let added: Either[UserError, Bool] := addUser(&!users, "alice", asSpan(&password));
closeDatabase(closeSqliteUsers(users));
when Left(left as failed: SqliteUsersFailed) do
let { db: Connection[Writing], error: UserError } := failed;
closeDatabase(db);
end case;
Standard.Ssh
Befehle, die der ssh-Client von OpenSSH auf einem anderen Host ausführt, und jedes Kind, das in begrenzter Zeit und Größe läuft.
- Hier gibt es kein SSH: Die Verbindung, ihre Schlüssel und die Prüfung des Host-Schlüssels bleiben OpenSSH überlassen.
- sshCommand startet ssh im Batch-Modus, mit einer Grenze für den Verbindungsaufbau und ohne Terminal: Es wird kein Passwort abgefragt.
- Der Schlüssel des Hosts muss bereits bekannt sein, sodass ein neuer Schlüssel ein Fehler ist und keine Rückfrage.
- Host und Benutzer werden geprüft, bevor sie ssh erreichen, sodass keiner von beiden eine Option davon sein kann.
- Jedes entfernte Wort wird für die entfernte Shell in einfache Anführungszeichen gesetzt: Kein Wort liest sich als mehr als eines oder als Code.
- Ein Kind, das länger läuft oder mehr schreibt als erlaubt, bekommt SIGTERM, es wird abgewartet, und der Aufruf schlägt fehl.
- Ins Kind wird nichts geschrieben: Ein SIGPIPE würde das Programm beenden, nicht den Aufruf.
case sshCommand("/usr/bin/ssh", "backup.example.org", "deploy", 22, 10) of
when Right(right as base: Command) do
let command: Command := addRemoteWord(addRemoteWord(base, "df"), "-h");
case runCommand(command, 30000, 65536) of
when Right(right as result: CommandResult) do
showResult(result);
when Left(left as error: SshError) do
reportFailure(error);
end case;
when Left(left as error: SshError) do
reportFailure(error);
end case;
Standard.String
Der Typ String, eine lineare Byte-Zeichenkette auf dem Heap, der ihre Bytes gehören.
- Ein String besteht aus Bytes; nichts hier prüft, ob sie UTF-8 sind.
- Er ist linear: destroyString gibt ihn zurück.
- Jede Position wird geprüft, und eine außerhalb des Bereichs hält das Programm an.
- Das Anlegen eines Strings bricht ab, wenn der Speicher nicht zu bekommen ist.
- Eine Funktion, die Text nur liest, nimmt einen Span; asSpan liefert alle Bytes, auch bei einem leeren String.
- Ein Span behält die Referenz auf seinen String; fromSpan kopiert die Bytes in einen eigenen String.
var line: String := fromLiteral("hello, world");
storeNth(&!line, 0, 72);
let head: String := fromSpan(getSpan(&line, 0, 4));
let size: Index := length(&line);
destroyString(head);
destroyString(line);
Standard.StringBuilder
Ein Builder, der Bytes an seinem Ende sammelt und als einen String übergibt.
- Jedes Schreiben hängt am Ende an; das Wachsen bricht ab, wenn der Speicher nicht zu bekommen ist.
- writeString kopiert die Bytes eines Spans, und ein leerer Span fügt nichts hinzu.
- finalize übergibt die gesammelten Bytes ohne Kopie als String.
- Ein Builder ist linear: finalize ist der einzige Weg, ihn zu beenden.
- Nichts hier prüft, ob die Bytes UTF-8 sind.
var builder: StringBuilder := makeStringBuilder();
writeString(&!builder, "id=");
writeByte(&!builder, 52);
let line: String := finalize(builder);
Standard.Task
Strukturierte Nebenläufigkeit: Tasks, die zu einem Scope gehören, und Kanäle mit zwei linearen Enden.
- Tasks laufen auf den Threads des Prozesses und können gleichzeitig laufen.
- spawn verschiebt sein Argument in den Task; es darf keine Referenz und keinen Span halten (H3002).
- Kein Wert eines Typs, der mit pragma Thread_Bound markiert ist, erreicht einen Task oder einen Kanal (H3112).
- Ein Verleih-Scope lässt seine Tasks lesen, was ein Ausleihen ihnen bereitstellt, am Aufruf geprüft.
- join ist das Einzige, was einen Task verbraucht, und jeder Task muss mit join beendet werden.
- closeScope kehrt erst zurück, wenn jeder Task des Scopes beendet ist.
- Ein Wert, der in einem Kanal bleibt, wenn sein Empfänger schließt, beendet das Programm mit einer Meldung.
- Ein Programm, in dem jeder Task auf einen anderen wartet, endet mit einer Meldung.
- abort in einem Task beendet den Prozess.
var scope: Scope := opened;
case spawn(&!scope, work, 21) of
when Right(right as task: Task[Int32]) do
printLn(join(task));
when Left(left as unused: Int32) do
printLn(-1);
end case;
closeScope(scope);
Standard.TaskArena
Ein Task, dem eine eigene Arena übergeben wird, die mit dem Task endet.
- spawnWithArena legt eine Arena mit mindestens der verlangten Kapazität an und startet den Task damit.
- Nur arenaAllocate und arenaSpan auf dieser Arena entnehmen ihr etwas; alles andere wird wie üblich allokiert.
- Die Arena ist Linear: Der Task gibt sie mit seinem Ergebnis zurück und kann sie nicht fallen lassen.
- Die Arena wird freigegeben, wenn der Task endet, bevor join das Ergebnis sieht; ihr benutzter Teil wird überschrieben.
- Das Argument wird wie bei spawn geprüft: keine Referenz und kein Span darin, in keiner Tiefe (H3002).
- Left gibt das Argument unberührt zurück, wenn die Arena oder der Task nicht angelegt werden konnte.
case spawnWithArena(&!scope, work, request, 65536) of
when Right(right as task: Task[Reply]) do
answer(join(task));
when Left(left as unused: Request) do
refuse(unused);
end case;
Standard.Template
Jinja2-Templates, beim Übersetzen des Programms geprüft, und was ihr erzeugter Code aufruft.
- Die Syntax ist die von Jinja2, abgesehen von dem, was abgelehnt oder anders gemacht wird.
- @template liest die Datei neben dem Modul und schreibt Hilde-Code, der in out schreibt.
- Jeder Name stammt aus page, einer Referenz auf einen Record-Typ; ein Name, der ihm fehlt, wird nicht übersetzt.
- Ein Wert, der nicht ausgegeben werden kann, oder ein ungefragt ausgegebenes Option ist ein Fehler in der Zeile des Templates.
- Jeder Wert wird für den Ort maskiert, an dem er landet: HTML-Text, Attribute, URL-Teile, JavaScript, CSS.
- Ein Wert an einer Stelle, die keine Maskierung sicher macht, etwa ein Kommentar oder ein Tag-Name, wird nicht übersetzt.
- Nur |safe schreibt einen Wert unmaskiert, und es lässt sich mit grep finden.
- Ein Modul, das @template verwendet, importiert dieses, da die Instanzen mit dem Import kommen.
var out: StringBuilder := makeStringBuilder();
@template("page.jinja", page, out);
return finalize(out);
Standard.Testing
Assertions für Tests, die hilde test ausführt.
- Jede Assertion gibt eine Zeile aus, die nennt, was sie prüft, und sie mit ok beendet, wenn es gilt.
- Eine Prüfung, die fehlschlägt, beendet das Programm mit einer Meldung auf der Standardfehlerausgabe und damit den Test.
- assertFailure markiert einen Schritt, den der Test nicht erreichen darf; assertSuccess einen, den er erreicht hat.
- underValgrind ist nur unter hilde test --valgrind wahr, um dessen Verlangsamung zu berücksichtigen.
assertTrue(isLeapYear(2024), "2024 is a leap year");
assertFalse(isLeapYear(1900), "1900, divided by 100 but not by 400, is not");
Standard.Text
Was man mit Text tut: ihn parsen, kodieren, vergleichen und zerlegen.
- Standard.String ist der Container; dieses Modul liest Text über einen Span[Nat8, R].
- Jeder Fehler ist ein Option, nie ein Sentinel: Nichts hier gibt -1 zurück.
- Nichts löst bei schlechter Eingabe eine Falle aus: Ein String, der keine Zahl ist oder nicht passt, ist None.
- Was zerlegt wird, kommt als Spans derselben Bytes zurück: Nichts wird kopiert.
- Was gebaut wird, ist ein frischer String, der dem Aufrufer gehört.
- Groß-/Kleinschreibung ändert nur ASCII-Buchstaben, ist also auf UTF-8 sicher.
- equal hält am ersten Unterschied an; für Geheimnisse constantTimeEqual verwenden.
- Eine Suche nach einem Teil braucht Zeit linear in den Längen von Text und Teil.
let port: Option[Nat64] := parseNat(trim(asSpan(&field)));
let parts: Buffer[Span[Nat8, Static]] := split("a,b,c", ",");
Standard.Time
Eine Wanduhr, die sagt, wie spät es ist, und eine monotone Uhr, die sagt, wie viel Zeit vergangen ist.
- Beide Uhren lesen in Nanosekunden, als Nat64.
- Die Wanduhr zählt ab der Epoche und kann zurückgestellt werden: Sie ist für eine Zeit, die ein Mensch liest.
- wallNanos liefert null, wenn die Wanduhr nicht gelesen werden konnte.
- Die monotone Uhr geht nie rückwärts; nur die Differenz zweier Messwerte bedeutet etwas.
- Ein Build ohne monotone Uhr sagt das über hasMonotonic, und monotonicNanos liefert None.
let stamp: Nat64 := wallNanos();
case monotonicNanos() of
when Some(value as start: Nat64) do
work();
report(start);
when None do
work();
end case;
Standard.Tls
Ein TLS-Client und -Server, über libtls auf OpenBSD und libretls auf Debian.
- Ein Client prüft stets das Zertifikat des Servers, gegen eine Zertifikatsdatei und den Host.
- Nichts schaltet diese Prüfung ab.
- Die Formen, die auf Tls13 enden, erlauben TLS 1.3 und nichts Älteres.
- Ein Server aus makeServer verlangt von seiner Gegenstelle kein Zertifikat.
- Ein Server aus makeVerifyingServer lehnt jeden Client ohne ein Zertifikat ab, das seine CA bestätigt.
- Jeder Fehler ist ein Left mit den eigenen Worten der Bibliothek nach dem Präfix Standard.Tls und einem Doppelpunkt.
- Diese Worte unterscheiden sich zwischen den beiden Bibliotheken.
- Ein Empfang nimmt höchstens 16 MiB, welche Anzahl auch verlangt wird.
let made: Either[String, Client] := connect("example.org", "443", "/etc/ssl/cert.pem");
let sent: Either[String, Unit] := send(&!client, asSpan(&request));
Standard.Toml
TOML, wie Version 1.0.0 seiner Spezifikation es definiert, streng gelesen und dann per Pfad abgefragt.
- Ein Dokument wird vom ersten bis zum letzten Byte geprüft, bevor ein Wert daraus gelesen wird.
- Ein Pfad besteht aus den Schlüsseln von oben nach unten, wie in TOML durch Punkte verbunden, und einem Array-Index in eckigen Klammern.
- Der leere Pfad ist das ganze Dokument; ein Pfad, der ins Leere oder zu einer anderen Art führt, liefert None.
- Das Lesen lehnt ab, statt zu raten: Das Left ist ein TomlFault mit Zeile und Spalte.
- Nichts hier bricht bei schlechter Eingabe ab.
- Arrays und Inline-Tabellen dürfen höchstens mostDepth tief verschachtelt sein, das sind 128.
- Ganzzahlen werden als Int64 gehalten, Floats sowie Datums- und Zeitwerte als der Text, in dem sie geschrieben waren.
case readToml(asSpan(&text)) of
when Right(right as parsed: Toml) do
let port: Option[Int64] := integerAt(&parsed, "servers[1].port");
destroyToml(parsed);
when Left(left as error: TomlError) do
report(error.line, error.column);
end case;
Standard.Tuples
Records aus zwei bis sechs Werten beliebiger Typen, mit Feldern, die nach ihrer Position benannt sind.
- Pair hält zwei Werte, Triple drei, und Tuple4, Tuple5 und Tuple6 vier bis sechs.
- Die Felder heißen first, second, third, fourth, fifth und sixth, in dieser Reihenfolge.
- Jedes Feld ist öffentlich: Ein Tupel wird mit benannten Argumenten gebildet und mit einem let zerlegt.
- Ein Feld darf von einem linearen Typ sein, etwa String.
let old: Pair[String, Nat64] := Pair(first => word, second => seen);
let { first: String, second: Nat64 } := old;
Standard.Unicode
Groß- und Kleinschreibung von UTF-8-Text und Vergleich ohne Rücksicht auf die Schreibung.
- Die Tabellen sind die der Unicode Character Database, Version 17.0.0.
- Die Abbildungen sind die vollständigen: Das deutsche scharfe s wird auf zwei große S abgebildet.
- Ein Codepunkt wird für sich abgebildet, ohne Regeln, die auf seine Nachbarn oder auf eine Sprache schauen.
- Daher wird das griechische große Sigma immer auf das kleine Sigma abgebildet, nie auf das Schluss-Sigma.
- Das gepunktete und das punktlose i des Türkischen sind nicht besonders behandelt.
- Text, der kein wohlgeformtes UTF-8 ist, wird abgelehnt, nicht repariert.
let shout: Option[String] := toUpper("Gruesse aus Koeln");
let same: Bool := equalIgnoringCase("STRASSE", asSpan(&street));
Standard.UnicodeProperties
Eigenschaften von Codepunkten aus der Unicode Character Database, Version 17.0.0.
- Es liefert die allgemeine Kategorie, das Schriftsystem und die Bahnen der einfachen Groß-/Kleinschreibungs-Faltung.
- Eine Bahn sind die Codepunkte, die auf denselben falten.
- Standard.Regex liest seine Unicode-Klassen und seine Schreibungsfaltung von hier.
- Ein Codepunkt ist ein Index; einer hinter U+10FFFF hat keine Eigenschaft.
- Kategorien und Schriftsysteme kommen als statischer Text zurück: Dafür wird nichts allokiert.
let category: Span[Nat8, Static] := generalCategory(65);
var ranges: Buffer[Index] := allocateEmpty();
let known: Bool := appendProperty("Greek", &!ranges);
Standard.Users
Benutzerkonten mit Argon2id-Passwörtern, Sperren und einer Grenze für fehlgeschlagene Versuche, über einen Store, den das Programm wählt.
- Ein Store ist jeder Typ mit einer Instanz von UserStore; Standard.SqliteUsers und Standard.DatabaseUsers sind fertige.
- Ein Name hat 1 bis 64 Bytes aus Buchstaben, Ziffern, Punkt, Unterstrich und Bindestrich und beginnt mit einem Buchstaben oder einer Ziffer.
- Ein Passwort hat 1 bis 1024 Bytes; nur sein Hash aus Standard.Password wird gespeichert.
- verify antwortet für einen unbekannten Namen und ein falsches Passwort gleichermaßen mit Rejected und verbraucht bei beiden einen Hash.
- Locked wird nur dem richtigen Passwort geantwortet, sodass eine Sperre dem nichts verrät, der es nicht kennt.
- Fehlgeschlagene Versuche werden je Client gezählt, höchstens limits.attempts innerhalb von limits.windowSeconds.
- Jede Zeit sind vom Aufrufer übergebene Sekunden; unter 0 wird abgelehnt.
- Kein HTTP und kein Cookie darin; sobald verify Accepted liefert, öffnet Standard.Session die Anmeldung.
case verify(&!users, "alice", asSpan(&typed), asSpan(&address), limits, now) of
when Right(right as verdict: Verdict) do
answer(verdict);
when Left(left as error: UserError) do
reportFailure(error);
end case;
Standard.Utf8
UTF-8: es dekodieren, validieren und bauen.
- Ein CodePoint, der existiert, ist ein gültiger Skalarwert, und makeCodePoint ist der einzige Weg, einen zu erzeugen.
- makeCodePoint lehnt ein Surrogat und alles über U+10FFFF ab.
- Ungültige Eingabe ist None, nie das Ersatzzeichen: Das Ersetzen ist Sache des Aufrufers.
- Ein einzelnes Fortsetzungsbyte, eine abgeschnittene Sequenz, ein Byte ab 0xF8 und eine überlange Form werden abgelehnt.
- So kann eine überlange Kodierung eines Schrägstrichs an einer Pfadprüfung nicht vorbeikommen.
- Das Kodieren kann nicht fehlschlagen, da jeder CodePoint kodierbar ist.
case makeCodePoint(233) of
when Some(value as point: CodePoint) do
appendCodePoint(&!builder, point);
when None do
skip;
end case;
let count: Option[Index] := countCodePoints(asSpan(&text));
Standard.Vault
Verschlüsselte Geheimnisse in einer .env-Datei, lesbar nur mit dem Schlüssel in dieser Datei und einem Pepper in einer eigenen Datei.
- Jeder Wert wird mit XChaCha20-Poly1305 versiegelt und mit HMAC-SHA3-512 signiert, im Format der Version 0xCC.
- Die Chiffrierschlüssel werden aus dem Schlüssel der .env und dem Pepper abgeleitet; keines allein liest einen Wert.
- Der Pepper hat genau 32 Bytes; eine Pepper-Datei anderer Größe wird abgelehnt.
- Jedes Schreiben ersetzt die .env atomar mit Modus 0600, Besitzer und Gruppe der alten Datei bleiben erhalten.
- Ein geöffneter Vault hält seine Chiffrierschlüssel in einem linearen Wert; closeVault überschreibt sie mit Nullen.
- Der Name eines Geheimnisses besteht aus A bis Z, 0 bis 9 und _, beginnt nicht mit einer Ziffer und hat höchstens 128 Bytes.
case openVault("/etc/app/secret/.app.env", "/var/lib/app/.app.pepper", "VAULT_KEY") of
when Right(right as opened: Vault) do
var vault: Vault := opened;
let stored: Either[VaultError, Unit] := setSecret(&!vault, "DB_PASSWORD", "s3cret");
closeVault(vault);
when Left(left as error: VaultError) do
skip;
end case;
Standard.Watch
Dateien und Verzeichnisse auf Änderungen beobachten: inotify auf Linux, kqueue auf OpenBSD und FreeBSD.
- Ein Watcher hält beliebig viele Beobachtungen, ab 0 in der Reihenfolge ihres Hinzufügens nummeriert.
- Die Beobachtung eines Verzeichnisses sieht, wenn ein Name darin angelegt, entfernt oder hinein- oder herausbewegt wird.
- Die Beobachtung einer Datei sieht, wenn sie geschrieben wird, ihre Attribute sich ändern und sie entfernt oder umbenannt wird.
- Auch das Ersetzen einer beobachteten Datei per Umbenennen wird gesehen, und ein symbolischer Link wird verfolgt.
- Ein Warten sagt, welche Beobachtung sich geändert hat, innerhalb einer Zeitgrenze, und sagt weniger, als geschehen ist, nie mehr.
- Änderungen, die zusammen eintreffen, werden möglicherweise einmal gemeldet: Auf das Beobachtete schauen, Meldungen nicht zählen.
- Auf anderen Plattformen ist watchAvailable false, und makeWatcher liefert None.
var watcher: Watcher := made;
let config: Option[Index] := watchFile(&!watcher, "/etc/app.conf");
case waitForChange(&!watcher, 5000) of
when Changed(watch as changed: Index) do
reload();
when WaitedOut do
skip;
when WatchFailed do
skip;
end case;
destroyWatcher(watcher);
Standard.X509
Schlüssel und X.509-Zertifikate: erzeugt, geschrieben, gelesen, signiert und geprüft.
- Ein Schlüssel ist NIST P-256, P-384 oder P-521 mit ECDSA, Ed25519, RSA oder ML-DSA nach FIPS 204.
- RSA-Schlüssel haben 2048, 3072 oder 4096 Bit und signieren mit PKCS 1 v1.5 über SHA-256.
- ML-DSA-Schlüssel kommen auf jedem System aus Standard.MlDsa und werden als PKCS 8 mit dem Seed und dem expandierten Schlüssel geschrieben.
- Ein Zertifikat oder eine Anfrage mit einem ML-DSA-Schlüssel wird nicht erzeugt: Diese Funktionen liefern None.
- Ein hier erzeugtes Zertifikat hat Version 3 und eine Seriennummer aus 127 Zufallsbits.
- Seine Basic Constraints und Key Usage sind als kritisch markiert, und es trägt die Schlüsselkennungen.
- Ein hier erzeugtes Blatt hat die erweiterten Schlüsselverwendungen eines Servers und eines Clients.
- DNS-Namen sind ein durch Kommas getrennter Text, und ein Text, den isDnsNameList ablehnt, wird abgelehnt.
- Ein privater Schlüssel wird als unverschlüsseltes PEM geschrieben: Wer den Text hat, hat den Schlüssel.
let key: Option[PrivateKey] := generateKey(EcP256());
let made: Option[Certificate] := selfSigned(&signer, "web", "example.org", 90, false);
Standard.Xml
XML 1.0, in Tokens gelesen, ohne Dokumenttyp, und dafür maskierter Text.
- Ein Dokument muss wohlgeformt und gültiges UTF-8 sein; nichts wird repariert.
- Jedes Element liefert einen Start, mit seinen Attributen, und ein Ende, auch ein leeres; Text ist ein Token.
- Die fünf vordefinierten Entitäten und jede Zeichenreferenz werden dekodiert; CDATA ist Text.
- Kommentare, Verarbeitungsanweisungen und die XML-Deklaration werden gelesen und verworfen.
- Eine Dokumenttyp-Deklaration wird abgelehnt, sodass es keine externe Entität (XXE) und keine Expansion gibt.
- Eine Referenz auf eine andere Entität als die fünf wird abgelehnt, ebenso eine Verschachtelung tiefer als 256.
- Jede Ablehnung nennt den Byte-Offset, an dem das Lesen anhielt.
case parseXml(asSpan(&text)) of
when Right(right as parsed: XmlDocument) do
let feed: Option[Index] := xmlFindStart(&parsed, 0, "feed");
destroyXmlDocument(parsed);
when Left(left as error: XmlError) do
report(error.offset);
end case;
Standard.Yaml
Der Teil von YAML, den Konfigurationsdateien verwenden, in die Werte von Standard.Json gelesen.
- Block-Mappings, Block-Sequenzen, einfache und angeführte Skalare sowie Block-Skalare werden gelesen.
- Eine Flow-Sequenz oder ein Flow-Mapping wird gelesen, wenn es in seiner Zeile endet.
- Ein einfacher Skalar ist null, ein Boolean, eine Zahl, wie JSON sie schreibt, oder sonst ein String, wie yes einer ist.
- Anker, Aliase, Tags, Direktiven und ein zweites Dokument werden abgelehnt, nicht ignoriert.
- So kann kein kleiner Text über Aliase zu einem riesigen Wert expandieren.
- Ein Tabulator in der Einrückung, ein Skalar über mehrere Zeilen und ein doppelter Schlüssel in einem Mapping werden abgelehnt.
- Eine Verschachtelung tiefer als 64 wird abgelehnt; jede Ablehnung nennt die Zeile, in der das Lesen anhielt.
case readYaml(asSpan(&text)) of
when Right(right as config: Json) do
apply(config);
when Left(left as error: YamlError) do
report(error.line);
end case;