diff options
author | Luke T. Shumaker <lukeshu@lukeshu.com> | 2024-10-09 00:00:32 -0600 |
---|---|---|
committer | Luke T. Shumaker <lukeshu@lukeshu.com> | 2024-10-09 00:00:32 -0600 |
commit | bed78039f9bf086d35a3ae5efc8e6701c50ed006 (patch) | |
tree | 38fd0e196f08673f43f984b4228a4b6b5c647431 | |
parent | 5dfff05adef9b1e09cff350c2dc551fdac6d227a (diff) |
wip idl rewrite
-rw-r--r-- | Makefile | 4 | ||||
-rw-r--r-- | lib9p/9P2000.L.txt | 56 | ||||
-rw-r--r-- | lib9p/9P2000.e.txt | 18 | ||||
-rw-r--r-- | lib9p/9P2000.txt | 155 | ||||
-rw-r--r-- | lib9p/9P2000.u.txt | 29 | ||||
-rwxr-xr-x | lib9p/9p.gen | 1059 | ||||
-rwxr-xr-x | lib9p/idl.gen | 1180 | ||||
-rw-r--r-- | lib9p/idl/00-README.md | 48 | ||||
-rw-r--r-- | lib9p/idl/01-9P2000.9p | 147 | ||||
-rw-r--r-- | lib9p/idl/02-9P2000.e.9p | 18 | ||||
-rw-r--r-- | lib9p/idl/02-9P2000.u.9p | 29 | ||||
-rw-r--r-- | lib9p/idl/03-9P2000.L.9p.wip | 56 |
12 files changed, 1480 insertions, 1319 deletions
@@ -19,7 +19,7 @@ lib9p/include/lib9p/linux-errno.h: %: %.gen 3rd-party/linux-errno.txt ./$^ >$@ generate/files += lib9p/9p.generated.c lib9p/include/lib9p/9p.generated.h -lib9p/9p.generated.c lib9p/include/lib9p/9p.generated.h &: lib9p/9p.gen lib9p/9P2000.txt lib9p/9P2000.u.txt lib9p/9P2000.e.txt +lib9p/9p.generated.c lib9p/include/lib9p/9p.generated.h &: lib9p/idl.gen lib9p/idl/*.9p ./$^ generate/files += libusb/include/libusb/tusb_helpers.h 3rd-party/MS-LCID.pdf 3rd-party/MS-LCID.txt @@ -47,7 +47,7 @@ generate-clean: sources_sh = 3rd-party/linux-errno.txt.gen sources_sh += libusb/include/libusb/tusb_helpers.h.gen -sources_py = lib9p/9p.gen +sources_py = lib9p/idl.gen sources_py += lib9p/include/lib9p/linux-errno.h.gen lint: shellcheck $(sources_sh) diff --git a/lib9p/9P2000.L.txt b/lib9p/9P2000.L.txt deleted file mode 100644 index 71ab171..0000000 --- a/lib9p/9P2000.L.txt +++ /dev/null @@ -1,56 +0,0 @@ -# 9P2000.L.txt - Definitions of 9P2000.L messages -# -# Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com> -# SPDX-Licence-Identifier: AGPL-3.0-or-later - -# "9P2000.L" Linux extension -# https://github.com/chaos/diod/blob/master/protocol.md -version "9P2000.L" - -from 9P2000.txt import * -from 9P2000.u.txt import Tauth, Tattach - -#6/Tlerror = "illegal" # analogous to 106/Terror -7/Rlerror = "ecode[4]" # analogous to 107/Rerror -8/Tstatfs = "TODO" -9/Rstatfs = "TODO" -12/Tlopen = "TODO" # analogous to 112/Topen -13/Rlopen = "TODO" # analogous to 113/Ropen -14/Tlcreate = "TODO" # analogous to 114/Tcreate -15/Rlcreate = "TODO" # analogous to 115/Rcreate -16/Tsymlink = "TODO" -17/Rsymlink = "TODO" -18/Tmknod = "TODO" -19/Rmknod = "TODO" -20/Trename = "TODO" -21/Rrename = "TODO" -22/Treadlink = "TODO" -23/Rreadlink = "TODO" -24/Tgetattr = "TODO" -25/Rgetattr = "TODO" -26/Tsetattr = "TODO" -27/Rsetattr = "TODO" -#... -30/Txattrwalk = "TODO" -31/Rxattrwalk = "TODO" -32/Txattrcreate = "TODO" -33/Rxattrcreate = "TODO" -#... -40/Treaddir = "TODO" -41/Rreaddir = "TODO" -#... -50/Tfsync = "TODO" -51/Rfsync = "TODO" -52/Tlock = "TODO" -53/Rlock = "TODO" -54/Tgetlock = "TODO" -55/Rgetlock = "TODO" -# ... -70/Tlink = "TODO" -71/Rlink = "TODO" -72/Tmkdir = "TODO" -73/Tmkdir = "TODO" -74/Trenameat = "TODO" -75/Rrenameat = "TODO" -76/Tunlinkat = "TODO" -77/Runlinkat = "TODO" diff --git a/lib9p/9P2000.e.txt b/lib9p/9P2000.e.txt deleted file mode 100644 index e60bce0..0000000 --- a/lib9p/9P2000.e.txt +++ /dev/null @@ -1,18 +0,0 @@ -# 9P2000e.e.txt - Definitions of 9P2000.e messages -# -# Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com> -# SPDX-Licence-Identifier: AGPL-3.0-or-later - -# "9P2000.e" Erlang extension -# https://erlangonxen.org/more/9p2000e -# https://github.com/cloudozer/ling/blob/master/doc/9p2000e.md -version "9P2000.e" - -from 9P2000.txt import * - -150/Tsession = "key[8]" -151/Rsession = "" -152/Tsread = "fid[4] nwname[2] nwname*(wname[s])" -153/Rsread = "data[d]" -154/Tswrite = "fid[4] nwname[2] nwname*(wname[s]) data[d]" -155/Rswrite = "count[4]" diff --git a/lib9p/9P2000.txt b/lib9p/9P2000.txt deleted file mode 100644 index bf7652d..0000000 --- a/lib9p/9P2000.txt +++ /dev/null @@ -1,155 +0,0 @@ -# 9P2000.txt - Definitions of 9P2000 messages -# -# Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com> -# SPDX-Licence-Identifier: AGPL-3.0-or-later - -# The format of each message (excluding the "size[4] msg_type[1] -# tag[2]" header) is written here as a sequence of -# "member_name[member_type]" struct members. -# -# The primitive member types types are the following single-character -# mnemonics: -# -# - 1 = u8 -# - 2 = u16le -# - 4 = u32le -# - 8 = u16le - -# "9P2000" base protocol -# https://ericvh.github.io/9p-rfc/rfc9p2000.html -# https://github.com/ericvh/9p-rfc/blob/master/9p2000.xml -# -# But due to incompleteness of the draft RFC, the Plan 9 manual -# section-5 and the Plan 9 headers (particularly fcall.h) are often -# better references. -version "9P2000" - -# data (u32le `n`, then `n` bytes of data) -d = "len[4] len*(dat[1])" - -# string (u16le `n`, then `n` bytes of UTF-8) -s = "len[2] len*(utf8[1])" - -# "d"? mode - (file permissions and attributes) -bitfield dm 32 - 31/DIR - 30/APPEND - 29/EXCL - # DMMOUNT has been around in Plan 9 forever, but is - # undocumented, and is explicitly excluded from the 9P2000 - # draft RFC. As I understand it, DMMOUNT indicates that the - # file is mounted by the kernel as a 9P transport; that the - # kernel has a lock on doing I/O on it, so userspace can't do - # I/O on it. - 28/_PLAN9_MOUNT - 27/AUTH - 26/TMP - #... - 8/OWNER_R - 7/OWNER_W - 6/OWNER_X - 5/GROUP_R - 4/GROUP_W - 3/GROUP_X - 2/OTHER_R - 1/OTHER_W - 0/OTHER_X - - PERM=0777 # {OWNER,GROUP,OTHER}_{R,W,X} - -# QID Type (see qid below) -bitfield qt 8 - 7/DIR - 6/APPEND - 5/EXCL - 4/_PLAN9_MOUNT # see DMMOUNT above - 3/AUTH - # Fun historical fact: QTTMP was a relatively late addition to - # Plan 9, in 2003-12. - 2/TMP - #1/unused - - # "The name QTFILE, defined to be zero, identifies the value - # of the type for a plain file." - FILE=0 - -# uni"Q"ue "ID"entification - "two files on the same server hierarchy -# are the same if and only if their qids are the same" -# -# - "path" is a unique uint64_t that does most of the work in the -# above statement about files being the same if their QIDs are the -# same; " If a file is deleted and recreated with the same name in -# the same directory, the old and new path components of the qids -# should be different" -# -# - "vers" "is a version number for a file; typically, it is -# incremented every time the file is modified. -# -# - "type" indicates "whether the file is a directory, append-only -# file, etc."; is an instance of the qid_type bitfield. -qid = "type[qt] vers[4] path[8]" - -# stat (TODO) -stat = "stat_size[2,val=end-&kern_type]" - "kern_type[2]" - "kern_dev[4]" - "file_qid[qid]" - "file_mode[dm]" - "file_atime[4]" - "file_mtime[4]" - "file_size[8]" - "file_name[s]" - "file_owner_uid[s]" - "file_owner_gid[s]" - "file_last_modified_uid[s]" - -# "O"pen flags (flags to pass to Topen and Tcreate) -bitfield o 8 - 0/rwx_0 # low bit of the 2-bit READ/WRITE/RDWR/EXEC enum - 1/rwx_1 # high bit of the 2-bit READ/WRITE/RDWR/EXEC enum - #2/unused - #3/unused - 4/TRUNC - #5/unused - 6/RCLOSE # remove-on-close - #7/unused - - READ = 0 # unlock read() - WRITE = 1 # unlock write() - RDWR = 2 # unlock read() and write() - EXEC = 3 # unlock read() for files, walk() for directories - -# In the 9P protocol, each message has a type, and message types come -# in pairs (except "Rerror"); "T" and "R"; T-messages are -# client->server requests, and R-messages are server->client responses -# (the client "Transmits" T-messages and "Receives" R-messages). The -# type of a message is represented by a u8 ID; T-messages are even and -# R-messages are odd. -100/Tversion = "max_msg_size[4] version[s]" -101/Rversion = "max_msg_size[4] version[s]" -102/Tauth = "afid[4] uname[s] aname[s]" -103/Rauth = "aqid[qid]" -104/Tattach = "fid[4] afid[4] uname[s] aname[s]" -105/Rattach = "qid[qid]" -#106/Terror = "illegal" -107/Rerror = "ename[s]" -108/Tflush = "oldtag[2]" -109/Rflush = "" -110/Twalk = "fid[4] newfid[4] nwname[2,max=16] nwname*(wname[s])" -111/Rwalk = "nwqid[2,max=16] nwqid*(wqid[qid])" -112/Topen = "fid[4] mode[o]" -113/Ropen = "qid[qid] iounit[4]" -114/Tcreate = "fid[4] name[s] perm[dm] mode[o]" -115/Rcreate = "qid[qid] iounit[4]" -116/Tread = "fid[4] offset[8] count[4]" -117/Rread = "data[d]" # for directories, `data` is the sequence "cnt*(entries[stat])" -118/Twrite = "fid[4] offset[8] data[d]" -119/Rwrite = "count[4]" -120/Tclunk = "fid[4]" -121/Rclunk = "" -122/Tremove = "fid[4]" -123/Rremove = "" -124/Tstat = "fid[4]" -125/Rstat = "nstat[2,val=end-&stat] stat[stat]" # See the "BUG" note in the RFC for the nstat field -126/Twstat = "fid[4] nstat[2,val=end-&stat] stat[stat]" # See the "BUG" note in the RFC for the nstat field -127/Rwstat = "" diff --git a/lib9p/9P2000.u.txt b/lib9p/9P2000.u.txt deleted file mode 100644 index 9f28f33..0000000 --- a/lib9p/9P2000.u.txt +++ /dev/null @@ -1,29 +0,0 @@ -# 9P2000.u.txt - Definitions of 9P2000.u messages -# -# Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com> -# SPDX-Licence-Identifier: AGPL-3.0-or-later - -# "9P2000.u" Unix extension -# https://ericvh.github.io/9p-rfc/rfc9p2000.u.html -# https://github.com/ericvh/9p-rfc/blob/master/9p2000.u.xml -version "9P2000.u" - -from 9P2000.txt import * - -stat += "file_extension[s]" - "file_owner_n_uid[4]" - "file_owner_n_gid[4]" - "file_last_modified_n_uid[4]" - -Tauth += "n_uname[4]" -Tattach += "n_uname[4]" - -Rerror += "errno[4]" - -dm += 23/DEVICE - 21/NAMEDPIPE - 20/SOCKET - 19/SETUID - 18/SETGID - -qt += 1/SYMLINK diff --git a/lib9p/9p.gen b/lib9p/9p.gen deleted file mode 100755 index 816ec0a..0000000 --- a/lib9p/9p.gen +++ /dev/null @@ -1,1059 +0,0 @@ -#!/usr/bin/env python -# lib9p/9p.gen - Generate C marshalers/unmarshalers for .txt files -# defining 9P protocol variants. -# -# Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com> -# SPDX-Licence-Identifier: AGPL-3.0-or-later - -import enum -import os.path -import re -from typing import Callable, Literal, Sequence - -# This strives to be "general-purpose" in that it just acts on the -# *.txt inputs; but (unfortunately?) there are a few special-cases in -# this script, marked with "SPECIAL". - -# Parse *.txt ################################################################## - - -class Atom(enum.Enum): - u8 = 1 - u16 = 2 - u32 = 4 - u64 = 8 - - @property - def name(self) -> str: - return str(self.value) - - @property - def static_size(self) -> int: - return self.value - - -class BitfieldVal: - name: str - val: str - ver: set[str] - - def __init__(self) -> None: - self.ver = set() - - -class Bitfield: - name: str - bits: list[str] - names: dict[str, BitfieldVal] - - @property - def static_size(self) -> int: - return int((len(self.bits) + 7) / 8) - - def bitname_is_valid(self, bitname: str, ver: str | None = None) -> bool: - assert bitname in self.bits - if not bitname: - return False - if bitname.startswith("_"): - return False - if ver and (ver not in self.names[bitname].ver): - return False - return True - - -# `msgid/structname = "member1 member2..."` -# `structname = "member1 member2..."` -# `structname += "member1 member2..."` -class Struct: - msgid: int | None = None - msgver: set[str] - name: str - members: list["Member"] - - def __init__(self) -> None: - self.msgver = set() - - @property - def static_size(self) -> int | None: - size = 0 - for member in self.members: - msize = member.static_size - if msize is None: - return None - size += msize - return size - - -class ExprVal: - name: str - - def __init__(self, name: str) -> None: - self.name = name - - -class ExprOp: - op: Literal["-", "+"] - - def __init__(self, op: Literal["-", "+"]) -> None: - self.op = op - - -# `cnt*(name[typ])` -# the `cnt*(...)` wrapper is optional -class Member: - cnt: str | None = None - name: str - typ: Atom | Bitfield | Struct - max: int | None = None - valexpr: list[ExprVal | ExprOp] = [] - ver: set[str] - - @property - def static_size(self) -> int | None: - if self.cnt: - return None - return self.typ.static_size - - -def parse_valexpr(valexpr: str) -> list[ExprVal | ExprOp]: - ret: list[ExprVal | ExprOp] = [] - for tok in re.split("([-+])", valexpr): - match tok: - case "-": - ret += [ExprOp(tok)] - case "+": - ret += [ExprOp(tok)] - case _: - ret += [ExprVal(tok)] - return ret - - -re_membername = "(?:[a-zA-Z_][a-zA-Z_0-9]*)" -re_memberspec = f"(?:(?P<cnt>{re_membername})\\*\\()?(?P<name>{re_membername})\\[(?P<typ>[^,]*)(?:,max=(?P<max>[0-9]+)|,val=(?P<val>[-+&a-zA-Z0-9_]+))*\\]\\)?" - - -def parse_members( - ver: str, - env: dict[str, Atom | Bitfield | Struct], - existing: list[Member], - specs: str, -) -> list[Member]: - ret = existing - for spec in specs.split(): - m = re.fullmatch(re_memberspec, spec) - if not m: - raise SyntaxError(f"invalid member spec {repr(spec)}") - - member = Member() - member.ver = {ver} - - member.name = m.group("name") - if any(x.name == member.name for x in ret): - raise ValueError(f"duplicate member name {repr(member.name)}") - - if m.group("typ") not in env: - raise NameError(f"Unknown type {repr(m.group(2))}") - member.typ = env[m.group("typ")] - - if cnt := m.group("cnt"): - if len(ret) == 0 or ret[-1].name != cnt: - raise ValueError(f"list count must be previous item: {repr(cnt)}") - if not isinstance(ret[-1].typ, Atom): - raise ValueError(f"list count must be an integer type: {repr(cnt)}") - member.cnt = cnt - - if maxstr := m.group("max"): - if (not isinstance(member.typ, Atom)) or member.cnt: - raise ValueError("',max=' may only be specified on a non-repeated atom") - member.max = int(maxstr) - - if valstr := m.group("val"): - if (not isinstance(member.typ, Atom)) or member.cnt: - raise ValueError("',val=' may only be specified on a non-repeated atom") - member.valexpr = parse_valexpr(valstr) - - ret += [member] - return ret - - -re_version = r'version\s+"(?P<version>[^"]+)"' -re_import = r"from\s+(?P<file>\S+)\s+import\s+(?P<syms>\S+(?:\s*,\s*\S+)*)\s*" -re_structspec = ( - r'(?:(?P<msgid>[0-9]+)/)?(?P<name>\S+)\s*(?P<op>\+?=)\s*"(?P<members>[^"]*)"' -) -re_structspec_cont = r'\s+"(?P<members>[^"]*)"' -re_bitfieldspec = r"bitfield\s+(?P<name>\S+)\s+(?P<size>[0-9]+)" -re_bitfieldspec_bit = r"(?:\s+|(?P<bitfield>\S+)\s*\+=\s*)(?P<bit>[0-9]+)/(?P<name>\S+)" -re_bitfieldspec_alias = ( - r"(?:\s+|(?P<bitfield>\S+)\s*\+=\s*)(?P<name>\S+)\s*=\s*(?P<val>.*)" -) - - -def parse_file( - filename: str, get_include: Callable[[str], tuple[str, list[Bitfield | Struct]]] -) -> tuple[str, list[Bitfield | Struct]]: - version: str | None = None - env: dict[str, Atom | Bitfield | Struct] = { - "1": Atom.u8, - "2": Atom.u16, - "4": Atom.u32, - "8": Atom.u64, - } - with open(filename, "r") as fh: - prev: Struct | Bitfield | None = None - for line in fh: - line = line.split("#", 1)[0].rstrip() - if not line: - continue - if m := re.fullmatch(re_version, line): - if version: - raise SyntaxError("must have exactly 1 version line") - version = m.group("version") - elif m := re.fullmatch(re_import, line): - if not version: - raise SyntaxError("must have exactly 1 version line") - other_version, other_typs = get_include(m.group("file")) - for symname in m.group("syms").split(sep=","): - symname = symname.strip() - for typ in other_typs: - if typ.name == symname or symname == "*": - match typ: - case Bitfield(): - for val in typ.names.values(): - if other_version in val.ver: - val.ver.add(version) - case Struct(): - if typ.msgid: - typ.msgver.add(version) - for member in typ.members: - if other_version in member.ver: - member.ver.add(version) - env[typ.name] = typ - elif m := re.fullmatch(re_structspec, line): - if not version: - raise SyntaxError("must have exactly 1 version line") - if m.group("op") == "+=" and m.group("msgid"): - raise SyntaxError("cannot += to a message that is not yet defined") - match m.group("op"): - case "=": - struct = Struct() - if m.group("msgid"): - struct.msgid = int(m.group("msgid")) - struct.msgver.add(version) - struct.name = m.group("name") - struct.members = parse_members( - version, env, [], m.group("members") - ) - env[struct.name] = struct - prev = struct - case "+=": - if m.group("name") not in env: - raise NameError(f"Unknown type {repr(m.group('name'))}") - _struct = env[m.group("name")] - if not isinstance(_struct, Struct): - raise NameError( - f"Type {repr(_struct.name)} is not a struct" - ) - struct = _struct - struct.members = parse_members( - version, env, struct.members, m.group("members") - ) - prev = struct - elif m := re.fullmatch(re_structspec_cont, line): - if not isinstance(prev, Struct): - raise SyntaxError( - "struct-continuation line must come after a struct line" - ) - assert version - prev.members = parse_members( - version, env, prev.members, m.group("members") - ) - elif m := re.fullmatch(re_bitfieldspec, line): - if not version: - raise SyntaxError("must have exactly 1 version line") - bf = Bitfield() - bf.name = m.group("name") - bf.bits = int(m.group("size")) * [""] - bf.names = {} - if len(bf.bits) not in [8, 16, 32, 64]: - raise ValueError(f"Bitfield {repr(bf.name)} has an unusual size") - env[bf.name] = bf - prev = bf - elif m := re.fullmatch(re_bitfieldspec_bit, line): - if m.group("bitfield"): - if m.group("bitfield") not in env: - raise NameError(f"Unknown bitfield {repr(m.group('bitfield'))}") - _bf = env[m.group("bitfield")] - if not isinstance(_bf, Bitfield): - raise NameError(f"Type {repr(_bf.name)} is not a bitfield") - bf = _bf - prev = bf - else: - if not isinstance(prev, Bitfield): - raise SyntaxError( - "bitfield-continuation line must come after a bitfield line" - ) - bf = prev - bit = int(m.group("bit")) - name = m.group("name") - if bit < 0 or bit >= len(bf.bits): - raise ValueError(f"{bf.name}: bit {bit} is out-of-bounds") - if bf.bits[bit]: - raise ValueError(f"{bf.name}: bit {bit} already assigned") - if name in bf.names: - raise ValueError(f"{bf.name}: name {name} already assigned") - - bf.bits[bit] = name - - assert version - val = BitfieldVal() - val.name = name - val.val = f"1<<{bit}" - val.ver.add(version) - bf.names[name] = val - elif m := re.fullmatch(re_bitfieldspec_alias, line): - if m.group("bitfield"): - if m.group("bitfield") not in env: - raise NameError(f"Unknown bitfield {repr(m.group('bitfield'))}") - _bf = env[m.group("bitfield")] - if not isinstance(_bf, Bitfield): - raise NameError(f"Type {repr(_bf.name)} is not a bitfield") - bf = _bf - prev = bf - else: - if not isinstance(prev, Bitfield): - raise SyntaxError( - "bitfield-continuation line must come after a bitfield line" - ) - bf = prev - name = m.group("name") - valstr = m.group("val") - if name in bf.names: - raise ValueError(f"{bf.name}: name {name} already assigned") - - assert version - val = BitfieldVal() - val.name = name - val.val = valstr - val.ver.add(version) - bf.names[name] = val - else: - raise SyntaxError(f"invalid line {repr(line)}") - if not version: - raise SyntaxError("must have exactly 1 version line") - - typs = [x for x in env.values() if not isinstance(x, Atom)] - - for typ in just_structs_all(typs): - valid_vals = ["end", *["&" + m.name for m in typ.members]] - for member in typ.members: - for tok in member.valexpr: - if isinstance(tok, ExprVal) and tok.name not in valid_vals: - raise ValueError( - f"{typ.name}.{member.name}: invalid val: {tok.name}" - ) - - return version, typs - - -# Generate C ################################################################### - - -def c_typename(idprefix: str, typ: Atom | Bitfield | Struct) -> str: - match typ: - case Atom(): - return f"uint{typ.value*8}_t" - case Bitfield(): - return f"{idprefix}{typ.name}_t" - case Struct(): - if typ.msgid is not None: - return f"struct {idprefix}msg_{typ.name}" - return f"struct {idprefix}{typ.name}" - case _: - raise ValueError(f"not a type: {typ.__class__.__name__}") - - -def c_verenum(idprefix: str, ver: str) -> str: - return f"{idprefix.upper()}VER_{ver.replace('.', '_')}" - - -def c_vercomment(versions: set[str]) -> str | None: - if "9P2000" in versions: - return None - return "/* " + (", ".join(sorted(versions))) + " */" - - -def c_vercond(idprefix: str, versions: set[str]) -> str: - if len(versions) == 1: - return f"(ctx->ctx->version=={c_verenum(idprefix, next(v for v in versions))})" - return ( - "( " + (" || ".join(c_vercond(idprefix, {v}) for v in sorted(versions))) + " )" - ) - - -def just_structs_all(typs: list[Bitfield | Struct]) -> Sequence[Struct]: - return list(typ for typ in typs if isinstance(typ, Struct)) - - -def just_structs_nonmsg(typs: list[Bitfield | Struct]) -> Sequence[Struct]: - return list(typ for typ in typs if isinstance(typ, Struct) and typ.msgid is None) - - -def just_structs_msg(typs: list[Bitfield | Struct]) -> Sequence[Struct]: - return list( - typ for typ in typs if isinstance(typ, Struct) and typ.msgid is not None - ) - - -def just_bitfields(typs: list[Bitfield | Struct]) -> Sequence[Bitfield]: - return list(typ for typ in typs if isinstance(typ, Bitfield)) - - -def gen_h(idprefix: str, versions: set[str], typs: list[Bitfield | Struct]) -> str: - ret = f"""/* Generated by `{' '.join(sys.argv)}`. DO NOT EDIT! */ - -#ifndef _LIB9P_9P_H_ -# error Do not include <lib9p/9p.generated.h> directly; include <lib9p/9p.h> instead -#endif - -#include <stdint.h> /* for uint{{n}}_t types */ -""" - - ret += f""" -/* versions *******************************************************************/ - -enum {idprefix}version {{ -""" - fullversions = ["unknown = 0", *sorted(versions)] - verwidth = max(len(v) for v in fullversions) - for ver in fullversions: - ret += f"\t{c_verenum(idprefix, ver)}," - ret += (" " * (verwidth - len(ver))) + ' /* "' + ver.split()[0] + '" */\n' - ret += f"\t{c_verenum(idprefix, 'NUM')},\n" - ret += "};\n" - ret += "\n" - ret += f"const char *{idprefix}version_str(enum {idprefix}version);\n" - - ret += """ -/* non-message types **********************************************************/ -""" - for bf in just_bitfields(typs): - ret += "\n" - ret += f"typedef uint{bf.static_size*8}_t {c_typename(idprefix, bf)};\n" - names = [ - *reversed([bf.bits[n] or f"_UNUSED_{n}" for n in range(0, len(bf.bits))]), - "", - *[k for k in bf.names if k not in bf.bits], - ] - namewidth = max(len(name) for name in names) - - ret += "\n" - for name in names: - if name == "": - ret += "\n" - continue - if name.startswith("_"): - cname = f"_{idprefix.upper()}{bf.name.upper()}_{name[1:]}" - else: - cname = f"{idprefix.upper()}{bf.name.upper()}_{name}" - if name in bf.names: - val = bf.names[name].val - else: - assert name.startswith("_UNUSED_") - val = f"1<<{name[len('_UNUSED_'):]}" - ret += f"#define {cname}{' '*(namewidth-len(name))} (({c_typename(idprefix, bf)})({val}))" - if (name in bf.names) and (comment := c_vercomment(bf.names[name].ver)): - ret += " " + comment - ret += "\n" - - for struct in just_structs_nonmsg(typs): - all_the_same = len(struct.members) == 0 or all( - m.ver == struct.members[0].ver for m in struct.members - ) - typewidth = max(len(c_typename(idprefix, m.typ)) for m in struct.members) - if not all_the_same: - namewidth = max(len(m.name) for m in struct.members) - - ret += "\n" - ret += c_typename(idprefix, struct) + " {\n" - for member in struct.members: - if member.valexpr: - continue - ctype = c_typename(idprefix, member.typ) - if (struct.name in ["d", "s"]) and member.cnt: # SPECIAL - ctype = "char" - ret += f"\t{ctype.ljust(typewidth)} {'*' if member.cnt else ' '}{member.name};" - if (not all_the_same) and (comment := c_vercomment(member.ver)): - ret += (" " * (namewidth - len(member.name))) + " " + comment - ret += "\n" - ret += "};\n" - - ret += """ -/* messages *******************************************************************/ - -""" - ret += f"enum {idprefix}msg_type {{ /* uint8_t */\n" - namewidth = max(len(msg.name) for msg in just_structs_msg(typs)) - for msg in just_structs_msg(typs): - ret += f"\t{idprefix.upper()}TYP_{msg.name.ljust(namewidth)} = {msg.msgid}," - if comment := c_vercomment(msg.msgver): - ret += " " + comment - ret += "\n" - ret += "};\n" - ret += "\n" - ret += f"const char *{idprefix}msg_type_str(enum {idprefix}msg_type);\n" - - for msg in just_structs_msg(typs): - ret += "\n" - if comment := c_vercomment(msg.msgver): - ret += comment + "\n" - ret += c_typename(idprefix, msg) + " {" - if not msg.members: - ret += "};\n" - continue - ret += "\n" - - all_the_same = len(msg.members) == 0 or all( - m.ver == msg.members[0].ver for m in msg.members - ) - typewidth = max(len(c_typename(idprefix, m.typ)) for m in msg.members) - if not all_the_same: - namewidth = max(len(m.name) for m in msg.members) - - for member in msg.members: - if member.valexpr: - continue - ret += f"\t{c_typename(idprefix, member.typ).ljust(typewidth)} {'*' if member.cnt else ' '}{member.name};" - if (not all_the_same) and (comment := c_vercomment(member.ver)): - ret += (" " * (namewidth - len(member.name))) + " " + comment - ret += "\n" - ret += "};\n" - - return ret - - -def gen_c(idprefix: str, versions: set[str], typs: list[Bitfield | Struct]) -> str: - ret = f"""/* Generated by `{' '.join(sys.argv)}`. DO NOT EDIT! */ - -#include <assert.h> -#include <stdbool.h> -#include <stddef.h> /* for size_t */ -#include <inttypes.h> /* for PRI* macros */ -#include <string.h> /* for memset() */ - -#include <lib9p/9p.h> - -#include "internal.h" -""" - - def used(arg: str) -> str: - return arg - - def unused(arg: str) -> str: - return f"UNUSED({arg})" - - # strings ################################################################## - ret += f""" -/* strings ********************************************************************/ - -static const char *version_strs[{c_verenum(idprefix, 'NUM')}] = {{ -""" - for ver in ["unknown", *sorted(versions)]: - ret += f'\t[{c_verenum(idprefix, ver)}] = "{ver}",\n' - ret += "};\n" - ret += f""" -const char *{idprefix}version_str(enum {idprefix}version ver) {{ - assert(0 <= ver && ver < {c_verenum(idprefix, 'NUM')}); - return version_strs[ver]; -}} - -static const char *msg_type_strs[0x100] = {{ -""" - id2name: dict[int, str] = {} - for msg in just_structs_msg(typs): - assert msg.msgid - id2name[msg.msgid] = msg.name - for n in range(0, 0x100): - ret += '\t[0x{:02X}] = "{}",\n'.format(n, id2name.get(n, "0x{:02X}".format(n))) - ret += "};\n" - ret += f""" -const char *{idprefix}msg_type_str(enum {idprefix}msg_type typ) {{ - assert(0 <= typ && typ <= 0xFF); - return msg_type_strs[typ]; -}} -""" - - # validate_* ############################################################### - ret += """ -/* validate_* *****************************************************************/ - -static ALWAYS_INLINE bool _validate_size_net(struct _validate_ctx *ctx, uint32_t n) { - if (__builtin_add_overflow(ctx->net_offset, n, &ctx->net_offset)) - /* If needed-net-size overflowed uint32_t, then - * there's no way that actual-net-size will live up to - * that. */ - return lib9p_error(ctx->ctx, LINUX_EBADMSG, "message is too short for content"); - if (ctx->net_offset > ctx->net_size) - return lib9p_error(ctx->ctx, LINUX_EBADMSG, "message is too short for content"); - return false; -} - -static ALWAYS_INLINE bool _validate_size_host(struct _validate_ctx *ctx, size_t n) { - if (__builtin_add_overflow(ctx->host_extra, n, &ctx->host_extra)) - /* If needed-host-size overflowed size_t, then there's - * no way that actual-net-size will live up to - * that. */ - return lib9p_error(ctx->ctx, LINUX_EBADMSG, "message is too short for content"); - return false; -} - -static ALWAYS_INLINE bool _validate_list(struct _validate_ctx *ctx, - size_t cnt, - _validate_fn_t item_fn, size_t item_host_size) { - for (size_t i = 0; i < cnt; i++) - if (_validate_size_host(ctx, item_host_size) || item_fn(ctx)) - return true; - return false; -} - -#define validate_1(ctx) _validate_size_net(ctx, 1) -#define validate_2(ctx) _validate_size_net(ctx, 2) -#define validate_4(ctx) _validate_size_net(ctx, 4) -#define validate_8(ctx) _validate_size_net(ctx, 8) -""" - for typ in typs: - inline = ( - " FLATTEN" - if (isinstance(typ, Struct) and typ.msgid is not None) - else " ALWAYS_INLINE" - ) - argfn = unused if (isinstance(typ, Struct) and not typ.members) else used - ret += "\n" - ret += f"static{inline} bool validate_{typ.name}(struct _validate_ctx *{argfn('ctx')}) {{" - - if typ.name == "d": # SPECIAL - # Optimize... maybe the compiler could figure out to do - # this, but let's make it obvious. - ret += "\n" - ret += "\tuint32_t base_offset = ctx->net_offset;\n" - ret += "\tif (validate_4(ctx))\n" - ret += "\t\treturn true;\n" - ret += "\tuint32_t len = decode_u32le(&ctx->net_bytes[base_offset]);\n" - ret += "\treturn _validate_size_net(ctx, len) || _validate_size_host(ctx, len);\n" - ret += "}\n" - continue - if typ.name == "s": # SPECIAL - # Add an extra nul-byte on the host, and validate UTF-8 - # (also, similar optimization to "d"). - ret += "\n" - ret += "\tuint32_t base_offset = ctx->net_offset;\n" - ret += "\tif (validate_2(ctx))\n" - ret += "\t\treturn true;\n" - ret += "\tuint16_t len = decode_u16le(&ctx->net_bytes[base_offset]);\n" - ret += "\tif (_validate_size_net(ctx, len) || _validate_size_host(ctx, ((size_t)len)+1))\n" - ret += "\t\treturn true;\n" - ret += "\tif (!is_valid_utf8_without_nul(&ctx->net_bytes[base_offset+2], len))\n" - ret += '\t\treturn lib9p_error(ctx->ctx, LINUX_EBADMSG, "message contains invalid UTF-8");\n' - ret += "\treturn false;\n" - ret += "}\n" - continue - - match typ: - case Bitfield(): - ret += "\n" - all_the_same = all( - val.ver == [*typ.names.values()][0].ver - for val in typ.names.values() - ) - if ( - all_the_same - and (len(typ.bits) == typ.static_size * 8) - and all(typ.bitname_is_valid(bitname) for bitname in typ.bits) - ): - ret += f"\treturn validate_{typ.static_size}(ctx));\n" - else: - ret += f"\t if (validate_{typ.static_size}(ctx))\n" - ret += "\t\treturn true;\n" - if all_the_same: - ret += ( - f"\tstatic const {c_typename(idprefix, typ)} mask = 0b" - + "".join( - "1" if typ.bitname_is_valid(bitname) else "0" - for bitname in reversed(typ.bits) - ) - + ";\n" - ) - else: - ret += f"\tstatic const {c_typename(idprefix, typ)} masks[{c_verenum(idprefix, 'NUM')}] = {{\n" - verwidth = max(len(ver) for ver in versions) - for ver in sorted(versions): - ret += ( - f"\t\t[{c_verenum(idprefix, ver)}]{' '*(verwidth-len(ver))} = 0b" - + "".join( - "1" if typ.bitname_is_valid(bitname, ver) else "0" - for bitname in reversed(typ.bits) - ) - + ",\n" - ) - ret += "\t};\n" - ret += f"\t{c_typename(idprefix, typ)} mask = masks[ctx->ctx->version];\n" - ret += f"\t{c_typename(idprefix, typ)} val = decode_u{typ.static_size*8}le(&ctx->net_bytes[ctx->net_offset-{typ.static_size}]);\n" - ret += f"\tif (val & ~mask)\n" - ret += f'\t\treturn lib9p_errorf(ctx->ctx, LINUX_EBADMSG, "unknown bits in {typ.name} bitfield: %#0{typ.static_size}"PRIx{typ.static_size*8},\n' - ret += "\t\t val & ~mask);\n" - ret += "\treturn false;\n" - case Struct(): - if len(typ.members) == 0: - ret += "\n\treturn false;\n" - ret += "}\n" - continue - - for member in typ.members: - if member.max or member.valexpr: - ret += f"\n\t{c_typename(idprefix, member.typ)} {member.name};" - mark_offset: set[str] = set() - for member in typ.members: - for tok in member.valexpr: - if ( - isinstance(tok, ExprVal) - and tok.name.startswith("&") - and tok.name[1:] not in mark_offset - ): - ret += f"\n\tuint32_t _{tok.name[1:]}_offset;" - mark_offset.add(tok.name[1:]) - - prefix0 = "\treturn " - prefix1 = "\t || " - prefix2 = "\t " - - struct_versions = typ.members[0].ver - - prefix = prefix0 - prev_size: int | None = None - for member in typ.members: - ret += f"\n{prefix}" - if member.ver != struct_versions: - ret += "( " + c_vercond(idprefix, member.ver) + " && " - if member.cnt is not None: - assert prev_size - ret += f"_validate_list(ctx, decode_u{prev_size*8}le(&ctx->net_bytes[ctx->net_offset-{prev_size}]), validate_{member.typ.name}, sizeof({c_typename(idprefix, member.typ)}))" - else: - if member.max or member.valexpr: - ret += "(" - if member.name in mark_offset: - ret += f"({{ _{member.name}_offset = ctx->net_offset; " - ret += f"validate_{member.typ.name}(ctx)" - if member.name in mark_offset: - ret += "; })" - if member.max or member.valexpr: - bytes = member.static_size - assert bytes - bits = bytes * 8 - ret += f" || ({{ {member.name} = decode_u{bits}le(&ctx->net_bytes[ctx->net_offset-{bytes}]); false; }}))" - if member.max: - ret += f"\n{prefix1}" - ret += f'({member.name} > UINT{bits}_C({member.max}) && lib9p_errorf(ctx->ctx, LINUX_EBADMSG, "{member.name} value too large (%"PRIu{bits}" > %"PRIu{bits}")", {member.name}, UINT{bits}_C({member.max})))' - if member.ver != struct_versions: - ret += " )" - prefix = prefix1 - prev_size = member.static_size - - for member in typ.members: - if member.valexpr: - ret += f"\n{prefix}" - ret += f"({{ uint32_t correct =" - for tok in member.valexpr: - match tok: - case ExprOp(): - ret += f" {tok.op}" - case ExprVal(name="end"): - ret += " ctx->net_offset" - case ExprVal(): - ret += f" _{tok.name[1:]}_offset" - ret += f"; (((uint32_t){member.name}) != correct) &&" - ret += f'\n{prefix2}lib9p_errorf(ctx->ctx, LINUX_EBADMSG, "{member.name} value wrong (actual:%"PRIu32" != correct:%"PRIu32")", (uint32_t){member.name}, correct); }})' - - ret += ";\n" - ret += "}\n" - - # unmarshal_* ############################################################## - ret += """ -/* unmarshal_* ****************************************************************/ - -static ALWAYS_INLINE void unmarshal_1(struct _unmarshal_ctx *ctx, uint8_t *out) { - *out = decode_u8le(&ctx->net_bytes[ctx->net_offset]); - ctx->net_offset += 1; -} - -static ALWAYS_INLINE void unmarshal_2(struct _unmarshal_ctx *ctx, uint16_t *out) { - *out = decode_u16le(&ctx->net_bytes[ctx->net_offset]); - ctx->net_offset += 2; -} - -static ALWAYS_INLINE void unmarshal_4(struct _unmarshal_ctx *ctx, uint32_t *out) { - *out = decode_u32le(&ctx->net_bytes[ctx->net_offset]); - ctx->net_offset += 4; -} - -static ALWAYS_INLINE void unmarshal_8(struct _unmarshal_ctx *ctx, uint64_t *out) { - *out = decode_u64le(&ctx->net_bytes[ctx->net_offset]); - ctx->net_offset += 8; -} -""" - for typ in typs: - inline = ( - " FLATTEN" - if (isinstance(typ, Struct) and typ.msgid is not None) - else " ALWAYS_INLINE" - ) - argfn = unused if (isinstance(typ, Struct) and not typ.members) else used - ret += "\n" - ret += f"static{inline} void unmarshal_{typ.name}(struct _unmarshal_ctx *{argfn('ctx')}, {c_typename(idprefix, typ)} *out) {{\n" - match typ: - case Bitfield(): - ret += f"\tunmarshal_{typ.static_size}(ctx, (uint{typ.static_size*8}_t *)out);\n" - case Struct(): - ret += "\tmemset(out, 0, sizeof(*out));\n" - - if typ.members: - struct_versions = typ.members[0].ver - for member in typ.members: - if member.valexpr: - ret += f"\tctx->net_offset += {member.static_size};\n" - continue - ret += "\t" - prefix = "\t" - if member.ver != struct_versions: - ret += "if ( " + c_vercond(idprefix, member.ver) + " ) " - prefix = "\t\t" - if member.cnt: - if member.ver != struct_versions: - ret += f"{{\n{prefix}" - ret += f"out->{member.name} = ctx->extra;\n" - ret += f"{prefix}ctx->extra += sizeof(out->{member.name}[0]) * out->{member.cnt};\n" - ret += f"{prefix}for (typeof(out->{member.cnt}) i = 0; i < out->{member.cnt}; i++)\n" - if typ.name in ["d", "s"]: # SPECIAL - ret += f"{prefix}\tunmarshal_{member.typ.name}(ctx, (uint8_t *)&out->{member.name}[i]);\n" - else: - ret += f"{prefix}\tunmarshal_{member.typ.name}(ctx, &out->{member.name}[i]);\n" - if member.ver != struct_versions: - ret += "\t}\n" - else: - ret += ( - f"unmarshal_{member.typ.name}(ctx, &out->{member.name});\n" - ) - if typ.name == "s": # SPECIAL - ret += "\tctx->extra++;\n" - ret += "\tout->utf8[out->len] = '\\0';\n" - ret += "}\n" - - # marshal_* ################################################################ - ret += """ -/* marshal_* ******************************************************************/ - -static ALWAYS_INLINE bool _marshal_too_large(struct _marshal_ctx *ctx) { - lib9p_errorf(ctx->ctx, LINUX_ERANGE, "%s too large to marshal into %s limit (limit=%"PRIu32")", - (ctx->net_bytes[4] % 2 == 0) ? "T-message" : "R-message", - ctx->ctx->version ? "negotiated" : ((ctx->net_bytes[4] % 2 == 0) ? "client" : "server"), - ctx->ctx->max_msg_size); - return true; -} - -static ALWAYS_INLINE bool marshal_1(struct _marshal_ctx *ctx, uint8_t *val) { - if (ctx->net_offset + 1 > ctx->ctx->max_msg_size) - return _marshal_too_large(ctx); - ctx->net_bytes[ctx->net_offset] = *val; - ctx->net_offset += 1; - return false; -} - -static ALWAYS_INLINE bool marshal_2(struct _marshal_ctx *ctx, uint16_t *val) { - if (ctx->net_offset + 2 > ctx->ctx->max_msg_size) - return _marshal_too_large(ctx); - encode_u16le(*val, &ctx->net_bytes[ctx->net_offset]); - ctx->net_offset += 2; - return false; -} - -static ALWAYS_INLINE bool marshal_4(struct _marshal_ctx *ctx, uint32_t *val) { - if (ctx->net_offset + 4 > ctx->ctx->max_msg_size) - return true; - encode_u32le(*val, &ctx->net_bytes[ctx->net_offset]); - ctx->net_offset += 4; - return false; -} - -static ALWAYS_INLINE bool marshal_8(struct _marshal_ctx *ctx, uint64_t *val) { - if (ctx->net_offset + 8 > ctx->ctx->max_msg_size) - return true; - encode_u64le(*val, &ctx->net_bytes[ctx->net_offset]); - ctx->net_offset += 8; - return false; -} -""" - for typ in typs: - inline = ( - " FLATTEN" - if (isinstance(typ, Struct) and typ.msgid is not None) - else " ALWAYS_INLINE" - ) - argfn = unused if (isinstance(typ, Struct) and not typ.members) else used - ret += "\n" - ret += f"static{inline} bool marshal_{typ.name}(struct _marshal_ctx *{argfn('ctx')}, {c_typename(idprefix, typ)} *{argfn('val')}) {{" - match typ: - case Bitfield(): - ret += "\n" - ret += f"\treturn marshal_{typ.static_size}(ctx, (uint{typ.static_size*8}_t *)val);\n" - case Struct(): - if len(typ.members) == 0: - ret += "\n\treturn false;\n" - ret += "}\n" - continue - - mark_offset = set() - for member in typ.members: - if member.valexpr: - if member.name not in mark_offset: - ret += f"\n\tuint32_t _{member.name}_offset;" - mark_offset.add(member.name) - for tok in member.valexpr: - if ( - isinstance(tok, ExprVal) - and tok.name.startswith("&") - and tok.name[1:] not in mark_offset - ): - ret += f"\n\tuint32_t _{tok.name[1:]}_offset;" - mark_offset.add(tok.name[1:]) - - prefix0 = "\treturn " - prefix1 = "\t || " - prefix2 = "\t " - - struct_versions = typ.members[0].ver - prefix = prefix0 - for member in typ.members: - ret += f"\n{prefix}" - if member.ver != struct_versions: - ret += "( " + c_vercond(idprefix, member.ver) + " && " - if member.name in mark_offset: - ret += f"({{ _{member.name}_offset = ctx->net_offset; " - if member.cnt: - ret += "({" - ret += f"\n{prefix2}\tbool err = false;" - ret += f"\n{prefix2}\tfor (typeof(val->{member.cnt}) i = 0; i < val->{member.cnt} && !err; i++)" - if typ.name in ["d", "s"]: # SPECIAL - ret += f"\n{prefix2}\t\terr = marshal_{member.typ.name}(ctx, (uint8_t *)&val->{member.name}[i]);" - else: - ret += f"\n{prefix2}\t\terr = marshal_{member.typ.name}(ctx, &val->{member.name}[i]);" - ret += f"\n{prefix2}\terr;" - ret += f"\n{prefix2}}})" - elif member.valexpr: - assert member.static_size - ret += ( - f"({{ ctx->net_offset += {member.static_size}; false; }})" - ) - else: - ret += f"marshal_{member.typ.name}(ctx, &val->{member.name})" - if member.name in mark_offset: - ret += "; })" - if member.ver != struct_versions: - ret += " )" - prefix = prefix1 - - for member in typ.members: - if member.valexpr: - assert member.static_size - ret += f"\n{prefix}" - ret += f"({{ encode_u{member.static_size*8}le(" - for tok in member.valexpr: - match tok: - case ExprOp(): - ret += f" {tok.op}" - case ExprVal(name="end"): - ret += " ctx->net_offset" - case ExprVal(): - ret += f" _{tok.name[1:]}_offset" - ret += f", &ctx->net_bytes[_{member.name}_offset]); false; }})" - - ret += ";\n" - ret += "}\n" - - # vtables ################################################################## - ret += f""" -/* vtables ********************************************************************/ - -#define _MSG(typ) [{idprefix.upper()}TYP_##typ] = {{ \\ - .basesize = sizeof(struct {idprefix}msg_##typ), \\ - .validate = validate_##typ, \\ - .unmarshal = (_unmarshal_fn_t)unmarshal_##typ, \\ - .marshal = (_marshal_fn_t)marshal_##typ, \\ - }} - -struct _vtable_version _{idprefix}vtables[{c_verenum(idprefix, 'NUM')}] = {{ -""" - - ret += f"\t[{c_verenum(idprefix, 'unknown')}] = {{ .msgs = {{\n" - for msg in just_structs_msg(typs): - if msg.name in ["Tversion", "Rversion", "Rerror"]: # SPECIAL - ret += f"\t\t_MSG({msg.name}),\n" - ret += "\t}},\n" - - for ver in sorted(versions): - ret += f"\t[{c_verenum(idprefix, ver)}] = {{ .msgs = {{\n" - for msg in just_structs_msg(typs): - if ver not in msg.msgver: - continue - ret += f"\t\t_MSG({msg.name}),\n" - ret += "\t}},\n" - ret += "};\n" - - ############################################################################ - return ret - - -################################################################################ - - -class Parser: - cache: dict[str, tuple[str, list[Bitfield | Struct]]] = {} - - def parse_file(self, filename: str) -> tuple[str, list[Bitfield | Struct]]: - filename = os.path.normpath(filename) - if filename not in self.cache: - - def get_include(other_filename: str) -> tuple[str, list[Bitfield | Struct]]: - return self.parse_file(os.path.join(filename, "..", other_filename)) - - self.cache[filename] = parse_file(filename, get_include) - return self.cache[filename] - - def all(self) -> tuple[set[str], list[Bitfield | Struct]]: - ret_versions: set[str] = set() - ret_typs: dict[str, Bitfield | Struct] = {} - for version, typs in self.cache.values(): - if version in ret_versions: - raise ValueError(f"duplicate protocol version {repr(version)}") - ret_versions.add(version) - for typ in typs: - if typ.name in ret_typs: - if typ != ret_typs[typ.name]: - raise ValueError(f"duplicate type name {repr(typ.name)}") - else: - ret_typs[typ.name] = typ - return ret_versions, list(ret_typs.values()) - - -if __name__ == "__main__": - import sys - - if len(sys.argv) < 2: - raise ValueError("requires at least 1 .txt filename") - parser = Parser() - for txtname in sys.argv[1:]: - parser.parse_file(txtname) - versions, typs = parser.all() - outdir = os.path.normpath(os.path.join(sys.argv[0], "..")) - with open(os.path.join(outdir, "include/lib9p/9p.generated.h"), "w") as fh: - fh.write(gen_h("lib9p_", versions, typs)) - with open(os.path.join(outdir, "9p.generated.c"), "w") as fh: - fh.write(gen_c("lib9p_", versions, typs)) diff --git a/lib9p/idl.gen b/lib9p/idl.gen new file mode 100755 index 0000000..1dafef9 --- /dev/null +++ b/lib9p/idl.gen @@ -0,0 +1,1180 @@ +#!/usr/bin/env python +# lib9p/idl.gen - Generate C marshalers/unmarshalers for .9p files +# defining 9P protocol variants. +# +# Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com> +# SPDX-Licence-Identifier: AGPL-3.0-or-later + +import enum +import os.path +import re +from abc import ABC, abstractmethod +from typing import Callable, Literal, TypeAlias, TypeVar + +# This strives to be "general-purpose" in that it just acts on the +# *.9p inputs; but (unfortunately?) there are a few special-cases in +# this script, marked with "SPECIAL". + +T = TypeVar("T") + +# Types ######################################################################## + +Type: TypeAlias = "Primitive | Number | Bitfield | Struct | Message" + + +class Primitive(enum.Enum): + u8 = 1 + u16 = 2 + u32 = 4 + u64 = 8 + + @property + def in_versions(self) -> set[str]: + return set() + + @property + def name(self) -> str: + return str(self.value) + + @property + def static_size(self) -> int: + return self.value + + +class Number: + name: str + in_versions: set[str] + + prim: Primitive + + def __init__(self) -> None: + self.in_versions = set() + + @property + def static_size(self) -> int: + return self.prim.static_size + + +class BitfieldVal: + name: str + in_versions: set[str] + + val: str + + def __init__(self) -> None: + self.in_versions = set() + + +class Bitfield: + name: str + in_versions: set[str] + + prim: Primitive + + bits: list[str] # bitnames + names: dict[str, BitfieldVal] # bits *and* aliases + + def __init__(self) -> None: + self.in_versions = set() + self.names = {} + + @property + def static_size(self) -> int: + return self.prim.static_size + + def bit_is_valid(self, bit: str | int, ver: str | None = None) -> bool: + """Return whether the given bit is valid in the given protocol + version. + + """ + bitname = self.bits[bit] if isinstance(bit, int) else bit + assert bitname in self.bits + if not bitname: + return False + if bitname.startswith("_"): + return False + if ver and (ver not in self.names[bitname].in_versions): + return False + return True + + +class ExprLit: + val: int + + def __init__(self, val: int) -> None: + self.val = val + + +class ExprSym: + name: str + + def __init__(self, name: str) -> None: + self.name = name + + +class ExprOp: + op: Literal["-", "+"] + + def __init__(self, op: Literal["-", "+"]) -> None: + self.op = op + + +class Expr: + tokens: list[ExprLit | ExprSym | ExprOp] + + def __init__(self) -> None: + self.tokens = [] + + def __bool__(self) -> bool: + return len(self.tokens) > 0 + + +class StructMember: + # from left-to-right when parsing + cnt: str | None = None + name: str + typ: Type + max: Expr + val: Expr + + in_versions: set[str] + + @property + def static_size(self) -> int | None: + if self.cnt: + return None + return self.typ.static_size + + +class Struct: + name: str + in_versions: set[str] + + members: list[StructMember] + + def __init__(self) -> None: + self.in_versions = set() + + @property + def static_size(self) -> int | None: + size = 0 + for member in self.members: + msize = member.static_size + if msize is None: + return None + size += msize + return size + + +class Message(Struct): + @property + def msgid(self) -> int: + assert len(self.members) >= 3 + assert self.members[1].name == "typ" + assert self.members[1].static_size == 1 + assert self.members[1].val + assert len(self.members[1].val.tokens) == 1 + assert isinstance(self.members[1].val.tokens[0], ExprLit) + return self.members[1].val.tokens[0].val + + +# Parse *.9p ################################################################### + +re_priname = "(?:1|2|4|8)" # primitive names +re_symname = "(?:[a-zA-Z_][a-zA-Z_0-9]*)" # "symbol" names; most *.9p-defined names +re_impname = r"(?:\*|" + re_symname + ")" # names we can import +re_msgname = r"(?:[TR][a-zA-Z_0-9]*)" # names a message can be + +re_memtype = f"(?:{re_symname}|{re_priname})" # typenames that a struct member can be + +re_expr = f"(?:(?:-|\\+|[0-9]+|&?{re_symname})+)" + +re_bitspec_bit = f"(?P<bit>[0-9]+)\\s*=\\s*(?P<name>{re_symname})" +re_bitspec_alias = f"(?P<name>{re_symname})\\s*=\\s*(?P<val>\\S+)" + +re_memberspec = f"(?:(?P<cnt>{re_symname})\\*\\()?(?P<name>{re_symname})\\[(?P<typ>{re_memtype})(?:,max=(?P<max>{re_expr})|,val=(?P<val>{re_expr}))*\\]\\)?" + + +def parse_bitspec(ver: str, bf: Bitfield, spec: str) -> None: + spec = spec.strip() + + bit: int | None + val: BitfieldVal + if m := re.fullmatch(re_bitspec_bit, spec): + bit = int(m.group("bit")) + name = m.group("name") + + val = BitfieldVal() + val.name = name + val.val = f"1<<{bit}" + val.in_versions.add(ver) + + if bit < 0 or bit >= len(bf.bits): + raise ValueError(f"{bf.name}: bit {bit} is out-of-bounds") + if bf.bits[bit]: + raise ValueError(f"{bf.name}: bit {bit} already assigned") + bf.bits[bit] = val.name + elif m := re.fullmatch(re_bitspec_alias, spec): + name = m.group("name") + valstr = m.group("val") + + val = BitfieldVal() + val.name = name + val.val = valstr + val.in_versions.add(ver) + else: + raise SyntaxError(f"invalid bitfield spec {repr(spec)}") + + if val.name in bf.names: + raise ValueError(f"{bf.name}: name {val.name} already assigned") + bf.names[val.name] = val + + +def parse_expr(expr: str) -> Expr: + assert re.fullmatch(re_expr, expr) + ret = Expr() + for tok in re.split("([-+])", expr): + if tok == "-" or tok == "+": + ret.tokens += [ExprOp(tok)] + elif re.fullmatch("[0-9]+", tok): + ret.tokens += [ExprLit(int(tok))] + else: + ret.tokens += [ExprSym(tok)] + return ret + + +def parse_members(ver: str, env: dict[str, Type], struct: Struct, specs: str) -> None: + for spec in specs.split(): + m = re.fullmatch(re_memberspec, spec) + if not m: + raise SyntaxError(f"invalid member spec {repr(spec)}") + + member = StructMember() + member.in_versions = {ver} + + member.name = m.group("name") + if any(x.name == member.name for x in struct.members): + raise ValueError(f"duplicate member name {repr(member.name)}") + + if m.group("typ") not in env: + raise NameError(f"Unknown type {repr(m.group(2))}") + member.typ = env[m.group("typ")] + + if cnt := m.group("cnt"): + if len(struct.members) == 0 or struct.members[-1].name != cnt: + raise ValueError(f"list count must be previous item: {repr(cnt)}") + if not isinstance(struct.members[-1].typ, Primitive): + raise ValueError(f"list count must be an integer type: {repr(cnt)}") + member.cnt = cnt + + if maxstr := m.group("max"): + if (not isinstance(member.typ, Primitive)) or member.cnt: + raise ValueError("',max=' may only be specified on a non-repeated atom") + member.max = parse_expr(maxstr) + else: + member.max = Expr() + + if valstr := m.group("val"): + if (not isinstance(member.typ, Primitive)) or member.cnt: + raise ValueError("',val=' may only be specified on a non-repeated atom") + member.val = parse_expr(valstr) + else: + member.val = Expr() + + struct.members += [member] + + +def re_string(grpname: str) -> str: + return f'"(?P<{grpname}>[^"]*)"' + + +re_line_version = f"version\\s+{re_string('version')}" +re_line_import = f"from\\s+(?P<file>\\S+)\\s+import\\s+(?P<syms>{re_impname}(?:\\s*,\\s*{re_impname})*)" +re_line_num = f"num\\s+(?P<name>{re_symname})\\s*=\\s*(?P<prim>{re_priname})" +re_line_bitfield = f"bitfield\\s+(?P<name>{re_symname})\\s*=\\s*(?P<prim>{re_priname})" +re_line_bitfield_ = f"bitfield\\s+(?P<name>{re_symname})\\s*\\+=\\s*{re_string('member')}" +re_line_struct = ( + f"struct\\s+(?P<name>{re_symname})\\s*(?P<op>\\+?=)\\s*{re_string('members')}" +) +re_line_msg = ( + f"msg\\s+(?P<name>{re_msgname})\\s*(?P<op>\\+?=)\\s*{re_string('members')}" +) +re_line_cont = f"\\s+{re_string('specs')}" # could be bitfield/struct/msg + + +def parse_file( + filename: str, get_include: Callable[[str], tuple[str, list[Type]]] +) -> tuple[str, list[Type]]: + version: str | None = None + env: dict[str, Type] = { + "1": Primitive.u8, + "2": Primitive.u16, + "4": Primitive.u32, + "8": Primitive.u64, + } + + def get_type(name: str, tc: type[T]) -> T: + nonlocal env + if name not in env: + raise NameError(f"Unknown type {repr(name)}") + ret = env[name] + if (not isinstance(ret, tc)) or (ret.__class__.__name__ != tc.__name__): + raise NameError(f"Type {repr(ret.name)} is not a {tc.__name__}") + return ret + + with open(filename, "r") as fh: + prev: Type | None = None + for line in fh: + line = line.split("#", 1)[0].rstrip() + if not line: + continue + if m := re.fullmatch(re_line_version, line): + if version: + raise SyntaxError("must have exactly 1 version line") + version = m.group("version") + continue + if not version: + raise SyntaxError("must have exactly 1 version line") + + if m := re.fullmatch(re_line_import, line): + other_version, other_typs = get_include(m.group("file")) + for symname in m.group("syms").split(sep=","): + symname = symname.strip() + for typ in other_typs: + if typ.name == symname or symname == "*": + match typ: + case Primitive(): + pass + case Number(): + typ.in_versions.add(version) + case Bitfield(): + typ.in_versions.add(version) + for val in typ.names.values(): + if other_version in val.in_versions: + val.in_versions.add(version) + case Struct(): # and Message() + typ.in_versions.add(version) + for member in typ.members: + if other_version in member.in_versions: + member.in_versions.add(version) + env[typ.name] = typ + elif m := re.fullmatch(re_line_num, line): + num = Number() + num.name = m.group("name") + num.in_versions.add(version) + + prim = env[m.group("prim")] + assert isinstance(prim, Primitive) + num.prim = prim + + env[num.name] = num + prev = num + elif m := re.fullmatch(re_line_bitfield, line): + bf = Bitfield() + bf.name = m.group("name") + + prim = env[m.group("prim")] + assert isinstance(prim, Primitive) + bf.prim = prim + + bf.bits = (prim.static_size * 8) * [""] + + env[bf.name] = bf + prev = bf + elif m := re.fullmatch(re_line_bitfield_, line): + bf = get_type(m.group("name"), Bitfield) + parse_bitspec(version, bf, m.group("member")) + + prev = bf + elif m := re.fullmatch(re_line_struct, line): + match m.group("op"): + case "=": + struct = Struct() + struct.name = m.group("name") + struct.in_versions.add(version) + struct.members = [] + parse_members(version, env, struct, m.group("members")) + + env[struct.name] = struct + prev = struct + case "+=": + struct = get_type(m.group("name"), Struct) + parse_members(version, env, struct, m.group("members")) + + prev = struct + elif m := re.fullmatch(re_line_msg, line): + match m.group("op"): + case "=": + msg = Message() + msg.name = m.group("name") + msg.in_versions.add(version) + msg.members = [] + parse_members(version, env, msg, m.group("members")) + + env[msg.name] = msg + prev = msg + case "+=": + msg = get_type(m.group("name"), Message) + parse_members(version, env, msg, m.group("members")) + + prev = msg + elif m := re.fullmatch(re_line_cont, line): + match prev: + case Bitfield(): + parse_bitspec(version, prev, m.group("specs")) + case Struct(): # and Message() + parse_members(version, env, prev, m.group("specs")) + case _: + raise SyntaxError( + "continuation line must come after a bitfield, struct, or msg line" + ) + else: + raise SyntaxError(f"invalid line {repr(line)}") + if not version: + raise SyntaxError("must have exactly 1 version line") + + typs: list[Type] = [x for x in env.values() if not isinstance(x, Primitive)] + + for typ in [typ for typ in typs if isinstance(typ, Struct)]: + valid_syms = ["end", *["&" + m.name for m in typ.members]] + for member in typ.members: + for tok in [*member.max.tokens, *member.val.tokens]: + if isinstance(tok, ExprSym) and tok.name not in valid_syms: + raise ValueError( + f"{typ.name}.{member.name}: invalid sym: {tok.name}" + ) + + return version, typs + + +# Generate C ################################################################### + + +def c_ver_enum(idprefix: str, ver: str) -> str: + return f"{idprefix.upper()}VER_{ver.replace('.', '_')}" + + +def c_ver_ifdef(idprefix: str, versions: set[str]) -> str: + return " || ".join( + f"defined(CONFIG_{idprefix.upper()}ENABLE_{c_ver_enum('', v)})" + for v in sorted(versions) + ) + + +def c_ver_cond(idprefix: str, versions: set[str]) -> str: + if len(versions) == 1: + return f"(ctx->ctx->version=={c_ver_enum(idprefix, next(v for v in versions))})" + return ( + "( " + (" || ".join(c_ver_cond(idprefix, {v}) for v in sorted(versions))) + " )" + ) + + +def c_typename(idprefix: str, typ: Type) -> str: + match typ: + case Primitive(): + return f"uint{typ.value*8}_t" + case Number(): + return f"{idprefix}{typ.name}_t" + case Bitfield(): + return f"{idprefix}{typ.name}_t" + case Message(): + return f"struct {idprefix}msg_{typ.name}" + case Struct(): + return f"struct {idprefix}{typ.name}" + case _: + raise ValueError(f"not a type: {typ.__class__.__name__}") + + +def gen_h(idprefix: str, versions: set[str], typs: list[Type]) -> str: + ret = f"""/* Generated by `{' '.join(sys.argv)}`. DO NOT EDIT! */ + +#ifndef _LIB9P_9P_H_ +# error Do not include <lib9p/9p.generated.h> directly; include <lib9p/9p.h> instead +#endif + +#include <stdint.h> /* for uint{{n}}_t types */ +""" + + _ifdef: list[str] = [] + + def push_ifdef(v: str) -> None: + nonlocal _ifdef + nonlocal ret + ret += f"#if {v}\n" + _ifdef += [v] + + def pop_ifdef(n: int) -> None: + nonlocal _ifdef + nonlocal ret + while len(_ifdef) > n: + ret += f"#endif /* {_ifdef[-1]}\n" + _ifdef = _ifdef[:-1] + + def set_ifdef(v: str) -> None: + nonlocal _ifdef + nonlocal ret + if v != _ifdef[-1]: + ret += f"#elif {v}\n" + _ifdef[-1] = v + + def pushorset_ifdef(n: int, v: str) -> None: + nonlocal _ifdef + nonlocal ret + if len(_ifdef) < n: + push_ifdef(v) + else: + set_ifdef(v) + + ret += f""" +/* versions *******************************************************************/ + +enum {idprefix}version {{ +""" + fullversions = ["unknown = 0", *sorted(versions)] + verwidth = max(len(v) for v in fullversions) + for ver in fullversions: + if ver in versions: + pushorset_ifdef(1, c_ver_ifdef(idprefix, {ver})) + ret += f"\t{c_ver_enum(idprefix, ver)}," + ret += (" " * (verwidth - len(ver))) + ' /* "' + ver.split()[0] + '" */\n' + pop_ifdef(0) + ret += f"\t{c_ver_enum(idprefix, 'NUM')},\n" + ret += "};\n" + ret += "\n" + ret += f"const char *{idprefix}version_str(enum {idprefix}version);\n" + + ret += """ +/* non-message types **********************************************************/ +""" + for typ in [typ for typ in typs if not isinstance(typ, Message)]: + ret += "\n" + pushorset_ifdef(1, c_ver_ifdef(idprefix, typ.in_versions)) + match typ: + case Number(): + ret += f"typedef {c_typename(idprefix, typ.prim)} {c_typename(idprefix, typ)};\n" + case Bitfield(): + ret += f"typedef {c_typename(idprefix, typ.prim)} {c_typename(idprefix, typ)};\n" + names = [ + *reversed( + [typ.bits[n] or f"_UNUSED_{n}" for n in range(0, len(typ.bits))] + ), + "", + *[k for k in typ.names if k not in typ.bits], + ] + namewidth = max(len(name) for name in names) + + ret += "\n" + for name in names: + if name == "": + ret += "\n" + continue + pushorset_ifdef( + 2, c_ver_ifdef(idprefix, typ.names[name].in_versions) + ) + if name.startswith("_"): + c_name = f"_{idprefix.upper()}{typ.name.upper()}_{name[1:]}" + else: + c_name = f"{idprefix.upper()}{typ.name.upper()}_{name}" + if name in typ.names: + val = typ.names[name].val + else: + assert name.startswith("_UNUSED_") + val = f"1<<{name[len('_UNUSED_'):]}" + ret += f"#define {c_name}{' '*(namewidth-len(name))} (({c_typename(idprefix, typ)})({val}))\n" + pop_ifdef(1) + case Struct(): + typewidth = max(len(c_typename(idprefix, m.typ)) for m in typ.members) + + ret += c_typename(idprefix, typ) + " {\n" + for member in typ.members: + if member.val: + continue + pushorset_ifdef(2, c_ver_ifdef(idprefix, member.in_versions)) + c_type = c_typename(idprefix, member.typ) + if (typ.name in ["d", "s"]) and member.cnt: # SPECIAL + c_type = "char" + ret += f"\t{c_type.ljust(typewidth)} {'*' if member.cnt else ' '}{member.name};\n" + pop_ifdef(1) + ret += "};\n" + pop_ifdef(0) + + ret += """ +/* messages *******************************************************************/ + +""" + ret += f"enum {idprefix}msg_type {{ /* uint8_t */\n" + namewidth = max(len(msg.name) for msg in typs if isinstance(msg, Message)) + for msg in [msg for msg in typs if isinstance(msg, Message)]: + pushorset_ifdef(1, c_ver_ifdef(idprefix, msg.in_versions)) + ret += f"\t{idprefix.upper()}TYP_{msg.name.ljust(namewidth)} = {msg.msgid},\n" + pop_ifdef(0) + ret += "};\n" + ret += "\n" + ret += f"const char *{idprefix}msg_type_str(enum {idprefix}msg_type);\n" + + for msg in [msg for msg in typs if isinstance(msg, Message)]: + ret += "\n" + pushorset_ifdef(1, c_ver_ifdef(idprefix, msg.in_versions)) + ret += c_typename(idprefix, msg) + " {" + if not msg.members: + ret += "};\n" + continue + ret += "\n" + + typewidth = max(len(c_typename(idprefix, m.typ)) for m in msg.members) + + for member in msg.members: + if member.val: + continue + pushorset_ifdef(2, c_ver_ifdef(idprefix, member.in_versions)) + ret += f"\t{c_typename(idprefix, member.typ).ljust(typewidth)} {'*' if member.cnt else ' '}{member.name};\n" + pop_ifdef(1) + ret += "};\n" + pop_ifdef(0) + + return ret + + +def c_expr(expr: Expr) -> str: + ret: list[str] = [] + for tok in expr.tokens: + match tok: + case ExprOp(): + ret += [tok.op] + case ExprLit(): + ret += [str(tok.val)] + case ExprSym(name="end"): + ret += ["ctx->net_offset"] + case ExprSym(): + ret += [f"_{tok.name[1:]}_offset"] + return " ".join(ret) + + +def gen_c(idprefix: str, versions: set[str], typs: list[Type]) -> str: + ret = f"""/* Generated by `{' '.join(sys.argv)}`. DO NOT EDIT! */ + +#include <assert.h> +#include <stdbool.h> +#include <stddef.h> /* for size_t */ +#include <inttypes.h> /* for PRI* macros */ +#include <string.h> /* for memset() */ + +#include <lib9p/9p.h> + +#include "internal.h" +""" + + _ifdef: list[str] = [] + + def push_ifdef(v: str) -> None: + nonlocal _ifdef + nonlocal ret + ret += f"#if {v}\n" + _ifdef += [v] + + def pop_ifdef(n: int) -> None: + nonlocal _ifdef + nonlocal ret + while len(_ifdef) > n: + ret += f"#endif /* {_ifdef[-1]}\n" + _ifdef = _ifdef[:-1] + + def set_ifdef(v: str) -> None: + nonlocal _ifdef + nonlocal ret + if v != _ifdef[-1]: + ret += f"#elif {v}\n" + _ifdef[-1] = v + + def pushorset_ifdef(n: int, v: str) -> None: + nonlocal _ifdef + nonlocal ret + if len(_ifdef) < n: + push_ifdef(v) + else: + set_ifdef(v) + + def used(arg: str) -> str: + return arg + + def unused(arg: str) -> str: + return f"UNUSED({arg})" + + # strings ################################################################## + ret += f""" +/* strings ********************************************************************/ + +static const char *version_strs[{c_ver_enum(idprefix, 'NUM')}] = {{ +""" + for ver in ["unknown", *sorted(versions)]: + pushorset_ifdef(1, c_ver_ifdef(idprefix, {ver})) + ret += f'\t[{c_ver_enum(idprefix, ver)}] = "{ver}",\n' + pop_ifdef(0) + ret += "};\n" + ret += f""" +const char *{idprefix}version_str(enum {idprefix}version ver) {{ + assert(0 <= ver && ver < {c_ver_enum(idprefix, 'NUM')}); + return version_strs[ver]; +}} + +static const char *msg_type_strs[0x100] = {{ +""" + id2name: dict[int, str] = {} + for msg in [msg for msg in typs if isinstance(msg, Message)]: + id2name[msg.msgid] = msg.name + for n in range(0, 0x100): + ret += '\t[0x{:02X}] = "{}",\n'.format(n, id2name.get(n, "0x{:02X}".format(n))) + ret += "};\n" + ret += f""" +const char *{idprefix}msg_type_str(enum {idprefix}msg_type typ) {{ + assert(0 <= typ && typ <= 0xFF); + return msg_type_strs[typ]; +}} +""" + + # validate_* ############################################################### + ret += """ +/* validate_* *****************************************************************/ + +static ALWAYS_INLINE bool _validate_size_net(struct _validate_ctx *ctx, uint32_t n) { + if (__builtin_add_overflow(ctx->net_offset, n, &ctx->net_offset)) + /* If needed-net-size overflowed uint32_t, then + * there's no way that actual-net-size will live up to + * that. */ + return lib9p_error(ctx->ctx, LINUX_EBADMSG, "message is too short for content"); + if (ctx->net_offset > ctx->net_size) + return lib9p_error(ctx->ctx, LINUX_EBADMSG, "message is too short for content"); + return false; +} + +static ALWAYS_INLINE bool _validate_size_host(struct _validate_ctx *ctx, size_t n) { + if (__builtin_add_overflow(ctx->host_extra, n, &ctx->host_extra)) + /* If needed-host-size overflowed size_t, then there's + * no way that actual-net-size will live up to + * that. */ + return lib9p_error(ctx->ctx, LINUX_EBADMSG, "message is too short for content"); + return false; +} + +static ALWAYS_INLINE bool _validate_list(struct _validate_ctx *ctx, + size_t cnt, + _validate_fn_t item_fn, size_t item_host_size) { + for (size_t i = 0; i < cnt; i++) + if (_validate_size_host(ctx, item_host_size) || item_fn(ctx)) + return true; + return false; +} + +#define validate_1(ctx) _validate_size_net(ctx, 1) +#define validate_2(ctx) _validate_size_net(ctx, 2) +#define validate_4(ctx) _validate_size_net(ctx, 4) +#define validate_8(ctx) _validate_size_net(ctx, 8) +""" + for typ in typs: + inline = "FLATTEN" if isinstance(typ, Message) else "ALWAYS_INLINE" + argfn = unused if (isinstance(typ, Struct) and not typ.members) else used + ret += "\n" + pushorset_ifdef(1, c_ver_ifdef(idprefix, typ.in_versions)) + ret += f"static {inline} bool validate_{typ.name}(struct _validate_ctx *{argfn('ctx')}) {{\n" + + if typ.name == "d": # SPECIAL + # Optimize... maybe the compiler could figure out to do + # this, but let's make it obvious. + ret += "\tuint32_t base_offset = ctx->net_offset;\n" + ret += "\tif (validate_4(ctx))\n" + ret += "\t\treturn true;\n" + ret += "\tuint32_t len = decode_u32le(&ctx->net_bytes[base_offset]);\n" + ret += "\treturn _validate_size_net(ctx, len) || _validate_size_host(ctx, len);\n" + ret += "}\n" + continue + if typ.name == "s": # SPECIAL + # Add an extra nul-byte on the host, and validate UTF-8 + # (also, similar optimization to "d"). + ret += "\tuint32_t base_offset = ctx->net_offset;\n" + ret += "\tif (validate_2(ctx))\n" + ret += "\t\treturn true;\n" + ret += "\tuint16_t len = decode_u16le(&ctx->net_bytes[base_offset]);\n" + ret += "\tif (_validate_size_net(ctx, len) || _validate_size_host(ctx, ((size_t)len)+1))\n" + ret += "\t\treturn true;\n" + ret += "\tif (!is_valid_utf8_without_nul(&ctx->net_bytes[base_offset+2], len))\n" + ret += '\t\treturn lib9p_error(ctx->ctx, LINUX_EBADMSG, "message contains invalid UTF-8");\n' + ret += "\treturn false;\n" + ret += "}\n" + continue + + match typ: + case Number(): + ret += f"\treturn validate_{typ.prim.name}(ctx);\n" + case Bitfield(): + ret += f"\t if (validate_{typ.static_size}(ctx))\n" + ret += "\t\treturn true;\n" + ret += f"\tstatic const {c_typename(idprefix, typ)} masks[{c_ver_enum(idprefix, 'NUM')}] = {{\n" + verwidth = max(len(ver) for ver in versions) + for ver in sorted(versions): + pushorset_ifdef(2, c_ver_ifdef(idprefix, {ver})) + ret += ( + f"\t\t[{c_ver_enum(idprefix, ver)}]{' '*(verwidth-len(ver))} = 0b" + + "".join( + "1" if typ.bit_is_valid(bitname, ver) else "0" + for bitname in reversed(typ.bits) + ) + + ",\n" + ) + pop_ifdef(1) + ret += "\t};\n" + ret += ( + f"\t{c_typename(idprefix, typ)} mask = masks[ctx->ctx->version];\n" + ) + ret += f"\t{c_typename(idprefix, typ)} val = decode_u{typ.static_size*8}le(&ctx->net_bytes[ctx->net_offset-{typ.static_size}]);\n" + ret += f"\tif (val & ~mask)\n" + ret += "\t\treturn lib9p_errorf(ctx->ctx,\n" + ret += f'\t\t LINUX_EBADMSG, "unknown bits in {typ.name} bitfield: %#0{typ.static_size}"PRIx{typ.static_size*8},\n' + ret += "\t\t val & ~mask);\n" + ret += "\treturn false;\n" + case Struct(): # and Message() + if len(typ.members) == 0: + ret += "\treturn false;\n" + ret += "}\n" + continue + + # Pass 1 + for member in typ.members: + pushorset_ifdef(2, c_ver_ifdef(idprefix, member.in_versions)) + if member.max or member.val: + ret += f"\t{c_typename(idprefix, member.typ)} {member.name};\n" + pop_ifdef(1) + + # Pass 2 + mark_offset: set[str] = set() + for member in typ.members: + for tok in [*member.max.tokens, *member.val.tokens]: + if isinstance(tok, ExprSym) and tok.name.startswith("&"): + if tok.name[1:] not in mark_offset: + ret += f"\tuint32_t _{tok.name[1:]}_offset;\n" + mark_offset.add(tok.name[1:]) + + # Pass 3 + ret += "\treturn false\n" + prev_size: int | None = None + for member in typ.members: + pushorset_ifdef(2, c_ver_ifdef(idprefix, member.in_versions)) + ret += f"\n\t|| " + if member.in_versions != typ.in_versions: + ret += "( " + c_ver_cond(idprefix, member.in_versions) + " && " + if member.cnt is not None: + assert prev_size + ret += f"_validate_list(ctx, decode_u{prev_size*8}le(&ctx->net_bytes[ctx->net_offset-{prev_size}]), validate_{member.typ.name}, sizeof({c_typename(idprefix, member.typ)}))" + else: + if member.max or member.val: + ret += "(" + if member.name in mark_offset: + ret += f"({{ _{member.name}_offset = ctx->net_offset; " + ret += f"validate_{member.typ.name}(ctx)" + if member.name in mark_offset: + ret += "; })" + if member.max or member.val: + bytes = member.static_size + assert bytes + bits = bytes * 8 + ret += f" || ({{ {member.name} = decode_u{bits}le(&ctx->net_bytes[ctx->net_offset-{bytes}]); false; }}))" + if member.in_versions != typ.in_versions: + ret += " )" + prev_size = member.static_size + + # Pass 4 + for member in typ.members: + if member.max: + pushorset_ifdef(2, c_ver_ifdef(idprefix, member.in_versions)) + ret += f"\n\t|| ({{ uint32_t max = {c_expr(member.max)}; (((uint32_t){member.name}) > max) &&\n" + ret += f'\n\t lib9p_errorf(ctx->ctx, LINUX_EBADMSG, "{member.name} value too large (%"PRIu32" > %"PRIu32")", {member.name}, max); }})\n' + if member.val: + pushorset_ifdef(2, c_ver_ifdef(idprefix, member.in_versions)) + ret += f"\n\t|| ({{ uint32_t exp = {c_expr(member.val)}; (((uint32_t){member.name}) != exp) &&\n" + ret += f'\n\t lib9p_errorf(ctx->ctx, LINUX_EBADMSG, "{member.name} value wrong (actual:%"PRIu32" != correct:%"PRIu32")", (uint32_t){member.name}, exp); }})\n' + + pop_ifdef(1) + ret += "\t;\n" + ret += "}\n" + pop_ifdef(0) + + # # unmarshal_* ############################################################## + # ret += """ + # /* unmarshal_* ****************************************************************/ + + # static ALWAYS_INLINE void unmarshal_1(struct _unmarshal_ctx *ctx, uint8_t *out) { + # *out = decode_u8le(&ctx->net_bytes[ctx->net_offset]); + # ctx->net_offset += 1; + # } + + # static ALWAYS_INLINE void unmarshal_2(struct _unmarshal_ctx *ctx, uint16_t *out) { + # *out = decode_u16le(&ctx->net_bytes[ctx->net_offset]); + # ctx->net_offset += 2; + # } + + # static ALWAYS_INLINE void unmarshal_4(struct _unmarshal_ctx *ctx, uint32_t *out) { + # *out = decode_u32le(&ctx->net_bytes[ctx->net_offset]); + # ctx->net_offset += 4; + # } + + # static ALWAYS_INLINE void unmarshal_8(struct _unmarshal_ctx *ctx, uint64_t *out) { + # *out = decode_u64le(&ctx->net_bytes[ctx->net_offset]); + # ctx->net_offset += 8; + # } + # """ + # for typ in typs: + # inline = ( + # " FLATTEN" + # if (isinstance(typ, Struct) and typ.msgid is not None) + # else " ALWAYS_INLINE" + # ) + # argfn = unused if (isinstance(typ, Struct) and not typ.members) else used + # ret += "\n" + # ret += f"static{inline} void unmarshal_{typ.name}(struct _unmarshal_ctx *{argfn('ctx')}, {c_typename(idprefix, typ)} *out) {{\n" + # match typ: + # case Bitfield(): + # ret += f"\tunmarshal_{typ.static_size}(ctx, (uint{typ.static_size*8}_t *)out);\n" + # case Struct(): + # ret += "\tmemset(out, 0, sizeof(*out));\n" + + # if typ.members: + # struct_versions = typ.members[0].ver + # for member in typ.members: + # if member.valexpr: + # ret += f"\tctx->net_offset += {member.static_size};\n" + # continue + # ret += "\t" + # prefix = "\t" + # if member.ver != struct_versions: + # ret += "if ( " + c_ver_cond(idprefix, member.ver) + " ) " + # prefix = "\t\t" + # if member.cnt: + # if member.ver != struct_versions: + # ret += f"{{\n{prefix}" + # ret += f"out->{member.name} = ctx->extra;\n" + # ret += f"{prefix}ctx->extra += sizeof(out->{member.name}[0]) * out->{member.cnt};\n" + # ret += f"{prefix}for (typeof(out->{member.cnt}) i = 0; i < out->{member.cnt}; i++)\n" + # if typ.name in ["d", "s"]: # SPECIAL + # ret += f"{prefix}\tunmarshal_{member.typ.name}(ctx, (uint8_t *)&out->{member.name}[i]);\n" + # else: + # ret += f"{prefix}\tunmarshal_{member.typ.name}(ctx, &out->{member.name}[i]);\n" + # if member.ver != struct_versions: + # ret += "\t}\n" + # else: + # ret += ( + # f"unmarshal_{member.typ.name}(ctx, &out->{member.name});\n" + # ) + # if typ.name == "s": # SPECIAL + # ret += "\tctx->extra++;\n" + # ret += "\tout->utf8[out->len] = '\\0';\n" + # ret += "}\n" + + # # marshal_* ################################################################ + # ret += """ + # /* marshal_* ******************************************************************/ + + # static ALWAYS_INLINE bool _marshal_too_large(struct _marshal_ctx *ctx) { + # lib9p_errorf(ctx->ctx, LINUX_ERANGE, "%s too large to marshal into %s limit (limit=%"PRIu32")", + # (ctx->net_bytes[4] % 2 == 0) ? "T-message" : "R-message", + # ctx->ctx->version ? "negotiated" : ((ctx->net_bytes[4] % 2 == 0) ? "client" : "server"), + # ctx->ctx->max_msg_size); + # return true; + # } + + # static ALWAYS_INLINE bool marshal_1(struct _marshal_ctx *ctx, uint8_t *val) { + # if (ctx->net_offset + 1 > ctx->ctx->max_msg_size) + # return _marshal_too_large(ctx); + # ctx->net_bytes[ctx->net_offset] = *val; + # ctx->net_offset += 1; + # return false; + # } + + # static ALWAYS_INLINE bool marshal_2(struct _marshal_ctx *ctx, uint16_t *val) { + # if (ctx->net_offset + 2 > ctx->ctx->max_msg_size) + # return _marshal_too_large(ctx); + # encode_u16le(*val, &ctx->net_bytes[ctx->net_offset]); + # ctx->net_offset += 2; + # return false; + # } + + # static ALWAYS_INLINE bool marshal_4(struct _marshal_ctx *ctx, uint32_t *val) { + # if (ctx->net_offset + 4 > ctx->ctx->max_msg_size) + # return true; + # encode_u32le(*val, &ctx->net_bytes[ctx->net_offset]); + # ctx->net_offset += 4; + # return false; + # } + + # static ALWAYS_INLINE bool marshal_8(struct _marshal_ctx *ctx, uint64_t *val) { + # if (ctx->net_offset + 8 > ctx->ctx->max_msg_size) + # return true; + # encode_u64le(*val, &ctx->net_bytes[ctx->net_offset]); + # ctx->net_offset += 8; + # return false; + # } + # """ + # for typ in typs: + # inline = ( + # " FLATTEN" + # if (isinstance(typ, Struct) and typ.msgid is not None) + # else " ALWAYS_INLINE" + # ) + # argfn = unused if (isinstance(typ, Struct) and not typ.members) else used + # ret += "\n" + # ret += f"static{inline} bool marshal_{typ.name}(struct _marshal_ctx *{argfn('ctx')}, {c_typename(idprefix, typ)} *{argfn('val')}) {{" + # match typ: + # case Bitfield(): + # ret += "\n" + # ret += f"\treturn marshal_{typ.static_size}(ctx, (uint{typ.static_size*8}_t *)val);\n" + # case Struct(): + # if len(typ.members) == 0: + # ret += "\n\treturn false;\n" + # ret += "}\n" + # continue + + # mark_offset = set() + # for member in typ.members: + # if member.valexpr: + # if member.name not in mark_offset: + # ret += f"\n\tuint32_t _{member.name}_offset;" + # mark_offset.add(member.name) + # for tok in member.valexpr: + # if ( + # isinstance(tok, ExprVal) + # and tok.name.startswith("&") + # and tok.name[1:] not in mark_offset + # ): + # ret += f"\n\tuint32_t _{tok.name[1:]}_offset;" + # mark_offset.add(tok.name[1:]) + + # prefix0 = "\treturn " + # prefix1 = "\t || " + # prefix2 = "\t " + + # struct_versions = typ.members[0].ver + # prefix = prefix0 + # for member in typ.members: + # ret += f"\n{prefix}" + # if member.ver != struct_versions: + # ret += "( " + c_ver_cond(idprefix, member.ver) + " && " + # if member.name in mark_offset: + # ret += f"({{ _{member.name}_offset = ctx->net_offset; " + # if member.cnt: + # ret += "({" + # ret += f"\n{prefix2}\tbool err = false;" + # ret += f"\n{prefix2}\tfor (typeof(val->{member.cnt}) i = 0; i < val->{member.cnt} && !err; i++)" + # if typ.name in ["d", "s"]: # SPECIAL + # ret += f"\n{prefix2}\t\terr = marshal_{member.typ.name}(ctx, (uint8_t *)&val->{member.name}[i]);" + # else: + # ret += f"\n{prefix2}\t\terr = marshal_{member.typ.name}(ctx, &val->{member.name}[i]);" + # ret += f"\n{prefix2}\terr;" + # ret += f"\n{prefix2}}})" + # elif member.valexpr: + # assert member.static_size + # ret += ( + # f"({{ ctx->net_offset += {member.static_size}; false; }})" + # ) + # else: + # ret += f"marshal_{member.typ.name}(ctx, &val->{member.name})" + # if member.name in mark_offset: + # ret += "; })" + # if member.ver != struct_versions: + # ret += " )" + # prefix = prefix1 + + # for member in typ.members: + # if member.valexpr: + # assert member.static_size + # ret += f"\n{prefix}" + # ret += f"({{ encode_u{member.static_size*8}le(" + # for tok in member.valexpr: + # match tok: + # case ExprOp(): + # ret += f" {tok.op}" + # case ExprVal(name="end"): + # ret += " ctx->net_offset" + # case ExprVal(): + # ret += f" _{tok.name[1:]}_offset" + # ret += f", &ctx->net_bytes[_{member.name}_offset]); false; }})" + + # ret += ";\n" + # ret += "}\n" + + # # vtables ################################################################## + # ret += f""" + # /* vtables ********************************************************************/ + + # #define _MSG(typ) [{idprefix.upper()}TYP_##typ] = {{ \\ + # .basesize = sizeof(struct {idprefix}msg_##typ), \\ + # .validate = validate_##typ, \\ + # .unmarshal = (_unmarshal_fn_t)unmarshal_##typ, \\ + # .marshal = (_marshal_fn_t)marshal_##typ, \\ + # }} + + # struct _vtable_version _{idprefix}vtables[{c_ver_enum(idprefix, 'NUM')}] = {{ + # """ + + # ret += f"\t[{c_ver_enum(idprefix, 'unknown')}] = {{ .msgs = {{\n" + # for msg in just_structs_msg(typs): + # if msg.name in ["Tversion", "Rversion", "Rerror"]: # SPECIAL + # ret += f"\t\t_MSG({msg.name}),\n" + # ret += "\t}},\n" + + # for ver in sorted(versions): + # ret += f"\t[{c_ver_enum(idprefix, ver)}] = {{ .msgs = {{\n" + # for msg in just_structs_msg(typs): + # if ver not in msg.msgver: + # continue + # ret += f"\t\t_MSG({msg.name}),\n" + # ret += "\t}},\n" + # ret += "};\n" + + ############################################################################ + return ret + + +################################################################################ + + +class Parser: + cache: dict[str, tuple[str, list[Type]]] = {} + + def parse_file(self, filename: str) -> tuple[str, list[Type]]: + filename = os.path.normpath(filename) + if filename not in self.cache: + + def get_include(other_filename: str) -> tuple[str, list[Type]]: + return self.parse_file(os.path.join(filename, "..", other_filename)) + + self.cache[filename] = parse_file(filename, get_include) + return self.cache[filename] + + def all(self) -> tuple[set[str], list[Type]]: + ret_versions: set[str] = set() + ret_typs: dict[str, Type] = {} + for version, typs in self.cache.values(): + if version in ret_versions: + raise ValueError(f"duplicate protocol version {repr(version)}") + ret_versions.add(version) + for typ in typs: + if typ.name in ret_typs: + if typ != ret_typs[typ.name]: + raise ValueError(f"duplicate type name {repr(typ.name)}") + else: + ret_typs[typ.name] = typ + return ret_versions, list(ret_typs.values()) + + +if __name__ == "__main__": + import sys + + if len(sys.argv) < 2: + raise ValueError("requires at least 1 .9p filename") + parser = Parser() + for txtname in sys.argv[1:]: + parser.parse_file(txtname) + versions, typs = parser.all() + outdir = os.path.normpath(os.path.join(sys.argv[0], "..")) + with open(os.path.join(outdir, "include/lib9p/9p.generated.h"), "w") as fh: + fh.write(gen_h("lib9p_", versions, typs)) + with open(os.path.join(outdir, "9p.generated.c"), "w") as fh: + fh.write(gen_c("lib9p_", versions, typs)) diff --git a/lib9p/idl/00-README.md b/lib9p/idl/00-README.md new file mode 100644 index 0000000..f53acf9 --- /dev/null +++ b/lib9p/idl/00-README.md @@ -0,0 +1,48 @@ +# 9P protocol definitions +<!-- + Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com> + SPDX-Licence-Identifier: AGPL-3.0-or-later + --> + +This directory contains several `*.9p` files, each of which describes +a 9P protocol variant. + +In the 9P protocol, each message has a type, and message types come +in pairs (except "Rerror"); "T" and "R"; T-messages are +client->server requests, and R-messages are server->client responses +(the client "Transmits" T-messages and "Receives" R-messages). The +type of a message is represented by a u8 ID; T-messages are even and +R-messages are odd. + +Messages are made up of the primitives; unsigned little-endian +integers, identified with the following single-character mnemonics: + + - 1 = u8 + - 2 = u16le + - 4 = u32le + - 8 = u16le + +Out of these primitives, we can make other numeric types, + + num NUMNAME = PRIMITIVE_TYPE + +bitfields, + + bitfield BFNAME = PRIMITIVE_TYPE "NBIT=NAME... ALIAS=VAL..." + +structures, + + struct STRUCTNAME = "FILENAME[FIELDTYPE]..." + +and messages (which are a special-case of structures). + + msg Tname = "size[4,val=end-&size] typ[1,val=TYP] tag[tag] REST..." + +Struct fields that have numeric types (either primitives or `num` +types) can add to their type `,val=` and/or `,max=` to specify what +the exact value must be and/or what the maximum (inclusive) value is. + +`,val=` and `,max` take a string of `+`/`-` tokens and values; a value +can either be a decimal numeric constant (eg: `107`), the `&fieldname` +to refer to the offset of a field name in that struct, or the special +value `end` to refer to the offset of the end of the struct. diff --git a/lib9p/idl/01-9P2000.9p b/lib9p/idl/01-9P2000.9p new file mode 100644 index 0000000..680d243 --- /dev/null +++ b/lib9p/idl/01-9P2000.9p @@ -0,0 +1,147 @@ +# 01-9P2000.9p - Definitions of 9P2000 messages +# +# Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com> +# SPDX-Licence-Identifier: AGPL-3.0-or-later + +# "9P2000" base protocol +# https://ericvh.github.io/9p-rfc/rfc9p2000.html +# https://github.com/ericvh/9p-rfc/blob/master/9p2000.xml +# +# But due to incompleteness of the draft RFC, the Plan 9 manual +# section-5 and the Plan 9 headers (particularly fcall.h) are often +# better references. +# +# https://github.com/plan9foundation/plan9/tree/main/sys/man/5 +# https://man.cat-v.org/plan_9/5/ +# https://github.com/plan9foundation/plan9/blob/main/sys/include/fcall.h +version "9P2000" + +# tag - identify a request/response pair +num tag = 2 + +# file identifier +num fid = 4 + +# data - u32le `n`, then `n` bytes of data +struct d = "len[4] len*(dat[1])" + +# string - u16le `n`, then `n` bytes of UTF-8, without any nul-bytes +struct s = "len[2] len*(utf8[1])" + +# "d"? mode - file permissions and attributes +bitfield dm = 4 + "31=DIR" + "30=APPEND" + "29=EXCL" + # DMMOUNT has been around in Plan 9 forever, but is + # undocumented, and is explicitly excluded from the 9P2000 + # draft RFC. As I understand it, DMMOUNT indicates that the + # file is mounted by the kernel as a 9P transport; that the + # kernel has a lock on doing I/O on it, so userspace can't do + # I/O on it. + "28=_PLAN9_MOUNT" + "27=AUTH" + "26=TMP" + #... + "8=OWNER_R" + "7=OWNER_W" + "6=OWNER_X" + "5=GROUP_R" + "4=GROUP_W" + "3=GROUP_X" + "2=OTHER_R" + "1=OTHER_W" + "0=OTHER_X" + + "PERM=0777" # {OWNER,GROUP,OTHER}_{R,W,X} + +# QID Type - see `struct qid` below +bitfield qt = 1 + "7=DIR" + "6=APPEND" + "5=EXCL" + "4=_PLAN9_MOUNT" # see "MOUNT" in "dm" above + "3=AUTH" + # Fun historical fact: QTTMP was a relatively late addition to + # Plan 9, in 2003-12. + "2=TMP" + #"1=unused" + + # "The name QTFILE, defined to be zero, identifies the value + # of the type for a plain file." + "FILE=0" + +# uni"Q"ue "ID"entification - "two files on the same server hierarchy +# are the same if and only if their qids are the same" +# +# - "path" is a unique uint64_t that does most of the work in the +# above statement about files being the same if their QIDs are the +# same; " If a file is deleted and recreated with the same name in +# the same directory, the old and new path components of the qids +# should be different" +# +# - "vers" "is a version number for a file; typically, it is +# incremented every time the file is modified. +# +# - "type" indicates "whether the file is a directory, append-only +# file, etc."; is an instance of the qid_type bitfield. +struct qid = "type[qt] vers[4] path[8]" + +# stat - file status +struct stat = "stat_size[2,val=end-&kern_type]" + "kern_type[2]" + "kern_dev[4]" + "file_qid[qid]" + "file_mode[dm]" + "file_atime[4]" + "file_mtime[4]" + "file_size[8]" + "file_name[s]" + "file_owner_uid[s]" + "file_owner_gid[s]" + "file_last_modified_uid[s]" + +# "O"pen flags (flags to pass to Topen and Tcreate) +bitfield o = 1 + "0=rwx_0" # low bit of the 2-bit READ/WRITE/RDWR/EXEC enum + "1=rwx_1" # high bit of the 2-bit READ/WRITE/RDWR/EXEC enum" + #"2=unused" + #"3=unused" + "4=TRUNC" + #"5=unused" + "6=RCLOSE" # remove-on-close + #"7=unused" + + "READ = 0" # make available for this FID: Tread() + "WRITE = 1" # make available for this FID: Twrite() + "RDWR = 2" # make available for this FID: Tread() and Twrite() + "EXEC = 3" # make available for this FID: Tread() + +msg Tversion = "size[4,val=end-&size] typ[1,val=100] tag[tag] max_msg_size[4] version[s]" +msg Rversion = "size[4,val=end-&size] typ[1,val=101] tag[tag] max_msg_size[4] version[s]" +msg Tauth = "size[4,val=end-&size] typ[1,val=102] tag[tag] afid[fid] uname[s] aname[s]" +msg Rauth = "size[4,val=end-&size] typ[1,val=103] tag[tag] aqid[qid]" +msg Tattach = "size[4,val=end-&size] typ[1,val=104] tag[tag] fid[fid] afid[fid] uname[s] aname[s]" +msg Rattach = "size[4,val=end-&size] typ[1,val=105] tag[tag] qid[qid]" +#msg Terror = "size[4,val=end-&size] typ[1,val=106] tag[tag] illegal" +msg Rerror = "size[4,val=end-&size] typ[1,val=107] tag[tag] ename[s]" +msg Tflush = "size[4,val=end-&size] typ[1,val=108] tag[tag] oldtag[2]" +msg Rflush = "size[4,val=end-&size] typ[1,val=109] tag[tag]" +msg Twalk = "size[4,val=end-&size] typ[1,val=110] tag[tag] fid[fid] newfid[fid] nwname[2,max=16] nwname*(wname[s])" +msg Rwalk = "size[4,val=end-&size] typ[1,val=111] tag[tag] nwqid[2,max=16] nwqid*(wqid[qid])" +msg Topen = "size[4,val=end-&size] typ[1,val=112] tag[tag] fid[fid] mode[o]" +msg Ropen = "size[4,val=end-&size] typ[1,val=113] tag[tag] qid[qid] iounit[4]" +msg Tcreate = "size[4,val=end-&size] typ[1,val=114] tag[tag] fid[fid] name[s] perm[dm] mode[o]" +msg Rcreate = "size[4,val=end-&size] typ[1,val=115] tag[tag] qid[qid] iounit[4]" +msg Tread = "size[4,val=end-&size] typ[1,val=116] tag[tag] fid[fid] offset[8] count[4]" +msg Rread = "size[4,val=end-&size] typ[1,val=117] tag[tag] data[d]" # for directories, `data` is the sequence "cnt*(entries[stat])" +msg Twrite = "size[4,val=end-&size] typ[1,val=118] tag[tag] fid[fid] offset[8] data[d]" +msg Rwrite = "size[4,val=end-&size] typ[1,val=119] tag[tag] count[4]" +msg Tclunk = "size[4,val=end-&size] typ[1,val=120] tag[tag] fid[fid]" +msg Rclunk = "size[4,val=end-&size] typ[1,val=121] tag[tag]" +msg Tremove = "size[4,val=end-&size] typ[1,val=122] tag[tag] fid[fid]" +msg Rremove = "size[4,val=end-&size] typ[1,val=123] tag[tag]" +msg Tstat = "size[4,val=end-&size] typ[1,val=124] tag[tag] fid[fid]" +msg Rstat = "size[4,val=end-&size] typ[1,val=125] tag[tag] nstat[2,val=end-&stat] stat[stat]" # See the "BUG" note in the RFC for the nstat field +msg Twstat = "size[4,val=end-&size] typ[1,val=126] tag[tag] fid[fid] nstat[2,val=end-&stat] stat[stat]" # See the "BUG" note in the RFC for the nstat field +msg Rwstat = "size[4,val=end-&size] typ[1,val=127] tag[tag]" diff --git a/lib9p/idl/02-9P2000.e.9p b/lib9p/idl/02-9P2000.e.9p new file mode 100644 index 0000000..b91b2c5 --- /dev/null +++ b/lib9p/idl/02-9P2000.e.9p @@ -0,0 +1,18 @@ +# 02-9P2000e.e.9p - Definitions of 9P2000.e messages +# +# Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com> +# SPDX-Licence-Identifier: AGPL-3.0-or-later + +# "9P2000.e" Erlang extension +# https://erlangonxen.org/more/9p2000e +# https://github.com/cloudozer/ling/blob/master/doc/9p2000e.md +version "9P2000.e" + +from ./01-9P2000.9p import * + +msg Tsession = "size[4,val=end-&size] typ[1,val=150] tag[tag] key[8]" +msg Rsession = "size[4,val=end-&size] typ[1,val=151] tag[tag]" +msg Tsread = "size[4,val=end-&size] typ[1,val=152] tag[tag] fid[4] nwname[2] nwname*(wname[s])" +msg Rsread = "size[4,val=end-&size] typ[1,val=153] tag[tag] data[d]" +msg Tswrite = "size[4,val=end-&size] typ[1,val=154] tag[tag] fid[4] nwname[2] nwname*(wname[s]) data[d]" +msg Rswrite = "size[4,val=end-&size] typ[1,val=155] tag[tag] count[4]" diff --git a/lib9p/idl/02-9P2000.u.9p b/lib9p/idl/02-9P2000.u.9p new file mode 100644 index 0000000..d3ca6f0 --- /dev/null +++ b/lib9p/idl/02-9P2000.u.9p @@ -0,0 +1,29 @@ +# 02-9P2000.u.9p - Definitions of 9P2000.u messages +# +# Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com> +# SPDX-Licence-Identifier: AGPL-3.0-or-later + +# "9P2000.u" Unix extension +# https://ericvh.github.io/9p-rfc/rfc9p2000.u.html +# https://github.com/ericvh/9p-rfc/blob/master/9p2000.u.xml +version "9P2000.u" + +from ./01-9P2000.9p import * + +struct stat += "file_extension[s]" + "file_owner_n_uid[4]" + "file_owner_n_gid[4]" + "file_last_modified_n_uid[4]" + +msg Tauth += "n_uname[4]" +msg Tattach += "n_uname[4]" + +msg Rerror += "errno[4]" + +bitfield dm += "23=DEVICE" + "21=NAMEDPIPE" + "20=SOCKET" + "19=SETUID" + "18=SETGID" + +bitfield qt += "1=SYMLINK" diff --git a/lib9p/idl/03-9P2000.L.9p.wip b/lib9p/idl/03-9P2000.L.9p.wip new file mode 100644 index 0000000..62186f9 --- /dev/null +++ b/lib9p/idl/03-9P2000.L.9p.wip @@ -0,0 +1,56 @@ +# 9P2000.L.txt - Definitions of 9P2000.L messages +# +# Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com> +# SPDX-Licence-Identifier: AGPL-3.0-or-later + +# "9P2000.L" Linux extension +# https://github.com/chaos/diod/blob/master/protocol.md +version "9P2000.L" + +from ./01-9P2000.9p import * +from ./02-9P2000.u.9p import Tauth, Tattach + +#msg Tlerror = "size[4,val=end-&size] typ[1,val=6] tag[tag] illegal" # analogous to 106/Terror +msg Rlerror = "size[4,val=end-&size] typ[1,val=7] tag[tag] ecode[4]" # analogous to 107/Rerror +msg Tstatfs = "size[4,val=end-&size] typ[1,val=8] tag[tag] TODO" +msg Rstatfs = "size[4,val=end-&size] typ[1,val=9] tag[tag] TODO" +msg Tlopen = "size[4,val=end-&size] typ[1,val=12] tag[tag] TODO" # analogous to 112/Topen +msg Rlopen = "size[4,val=end-&size] typ[1,val=13] tag[tag] TODO" # analogous to 113/Ropen +msg Tlcreate = "size[4,val=end-&size] typ[1,val=14] tag[tag] TODO" # analogous to 114/Tcreate +msg Rlcreate = "size[4,val=end-&size] typ[1,val=15] tag[tag] TODO" # analogous to 115/Rcreate +msg Tsymlink = "size[4,val=end-&size] typ[1,val=16] tag[tag] TODO" +msg Rsymlink = "size[4,val=end-&size] typ[1,val=17] tag[tag] TODO" +msg Tmknod = "size[4,val=end-&size] typ[1,val=18] tag[tag] TODO" +msg Rmknod = "size[4,val=end-&size] typ[1,val=19] tag[tag] TODO" +msg Trename = "size[4,val=end-&size] typ[1,val=20] tag[tag] TODO" +msg Rrename = "size[4,val=end-&size] typ[1,val=21] tag[tag] TODO" +msg Treadlink = "size[4,val=end-&size] typ[1,val=22] tag[tag] TODO" +msg Rreadlink = "size[4,val=end-&size] typ[1,val=23] tag[tag] TODO" +msg Tgetattr = "size[4,val=end-&size] typ[1,val=24] tag[tag] TODO" +msg Rgetattr = "size[4,val=end-&size] typ[1,val=25] tag[tag] TODO" +msg Tsetattr = "size[4,val=end-&size] typ[1,val=26] tag[tag] TODO" +msg Rsetattr = "size[4,val=end-&size] typ[1,val=27] tag[tag] TODO" +#... +msg Txattrwalk = "size[4,val=end-&size] typ[1,val=30] tag[tag] TODO" +msg Rxattrwalk = "size[4,val=end-&size] typ[1,val=31] tag[tag] TODO" +msg Txattrcreate = "size[4,val=end-&size] typ[1,val=32] tag[tag] TODO" +msg Rxattrcreate = "size[4,val=end-&size] typ[1,val=33] tag[tag] TODO" +#... +msg Treaddir = "size[4,val=end-&size] typ[1,val=40] tag[tag] TODO" +msg Rreaddir = "size[4,val=end-&size] typ[1,val=41] tag[tag] TODO" +#... +msg Tfsync = "size[4,val=end-&size] typ[1,val=50] tag[tag] TODO" +msg Rfsync = "size[4,val=end-&size] typ[1,val=51] tag[tag] TODO" +msg Tlock = "size[4,val=end-&size] typ[1,val=52] tag[tag] TODO" +msg Rlock = "size[4,val=end-&size] typ[1,val=53] tag[tag] TODO" +msg Tgetlock = "size[4,val=end-&size] typ[1,val=54] tag[tag] TODO" +msg Rgetlock = "size[4,val=end-&size] typ[1,val=55] tag[tag] TODO" +# ... +msg Tlink = "size[4,val=end-&size] typ[1,val=70] tag[tag] TODO" +msg Rlink = "size[4,val=end-&size] typ[1,val=71] tag[tag] TODO" +msg Tmkdir = "size[4,val=end-&size] typ[1,val=72] tag[tag] TODO" +msg Tmkdir = "size[4,val=end-&size] typ[1,val=73] tag[tag] TODO" +msg Trenameat = "size[4,val=end-&size] typ[1,val=74] tag[tag] TODO" +msg Rrenameat = "size[4,val=end-&size] typ[1,val=75] tag[tag] TODO" +msg Tunlinkat = "size[4,val=end-&size] typ[1,val=76] tag[tag] TODO" +msg Runlinkat = "size[4,val=end-&size] typ[1,val=77] tag[tag] TODO" |