diff options
Diffstat (limited to 'lib9p/idl')
-rw-r--r-- | lib9p/idl/0000-README.md | 73 | ||||
-rw-r--r-- | lib9p/idl/0000-TODO.md | 11 | ||||
-rw-r--r-- | lib9p/idl/1992-9P0.9p.wip | 166 | ||||
-rw-r--r-- | lib9p/idl/1995-9P1.9p.wip | 141 | ||||
-rw-r--r-- | lib9p/idl/1996-Styx.9p.wip | 66 | ||||
-rw-r--r-- | lib9p/idl/2002-9P2000.9p | 122 | ||||
-rw-r--r-- | lib9p/idl/2003-9P2000.p9p.9p | 49 | ||||
-rw-r--r-- | lib9p/idl/2005-9P2000.u.9p | 35 | ||||
-rw-r--r-- | lib9p/idl/2010-9P2000.L.9p | 230 | ||||
-rw-r--r-- | lib9p/idl/2010-9P2000.L.9p.wip | 56 | ||||
-rw-r--r-- | lib9p/idl/2012-9P2000.e.9p | 8 | ||||
-rw-r--r-- | lib9p/idl/__init__.py | 878 |
12 files changed, 1599 insertions, 236 deletions
diff --git a/lib9p/idl/0000-README.md b/lib9p/idl/0000-README.md index cec27e2..84cf865 100644 --- a/lib9p/idl/0000-README.md +++ b/lib9p/idl/0000-README.md @@ -1,7 +1,7 @@ <!-- - 0000-README.md - Overview of 9P protocol definitions + lib9p/idl/0000-README.md - Overview of 9P protocol definitions - Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com> + Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com> SPDX-License-Identifier: AGPL-3.0-or-later --> @@ -17,35 +17,82 @@ client->server requests, and R-messages are server->client responses 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: +9P messages are exchanged over a reliable bidirectional in-order octet +stream. 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, +Out of these primitives, we can make more complex types: + +## User-defined types + +### Numeric types num NUMNAME = PRIMITIVE_TYPE + "NAME=VAL"... + +Besides just being an alias for a primitive type, a numeric type may +define 0 or more named constants of that type, each wrapped in +"quotes". + +### Bitfields + + bitfield BFNAME = PRIMITIVE_TYPE + "bit NBIT=NAME"... + "bit NBIT=reserved(NAME)"... + "bit NBIT=num(NUMNAME)"... + "alias NAME=VAL"... + "mask NAME=VAL"... + "num(NUMNAME) NAME=VAL"... -bitfields, +The same NBIT may not be defined multiple times. The same NAME may +not be defined multiple times. - bitfield BFNAME = PRIMITIVE_TYPE "NBIT=NAME... ALIAS=VAL..." + - A `reserved(...)` bit indicates that the bit is named but is not + allowed to be used. + - `num(...)` bits embed a numeric/enumerated field within a set of + bits. Once several bits have been allocated to a numeric field + with `bit NBIT=num(NUMNAME)`, constant values for that field may be + declared with `num(NUMNAME) NAME=VAL`. For each numeric field, a + `mask NUMNAME=BITMASK` is automatically declared. + - A `mask` defines a bitmask that selects several bits. + - An `alias` defines a convenience alias for a bit or set of bits. -structures, +### Structures - struct STRUCTNAME = "FILENAME[FIELDTYPE]..." + struct STRUCTNAME = "FIELDNAME[FIELDTYPE]..." -and messages (which are a special-case of structures). +Or a special-case for structs that are messages; `msg` has the same +syntax as `struct`, but has restrictions on the STRUCTNAME and the +first 3 fields must all be declared in the same way: 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. +A field that is repeated a variable number of times be wrapped in +parenthesis and prefixed with the fieldname containing that count: +`OTHERFIELDNAME*(FIELDNAME[FIELDTYPE])`. `,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. +can be + - a decimal numeric constant (eg: `107`), + - `&fieldname` to refer to the offset of a field name in that struct, + - the special value `end` to refer to the offset of the end of the + struct, + - the special value `u{8,16,32,64}_max` to refer to the constant + value `(1<<{n})-1`, or + - the special value `s{8,16,32,64}_max` to refer to the constant value + `(1<<({n}-1))-1`. + +## Parser + +A parser for this syntax is given in `__init__.py`. However, +`__init__.py` places the somewhat arbitrary undocumented restrictions +on fields referenced as the count of a repeated field. diff --git a/lib9p/idl/0000-TODO.md b/lib9p/idl/0000-TODO.md new file mode 100644 index 0000000..e52902f --- /dev/null +++ b/lib9p/idl/0000-TODO.md @@ -0,0 +1,11 @@ +<!-- + lib9p/idl/0000-TODO.md - Changes I intend to make to idl/__init__.py + and proto.gen + + Copyright (C) 2025 Luke T. Shumaker <lukeshu@lukeshu.com> + SPDX-License-Identifier: AGPL-3.0-or-later + --> + +- Decide how to handle duplicate type names from different versions +- Decide how to handle duplicate `enum lib9p_msg_type` names and + values diff --git a/lib9p/idl/1992-9P0.9p.wip b/lib9p/idl/1992-9P0.9p.wip index 4278fa3..a434ba2 100644 --- a/lib9p/idl/1992-9P0.9p.wip +++ b/lib9p/idl/1992-9P0.9p.wip @@ -1,9 +1,22 @@ -# 1992-9P0.9p - Definitions of 9P0 (Plan 9 1st ed) messages +# lib9p/idl/1992-9P0.9p - Definitions of 9P0 (Plan 9 1st ed) messages # -# Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com> +# Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com> # SPDX-License-Identifier: AGPL-3.0-or-later -# https://man.cat-v.org/plan_9_1st_ed/5/ +# The original 9P protocol ("9P0"), from Plan 9 1st edition. +# +# Documentation: +# - https://github.com/plan9foundation/plan9/tree/1e-1993-01-03/sys/man/5 +# - https://github.com/plan9foundation/plan9/tree/1e-1993-01-03/sys/man/6/auth +# - https://man.cat-v.org/plan_9_1st_ed/5/ +# - https://man.cat-v.org/plan_9_1st_ed/6/auth +# +# Implementation references: +# - https://github.com/plan9foundation/plan9/blob/1e-1993-01-03/sys/include/fcall.h (MAXFDATA) +# - https://github.com/plan9foundation/plan9/blob/1e-1993-01-03/sys/include/libc.h (`ch` bits) +# - https://github.com/plan9foundation/plan9/blob/1e-1993-01-03/sys/src/fs/port/fcall.c (`stat`) +# - https://github.com/plan9foundation/plan9/blob/1e-1993-01-03/sys/src/fs/port/fs.c (`offset:max`) +# - https://github.com/plan9foundation/plan9/blob/1e-1993-01-03/sys/src/fs/port/portdata.h (MAXDAT) version "9P0" # tag - identify a request/response pair @@ -15,39 +28,114 @@ num fid = 2 # uni"Q"ue "ID"entification struct qid = "path[4] version[4]" -# a nul-padded string -struct name = 28*(txt[1]) - -msg Tnop = "typ[1,val=TODO] tag[tag,val=0xFFFF]" -msg Rnop = "typ[1,val=TODO] tag[tag,val=0xFFFF]" -msg Tsession = "typ[1,val=TODO] tag[tag,val=0xFFFF]" -msg Rsession = "typ[1,val=TODO] tag[tag,val=0xFFFF]" -msg Rerror = "typ[1,val=TODO] tag[tag] ename[64]" -msg Tflush = "typ[1,val=TODO] tag[tag] oldtag[tag]" -msg Rflush = "typ[1,val=TODO] tag[tag]" -msg Tauth = "typ[1,val=TODO] tag[tag] fid[fid] uid[28] chal[36]" -msg Rauth = "typ[1,val=TODO] tag[tag] fid[fid] chal[30]" -msg Tattach = "typ[1,val=TODO] tag[tag] fid[fid] uid[28] aname[28] auth[28]" -msg Rattach = "typ[1,val=TODO] tag[tag] fid[fid] qid[8]" -msg Tclone = "typ[1,val=TODO] tag[tag] fid[fid] newfid[fid]" -msg Rclone = "typ[1,val=TODO] tag[tag] fid[fid]" -msg Tclwalk = "typ[1,val=TODO] tag[tag] fid[fid] newfid[fid] name[28]" -msg Rclwalk = "typ[1,val=TODO] tag[tag] fid[fid] qid[8]" -msg Twalk = "typ[1,val=TODO] tag[tag] fid[fid] name[28]" -msg Rwalk = "typ[1,val=TODO] tag[tag] fid[fid] qid[8]" -msg Topen = "typ[1,val=TODO] tag[tag] fid[fid] mode[1]" -msg Ropen = "typ[1,val=TODO] tag[tag] fid[fid] qid[8]" -msg Tcreate = "typ[1,val=TODO] tag[tag] fid[fid] name[28] perm[4] mode[1]" -msg Rcreate = "typ[1,val=TODO] tag[tag] fid[fid] qid[8]" -msg Tread = "typ[1,val=TODO] tag[tag] fid[fid] offset[8] count[2,max=8192]" -msg Rread = "typ[1,val=TODO] tag[tag] fid[fid] count[2,max=8192] pad[1] count*(data[1])" -msg Twrite = "typ[1,val=TODO] tag[tag] fid[fid] offset[8] count[2,max=8192] pad[1] count*(data[1])" -msg Rwrite = "typ[1,val=TODO] tag[tag] fid[fid] count[2,max=8192]" -msg Tclunk = "typ[1,val=TODO] tag[tag] fid[fid]" -msg Rclunk = "typ[1,val=TODO] tag[tag] fid[fid]" -msg Tremove = "typ[1,val=TODO] tag[tag] fid[fid]" -msg Rremove = "typ[1,val=TODO] tag[tag] fid[fid]" -msg Tstat = "typ[1,val=TODO] tag[tag] fid[fid]" -msg Rstat = "typ[1,val=TODO] tag[tag] fid[fid] stat[116]" -msg Twstat = "typ[1,val=TODO] tag[tag] fid[fid] stat[116]" -msg Rwstat = "typ[1,val=TODO] tag[tag] fid[fid]" +# a nul-terminated+padded string +struct name = "28*(txt[1])" + +# a nul-terminated+padded string +struct errstr = "64*(txt[1])" + +# "O"pen flags (flags to pass to Topen and Tcreate) +# Unused bits are *ignored*. +bitfield o = 1 + "bit 0=num(MODE)" # low bit of the 2-bit READ/WRITE/RDWR/EXEC enum + "bit 1=num(MODE)" # high bit of the 2-bit READ/WRITE/RDWR/EXEC enum + #"bit 2=unused" + #"bit 3=unused" + "bit 4=TRUNC" + "bit 5=reserved(CEXEC)" # close-on-exec + "bit 6=RCLOSE" # remove-on-close + #"bit 7=unused" + + "num(MODE) READ = 0" # make available for this FID: Tread() + "num(MODE) WRITE = 1" # make available for this FID: Twrite() + "num(MODE) RDWR = 2" # make available for this FID: Tread() and Twrite() + "num(MODE) EXEC = 3" # make available for this FID: Tread() + + "mask FLAG = 0b11111100" + +# "CH"annel flags - file permissions and attributes (a "channel" is +# what a file handle is called inside of the Plan 9 kernel). +bitfield ch = 4 + "bit 31=DIR" + "bit 30=APPEND" + "bit 29=EXCL" + #... + "bit 8=OWNER_R" + "bit 7=OWNER_W" + "bit 6=OWNER_X" + "bit 5=GROUP_R" + "bit 4=GROUP_W" + "bit 3=GROUP_X" + "bit 2=OTHER_R" + "bit 1=OTHER_W" + "bit 0=OTHER_X" + + "mask PERM=0777" # {OWNER,GROUP,OTHER}_{R,W,X} + +struct stat = "file_name[name]" + "file_owner[name]" + "file_group[name]" + "file_qid[qid]" + "file_mode[ch]" + "file_atime[4]" + "file_mtime[4]" + "file_size[8]" + "kern_type[2]" + "kern_dev[2]" + +# Authentication uses symetric-key encryption, using a per-client +# secret-key. The encryption scheme is beyond the scope of this +# document. +struct auth_ticket = "15*(dat[1])" +struct encrypted_auth_challenge = "36*(ciphertext[1])" +struct cleartext_auth_challenge = "magic[1,val=1] 7*(client_challenge[1]) server[name]" +struct encrypted_auth_response = "30*(ciphertext[1])" +struct cleartext_auth_response = "magic[1,val=4] 7*(client_challenge[1]) ticket[auth_ticket]" + +# A 9P0 session goes: +# +# [nop()] +# session() +# [auth_tok=auth()] +# attach([auth_tok]) +# ... + +msg Tmux = "typ[1,val=48] mux[2]" # Undocumented, but implemented by mux(3) / libmux.a +#msg Rmux = "typ[1,val=49] illegal" +msg Tnop = "typ[1,val=50] tag[tag,val=0xFFFF]" +msg Rnop = "typ[1,val=51] tag[tag,val=0xFFFF]" +msg Tsession = "typ[1,val=52] tag[tag,val=0xFFFF]" +msg Rsession = "typ[1,val=53] tag[tag,val=0xFFFF]" +#msg Terror = "typ[1,val=54] illegal" +msg Rerror = "typ[1,val=55] tag[tag] ename[errstr]" +msg Tflush = "typ[1,val=56] tag[tag] oldtag[tag]" +msg Rflush = "typ[1,val=57] tag[tag]" +msg Tattach = "typ[1,val=58] tag[tag] fid[fid] uid[name] aname[name] auth[auth_ticket] 13*(pad[1])" # Pad to allow auth_tickets up to 28 bytes. +msg Rattach = "typ[1,val=59] tag[tag] fid[fid] qid[qid]" +msg Tclone = "typ[1,val=60] tag[tag] fid[fid] newfid[fid]" +msg Rclone = "typ[1,val=61] tag[tag] fid[fid]" +msg Twalk = "typ[1,val=62] tag[tag] fid[fid] name[name]" +msg Rwalk = "typ[1,val=63] tag[tag] fid[fid] qid[qid]" +msg Topen = "typ[1,val=64] tag[tag] fid[fid] mode[o]" +msg Ropen = "typ[1,val=65] tag[tag] fid[fid] qid[qid]" +msg Tcreate = "typ[1,val=66] tag[tag] fid[fid] name[name] perm[ch] mode[o]" +msg Rcreate = "typ[1,val=67] tag[tag] fid[fid] qid[qid]" +# For `count:max`, see 1e/2e/3e `sys/include/fcall.h:MAXFDATA` or 1e/2e `sys/src/fs/port/portdata.h:MAXDAT`. +# For read `offset:max`, see 1e/2e/3e `sys/src/fs/port/fs.c:f_read()` or 3e `sys/src/lib9p/srv.c:srv():case Tread`. +# For write `offset:max`, see 1e/2e/3e `sys/src/fs/port/fs.c:f_write()`. +msg Tread = "typ[1,val=68] tag[tag] fid[fid] offset[8,max=s64_max] count[2,max=8192]" +msg Rread = "typ[1,val=69] tag[tag] fid[fid] count[2,max=8192] pad[1] count*(data[1])" +msg Twrite = "typ[1,val=70] tag[tag] fid[fid] offset[8,max=s64_max] count[2,max=8192] pad[1] count*(data[1])" +msg Rwrite = "typ[1,val=71] tag[tag] fid[fid] count[2,max=8192]" +msg Tclunk = "typ[1,val=72] tag[tag] fid[fid]" +msg Rclunk = "typ[1,val=73] tag[tag] fid[fid]" +msg Tremove = "typ[1,val=74] tag[tag] fid[fid]" +msg Rremove = "typ[1,val=75] tag[tag] fid[fid]" +msg Tstat = "typ[1,val=76] tag[tag] fid[fid]" +msg Rstat = "typ[1,val=77] tag[tag] fid[fid] stat[stat]" +msg Twstat = "typ[1,val=78] tag[tag] fid[fid] stat[stat]" +msg Rwstat = "typ[1,val=79] tag[tag] fid[fid]" +msg Tclwalk = "typ[1,val=80] tag[tag] fid[fid] newfid[fid] name[name]" +msg Rclwalk = "typ[1,val=81] tag[tag] fid[fid] qid[qid]" +msg Tauth = "typ[1,val=82] tag[tag] fid[fid] uid[name] chal[encrypted_auth_challenge]" # chal is an encrypted +msg Rauth = "typ[1,val=83] tag[tag] fid[fid] chal[encrypted_auth_response]" diff --git a/lib9p/idl/1995-9P1.9p.wip b/lib9p/idl/1995-9P1.9p.wip index 55814d4..660e24a 100644 --- a/lib9p/idl/1995-9P1.9p.wip +++ b/lib9p/idl/1995-9P1.9p.wip @@ -1,52 +1,107 @@ -# 1995-9P1.9p - Definitions of 9P1 (Plan 9 2nd ed and 3rd ed) messages +# lib9p/idl/1995-9P1.9p - Definitions of 9P1 (Plan 9 2nd ed and 3rd ed) messages # -# Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com> +# Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com> # SPDX-License-Identifier: AGPL-3.0-or-later -# https://man.cat-v.org/plan_9_2nd_ed/5/ -# https://man.cat-v.org/plan_9_3rd_ed/5/ +# Plan 9 2nd and 3rd edition used a version of 9P lightly revised from +# the 1st edition version, re-thinking authentication. +# +# 2nd edition documentation: +# - https://github.com/plan9foundation/plan9/tree/2e-1995-04-05/sys/man/5 +# - https://github.com/plan9foundation/plan9/tree/2e-1995-04-05/sys/man/6/auth +# - https://man.cat-v.org/plan_9_2nd_ed/5/ +# - https://man.cat-v.org/plan_9_2nd_ed/6/auth +# +# 2nd edition implementation references: +# - https://github.com/plan9foundation/plan9/blob/2e-1995-04-05/sys/include/fcall.h (MAXFDATA) +# - https://github.com/plan9foundation/plan9/blob/2e-1995-04-05/sys/include/libc.h (`ch` bits) +# - https://github.com/plan9foundation/plan9/blob/2e-1995-04-05/sys/include/auth.h (auth matic) +# - https://github.com/plan9foundation/plan9/blob/2e-1995-04-05/sys/src/fs/port/fcall.c (`stat`) +# - https://github.com/plan9foundation/plan9/blob/2e-1995-04-05/sys/src/fs/port/fs.c (`offset:max`) +# - https://github.com/plan9foundation/plan9/blob/2e-1995-04-05/sys/src/fs/port/portdata.h (`MAXDAT`) +# - https://github.com/plan9foundation/plan9/blob/2e-1995-04-05/sys/src/libauth/convM2T.c (`auth_ticket`) +# +# 3rd edition documentation: +# - https://github.com/plan9foundation/plan9/tree/3e-2001-03-28/sys/man/5 +# - https://github.com/plan9foundation/plan9/tree/3e-2001-03-28/sys/man/6/auth +# - https://man.cat-v.org/plan_9_3rd_ed/5/ +# - https://man.cat-v.org/plan_9_3rd_ed/6/auth +# +# 3rd edition implementation references: +# - https://github.com/plan9foundation/plan9/blob/3e-2001-03-28/sys/include/fcall.h (MAXFDATA) +# - https://github.com/plan9foundation/plan9/blob/3e-2001-03-28/sys/include/libc.h (`ch` bits) +# - https://github.com/plan9foundation/plan9/blob/3e-2001-03-28/sys/include/auth.h (auth magic) +# - https://github.com/plan9foundation/plan9/blob/3e-2001-03-28/sys/src/fs/port/fcall.c (`stat`) +# - https://github.com/plan9foundation/plan9/blob/3e-2001-03-28/sys/src/fs/port/fs.c (`offset:max`) +# - https://github.com/plan9foundation/plan9/blob/3e-2001-03-28/sys/src/lib9p/srv.c (read `offset:max`) +# - https://github.com/plan9foundation/plan9/blob/3e-2001-03-28/sys/src/libauth/convM2T.c (`auth_ticket`) version "9P1" -# tag - identify a request/response pair -num tag = 2 +from ./1992-9P0.9p import tag, fid, qid, name, errstr, o, ch, stat -# file identifier - like a UNIX file-descriptor -num fid = 2 +# CHMOUNT is undocumented (and is explicitly excluded from the 9P2000 +# draft RFC). As I understand it, CHMOUNT 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. +bitfield ch += "bit 28=_PLAN9_MOUNT" -# uni"Q"ue "ID"entification -struct qid = "path[4] version[4]" +# Authentication uses DES encryption. The client obtains a ticket and +# a nonce-key from a separate auth-server; how it does this is beyond +# the scope of this document. +struct random = "8*(dat[1])" +struct domain_name = "48*(txt[1])" +struct des_key = "7*(dat[1])" +struct encrypted_ticket = "72*(ciphertext[1])" # encrypted by auth-server with server-key +struct cleartext_ticket = "magic[1,val=64] server_chal[random] client_uid[name] server_uid[name] nonce_key[des_key]" +struct encrypted_authenticator_challenge = "13*(ciphertext[1])" # encrypted by client with nonce-key obtained from auth-server +struct cleartext_authenticator_challenge = "magic[1,val=66] server_chal[random] replay_count[4]" +struct encrypted_authenticator_response = "13*(ciphertext[1])" # encrypted by server with nonce-key obtained from ticket +struct cleartext_authenticator_response = "magic[1,val=67] client_chal[random] replay_count[4]" -# a nul-padded string -struct name = 28*(txt[1]) +# A 9P0 session goes: +# +# [nop()] +# auth_tok=[session()] +# attach(auth_tok) +# ... -msg Tnop = "typ[1,val=TODO] tag[tag,val=0xFFFF]" -msg Rnop = "typ[1,val=TODO] tag[tag,val=0xFFFF]" -msg Tsession = "typ[1,val=TODO] tag[tag,val=0xFFFF] chal[8]" -msg Rsession = "typ[1,val=TODO] tag[tag,val=0xFFFF] chal[8] authid[28] authdom[48]" -msg Rerror = "typ[1,val=TODO] tag[tag] ename[64]" -msg Tflush = "typ[1,val=TODO] tag[tag] oldtag[tag]" -msg Rflush = "typ[1,val=TODO] tag[tag]" -msg Tattach = "typ[1,val=TODO] tag[tag] fid[fid] uid[28] aname[28] ticket[72] auth[13]" -msg Rattach = "typ[1,val=TODO] tag[tag] fid[fid] qid[8] rauth[13]" -msg Tclone = "typ[1,val=TODO] tag[tag] fid[fid] newfid[fid]" -msg Rclone = "typ[1,val=TODO] tag[tag] fid[fid]" -msg Tclwalk = "typ[1,val=TODO] tag[tag] fid[fid] newfid[fid] name[28]" -msg Rclwalk = "typ[1,val=TODO] tag[tag] fid[fid] qid[8]" -msg Twalk = "typ[1,val=TODO] tag[tag] fid[fid] name[28]" -msg Rwalk = "typ[1,val=TODO] tag[tag] fid[fid] qid[8]" -msg Topen = "typ[1,val=TODO] tag[tag] fid[fid] mode[1]" -msg Ropen = "typ[1,val=TODO] tag[tag] fid[fid] qid[8]" -msg Tcreate = "typ[1,val=TODO] tag[tag] fid[fid] name[28] perm[4] mode[1]" -msg Rcreate = "typ[1,val=TODO] tag[tag] fid[fid] qid[8]" -msg Tread = "typ[1,val=TODO] tag[tag] fid[fid] offset[8] count[2,max=8192]" -msg Rread = "typ[1,val=TODO] tag[tag] fid[fid] count[2,max=8192] pad[1] count*(data[1])" -msg Twrite = "typ[1,val=TODO] tag[tag] fid[fid] offset[8] count[2,max=8192] pad[1] count*(data[1])" -msg Rwrite = "typ[1,val=TODO] tag[tag] fid[fid] count[2,max=8192]" -msg Tclunk = "typ[1,val=TODO] tag[tag] fid[fid]" -msg Rclunk = "typ[1,val=TODO] tag[tag] fid[fid]" -msg Tremove = "typ[1,val=TODO] tag[tag] fid[fid]" -msg Rremove = "typ[1,val=TODO] tag[tag] fid[fid]" -msg Tstat = "typ[1,val=TODO] tag[tag] fid[fid]" -msg Rstat = "typ[1,val=TODO] tag[tag] fid[fid] stat[116]" -msg Twstat = "typ[1,val=TODO] tag[tag] fid[fid] stat[116]" -msg Rwstat = "typ[1,val=TODO] tag[tag] fid[fid]" +#from ./1992-9P0.9p import Tmux # typ=48 ; removed +#from ./1992-9P0.9p import Rmux # typ=49 ; removed +from ./1992-9P0.9p import Tnop # typ=50 +from ./1992-9P0.9p import Rnop # typ=51 +#from ./1992-9P0.9p import Tsession # typ=52 ; revised, now has typ=84 +#from ./1992-9P0.9p import Rsession # typ=53 ; revised, now has typ=85 +#from ./1992-9P0.9p import Terror # typ=54 ; never existed +from ./1992-9P0.9p import Rerror # typ=55 +from ./1992-9P0.9p import Tflush # typ=56 +from ./1992-9P0.9p import Rflush # typ=57 +#from ./1992-9P0.9p import Tattach # typ=58 ; revised, now has typ=86 +#from ./1992-9P0.9p import Rattach # typ=59 ; revised, now has typ=87 +from ./1992-9P0.9p import Tclone # typ=60 +from ./1992-9P0.9p import Rclone # typ=61 +from ./1992-9P0.9p import Twalk # typ=62 +from ./1992-9P0.9p import Rwalk # typ=63 +from ./1992-9P0.9p import Topen # typ=64 +from ./1992-9P0.9p import Ropen # typ=65 +from ./1992-9P0.9p import Tcreate # typ=66 +from ./1992-9P0.9p import Rcreate # typ=67 +from ./1992-9P0.9p import Tread # typ=68 +from ./1992-9P0.9p import Rread # typ=69 +from ./1992-9P0.9p import Twrite # typ=70 +from ./1992-9P0.9p import Rwrite # typ=71 +from ./1992-9P0.9p import Tclunk # typ=72 +from ./1992-9P0.9p import Rclunk # typ=73 +from ./1992-9P0.9p import Tremove # typ=74 +from ./1992-9P0.9p import Rremove # typ=75 +from ./1992-9P0.9p import Tstat # typ=76 +from ./1992-9P0.9p import Rstat # typ=77 +from ./1992-9P0.9p import Twstat # typ=78 +from ./1992-9P0.9p import Rwstat # typ=79 +from ./1992-9P0.9p import Tclwalk # typ=80 +from ./1992-9P0.9p import Rclwalk # typ=81 +#from ./1992-9P0.9p import Tauth # typ=82 ; merged into Tsession +#from ./1992-9P0.9p import Rauth # typ=83 ; merged into Rsession +msg Tsession = "typ[1,val=84] tag[tag,val=0xFFFF] chal[random]" +msg Rsession = "typ[1,val=85] tag[tag,val=0xFFFF] chal[random] server_name[name] server_domain[domain_name]" +msg Tattach = "typ[1,val=86] tag[tag] fid[fid] uid[name] aname[name] ticket[encrypted_ticket] auth[encrypted_authenticator_challenge]" +msg Rattach = "typ[1,val=87] tag[tag] fid[fid] qid[qid] rauth[encrypted_authenticator_response]" diff --git a/lib9p/idl/1996-Styx.9p.wip b/lib9p/idl/1996-Styx.9p.wip index 2feb24f..3cb3774 100644 --- a/lib9p/idl/1996-Styx.9p.wip +++ b/lib9p/idl/1996-Styx.9p.wip @@ -1,15 +1,59 @@ -# 1996-Styx.9p - Definitions of Styx messages +# lib9p/idl/1996-Styx.9p - Definitions of Styx messages # -# Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com> +# Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com> # SPDX-License-Identifier: AGPL-3.0-or-later # Styx was a variant of the 9P protocol used by the Inferno operating -# system. Message framing looks like 9P1 (1995), but semantics look -# more like 9P2000 (2002). I am not sure whether there are Styx -# protocol differences between Inferno 1e, 2e, or 3e (4e adopted -# 9P2000). -# -# - 1996 beta -# - 1997 1.0 -# - 1999 2nd ed -# - 2001 3rd ed +# system (before it switched to 9P2000 in 4th edition). It looks +# exactly like 9P1 but with different message-type numbers and without +# `clwalk` or `session`, and no authentication in `attach`. +# +# There do not appear to be Styx protocol differences between Inferno +# 1e, 2e, or 3e. +# +# - 1996 beta https://github.com/inferno-os/inferno-1e0/blob/main/ see `man/html/proto*.htm`, `include/styx.h`, and `Linux/386/include/lib9.h` +# - 1997 1e https://github.com/inferno-os/inferno-1e1/blob/master/ see `man/html/mpgs{113..124}.htm`, `include/styx.h`, `os/port/lib.h`, and `os/fs/fs.c` +# - 1999 2e https://github.com/inferno-os/inferno-2e/blob/master/ see `include/styx.h`, `os/port/lib.h`, and `os/kfs/fs.c` (no public manpages) +# - 2001 3e https://github.com/inferno-os/inferno-3e/blob/master/ see `include/man/5/`, `include/styx.h`, `os/port/lib.h`, and `os/kfs/fs.c` +version "Styx" + +from ./1992-9P1.9p import tag, fid, qid, name, errstr, o, ch, stat + +# A Styx session goes: +# +# [nop()] +# attach() +# ... + +msg Tnop = "typ[1,val=0] tag[tag,val=0xFFFF]" +msg Rnop = "typ[1,val=1] tag[tag,val=0xFFFF]" +#msg Terror = "typ[1,val=2] illegal" +msg Rerror = "typ[1,val=3] tag[tag] ename[errstr]" +msg Tflush = "typ[1,val=4] tag[tag] oldtag[tag]" +msg Rflush = "typ[1,val=5] tag[tag]" +msg Tclone = "typ[1,val=6] tag[tag] fid[fid] newfid[fid]" +msg Rclone = "typ[1,val=7] tag[tag] fid[fid]" +msg Twalk = "typ[1,val=8] tag[tag] fid[fid] name[name]" +msg Rwalk = "typ[1,val=9] tag[tag] fid[fid] qid[qid]" +msg Topen = "typ[1,val=10] tag[tag] fid[fid] mode[o]" +msg Ropen = "typ[1,val=11] tag[tag] fid[fid] qid[qid]" +msg Tcreate = "typ[1,val=12] tag[tag] fid[fid] name[name] perm[ch] mode[o]" +msg Rcreate = "typ[1,val=13] tag[tag] fid[fid] qid[qid]" +# For `offset:max`, see `fs.c` `f_read()` and `f_write()`. +# For `count:max`, see `styx.h:MAXFDATA`. +msg Tread = "typ[1,val=14] tag[tag] fid[fid] offset[8,max=s64_max] count[2,max=8192]" +msg Rread = "typ[1,val=15] tag[tag] fid[fid] count[2,max=8192] pad[1] count*(data[1])" +msg Twrite = "typ[1,val=16] tag[tag] fid[fid] offset[8,max=s64_max] count[2,max=8192] pad[1] count*(data[1])" +msg Rwrite = "typ[1,val=17] tag[tag] fid[fid] count[2,max=8192]" +msg Tclunk = "typ[1,val=18] tag[tag] fid[fid]" +msg Rclunk = "typ[1,val=19] tag[tag] fid[fid]" +msg Tremove = "typ[1,val=20] tag[tag] fid[fid]" +msg Rremove = "typ[1,val=21] tag[tag] fid[fid]" +msg Tstat = "typ[1,val=22] tag[tag] fid[fid]" +msg Rstat = "typ[1,val=23] tag[tag] fid[fid] stat[stat]" +msg Twstat = "typ[1,val=24] tag[tag] fid[fid] stat[stat]" +msg Rwstat = "typ[1,val=25] tag[tag] fid[fid]" +#msg Tsession = "typ[1,val=26]" # The 1e kernel used Tsession in structs internally, but never transmitted it. +#msg Rsession = "typ[1,val=27]" # Implied by Tsession. +msg Tattach = "typ[1,val=28] tag[tag] fid[fid] uid[name] aname[name]" +msg Rattach = "typ[1,val=29] tag[tag] fid[fid] qid[qid]" diff --git a/lib9p/idl/2002-9P2000.9p b/lib9p/idl/2002-9P2000.9p index 47d402a..2b51612 100644 --- a/lib9p/idl/2002-9P2000.9p +++ b/lib9p/idl/2002-9P2000.9p @@ -1,6 +1,6 @@ -# 2002-9P2000.9p - Definitions of 9P2000 messages +# lib9p/idl/2002-9P2000.9p - Definitions of 9P2000 messages # -# Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com> +# Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com> # SPDX-License-Identifier: AGPL-3.0-or-later # "9P2000" base protocol @@ -9,74 +9,77 @@ # # 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. +# better references. The s{32,64}_max limitations are not documented +# in the draft RFC or the manual pages, but have been enforced by +# lib9p/srv.c in every release of Plan 9 4e. # # 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 +# https://github.com/plan9foundation/plan9/blob/main/sys/include/libc.h +# https://github.com/plan9foundation/plan9/blob/main/sys/src/lib9p/srv.c version "9P2000" # tag - identify a request/response pair num tag = 2 + "NOTAG = u16_max" # file identifier - like a UNIX file-descriptor num fid = 4 - -# data - u32le `n`, then `n` bytes of data -struct d = "len[4] len*(dat[1])" + "NOFID = u32_max" # 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 +# "D"ir-entry "M"ode - 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" + "bit 31=DIR" + "bit 30=APPEND" + "bit 29=EXCL" + # DMMOUNT has been around in Plan 9 since 2e (CHMOUNT in <4e), + # 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. + "bit 28=_PLAN9_MOUNT" + "bit 27=AUTH" + "bit 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" + "bit 8=OWNER_R" + "bit 7=OWNER_W" + "bit 6=OWNER_X" + "bit 5=GROUP_R" + "bit 4=GROUP_W" + "bit 3=GROUP_X" + "bit 2=OTHER_R" + "bit 1=OTHER_W" + "bit 0=OTHER_X" - "PERM_MASK=0777" # {OWNER,GROUP,OTHER}_{R,W,X} + "mask 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" + "bit 7=DIR" + "bit 6=APPEND" + "bit 5=EXCL" + "bit 4=_PLAN9_MOUNT" # See "_PLAN9_MOUNT" in "dm" above. + "bit 3=AUTH" # Fun historical fact: QTTMP was a relatively late addition to # Plan 9, in 2003-12. - "2=TMP" - #"1=unused" + "bit 2=TMP" + #"bit 1=unused" # "The name QTFILE, defined to be zero, identifies the value # of the type for a plain file." - "FILE=0" + "alias 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 +# 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" # @@ -102,23 +105,30 @@ struct stat = "stat_size[2,val=end-&kern_type]" "file_last_modified_uid[s]" # "O"pen flags (flags to pass to Topen and Tcreate) +# Unused bits *must* be 0. bitfield o = 1 - "0=mode_0" # low bit of the 2-bit READ/WRITE/RDWR/EXEC enum - "1=mode_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" + "bit 0=num(MODE)" # low bit of the 2-bit READ/WRITE/RDWR/EXEC enum + "bit 1=num(MODE)" # high bit of the 2-bit READ/WRITE/RDWR/EXEC enum + #"bit 2=unused" + #"bit 3=unused" + "bit 4=TRUNC" + "bit 5=reserved(CEXEC)" # close-on-exec + "bit 6=RCLOSE" # remove-on-close + #"bit 7=unused" + + "num(MODE) READ = 0" # make available for this FID: Tread() + "num(MODE) WRITE = 1" # make available for this FID: Twrite() + "num(MODE) RDWR = 2" # make available for this FID: Tread() and Twrite() + "num(MODE) EXEC = 3" # make available for this FID: Tread() - "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() + "mask FLAG = 0b11111100" - "MODE_MASK = 0b00000011" - "FLAG_MASK = 0b11111100" +# A 9P2000 session goes: +# +# version() +# [auth_fid=auth] +# attach([auth_fid]) +# ... 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]" @@ -136,10 +146,10 @@ 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 Tread = "size[4,val=end-&size] typ[1,val=116] tag[tag] fid[fid] offset[8,max=s64_max] count[4,max=s32_max]" # See 4e `sys/src/lib9p/srv.c:sread()` for `offset:max` and `count:max`. +msg Rread = "size[4,val=end-&size] typ[1,val=117] tag[tag] count[4,max=s32_max] count*(data[1])" # `max` is inherited from Tread, 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,max=s64_max] count[4,max=s32_max] count*(data[1])" # See 4e `sys/src/lib9p/srv.c:swrite()` for `offset:max` and `count:max`. +msg Rwrite = "size[4,val=end-&size] typ[1,val=119] tag[tag] count[4,max=s32_max]" # `max` is inherited from Twrite. 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]" diff --git a/lib9p/idl/2003-9P2000.p9p.9p b/lib9p/idl/2003-9P2000.p9p.9p new file mode 100644 index 0000000..3f6a524 --- /dev/null +++ b/lib9p/idl/2003-9P2000.p9p.9p @@ -0,0 +1,49 @@ +# lib9p/idl/2003-9P2000.p9p.9p - Definitions of plan9port extension messages +# +# Copyright (C) 2025 Luke T. Shumaker <lukeshu@lukeshu.com> +# SPDX-License-Identifier: AGPL-3.0-or-later + +# Plan 9 from User Space (a.k.a. plan9port)'s lib9pclient:fsopenfd(3) +# and 9pserve(4) proxy server (which takes the place of the Plan 9 +# kernel) add a special-purpose `openfd` command to 9P2000. +# +# https://9fans.github.io/plan9port/man/man9/openfd.htlm +# https://github.com/9fans/plan9port/blob/master/man/man9/openfd.9p +# https://github.com/9fans/plan9port/commit/32f69c36e0eec1227934bbd34854bfebd88686f2 +# https://github.com/9fans/plan9port/pull/692 + +# BUG: There is no version-string for this extension; plan9port still +# calls it vanilla "9P2000". +version "9P2000.p9p" + +from ./2002-9P2000.9p import * + +# On Plan 9 the usual 9P client is the kernel, not normal userspace +# programs. The kernel multiplexes multiple userspace processes onto +# a single 9P connection; the userspace programs make 9P calls by +# making syscalls. plan9port emulates this by having mock syscalls +# that make 9P client calls over an AF_UNIX socket to a local +# `9pserve` daemon that does the multiplexing (you add an "fs" prefix; +# e.g. you replace syscall:`open()` with lib9pclient:`fsopen()`). +# +# "Unfortunately", programs in plan9port must deal both with 9P files +# and native "Unix" files; and need to turn an 9P FID into a native +# file descriptor. To do this, the `9pserve` program and lib9pclient +# add an extension call to 9P2000: Topenfd/Ropenfd/fsopenfd(). +# +# An AF_UNIX socket has the ability to send a file descriptor over it +# via an out-of-band "socket control message" ("CMSG" or "SCM"). +# +# Topenfd asks 9pserve to create a socketpair() file descriptor that +# 9pserve will pump to/from a FID, and then send that pipe file +# descriptor over a control-message to the client program. +# +# When replying, the server sends not just an in-band Ropenfd message, +# but also an out-of-band control-message with a socketpair() file +# descriptor. A successful call results in the FID being clunked. +msg Topenfd = "size[4,val=end-&size] typ[1,val=98] tag[tag] fid[fid] mode[o]" +msg Ropenfd = "size[4,val=end-&size] typ[1,val=99] tag[tag] qid[qid] iounit[4] unixfd[4]" +# BUG: The "unixfd" field nominally indicates the the file descriptor +# of the pipe, but really 9pserve doesn't know which FD it will end up +# on the client process, and lib9pclient ignores the value here and +# overwrites it with the file descriptor indicated from the CMSG diff --git a/lib9p/idl/2005-9P2000.u.9p b/lib9p/idl/2005-9P2000.u.9p index 8b59efa..6c2f2dc 100644 --- a/lib9p/idl/2005-9P2000.u.9p +++ b/lib9p/idl/2005-9P2000.u.9p @@ -1,6 +1,6 @@ -# 2005-9P2000.u.9p - Definitions of 9P2000.u messages +# lib9p/idl/2005-9P2000.u.9p - Definitions of 9P2000.u messages # -# Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com> +# Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com> # SPDX-License-Identifier: AGPL-3.0-or-later # "9P2000.u" Unix extension @@ -10,20 +10,27 @@ version "9P2000.u" from ./2002-9P2000.9p import * +# numeric user ID +num nuid = 4 + "NONUID = u32_max" + +num errno = 4 + "NOERROR = 0" + struct stat += "file_extension[s]" - "file_owner_n_uid[4]" - "file_owner_n_gid[4]" - "file_last_modified_n_uid[4]" + "file_owner_n_uid[nuid]" + "file_owner_n_gid[nuid]" + "file_last_modified_n_uid[nuid]" -msg Tauth += "n_uname[4]" -msg Tattach += "n_uname[4]" +msg Tauth += "n_uid[nuid]" +msg Tattach += "n_uid[nuid]" -msg Rerror += "errno[4]" +msg Rerror += "errno[errno]" -bitfield dm += "23=DEVICE" - "21=NAMEDPIPE" - "20=SOCKET" - "19=SETUID" - "18=SETGID" +bitfield dm += "bit 23=DEVICE" + "bit 21=PIPE" + "bit 20=SOCKET" + "bit 19=SETUID" + "bit 18=SETGID" -bitfield qt += "1=SYMLINK" +bitfield qt += "bit 1=SYMLINK" diff --git a/lib9p/idl/2010-9P2000.L.9p b/lib9p/idl/2010-9P2000.L.9p new file mode 100644 index 0000000..d81a15b --- /dev/null +++ b/lib9p/idl/2010-9P2000.L.9p @@ -0,0 +1,230 @@ +# lib9p/idl/2010-9P2000.L.9p - Definitions of 9P2000.L messages +# +# Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com> +# SPDX-License-Identifier: AGPL-3.0-or-later + +# "9P2000.L" Linux extension +# https://github.com/chaos/diod/blob/master/protocol.md +# https://github.com/chaos/diod/blob/master/src/libnpfs/protocol.h +version "9P2000.L" + +from ./2002-9P2000.9p import tag, fid, s, qt, qid +from ./2002-9P2000.9p import Rerror +from ./2002-9P2000.9p import Tversion, Rversion, Tflush, Rflush, Twalk, Rwalk, Tread, Rread, Twrite, Rwrite, Tclunk, Rclunk, Tremove, Rremove +from ./2005-9P2000.u.9p import nuid, errno, Tauth, Rauth, Tattach, Rattach + +#num errno += # TODO + +num super_magic = 4 + # See <linux/magic.h> (linux.git include/uapi/linux/magic.h). + # + # To quote `util-linux.git:include/statfs_magic.h`: + # "Unfortunately, Linux kernel header file <linux/magic.h> is + # incomplete mess and kernel returns by statfs f_type many numbers + # that are nowhere specified (in API)." + # + # util-linux <statfs_magic.h> is also incomplete. As is the + # statfs(2) man-page. + # + # I'm working on a patchset to the kernel to get <linux/magic.h> + # to be complete, but in the mean-time I'm just not going to + # bother with putting a list here. + # + # TODO + "V9FS_MAGIC=0x01021997" + +# "L"inux "O"pen flags (flags to pass to Tlopen and Tlcreate) +# +# The values are not specified in in protocol.md, but are specified in +# protocol.h (and are different than the Linux kernel's values, which +# vary by architecture). +bitfield lo = 4 + "bit 0=num(MODE)" # low bit of the 2-bit RDONLY/WRONLY/RDWR/NOACCESS enum + "bit 1=num(MODE)" # high bit of the 2-bit RDONLY/WRONLY/RDWR/NOACCESS enum + #"bit 2=unused" + #"bit 3=unused" + #"bit 4=unused" + #"bit 5=unused" + "bit 6=CREATE" + "bit 7=EXCL" + "bit 8=NOCTTY" + "bit 9=TRUNC" + "bit 10=APPEND" + "bit 11=NONBLOCK" + "bit 12=DSYNC" + "bit 13=BSD_FASYNC" + "bit 14=DIRECT" + "bit 15=LARGEFILE" + "bit 16=DIRECTORY" + "bit 17=NOFOLLOW" + "bit 18=NOATIME" + "bit 19=CLOEXEC" + "bit 20=SYNC" + + "num(MODE) RDONLY = 0" + "num(MODE) WRONLY = 1" + "num(MODE) RDWR = 2" + "num(MODE) NOACCESS = 3" + + "mask FLAG = 0b111111111111111000000" + +# "D"irentry "T"ype +# +# These match the Linux kernel's values. +num dt = 1 + "UNKNOWN = 0" + "PIPE = 1" + "CHAR_DEV = 2" + "DIRECTORY = 4" + "BLOCK_DEV = 6" # proof it's not a bitfield + "REGULAR = 8" + "SYMLINK = 10" # proof it's not a bitfield + "SOCKET = 12" # proof it's not a bitfield + "_WHITEOUT = 14" # proof it's not a bitfield + +# Mode +# +# These match the Linux kernel's values. Why is this 32-bits wide +# instead of just 16? Who knows? +bitfield mode = 4 + #... + "bit 15=num(FMT)" # bit of the 4-bit FMT_ enum + "bit 14=num(FMT)" # bit of the 4-bit FMT_ enum + "bit 13=num(FMT)" # bit of the 4-bit FMT_ enum + "bit 12=num(FMT)" # bit of the 4-bit FMT_ enum + #... + "bit 11=PERM_SETGROUP" + "bit 10=PERM_SETUSER" + "bit 9=PERM_STICKY" + "bit 8=PERM_OWNER_R" + "bit 7=PERM_OWNER_W" + "bit 6=PERM_OWNER_X" + "bit 5=PERM_GROUP_R" + "bit 4=PERM_GROUP_W" + "bit 3=PERM_GROUP_X" + "bit 2=PERM_OTHER_R" + "bit 1=PERM_OTHER_W" + "bit 0=PERM_OTHER_X" + + "num(FMT) PIPE = dt.PIPE<<12" + "num(FMT) CHAR_DEV = dt.CHAR_DEV<<12" + "num(FMT) DIRECTORY = dt.DIRECTORY<<12" + "num(FMT) BLOCK_DEV = dt.BLOCK_DEV<<12" + "num(FMT) REGULAR = dt.REGULAR<<12" + "num(FMT) SYMLINK = dt.SYMLINK<<12" + "num(FMT) SOCKET = dt.SOCKET<<12" + + "mask PERM = 07777" # PERM_* + +# A boolean value that is for some reason 4 bytes wide. +num b4 = 4 + "FALSE=0" + "TRUE=1" + # all other values are true also + +bitfield getattr = 8 + "bit 0=MODE" + "bit 1=NLINK" + "bit 2=UID" + "bit 3=GID" + "bit 4=RDEV" + "bit 5=ATIME" + "bit 6=MTIME" + "bit 7=CTIME" + "bit 8=INO" + "bit 9=SIZE" + "bit 10=BLOCKS" + + "bit 11=BTIME" + "bit 12=GEN" + "bit 13=DATA_VERSION" + + "alias BASIC=0x000007ff" # Mask for fields up to BLOCKS + "alias ALL =0x00003fff" # Mask for All fields above + +bitfield setattr = 4 + "bit 0=MODE" + "bit 1=UID" + "bit 2=GID" + "bit 3=SIZE" + "bit 4=ATIME" + "bit 5=MTIME" + "bit 6=CTIME" + "bit 7=ATIME_SET" + "bit 8=MTIME_SET" + +num lock_type = 1 + "RDLCK=0" + "WRLCK=1" + "UNLCK=2" + +bitfield lock_flags = 4 + "bit 0=BLOCK" + "bit 1=RECLAIM" + +num lock_status = 1 + "SUCCESS=0" + "BLOCKED=1" + "ERROR=2" + "GRACE=3" + +#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[errno]" # analogous to 107/Rerror +msg Tstatfs = "size[4,val=end-&size] typ[1,val=8] tag[tag] fid[fid]" +msg Rstatfs = "size[4,val=end-&size] typ[1,val=9] tag[tag]" # Description | statfs | statvfs + "type[super_magic]" # Type of filesystem | f_type | - + "bsize[4]" # Block size in bytes | f_bsize | f_bsize + # - # Fragment size in bytes | f_frsize (since Linux 2.6) | f_frsize + "blocks[8]" # Size of FS in f_frsize units | f_blocks | f_blocks + "bfree[8]" # Number of free blocks | f_bfree | f_bfree + "bavail[8]" # Number of free blocks for unprivileged users | f_bavail | b_avail + "files[8]" # Number of inodes | f_files | f_files + "ffree[8]" # Number of free inodes | f_ffree | f_ffree + # - # Number of free inodes for unprivileged users | - | f_favail + "fsid[8]" # Filesystem instance ID | f_fsid | f_fsid + # - # Mount flags | f_flags (since Linux 2.6.36) | f_flag + "namelen[4]" # Maximum filename length | f_namemax | f_namemax +msg Tlopen = "size[4,val=end-&size] typ[1,val=12] tag[tag] fid[fid] flags[lo]" # analogous to 112/Topen +msg Rlopen = "size[4,val=end-&size] typ[1,val=13] tag[tag] qid[qid] iounit[4]" # analogous to 113/Ropen +msg Tlcreate = "size[4,val=end-&size] typ[1,val=14] tag[tag] fid[fid] name[s] flags[lo] mode[mode] gid[nuid]" # analogous to 114/Tcreate +msg Rlcreate = "size[4,val=end-&size] typ[1,val=15] tag[tag] qid[qid] iounit[4]" # analogous to 115/Rcreate +msg Tsymlink = "size[4,val=end-&size] typ[1,val=16] tag[tag] fid[fid] name[s] symtgt[s] gid[nuid]" +msg Rsymlink = "size[4,val=end-&size] typ[1,val=17] tag[tag] qid[qid]" +msg Tmknod = "size[4,val=end-&size] typ[1,val=18] tag[tag] dfid[fid] name[s] mode[mode] major[4] minor[4] gid[nuid]" +msg Rmknod = "size[4,val=end-&size] typ[1,val=19] tag[tag] qid[qid]" +msg Trename = "size[4,val=end-&size] typ[1,val=20] tag[tag] fid[fid] dfid[fid] name[s]" +msg Rrename = "size[4,val=end-&size] typ[1,val=21] tag[tag]" +msg Treadlink = "size[4,val=end-&size] typ[1,val=22] tag[tag] fid[fid]" +msg Rreadlink = "size[4,val=end-&size] typ[1,val=23] tag[tag] target[s]" +msg Tgetattr = "size[4,val=end-&size] typ[1,val=24] tag[tag] fid[fid] request_mask[getattr]" +msg Rgetattr = "size[4,val=end-&size] typ[1,val=25] tag[tag] valid[getattr] qid[qid] mode[mode] uid[nuid] gid[nuid] nlink[8]" + "rdev[8] filesize[8] blksize[8] blocks[8]" + "atime_sec[8] atime_nsec[8] mtime_sec[8] mtime_nsec[8]" + "ctime_sec[8] ctime_nsec[8] btime_sec[8] btime_nsec[8]" + "gen[8] data_version[8]" +msg Tsetattr = "size[4,val=end-&size] typ[1,val=26] tag[tag] fid[fid] valid[setattr] mode[mode] uid[nuid] gid[nuid] filesize[8] atime_sec[8] atime_nsec[8] mtime_sec[8] mtime_nsec[8]" +msg Rsetattr = "size[4,val=end-&size] typ[1,val=27] tag[tag]" +#... +msg Txattrwalk = "size[4,val=end-&size] typ[1,val=30] tag[tag] fid[fid] newfid[fid] name[s]" +msg Rxattrwalk = "size[4,val=end-&size] typ[1,val=31] tag[tag] attr_size[8]" +msg Txattrcreate = "size[4,val=end-&size] typ[1,val=32] tag[tag] fid[fid] name[s] attr_size[8] flags[4]" +msg Rxattrcreate = "size[4,val=end-&size] typ[1,val=33] tag[tag]" +#... +msg Treaddir = "size[4,val=end-&size] typ[1,val=40] tag[tag] fid[fid] offset[8] count[4]" +msg Rreaddir = "size[4,val=end-&size] typ[1,val=41] tag[tag] count[4] count*(data[1])" # data is "qid[qid] offset[8] type[dt] name[s]" +#... +msg Tfsync = "size[4,val=end-&size] typ[1,val=50] tag[tag] fid[fid] datasync[b4]" +msg Rfsync = "size[4,val=end-&size] typ[1,val=51] tag[tag]" +msg Tlock = "size[4,val=end-&size] typ[1,val=52] tag[tag] fid[fid] type[lock_type] flags[lock_flags] start[8] length[8] proc_id[4] client_id[s]" +msg Rlock = "size[4,val=end-&size] typ[1,val=53] tag[tag] status[lock_status]" +msg Tgetlock = "size[4,val=end-&size] typ[1,val=54] tag[tag] fid[fid] type[lock_type] start[8] length[8] proc_id[4] client_id[s]" +msg Rgetlock = "size[4,val=end-&size] typ[1,val=55] tag[tag] type[lock_type] start[8] length[8] proc_id[4] client_id[s]" +# ... +msg Tlink = "size[4,val=end-&size] typ[1,val=70] tag[tag] dfid[fid] fid[fid] name[s]" +msg Rlink = "size[4,val=end-&size] typ[1,val=71] tag[tag]" +msg Tmkdir = "size[4,val=end-&size] typ[1,val=72] tag[tag] dfid[fid] name[s] mode[mode] gid[nuid]" +msg Rmkdir = "size[4,val=end-&size] typ[1,val=73] tag[tag] qid[qid]" +msg Trenameat = "size[4,val=end-&size] typ[1,val=74] tag[tag] olddirfid[fid] oldname[s] newdirfid[fid] newname[s]" +msg Rrenameat = "size[4,val=end-&size] typ[1,val=75] tag[tag]" +msg Tunlinkat = "size[4,val=end-&size] typ[1,val=76] tag[tag] dirfd[fid] name[s] flags[4]" +msg Runlinkat = "size[4,val=end-&size] typ[1,val=77] tag[tag]" diff --git a/lib9p/idl/2010-9P2000.L.9p.wip b/lib9p/idl/2010-9P2000.L.9p.wip deleted file mode 100644 index 5261f7e..0000000 --- a/lib9p/idl/2010-9P2000.L.9p.wip +++ /dev/null @@ -1,56 +0,0 @@ -# 2010-9P2000.L.9p - Definitions of 9P2000.L messages -# -# Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com> -# SPDX-License-Identifier: AGPL-3.0-or-later - -# "9P2000.L" Linux extension -# https://github.com/chaos/diod/blob/master/protocol.md -version "9P2000.L" - -from ./2002-9P2000.9p import * -from ./2005-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" diff --git a/lib9p/idl/2012-9P2000.e.9p b/lib9p/idl/2012-9P2000.e.9p index 27db50f..dde9d96 100644 --- a/lib9p/idl/2012-9P2000.e.9p +++ b/lib9p/idl/2012-9P2000.e.9p @@ -1,6 +1,6 @@ -# 2012-9P2000.e.9p - Definitions of 9P2000.e messages +# lib9p/idl/2012-9P2000.e.9p - Definitions of 9P2000.e messages # -# Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com> +# Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com> # SPDX-License-Identifier: AGPL-3.0-or-later # "9P2000.e" Erlang extension @@ -13,6 +13,6 @@ from ./2002-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 Rsread = "size[4,val=end-&size] typ[1,val=153] tag[tag] count[4] count*(data[1])" +msg Tswrite = "size[4,val=end-&size] typ[1,val=154] tag[tag] fid[4] nwname[2] nwname*(wname[s]) count[4] count*(data[1])" msg Rswrite = "size[4,val=end-&size] typ[1,val=155] tag[tag] count[4]" diff --git a/lib9p/idl/__init__.py b/lib9p/idl/__init__.py new file mode 100644 index 0000000..2d09217 --- /dev/null +++ b/lib9p/idl/__init__.py @@ -0,0 +1,878 @@ +# lib9p/idl/__init__.py - A parser for .9p specification files. +# +# Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com> +# SPDX-License-Identifier: AGPL-3.0-or-later + +import enum +import os.path +import re +import typing + +# pylint: disable=unused-variable +__all__ = [ + # entrypoint + "Parser", + # types + "Type", + "Primitive", + *["Expr", "ExprTok", "ExprOp", "ExprLit", "ExprSym", "ExprOff", "ExprNum"], + "Number", + *["Bitfield", "Bit", "BitCat", "BitNum", "BitAlias"], + *["Struct", "StructMember"], + "Message", +] + +# The syntax that this parses is described in `./0000-README.md`. + +# Utilities #################################################################### + + +def get_type(env: dict[str, "Type"], name: str, tc: type["T"]) -> "T": + if name not in env: + raise NameError(f"Unknown type {name!r}") + ret = env[name] + if (not isinstance(ret, tc)) or (ret.__class__.__name__ != tc.__name__): + raise NameError(f"Type {ret.typname!r} is not a {tc.__name__}") + return ret + + +# Types ######################################################################## + + +class Primitive(enum.Enum): + u8 = 1 + u16 = 2 + u32 = 4 + u64 = 8 + + @property + def in_versions(self) -> set[str]: + return set() + + @property + def typname(self) -> str: + return str(self.value) + + @property + def static_size(self) -> int: + return self.value + + def min_size(self, version: str) -> int: + return self.value + + def max_size(self, version: str) -> int: + return self.value + + +class ExprOp: + op: typing.Literal["-", "+", "<<"] + + def __init__(self, op: typing.Literal["-", "+", "<<"]) -> None: + self.op = op + + +class ExprLit: + val: int + + def __init__(self, val: int) -> None: + self.val = val + + +class ExprSym: + symname: str + + def __init__(self, name: str) -> None: + self.symname = name + + +class ExprOff: + membname: str + + def __init__(self, name: str) -> None: + self.membname = name + + +class ExprNum: + numname: str + valname: str + + def __init__(self, numname: str, valname: str) -> None: + self.numname = numname + self.valname = valname + + +type ExprTok = ExprOp | ExprLit | ExprSym | ExprOff | ExprNum + + +class Expr: + tokens: typing.Sequence[ExprTok] + const: int | None + + def __init__( + self, env: dict[str, "Type"], tokens: typing.Sequence[ExprTok] = () + ) -> None: + self.tokens = tokens + self.const = self._const(env, tokens) + + def _const( + self, env: dict[str, "Type"], toks: typing.Sequence[ExprTok] + ) -> int | None: + if not toks: + return None + + def read_val() -> int | None: + nonlocal toks + assert toks + neg = False + match toks[0]: + case ExprOp(op="-"): + neg = True + toks = toks[1:] + assert not isinstance(toks[0], ExprOp) + val: int + match toks[0]: + case ExprLit(): + val = toks[0].val + case ExprSym(): + if m := re.fullmatch(r"^u(8|16|32|64)_max$", toks[0].symname): + n = int(m.group(1)) + val = (1 << n) - 1 + elif m := re.fullmatch(r"^s(8|16|32|64)_max$", toks[0].symname): + n = int(m.group(1)) + val = (1 << (n - 1)) - 1 + else: + return None + case ExprOff(): + return None + case ExprNum(): + num = get_type(env, toks[0].numname, Number) + if toks[0].valname not in num.vals: + raise NameError( + f"Type {toks[0].numname!r} does not have a value {toks[0].valname!r}" + ) + _val = num.vals[toks[0].valname].const + if _val is None: + return None + val = _val + toks = toks[1:] + return -val if neg else val + + ret = read_val() + if ret is None: + return None + while toks: + assert isinstance(toks[0], ExprOp) + op = toks[0].op + toks = toks[1:] + operand = read_val() + if operand is None: + return None + match op: + case "+": + ret = ret + operand + case "-": + ret = ret - operand + case "<<": + ret = ret << operand + return ret + + def __bool__(self) -> bool: + return len(self.tokens) > 0 + + +class Number: + typname: str + in_versions: set[str] + + prim: Primitive + + vals: dict[str, Expr] + + def __init__(self) -> None: + self.in_versions = set() + self.vals = {} + + @property + def static_size(self) -> int: + return self.prim.static_size + + def min_size(self, version: str) -> int: + return self.static_size + + def max_size(self, version: str) -> int: + return self.static_size + + +class BitAlias: + bitname: str + in_versions: set[str] + val: Expr + + def __init__(self, name: str, val: Expr) -> None: + if val.const is None: + raise ValueError(f"{name!r} value is not constant") + self.bitname = name + self.in_versions = set() + self.val = val + + +class BitNum: + numname: str + mask: int + vals: dict[str, BitAlias] + + def __init__(self, name: str) -> None: + self.numname = name + self.mask = 0 + self.vals = {} + + +type BitCat = typing.Literal["UNUSED", "USED", "RESERVED"] | BitNum + + +class Bit: + bitname: str + in_versions: set[str] + num: int + cat: BitCat + + def __init__(self, num: int) -> None: + self.bitname = "" + self.in_versions = set() + self.num = num + self.cat = "UNUSED" + + +class Bitfield: + typname: str + in_versions: set[str] + prim: Primitive + + bits: list[Bit] + nums: dict[str, BitNum] + masks: dict[str, BitAlias] + aliases: dict[str, BitAlias] + + names: set[str] + + def __init__(self, name: str, prim: Primitive) -> None: + self.typname = name + self.in_versions = set() + self.prim = prim + + self.bits = [Bit(i) for i in range(prim.static_size * 8)] + self.nums = {} + self.masks = {} + self.aliases = {} + + self.names = set() + + @property + def static_size(self) -> int: + return self.prim.static_size + + def min_size(self, version: str) -> int: + return self.static_size + + def max_size(self, version: str) -> int: + return self.static_size + + +class StructMember: + # from left-to-right when parsing + cnt: "StructMember| int | None" = None + membname: str + typ: "Type" + max: Expr + val: Expr + + in_versions: set[str] + + @property + def min_cnt(self) -> int: + assert self.cnt + if isinstance(self.cnt, int): + return self.cnt + if not isinstance(self.cnt.typ, Primitive): + raise ValueError( + f"list count must be an integer type: {self.cnt.membname!r}" + ) + if self.cnt.val: # TODO: allow this? + raise ValueError(f"list count may not have ,val=: {self.cnt.membname!r}") + return 0 + + @property + def max_cnt(self) -> int: + assert self.cnt + if isinstance(self.cnt, int): + return self.cnt + if not isinstance(self.cnt.typ, Primitive): + raise ValueError( + f"list count must be an integer type: {self.cnt.membname!r}" + ) + if self.cnt.val: # TODO: allow this? + raise ValueError(f"list count may not have ,val=: {self.cnt.membname!r}") + if self.cnt.max: + # TODO: be more flexible? + val = self.cnt.max.const + if val is None: + raise ValueError( + f"list count ,max= must be a constant value: {self.cnt.membname!r}" + ) + return val + return (1 << (self.cnt.typ.value * 8)) - 1 + + @property + def static_size(self) -> int | None: + if self.cnt: + return None + return self.typ.static_size + + def min_size(self, version: str) -> int: + cnt = self.min_cnt if self.cnt else 1 + return cnt * self.typ.min_size(version) + + def max_size(self, version: str) -> int: + cnt = self.max_cnt if self.cnt else 1 + return cnt * self.typ.max_size(version) + + +class Struct: + typname: 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: + if member.in_versions < self.in_versions: + return None + msize = member.static_size + if msize is None: + return None + size += msize + return size + + def min_size(self, version: str) -> int: + return sum( + member.min_size(version) + for member in self.members + if (version in member.in_versions) + ) + + def max_size(self, version: str) -> int: + return sum( + member.max_size(version) + for member in self.members + if (version in member.in_versions) + ) + + +class Message(Struct): + @property + def msgid(self) -> int: + assert len(self.members) >= 3 + assert self.members[1].membname == "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 + + +type Type = Primitive | Number | Bitfield | Struct | Message +type UserType = Number | Bitfield | Struct | Message +T = typing.TypeVar("T", Number, Bitfield, Struct, Message) + +# Parse ######################################################################## + +# common elements ###################### + +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_symname_u = "(?:[A-Z_][A-Z_0-9]*)" # upper-case "symbol" names; bit names +re_symname_l = "(?:[a-z_][a-z_0-9]*)" # lower-case "symbol" names; bit 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 + +valid_syms = [ + "end", + "u8_max", + "u16_max", + "u32_max", + "u64_max", + "s8_max", + "s16_max", + "s32_max", + "s64_max", +] + +_re_expr_op = r"(?:-|\+|<<)" + +_res_expr_val = { + "lit_2": r"0b[01]+", + "lit_8": r"0[0-7]+", + "lit_10": r"0(?![0-9bxX])|[1-9][0-9]*", + "lit_16": r"0[xX][0-9a-fA-F]+", + "sym": "|".join(valid_syms), # pre-defined symbols + "off": f"&{re_symname}", # offset of a field this struct + "num": f"{re_symname}\\.{re_symname}", # `num` values +} + +re_expr_tok = ( + "(?:" + + "|".join( + [ + f"(?P<op>{_re_expr_op})", + *[f"(?P<{k}>{v})" for k, v in _res_expr_val.items()], + ] + ) + + ")" +) + +_re_expr_val = "(?:" + "|".join(_res_expr_val.values()) + ")" + +re_expr = f"(?:\\s*(?:-\\s*)?{_re_expr_val}\\s*(?:{_re_expr_op}\\s*(?:-\\s*)?{_re_expr_val}\\s*)*)" + + +def parse_expr(env: dict[str, Type], expr: str) -> Expr: + assert re.fullmatch(re_expr, expr) + tokens: list[ExprTok] = [] + for m in re.finditer(re_expr_tok, expr): + if tok := m.group("op"): + tokens.append(ExprOp(typing.cast(typing.Literal["-", "+", "<<"], tok))) + elif tok := m.group("lit_2"): + tokens.append(ExprLit(int(tok[2:], 2))) + elif tok := m.group("lit_8"): + tokens.append(ExprLit(int(tok[1:], 8))) + elif tok := m.group("lit_10"): + tokens.append(ExprLit(int(tok, 10))) + elif tok := m.group("lit_16"): + tokens.append(ExprLit(int(tok[2:], 16))) + elif tok := m.group("sym"): + tokens.append(ExprSym(tok)) + elif tok := m.group("off"): + tokens.append(ExprOff(tok[1:])) + elif tok := m.group("num"): + [numname, valname] = tok.split(".", 1) + tokens.append(ExprNum(numname, valname)) + else: + assert False + return Expr(env, tokens) + + +# numspec ############################## + +re_numspec = f"(?P<name>{re_symname})\\s*=\\s*(?P<val>{re_expr})" + + +def parse_numspec(env: dict[str, Type], ver: str, n: Number, spec: str) -> None: + spec = spec.strip() + + if m := re.fullmatch(re_numspec, spec): + name = m.group("name") + if name in n.vals: + raise ValueError(f"{n.typname}: name {name!r} already assigned") + val = parse_expr(env, m.group("val")) + if val is None: + raise ValueError( + f"{n.typname}: {name!r} value is not constant: {m.group('val')!r}" + ) + n.vals[name] = val + else: + raise SyntaxError(f"invalid num spec {spec!r}") + + +# bitspec ############################## + +re_bitspec_bit = ( + "bit\\s+(?P<bitnum>[0-9]+)\\s*=\\s*(?:" + + "|".join( + [ + f"(?P<name_used>{re_symname_u})", + f"reserved\\((?P<name_reserved>{re_symname_u})\\)", + f"num\\((?P<name_num>{re_symname_u})\\)", + ] + ) + + ")" +) +re_bitspec_mask = f"mask\\s+(?P<name>{re_symname_u})\\s*=\\s*(?P<val>{re_expr})" +re_bitspec_alias = f"alias\\s+(?P<name>{re_symname_u})\\s*=\\s*(?P<val>{re_expr})" +re_bitspec_num = f"num\\((?P<num>{re_symname_u})\\)\\s+(?P<name>{re_symname_u})\\s*=\\s*(?P<val>{re_expr})" + + +def parse_bitspec(env: dict[str, Type], ver: str, bf: Bitfield, spec: str) -> None: + spec = spec.strip() + + def check_name(name: str, is_num: bool = False) -> None: + if name == "MASK": + raise ValueError(f"{bf.typname}: bit name may not be {'MASK'!r}: {name!r}") + if name.endswith("_MASK"): + raise ValueError( + f"{bf.typname}: bit name may not end with {'_MASK'!r}: {name!r}" + ) + if name in bf.names and not (is_num and name in bf.nums): + raise ValueError(f"{bf.typname}: bit name already assigned: {name!r}") + + if m := re.fullmatch(re_bitspec_bit, spec): + bitnum = int(m.group("bitnum")) + if bitnum < 0 or bitnum >= len(bf.bits): + raise ValueError(f"{bf.typname}: bit num {bitnum} out-of-bounds") + bit = bf.bits[bitnum] + if bit.cat != "UNUSED": + raise ValueError(f"{bf.typname}: bit num {bitnum} already assigned") + if name := m.group("name_used"): + bit.bitname = name + bit.cat = "USED" + bit.in_versions.add(ver) + elif name := m.group("name_reserved"): + bit.bitname = name + bit.cat = "RESERVED" + bit.in_versions.add(ver) + elif name := m.group("name_num"): + bit.bitname = name + if name not in bf.nums: + bf.nums[name] = BitNum(name) + bf.nums[name].mask |= 1 << bit.num + bit.cat = bf.nums[name] + bit.in_versions.add(ver) + if bit.bitname: + check_name(name, isinstance(bit.cat, BitNum)) + bf.names.add(bit.bitname) + elif m := re.fullmatch(re_bitspec_mask, spec): + mask = BitAlias(m.group("name"), parse_expr(env, m.group("val"))) + mask.in_versions.add(ver) + check_name(mask.bitname) + bf.masks[mask.bitname] = mask + bf.names.add(mask.bitname) + elif m := re.fullmatch(re_bitspec_alias, spec): + alias = BitAlias(m.group("name"), parse_expr(env, m.group("val"))) + alias.in_versions.add(ver) + check_name(alias.bitname) + bf.aliases[alias.bitname] = alias + bf.names.add(alias.bitname) + elif m := re.fullmatch(re_bitspec_num, spec): + numname = m.group("num") + alias = BitAlias(m.group("name"), parse_expr(env, m.group("val"))) + alias.in_versions.add(ver) + check_name(alias.bitname) + if numname not in bf.nums: + raise NameError( + f"{bf.typname}: nested num not allocated any bits: {numname!r}" + ) + assert alias.val.const is not None + if alias.val.const & ~bf.nums[numname].mask: + raise ValueError( + f"{bf.typname}: {alias.bitname!r} does not fit within bitmask: val={alias.val.const:b} mask={bf.nums[numname].mask}" + ) + bf.nums[numname].vals[alias.bitname] = alias + bf.names.add(alias.bitname) + else: + raise SyntaxError(f"invalid bitfield spec {spec!r}") + + +# struct members ####################### + + +re_memberspec = f"(?:(?P<cnt>{re_symname}|[1-9][0-9]*)\\*\\()?(?P<name>{re_symname})\\[(?P<typ>{re_memtype})(?:,max=(?P<max>{re_expr})|,val=(?P<val>{re_expr}))*\\]\\)?" + + +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 {spec!r}") + + member = StructMember() + member.in_versions = {ver} + + member.membname = m.group("name") + if any(x.membname == member.membname for x in struct.members): + raise ValueError(f"duplicate member name {member.membname!r}") + + if m.group("typ") not in env: + raise NameError(f"Unknown type {m.group('typ')!r}") + member.typ = env[m.group("typ")] + + if cnt := m.group("cnt"): + if cnt.isnumeric(): + member.cnt = int(cnt) + else: + if len(struct.members) == 0 or struct.members[-1].membname != cnt: + raise ValueError(f"list count must be previous item: {cnt!r}") + member.cnt = struct.members[-1] + _ = member.max_cnt # force validation + + if maxstr := m.group("max"): + if ( + not isinstance(member.typ, Primitive) + and not isinstance(member.typ, Number) + ) or member.cnt: + raise ValueError( + "',max=' may only be specified on a non-repeated numeric type" + ) + member.max = parse_expr(env, maxstr) + else: + member.max = Expr(env) + + if valstr := m.group("val"): + if ( + not isinstance(member.typ, Primitive) + and not isinstance(member.typ, Number) + ) or member.cnt: + raise ValueError( + "',val=' may only be specified on a non-repeated numeric type" + ) + member.val = parse_expr(env, valstr) + else: + member.val = Expr(env) + + struct.members += [member] + + +# main parser ########################## + + +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: typing.Callable[[str], tuple[str, list[UserType]]] +) -> tuple[str, list[UserType]]: + version: str | None = None + env: dict[str, Type] = { + "1": Primitive.u8, + "2": Primitive.u16, + "4": Primitive.u32, + "8": Primitive.u64, + } + + with open(filename, "r", encoding="utf-8") as fh: + prev: Type | None = None + for lineno, line in enumerate(fh): + try: + 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() + found = False + for typ in other_typs: + if symname in (typ.typname, "*"): + found = True + match typ: + case Primitive(): + pass + case Number(): + typ.in_versions.add(version) + case Bitfield(): + typ.in_versions.add(version) + for bf_bit in typ.bits: + if other_version in bf_bit.in_versions: + bf_bit.in_versions.add(version) + for bf_num in typ.nums.values(): + for bf_val in bf_num.vals.values(): + if other_version in bf_val.in_versions: + bf_val.in_versions.add(version) + for bf_mask in typ.masks.values(): + if other_version in bf_mask.in_versions: + bf_mask.in_versions.add(version) + for bf_alias in typ.aliases.values(): + if other_version in bf_alias.in_versions: + bf_alias.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) + if typ.typname in env and env[typ.typname] != typ: + raise ValueError( + f"duplicate type name {typ.typname!r}" + ) + env[typ.typname] = typ + if symname != "*" and not found: + raise ValueError( + f"import: {m.group('file')}: no symbol {symname!r}" + ) + elif m := re.fullmatch(re_line_num, line): + num = Number() + num.typname = m.group("name") + num.in_versions.add(version) + + prim = env[m.group("prim")] + assert isinstance(prim, Primitive) + num.prim = prim + + if num.typname in env: + raise ValueError(f"duplicate type name {num.typname!r}") + env[num.typname] = num + prev = num + elif m := re.fullmatch(re_line_bitfield, line): + prim = env[m.group("prim")] + assert isinstance(prim, Primitive) + + bf = Bitfield(m.group("name"), prim) + bf.in_versions.add(version) + + if bf.typname in env: + raise ValueError(f"duplicate type name {bf.typname!r}") + env[bf.typname] = bf + prev = bf + elif m := re.fullmatch(re_line_bitfield_, line): + bf = get_type(env, m.group("name"), Bitfield) + parse_bitspec(env, version, bf, m.group("member")) + + prev = bf + elif m := re.fullmatch(re_line_struct, line): + match m.group("op"): + case "=": + struct = Struct() + struct.typname = m.group("name") + struct.in_versions.add(version) + struct.members = [] + parse_members(version, env, struct, m.group("members")) + + if struct.typname in env: + raise ValueError( + f"duplicate type name {struct.typname!r}" + ) + env[struct.typname] = struct + prev = struct + case "+=": + struct = get_type(env, 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.typname = m.group("name") + msg.in_versions.add(version) + msg.members = [] + parse_members(version, env, msg, m.group("members")) + + if msg.typname in env: + raise ValueError(f"duplicate type name {msg.typname!r}") + env[msg.typname] = msg + prev = msg + case "+=": + msg = get_type(env, 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(env, version, prev, m.group("specs")) + case Number(): + parse_numspec(env, 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("invalid line") + except (SyntaxError, NameError, ValueError) as e: + e2 = SyntaxError(str(e)) + e2.filename = filename + e2.lineno = lineno + 1 + e2.text = line + raise e2 from e + if not version: + raise SyntaxError("must have exactly 1 version line") + + typs: list[UserType] = [x for x in env.values() if not isinstance(x, Primitive)] + + for typ in [typ for typ in typs if isinstance(typ, Struct)]: + for member in typ.members: + if ( + not isinstance(member.typ, Primitive) + and member.typ.in_versions < member.in_versions + ): + raise ValueError( + f"{typ.typname}.{member.membname}: type {member.typ.typname} does not exist in {member.in_versions.difference(member.typ.in_versions)}" + ) + for tok in [*member.max.tokens, *member.val.tokens]: + if isinstance(tok, ExprOff) and not any( + m.membname == tok.membname for m in typ.members + ): + raise NameError( + f"{typ.typname}.{member.membname}: invalid offset: &{tok.membname}" + ) + + return version, typs + + +# Filesystem ################################################################### + + +class Parser: + cache: dict[str, tuple[str, list[UserType]]] = {} + + def parse_file(self, filename: str) -> tuple[str, list[UserType]]: + filename = os.path.normpath(filename) + if filename not in self.cache: + + def get_include(other_filename: str) -> tuple[str, list[UserType]]: + 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[UserType]]: + ret_versions: set[str] = set() + ret_typs: dict[str, UserType] = {} + for version, typs in self.cache.values(): + if version in ret_versions: + raise ValueError(f"duplicate protocol version {version!r}") + ret_versions.add(version) + for typ in typs: + if typ.typname in ret_typs: + if typ != ret_typs[typ.typname]: + raise ValueError(f"duplicate type name {typ.typname!r}") + else: + ret_typs[typ.typname] = typ + msgids: set[int] = set() + for typ in ret_typs.values(): + if isinstance(typ, Message): + if typ.msgid in msgids: + raise ValueError(f"duplicate msgid {typ.msgid!r}") + msgids.add(typ.msgid) + return ret_versions, list(ret_typs.values()) |