├── .gitignore ├── README.md ├── 9test ├── 9psrv ├── LICENSE ├── testclient.lua └── 9p.lua /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *.so 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | lua9p 2 | ===== 3 | 4 | Lua9p is a 9P client written in Lua. It depends on [luadata](https://github.com/lneto/luadata). 5 | 6 | ## TODO 7 | 8 | * auth 9 | 10 | -------------------------------------------------------------------------------- /9test: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # SPDX-License-Identifier: MIT 3 | # Copyright (c) 2014-2020 Iruatã Martins dos Santos Souza 4 | 5 | if [ -z `which dialexec` ]; then 6 | (lua testclient.lua < /tmp/srv-cli) > /tmp/cli-srv 7 | else 8 | dialexec localhost:9999 lua testclient.lua 9 | fi 10 | 11 | -------------------------------------------------------------------------------- /9psrv: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # SPDX-License-Identifier: MIT 3 | # Copyright (c) 2014-2020 Iruatã Martins dos Santos Souza 4 | 5 | if [ -z `which listen1` ]; then 6 | rm -f /tmp/cli-srv /tmp/srv-cli 7 | mkfifo /tmp/cli-srv; mkfifo /tmp/srv-cli 8 | $HOME/Downloads/u9fs/u9fs -nz -a none -D /tmp/srv-cli 9 | else 10 | sudo $PLAN9/bin/listen1 'tcp!localhost!9999' $HOME/Downloads/u9fs/u9fs -nz -a none -D 11 | fi 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2014-2020 Iruatã Martins dos Santos Souza 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /testclient.lua: -------------------------------------------------------------------------------- 1 | -- SPDX-License-Identifier: MIT 2 | -- Copyright (c) 2014-2020 Iruatã Martins dos Santos Souza 3 | 4 | local data = require'data' 5 | local np = require'9p' 6 | 7 | local conn = np.newconn(io.read, 8 | function (buf) 9 | io.write(buf) 10 | io.output():flush() 11 | end) 12 | 13 | conn:attach("iru", "") 14 | 15 | local f, g = conn:newfid(), conn:newfid() 16 | 17 | conn:walk(conn.rootfid, f, "/tmp") 18 | conn:clone(f, g) 19 | 20 | conn:create(g, "file", 420, 1) 21 | 22 | local ftext = "this is a test\n" 23 | local buf = data.new(ftext) 24 | 25 | local n = conn:write(g, 0, buf) 26 | if n ~= #buf then 27 | error("test: expected to write " .. #buf .. " bytes but wrote " .. n) 28 | end 29 | 30 | conn:clunk(g) 31 | 32 | if pcall(np.walk, conn, conn.rootfid, g, 33 | "/tmp/.lua9p.non.existant..") ~= false then 34 | error("test: succeeded when shouldn't (walking to non-existing file)") 35 | end 36 | 37 | conn:walk(conn.rootfid, g, "/tmp/file") 38 | conn:open(g, 0) 39 | 40 | local st = conn:stat(g) 41 | -- Remove last byte of the file 42 | st.length = st.length - 1 43 | 44 | conn:wstat(g, st) 45 | 46 | buf = conn:read(g, 0, st.length) 47 | 48 | conn:remove(g) 49 | 50 | buf:layout{str = {0, #buf, 'string'}} 51 | 52 | -- The trailing \n was removed by wstat, we add it again to check the read 53 | if buf.str .. "\n" == ftext then 54 | io.stderr:write("test ok\n") 55 | else 56 | error("test failed") 57 | end 58 | 59 | conn:clunk(f) 60 | conn:clunk(conn.rootfid) 61 | -------------------------------------------------------------------------------- /9p.lua: -------------------------------------------------------------------------------- 1 | -- SPDX-License-Identifier: MIT 2 | -- Copyright (c) 2014-2020 Iruatã Martins dos Santos Souza 3 | 4 | local data = require'data' 5 | 6 | local np = {} 7 | 8 | -- message types 9 | local Tversion = 100 10 | local Rversion = 101 11 | local Tauth = 102 12 | local Rauth = 103 13 | local Tattach = 104 14 | local Rattach = 105 15 | local Rerror = 107 16 | local Tflush = 108 17 | local Rflush = 109 18 | local Twalk = 110 19 | local Rwalk = 111 20 | local Topen = 112 21 | local Ropen = 113 22 | local Tcreate = 114 23 | local Rcreate = 115 24 | local Tread = 116 25 | local Rread = 117 26 | local Twrite = 118 27 | local Rwrite = 119 28 | local Tclunk = 120 29 | local Rclunk = 121 30 | local Tremove = 122 31 | local Rremove = 123 32 | local Tstat = 124 33 | local Rstat = 125 34 | local Twstat = 126 35 | local Rwstat = 127 36 | local Tmax = 128 37 | 38 | local HEADSZ = 7 39 | local FIDSZ = 4 40 | local QIDSZ = 13 41 | local IOHEADSZ = 24 -- io (Twrite/Rread) header size, i.e. minimum msize 42 | 43 | 44 | function np.newfid(conn) 45 | local f = conn.fidfree 46 | 47 | if f then 48 | conn.fidfree = f.next 49 | else 50 | f = { 51 | fid = conn.nextfid, 52 | qid = nil, 53 | next = conn.fidactive, 54 | } 55 | 56 | conn.nextfid = conn.nextfid + 1; 57 | conn.fidactive = f 58 | end 59 | 60 | return f 61 | end 62 | 63 | local function freefid(conn, f) 64 | f.next = conn.fidfree 65 | conn.fidfree = f 66 | end 67 | 68 | local function tag(conn) 69 | local t = conn.curtag 70 | conn.curtag = (conn.curtag + 1) % 0xFFFF 71 | return t 72 | end 73 | 74 | -- Returns a 9P number in table format. Offset and size in bytes 75 | local function num9p(offset, size) 76 | return {offset*8, size*8, 'number', 'le'} 77 | end 78 | 79 | local function putstr(to, s) 80 | if #s > #to - 2 then 81 | return 0 82 | end 83 | 84 | local p = to:segment() 85 | p:layout{ 86 | len = num9p(0, 2), 87 | s = {2, #s, 's'}, 88 | } 89 | 90 | p.len = #s 91 | p.s = s 92 | return 2 + #s 93 | end 94 | 95 | local function getstr(from) 96 | local p = from:segment():layout{len = num9p(0, 2)} 97 | p:layout{str = {2, p.len, 's'}} 98 | 99 | return p.str or "" 100 | end 101 | 102 | local function readmsg(conn, type) 103 | local rawsize = conn.user.read(4) 104 | local bsize = data.new(rawsize):segment() 105 | local size = bsize:layout{size = num9p(0, 4)}.size 106 | 107 | local rawrest = conn.user.read(size - 4) 108 | 109 | local buf = data.new(rawsize .. rawrest):segment() 110 | 111 | local p = buf:layout{ 112 | size = num9p(0, 4), 113 | type = num9p(4, 1) 114 | } 115 | 116 | if p.type ~= type then 117 | if p.type == Rerror then 118 | error(getstr(p:segment(HEADSZ))) 119 | else 120 | error("Wrong response type " .. p.type .. ", expected " .. type) 121 | end 122 | end 123 | 124 | return buf 125 | end 126 | 127 | local function writemsg(conn, buf) 128 | conn.user.write(tostring(buf)) 129 | end 130 | 131 | local LQid = data.layout{ 132 | type = num9p(0, 1), 133 | version = num9p(1, 4), 134 | path = num9p(5, 8), 135 | } 136 | 137 | local function getqid(from) 138 | if #from < QIDSZ then 139 | return nil 140 | end 141 | 142 | local p = from:segment():layout(LQid) 143 | local qid = {} 144 | 145 | qid.type = p.type 146 | qid.version = p.version 147 | qid.path = p.path 148 | 149 | return qid 150 | end 151 | 152 | local function putqid(to, qid) 153 | if #to < QIDSZ then 154 | return nil 155 | end 156 | 157 | local p = to:segment():layout(LQid) 158 | p.type = qid.type 159 | p.version = qid.version 160 | p.path = qid.path 161 | return to 162 | end 163 | 164 | local Lstat = data.layout{ 165 | size = num9p(0, 2), 166 | type = num9p(2, 2), 167 | dev = num9p(4, 4), 168 | qid = num9p(8, QIDSZ), 169 | mode = num9p(21, 4), 170 | atime = num9p(25, 4), 171 | mtime = num9p(29, 4), 172 | length = num9p(33, 8), 173 | } 174 | 175 | local function getstat(seg) 176 | local p = seg:segment():layout(Lstat) 177 | local st = {} 178 | 179 | st.size = p.size 180 | st.type = p.type 181 | st.dev = p.dev 182 | st.qid = getqid(seg:segment(8)) 183 | if not st.qid then 184 | return nil 185 | end 186 | 187 | st.mode = p.mode 188 | st.atime = p.atime 189 | st.mtime = p.mtime 190 | st.length = p.length 191 | st.name = getstr(seg:segment(41)) 192 | st.uid = getstr(seg:segment(41 + 2 + #st.name)) 193 | st.gid = getstr(seg:segment(41 + 2 + #st.name + 2 + #st.uid)) 194 | st.muid = getstr(seg:segment(41 + 2 + #st.name + 2 + #st.uid + 2 + #st.gid)) 195 | 196 | return st 197 | end 198 | 199 | local function putstat(to, st) 200 | local p = to:segment():layout(Lstat) 201 | 202 | p.size = st.size 203 | p.type = st.type 204 | p.dev = st.dev 205 | 206 | if not putqid(to:segment(8), st.qid) then 207 | return nil 208 | end 209 | 210 | p.mode = st.mode 211 | p.atime = st.atime 212 | p.mtime = st.mtime 213 | p.length = st.length 214 | putstr(to:segment(41), st.name) 215 | putstr(to:segment(41 + 2 + #st.name), st.uid) 216 | putstr(to:segment(41 + 2 + #st.name + 2 + #st.uid), st.gid) 217 | putstr(to:segment(41 + 2 + #st.name + 2 + #st.uid + 2 + #st.gid), st.muid) 218 | 219 | return to 220 | end 221 | 222 | local function putheader(to, type, size, tag) 223 | local Lheader = data.layout{ 224 | size = num9p(0, 4), 225 | type = num9p(4, 1), 226 | tag = num9p(5, 2), 227 | } 228 | 229 | local p = to:segment():layout(Lheader) 230 | 231 | p.size = HEADSZ + size 232 | p.type = type 233 | p.tag = tag 234 | return p.size 235 | end 236 | 237 | 238 | local function doversion(conn, msize) 239 | local LXversion = data.layout{msize = num9p(HEADSZ, 4)} 240 | 241 | local buf = data.new(19) 242 | buf:layout(LXversion) 243 | buf.msize = msize or 8192+IOHEADSZ 244 | 245 | local n = putstr(buf:segment(HEADSZ + 4), "9P2000") 246 | n = putheader(buf, Tversion, 4 + n, tag(conn)) 247 | writemsg(conn, buf) 248 | 249 | local buf = readmsg(conn, Rversion) 250 | buf:layout(LXversion) 251 | 252 | if buf.msize < IOHEADSZ then 253 | error("short msize") 254 | end 255 | 256 | return buf.msize 257 | end 258 | 259 | local function doattach(conn, uname, aname) 260 | local LTattach = data.layout{ 261 | fid = num9p(HEADSZ, FIDSZ), 262 | afid = num9p(HEADSZ + FIDSZ, 4), 263 | } 264 | 265 | local tx = conn.txbuf:segment() 266 | tx:layout(LTattach) 267 | 268 | local fid = conn:newfid() 269 | tx.fid = fid.fid 270 | tx.afid = -1 271 | local n = putstr(tx:segment(HEADSZ + FIDSZ + FIDSZ), uname) 272 | n = n + putstr(tx:segment(HEADSZ + FIDSZ + FIDSZ + n), aname) 273 | 274 | n = putheader(tx, Tattach, FIDSZ + FIDSZ + n, tag(conn)) 275 | writemsg(conn, tx:segment(0, n)) 276 | 277 | local rx = readmsg(conn, Rattach) 278 | 279 | fid.qid = getqid(rx:segment(HEADSZ)) 280 | if not fid.qid then 281 | error("attach: overflow copying qid") 282 | end 283 | 284 | return fid 285 | end 286 | 287 | function np.newconn(read, write) 288 | local conn = np 289 | conn.curtag = 0xFFFF 290 | 291 | conn.fidfree = nil 292 | conn.fidactive = nil 293 | conn.nextfid = 0 294 | 295 | conn.user = {read = read, write = write} 296 | return conn 297 | end 298 | 299 | function np.attach(conn, uname, aname, msize) 300 | local msize = doversion(conn, msize) 301 | conn.txbuf = data.new(msize) 302 | conn.rootfid = doattach(conn, uname, aname) 303 | 304 | end 305 | 306 | local function breakpath(path) 307 | local tab = {} 308 | local i = 1 309 | 310 | if path == '/' then 311 | return {'/'} 312 | end 313 | 314 | while i <= #path do 315 | local start, stop = string.find(path, '[^/]+', i) 316 | if not start then break end 317 | 318 | local elem = string.sub(path, start, stop) 319 | table.insert(tab, elem) 320 | i = stop + 1 321 | end 322 | return tab 323 | end 324 | 325 | -- path == nil clones ofid to nfid 326 | function np.walk(conn, ofid, nfid, path) 327 | local LTwalk = data.layout{ 328 | fid = num9p(HEADSZ, FIDSZ), 329 | newfid = num9p(HEADSZ + FIDSZ, FIDSZ), 330 | nwname = num9p(HEADSZ + FIDSZ + FIDSZ, 2), 331 | } 332 | 333 | local tx = conn.txbuf:segment() 334 | tx:layout(LTwalk) 335 | tx.fid = ofid.fid 336 | tx.newfid = nfid.fid 337 | 338 | local n = 0 339 | if path then 340 | local names = breakpath(path) 341 | tx.nwname = #names 342 | for i = 1, #names do 343 | n = n + putstr(tx:segment(HEADSZ + FIDSZ + FIDSZ + 2 + n), names[i]) 344 | end 345 | else 346 | tx.nwname = 0 347 | end 348 | 349 | n = putheader(tx, Twalk, FIDSZ + FIDSZ + 2 + n, tag(conn)) 350 | writemsg(conn, tx:segment(0, n)) 351 | 352 | local rx = readmsg(conn, Rwalk) 353 | rx:layout{nwqid = num9p(HEADSZ, 2)} 354 | 355 | -- clone succeeded 356 | if rx.nwqid == 0 and not path then 357 | nfid.qid = ofid.qid 358 | return 359 | end 360 | 361 | -- walk succeeded 362 | if rx.nwqid == tx.nwname then 363 | nfid.qid = getqid(rx:segment(HEADSZ + 2 + (rx.nwqid-1)*QIDSZ)) 364 | return 365 | end 366 | 367 | error("file '" .. path .. "' not found") 368 | end 369 | 370 | function np.clone(conn, ofid, nfid) 371 | np.walk(conn, ofid, nfid) 372 | end 373 | 374 | function np.open(conn, fid, mode) 375 | local tx = conn.txbuf:segment():layout{ 376 | fid = num9p(HEADSZ, FIDSZ), 377 | mode = num9p(HEADSZ + FIDSZ, 1), 378 | } 379 | 380 | tx.fid = fid.fid 381 | tx.mode = mode 382 | 383 | local n = putheader(tx, Topen, 5, tag(conn)) 384 | writemsg(conn, tx:segment(0, n)) 385 | 386 | local rx = readmsg(conn, Ropen) 387 | 388 | fid.qid = getqid(rx:segment(HEADSZ)) 389 | if not fid.qid then 390 | error("overflow copying qid") 391 | end 392 | end 393 | 394 | function np.create(conn, fid, name, perm, mode) 395 | local tx = conn.txbuf:segment() 396 | local n = putstr(tx:segment(11), name) 397 | 398 | tx:layout{ 399 | fid = num9p(HEADSZ, FIDSZ), 400 | perm = num9p(HEADSZ + FIDSZ + n, 4), 401 | mode = num9p(HEADSZ + FIDSZ + n + 4, 1), 402 | } 403 | 404 | tx.fid = fid.fid 405 | tx.perm = perm 406 | tx.mode = mode 407 | 408 | local n = putheader(tx, Tcreate, n + 9, tag(conn)) 409 | writemsg(conn, tx:segment(0, n)) 410 | 411 | local rx = readmsg(conn, Rcreate) 412 | 413 | fid.qid = getqid(rx:segment(HEADSZ)) 414 | if not fid.qid then 415 | error("overflow copying qid") 416 | end 417 | end 418 | 419 | function np.read(conn, fid, offset, count) 420 | local tx = conn.txbuf:segment():layout{ 421 | fid = num9p(HEADSZ, FIDSZ), 422 | offset = num9p(HEADSZ + FIDSZ, 8), 423 | count = num9p(HEADSZ + FIDSZ + 8, 4), 424 | } 425 | 426 | tx.fid = fid.fid 427 | tx.offset = offset 428 | tx.count = count 429 | 430 | local n = putheader(tx, Tread, FIDSZ + 8 + 4, tag(conn)) 431 | writemsg(conn, tx:segment(0, n)) 432 | 433 | local rx = readmsg(conn, Rread) 434 | rx:layout{count = num9p(HEADSZ, 4)} 435 | return rx:segment(HEADSZ + 4, rx.count) 436 | end 437 | 438 | function np.write(conn, fid, offset, seg) 439 | local tx = conn.txbuf:segment():layout{ 440 | fid = num9p(HEADSZ, FIDSZ), 441 | offset = num9p(HEADSZ + FIDSZ, 8), 442 | count = num9p(HEADSZ + FIDSZ + 8, 4), 443 | } 444 | 445 | tx.fid = fid.fid 446 | tx.offset = offset 447 | tx.count = #seg 448 | 449 | local n = putheader(tx, Twrite, FIDSZ + 8 + 4 + #seg, tag(conn)) 450 | writemsg(conn, tx:segment(0, n - #seg)) 451 | writemsg(conn, seg:segment(0, #seg)) 452 | 453 | local rx = readmsg(conn, Rwrite) 454 | rx:layout{count = num9p(HEADSZ, 4)} 455 | return rx.count 456 | end 457 | 458 | local function clunkrm(conn, type, fid) 459 | local tx = conn.txbuf:segment():layout{fid = num9p(HEADSZ, FIDSZ)} 460 | tx.fid = fid.fid 461 | 462 | local n = putheader(tx, type, FIDSZ, tag(conn)) 463 | writemsg(conn, tx:segment(0, n)) 464 | 465 | readmsg(conn, type+1) 466 | freefid(conn, fid) 467 | end 468 | 469 | function np.clunk(conn, fid) 470 | return clunkrm(conn, Tclunk, fid) 471 | end 472 | 473 | function np.remove(conn, fid) 474 | return clunkrm(conn, Tremove, fid) 475 | end 476 | 477 | function np.stat(conn, fid) 478 | local tx = conn.txbuf:segment():layout{fid = num9p(HEADSZ, FIDSZ)} 479 | tx.fid = fid.fid 480 | 481 | local n = putheader(tx, Tstat, FIDSZ, tag(conn)) 482 | writemsg(conn, tx:segment(0, n)) 483 | 484 | local rx = readmsg(conn, Rstat) 485 | return getstat(rx:segment(HEADSZ + 2)) 486 | end 487 | 488 | function np.wstat(conn, fid, st) 489 | local tx = conn.txbuf:segment():layout{ 490 | fid = num9p(HEADSZ, FIDSZ), 491 | stsize = num9p(HEADSZ + FIDSZ, 2), 492 | } 493 | 494 | tx.fid = fid.fid 495 | tx.stsize = st.size + 2 496 | 497 | local n = putheader(tx, Twstat, FIDSZ + 2 + tx.stsize, tag(conn)) 498 | writemsg(conn, tx:segment(0, n - tx.stsize)) 499 | 500 | local seg = conn.txbuf:segment(n - tx.stsize) 501 | 502 | if not putstat(seg, st) then 503 | error("tx buffer too small") 504 | end 505 | 506 | writemsg(conn, seg:segment(0, tx.stsize)) 507 | return readmsg(conn, Rwstat) 508 | end 509 | 510 | return np 511 | --------------------------------------------------------------------------------