summaryrefslogtreecommitdiff
path: root/lib9p/idl
diff options
context:
space:
mode:
authorLuke T. Shumaker <lukeshu@lukeshu.com>2025-02-03 21:17:57 -0700
committerLuke T. Shumaker <lukeshu@lukeshu.com>2025-02-03 21:17:57 -0700
commit2511735ccc4d8bffa66e91d6395957ed0e1e6ee0 (patch)
tree739399d3cd759e6a69231f02f242f88bf04efc15 /lib9p/idl
parent1e2b67047da4ad0f567ef5956f813b2d33b3cfa6 (diff)
parent1529d75b21b3e719e15988fb16abc2e02d5ddcb3 (diff)
Merge branch 'lukeshu/9p-idl-defs'
Diffstat (limited to 'lib9p/idl')
-rw-r--r--lib9p/idl/0000-TODO.md5
-rw-r--r--lib9p/idl/1992-9P0.9p.wip125
-rw-r--r--lib9p/idl/1995-9P1.9p.wip144
-rw-r--r--lib9p/idl/1996-Styx.9p.wip64
-rw-r--r--lib9p/idl/2002-9P2000.9p41
-rw-r--r--lib9p/idl/2003-9P2000.p9p.9p (renamed from lib9p/idl/2003-9P2000.p9p.9p.wip)3
-rw-r--r--lib9p/idl/2010-9P2000.L.9p6
-rw-r--r--lib9p/idl/2012-9P2000.e.9p4
-rw-r--r--lib9p/idl/__init__.py257
9 files changed, 432 insertions, 217 deletions
diff --git a/lib9p/idl/0000-TODO.md b/lib9p/idl/0000-TODO.md
new file mode 100644
index 0000000..1cb02db
--- /dev/null
+++ b/lib9p/idl/0000-TODO.md
@@ -0,0 +1,5 @@
+- Decide how to handle duplicate type names from different versions
+- Decide how to handle duplicate `enum lib9p_msg_type` names and
+ values
+- Clean up the iterate-over-all-msgids-in-a-version code
+- Allow for const `.cnt` instead of only having previous-member `.cnt`
diff --git a/lib9p/idl/1992-9P0.9p.wip b/lib9p/idl/1992-9P0.9p.wip
index 15997d9..086e8e4 100644
--- a/lib9p/idl/1992-9P0.9p.wip
+++ b/lib9p/idl/1992-9P0.9p.wip
@@ -3,7 +3,20 @@
# 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,8 +28,77 @@ num fid = 2
# uni"Q"ue "ID"entification
struct qid = "path[4] version[4]"
-# a nul-padded string
-struct name = 28*(txt[1])
+# 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
+ "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=_reserved_CEXEC" # close-on-exec
+ "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()
+
+ "MODE_MASK = 0b00000011"
+ "FLAG_MASK = 0b11111100"
+
+# "C"??? "H"??? - file permissions and attributes
+bitfield ch = 4
+ "31=DIR"
+ "30=APPEND"
+ "29=EXCL"
+ #...
+ "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_MASK=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"
@@ -25,32 +107,35 @@ 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[64]"
+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[28] aname[28] auth[28]"
-msg Rattach = "typ[1,val=59] tag[tag] fid[fid] qid[8]"
+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[28]"
-msg Rwalk = "typ[1,val=63] tag[tag] fid[fid] qid[8]"
-msg Topen = "typ[1,val=64] tag[tag] fid[fid] mode[1]"
-msg Ropen = "typ[1,val=65] tag[tag] fid[fid] qid[8]"
-msg Tcreate = "typ[1,val=66] tag[tag] fid[fid] name[28] perm[4] mode[1]"
-msg Rcreate = "typ[1,val=67] tag[tag] fid[fid] qid[8]"
-msg Tread = "typ[1,val=68] tag[tag] fid[fid] offset[8] count[2,max=8192]"
+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] 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[116]"
-msg Twstat = "typ[1,val=78] tag[tag] fid[fid] stat[116]"
+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[28]"
-msg Rclwalk = "typ[1,val=81] tag[tag] fid[fid] qid[8]"
-msg Tauth = "typ[1,val=82] tag[tag] fid[fid] uid[28] chal[36]"
-msg Rauth = "typ[1,val=83] tag[tag] fid[fid] chal[30]"
+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 2812cda..2caf39d 100644
--- a/lib9p/idl/1995-9P1.9p.wip
+++ b/lib9p/idl/1995-9P1.9p.wip
@@ -3,57 +3,105 @@
# 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 += "28=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=50] tag[tag,val=0xFFFF]"
-msg Rnop = "typ[1,val=51] tag[tag,val=0xFFFF]"
-#msg Tosession = "typ[1,val=52] illegal"
-#msg Rosession = "typ[1,val=53] illegal"
-#msg Terror = "typ[1,val=54] illegal"
-msg Rerror = "typ[1,val=55] tag[tag] ename[64]"
-msg Tflush = "typ[1,val=56] tag[tag] oldtag[tag]"
-msg Rflush = "typ[1,val=57] tag[tag]"
-#msg Toattach = "typ[1,val=58] illegal"
-#msg Roattach = "typ[1,val=59] illegal"
-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[28]"
-msg Rwalk = "typ[1,val=63] tag[tag] fid[fid] qid[8]"
-msg Topen = "typ[1,val=64] tag[tag] fid[fid] mode[1]"
-msg Ropen = "typ[1,val=65] tag[tag] fid[fid] qid[8]"
-msg Tcreate = "typ[1,val=66] tag[tag] fid[fid] name[28] perm[4] mode[1]"
-msg Rcreate = "typ[1,val=67] tag[tag] fid[fid] qid[8]"
-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] 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[116]"
-msg Twstat = "typ[1,val=78] tag[tag] fid[fid] stat[116]"
-msg Rwstat = "typ[1,val=79] tag[tag] fid[fid]"
-msg Tclwalk = "typ[1,val=80] tag[tag] fid[fid] newfid[fid] name[28]"
-msg Rclwalk = "typ[1,val=81] tag[tag] fid[fid] qid[8]"
-#msg Toauth = typ[1,val=82] illegal"
-#msg Roauth = typ[1,val=83] illegal"
-msg Tsession = "typ[1,val=84] tag[tag,val=0xFFFF] chal[8]"
-msg Rsession = "typ[1,val=85] tag[tag,val=0xFFFF] chal[8] authid[28] authdom[48]"
-msg Tattach = "typ[1,val=86] tag[tag] fid[fid] uid[28] aname[28] ticket[72] auth[13]"
-msg Rattach = "typ[1,val=87] tag[tag] fid[fid] qid[8] rauth[13]"
+#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 d9d3399..6ba4509 100644
--- a/lib9p/idl/1996-Styx.9p.wip
+++ b/lib9p/idl/1996-Styx.9p.wip
@@ -1,15 +1,59 @@
# 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 f0df7d1..438e02f 100644
--- a/lib9p/idl/2002-9P2000.9p
+++ b/lib9p/idl/2002-9P2000.9p
@@ -16,6 +16,7 @@
# 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"
@@ -27,26 +28,20 @@ num tag = 2
num fid = 4
"NOFID = ~0"
-# data - u32le `n`, then `n` bytes of data
-struct d = "len[4] len*(dat[1])"
-
-# data - s32le `n`, then `n` bytes of data
-struct d_signed = "len[4,max=s32_max] 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
+# "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.
+ # DMMOUNT has been around in Plan 9 forever (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.
"28=_PLAN9_MOUNT"
"27=AUTH"
"26=TMP"
@@ -68,7 +63,7 @@ bitfield qt = 1
"7=DIR"
"6=APPEND"
"5=EXCL"
- "4=_PLAN9_MOUNT" # see "MOUNT" in "dm" above
+ "4=_PLAN9_MOUNT" # See "_PLAN9_MOUNT" in "dm" above.
"3=AUTH"
# Fun historical fact: QTTMP was a relatively late addition to
# Plan 9, in 2003-12.
@@ -110,13 +105,14 @@ 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"
+ #"5=_reserved_CEXEC" # close-on-exec
"6=RCLOSE" # remove-on-close
#"7=unused"
@@ -128,6 +124,13 @@ bitfield o = 1
"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]"
msg Tauth = "size[4,val=end-&size] typ[1,val=102] tag[tag] afid[fid] uname[s] aname[s]"
@@ -144,10 +147,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,max=s64_max] count[4,max=s32_max]"
-msg Rread = "size[4,val=end-&size] typ[1,val=117] tag[tag] data[d_signed]" # 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] data[d_signed]"
-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.wip b/lib9p/idl/2003-9P2000.p9p.9p
index c42584f..3f6a524 100644
--- a/lib9p/idl/2003-9P2000.p9p.9p.wip
+++ b/lib9p/idl/2003-9P2000.p9p.9p
@@ -14,6 +14,7 @@
# BUG: There is no version-string for this extension; plan9port still
# calls it vanilla "9P2000".
+version "9P2000.p9p"
from ./2002-9P2000.9p import *
@@ -41,7 +42,7 @@ from ./2002-9P2000.9p import *
# 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=98] tag[tag] qid[qid] iounit[4] unixfd[4]"
+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
diff --git a/lib9p/idl/2010-9P2000.L.9p b/lib9p/idl/2010-9P2000.L.9p
index 3e5db69..e21d411 100644
--- a/lib9p/idl/2010-9P2000.L.9p
+++ b/lib9p/idl/2010-9P2000.L.9p
@@ -7,7 +7,7 @@
# https://github.com/chaos/diod/blob/master/protocol.md
version "9P2000.L"
-from ./2002-9P2000.9p import tag, fid, s, d, d_signed, qt, qid
+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, Tauth, Rauth, Tattach, Rattach
@@ -89,7 +89,7 @@ msg Txattrcreate = "size[4,val=end-&size] typ[1,val=32] tag[tag] fid[fid] name[s
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] data[d]" # data is "qid[qid] offset[8] type[1] name[s]"
+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[1] name[s]"
#...
msg Tfsync = "size[4,val=end-&size] typ[1,val=50] tag[tag] fid[fid] datasync[4]"
msg Rfsync = "size[4,val=end-&size] typ[1,val=51] tag[tag]"
@@ -101,7 +101,7 @@ msg Rgetlock = "size[4,val=end-&size] typ[1,val=55] tag[tag] type[1] start[8
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[4] gid[nuid]"
-msg Tmkdir = "size[4,val=end-&size] typ[1,val=73] tag[tag] qid[qid]"
+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]"
diff --git a/lib9p/idl/2012-9P2000.e.9p b/lib9p/idl/2012-9P2000.e.9p
index ef80796..dde9d96 100644
--- a/lib9p/idl/2012-9P2000.e.9p
+++ b/lib9p/idl/2012-9P2000.e.9p
@@ -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
index 41664f1..a01c38f 100644
--- a/lib9p/idl/__init__.py
+++ b/lib9p/idl/__init__.py
@@ -433,121 +433,144 @@ def parse_file(
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:
+ 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")
- 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 typ.name == symname or symname == "*":
- found = True
- 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
- if symname != "*" and not found:
- raise ValueError(
- f"import: {m.group('file')}: no symbol {repr(symname)}"
- )
- 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")
- bf.in_versions.add(version)
-
- 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 Number():
- parse_numspec(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 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 typ.name == symname or symname == "*":
+ found = True
+ 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)
+ if typ.name in env and env[typ.name] != typ:
+ raise ValueError(
+ f"duplicate type name {repr(typ.name)}"
+ )
+ env[typ.name] = typ
+ if symname != "*" and not found:
+ raise ValueError(
+ f"import: {m.group('file')}: no symbol {repr(symname)}"
+ )
+ 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
+
+ if num.name in env:
+ raise ValueError(f"duplicate type name {repr(num.name)}")
+ env[num.name] = num
+ prev = num
+ elif m := re.fullmatch(re_line_bitfield, line):
+ bf = Bitfield()
+ bf.name = m.group("name")
+ bf.in_versions.add(version)
+
+ prim = env[m.group("prim")]
+ assert isinstance(prim, Primitive)
+ bf.prim = prim
+
+ bf.bits = (prim.static_size * 8) * [""]
+
+ if bf.name in env:
+ raise ValueError(f"duplicate type name {repr(bf.name)}")
+ 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"))
+
+ if struct.name in env:
+ raise ValueError(
+ f"duplicate type name {repr(struct.name)}"
+ )
+ 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"))
+
+ if msg.name in env:
+ raise ValueError(
+ f"duplicate type name {repr(msg.name)}"
+ )
+ 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 Number():
+ parse_numspec(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")
@@ -601,4 +624,10 @@ class Parser:
raise ValueError(f"duplicate type name {repr(typ.name)}")
else:
ret_typs[typ.name] = 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 {repr(typ.msgid)}")
+ msgids.add(typ.msgid)
return ret_versions, list(ret_typs.values())