{
  "id": "P155",
  "slug": "file-share",
  "key": "P155-file-share",
  "title": "File share",
  "summary": "Documents reached by path and saved by a handler that looks the path up twice and one that resolves it once: a move between the check and the write sends a save to a document the caller does not own.",
  "entry": "main.eml",
  "ui": "terminal",
  "readme": "# P155 - File share\n\nDocuments reached by path, saved by two handlers - one that looks the\npath up to check and again to write, one that resolves it once - with an\nadmin moving a path at a chosen point.\n\n- `main.eml` - the menu, the share, one save seen by both handlers, every\n  save, and choosing the move\n- `share.eml` - the paths, the owners, the users, the points a move can\n  fall at, resolving a path, the owner of a document, a move, and a save\n  by either handler\n\nHow each part works:\n\n- Three paths: /reports/q3 to ann's doc-a, /reports/archive to root's\n  doc-b, /notes/bob to bob's doc-c. The move an admin makes points\n  /reports/q3 at doc-b, the archive.\n- As in the corpus case `path-resolved-twice`, the handler that looks the\n  path up twice checks that the caller owns the document, then looks the\n  path up again to write. If the move falls between the two, ann's save of\n  /reports/q3 passes the check on her doc-a and writes root's doc-b: the\n  value did not change under the write, the subject did.\n- The handler that resolves the path once checks and writes the same\n  document, and never writes one the caller does not own. A move before\n  the check refuses both; a move after the write changes neither.\n- Over every user, path and point - 24 saves - looking up twice writes\n  another document than it checked exactly when the saved path is moved\n  between check and write. No test that moves a path first or last can\n  see it.\n\nWhat is checked: menu choices 1 to 5; the user ann or bob; a path from 1 to\n3; the point never, before, between or after; for the move, a path from 1\nto 3 and doc-a, doc-b or doc-c. An empty answer cancels.\n\nSessions: `sessions/basic.in` gives:\n- the share; ann saving /reports/q3 with the move between, before and\n  after, and bob with no move;\n- every save; the move changed to /notes/bob pointed at doc-a, every save\n  again, and bob saving /notes/bob with the move between.\n\n`sessions/bad-input.in` gives:\n- menu choices 0 and x;\n- a save: a cancel and cy for the user, a cancel and 4 for the path, a\n  cancel and later for the point;\n- the move: a cancel and 9 for the path, a cancel and doc-z for the\n  document, then /reports/archive pointed at doc-b, where it already\n  points; every save, none of them wrong.\n\nBuilt on the verified corpus case `path-resolved-twice` (a permission\ncheck and a write that each resolve the name, against resolving it once,\nwith the rename at every point).\n",
  "modules": [
    {
      "name": "main.eml",
      "eml": "# P155 file share: the paths, documents and owners, one save seen by both\n# handlers with a move at a chosen point, every user, path and point, and\n# choosing the move.\nimport share\n\ndef plain(s):\n    0 => i\n    len(s) => j\n    while i < j and s[i] == \" \":\n        i + 1 => i\n    while j > i and s[j - 1] == \" \":\n        j - 1 => j\n    return s[i:j]\n\ndef pad(s, n):\n    s => out\n    while len(out) < n:\n        out + \" \" => out\n    return out\n\ndef show(move):\n    \"The paths and the documents they point at:\" ^0\n    for p in share.paths():\n        (\"  \" + pad(p[0], 18) + pad(p[1], 7) + \"owned by \" + share.owner(p[1])) ^0\n    (\"The move an admin makes: \" + move[0] + \" is pointed at \" + move[1] + \", \" + share.owner(move[1]) + \"'s document.\") ^0\n\ndef said(r, user):\n    if r[0] == \"refused\":\n        return \"refused: not \" + user + \"'s document\"\n    share.owner(r[1]) => o\n    if o == user:\n        return \"wrote \" + r[1] + \", \" + o + \"'s document\"\n    return \"wrote \" + r[1] + \", \" + o + \"'s document - not \" + user + \"'s\"\n\ndef one_save(move):\n    plain(input(\"user (ann or bob)> \")) => u\n    if u == \"\":\n        \"Cancelled.\" ^0\n        return\n    if u != \"ann\" and u != \"bob\":\n        \"Type ann or bob.\" ^0\n        return\n    plain(input(\"path (1 to 3)> \")) => t\n    if t == \"\":\n        \"Cancelled.\" ^0\n        return\n    if t != \"1\" and t != \"2\" and t != \"3\":\n        \"Type a path from 1 to 3.\" ^0\n        return\n    share.paths()[int(t) - 1][0] => name\n    plain(input(\"the move happens (never, before, between, after)> \")) => at\n    if at == \"\":\n        \"Cancelled.\" ^0\n        return\n    False => ok\n    for p in share.points():\n        if p == at:\n            True => ok\n    if not ok:\n        \"Type never, before, between or after.\" ^0\n        return\n    share.save(\"twice\", u, name, at, move) => a\n    share.save(\"once\", u, name, at, move) => b\n    (u + \" saves \" + name + \", the move \" + at + \":\") ^0\n    (\"  looks the path up twice:  \" + said(a, u)) ^0\n    (\"  resolves it once:         \" + said(b, u)) ^0\n    if a[0] == \"written\" and a[1] != b[1]:\n        (\"The check passed on \" + b[1] + \" and the write went to \" + a[1] + \": between the two lookups the path was moved.\") ^0\n\ndef every(move):\n    0 => n2\n    0 => bad2\n    0 => n1\n    0 => bad1\n    [] => wrongs\n    for u in share.users():\n        for p in share.paths():\n            for at in share.points():\n                share.save(\"twice\", u, p[0], at, move) => a\n                share.save(\"once\", u, p[0], at, move) => b\n                if a[0] == \"written\":\n                    n2 + 1 => n2\n                    if share.owner(a[1]) != u:\n                        bad2 + 1 => bad2\n                        wrongs + [u + \" saving \" + p[0] + \" with the move \" + at + \" wrote \" + a[1] + \", \" + share.owner(a[1]) + \"'s\"] => wrongs\n                if b[0] == \"written\":\n                    n1 + 1 => n1\n                    if share.owner(b[1]) != u:\n                        bad1 + 1 => bad1\n    (\"Every user, path and point of the move (\" + str(len(share.users()) * len(share.paths()) * len(share.points())) + \" saves):\") ^0\n    (\"  looks the path up twice:  \" + str(n2) + \" writes, \" + str(bad2) + \" to a document the caller does not own\") ^0\n    (\"  resolves it once:         \" + str(n1) + \" writes, \" + str(bad1) + \" to a document the caller does not own\") ^0\n    for w in wrongs:\n        (\"  \" + w) ^0\n    if bad2 > 0:\n        \"Only a move between the check and the write does it, which is why no test that moves a path first or last can see it.\" ^0\n\ndef set_move(move):\n    plain(input(\"path to move (1 to 3)> \")) => t\n    if t == \"\":\n        \"Cancelled.\" ^0\n        return move\n    if t != \"1\" and t != \"2\" and t != \"3\":\n        \"Type a path from 1 to 3.\" ^0\n        return move\n    plain(input(\"point it at (doc-a, doc-b or doc-c)> \")) => d\n    if d == \"\":\n        \"Cancelled.\" ^0\n        return move\n    if d != \"doc-a\" and d != \"doc-b\" and d != \"doc-c\":\n        \"Type doc-a, doc-b or doc-c.\" ^0\n        return move\n    share.paths()[int(t) - 1][0] => name\n    (\"The move is now: \" + name + \" pointed at \" + d + \", \" + share.owner(d) + \"'s document.\") ^0\n    return [name, d]\n\n[\"/reports/q3\", \"doc-b\"] => move\n\n\"== File share ==\" ^0\n\"Documents reached by path, saved by a handler that looks the path up twice and one that resolves it once.\" ^0\nTrue => running\nwhile running:\n    \"\" ^0\n    \"1) the share  2) a save  3) every save  4) choose the move  5) quit\" ^0\n    plain(input(\"choice> \")) => choice\n    if choice == \"1\":\n        show(move)\n    elif choice == \"2\":\n        one_save(move)\n    elif choice == \"3\":\n        every(move)\n    elif choice == \"4\":\n        set_move(move) => move\n    elif choice == \"5\":\n        False => running\n    else:\n        \"Pick a number from 1 to 5.\" ^0\n\"Bye.\" ^0\n",
      "python": "import share\n\ndef plain(s):\n    i = 0\n    j = len(s)\n    while i < j and s[i] == \" \":\n        i = i + 1\n    while j > i and s[j - 1] == \" \":\n        j = j - 1\n    return s[i:j]\n\ndef pad(s, n):\n    out = s\n    while len(out) < n:\n        out = out + \" \"\n    return out\n\ndef show(move):\n    print(\"The paths and the documents they point at:\")\n    for p in share.paths():\n        print(\"  \" + pad(p[0], 18) + pad(p[1], 7) + \"owned by \" + share.owner(p[1]))\n    print(\"The move an admin makes: \" + move[0] + \" is pointed at \" + move[1] + \", \" + share.owner(move[1]) + \"'s document.\")\n\ndef said(r, user):\n    if r[0] == \"refused\":\n        return \"refused: not \" + user + \"'s document\"\n    o = share.owner(r[1])\n    if o == user:\n        return \"wrote \" + r[1] + \", \" + o + \"'s document\"\n    return \"wrote \" + r[1] + \", \" + o + \"'s document - not \" + user + \"'s\"\n\ndef one_save(move):\n    u = plain(input(\"user (ann or bob)> \"))\n    if u == \"\":\n        print(\"Cancelled.\")\n        return\n    if u != \"ann\" and u != \"bob\":\n        print(\"Type ann or bob.\")\n        return\n    t = plain(input(\"path (1 to 3)> \"))\n    if t == \"\":\n        print(\"Cancelled.\")\n        return\n    if t != \"1\" and t != \"2\" and t != \"3\":\n        print(\"Type a path from 1 to 3.\")\n        return\n    name = share.paths()[int(t) - 1][0]\n    at = plain(input(\"the move happens (never, before, between, after)> \"))\n    if at == \"\":\n        print(\"Cancelled.\")\n        return\n    ok = False\n    for p in share.points():\n        if p == at:\n            ok = True\n    if not ok:\n        print(\"Type never, before, between or after.\")\n        return\n    a = share.save(\"twice\", u, name, at, move)\n    b = share.save(\"once\", u, name, at, move)\n    print(u + \" saves \" + name + \", the move \" + at + \":\")\n    print(\"  looks the path up twice:  \" + said(a, u))\n    print(\"  resolves it once:         \" + said(b, u))\n    if a[0] == \"written\" and a[1] != b[1]:\n        print(\"The check passed on \" + b[1] + \" and the write went to \" + a[1] + \": between the two lookups the path was moved.\")\n\ndef every(move):\n    n2 = 0\n    bad2 = 0\n    n1 = 0\n    bad1 = 0\n    wrongs = []\n    for u in share.users():\n        for p in share.paths():\n            for at in share.points():\n                a = share.save(\"twice\", u, p[0], at, move)\n                b = share.save(\"once\", u, p[0], at, move)\n                if a[0] == \"written\":\n                    n2 = n2 + 1\n                    if share.owner(a[1]) != u:\n                        bad2 = bad2 + 1\n                        wrongs = wrongs + [u + \" saving \" + p[0] + \" with the move \" + at + \" wrote \" + a[1] + \", \" + share.owner(a[1]) + \"'s\"]\n                if b[0] == \"written\":\n                    n1 = n1 + 1\n                    if share.owner(b[1]) != u:\n                        bad1 = bad1 + 1\n    print(\"Every user, path and point of the move (\" + str(len(share.users()) * len(share.paths()) * len(share.points())) + \" saves):\")\n    print(\"  looks the path up twice:  \" + str(n2) + \" writes, \" + str(bad2) + \" to a document the caller does not own\")\n    print(\"  resolves it once:         \" + str(n1) + \" writes, \" + str(bad1) + \" to a document the caller does not own\")\n    for w in wrongs:\n        print(\"  \" + w)\n    if bad2 > 0:\n        print(\"Only a move between the check and the write does it, which is why no test that moves a path first or last can see it.\")\n\ndef set_move(move):\n    t = plain(input(\"path to move (1 to 3)> \"))\n    if t == \"\":\n        print(\"Cancelled.\")\n        return move\n    if t != \"1\" and t != \"2\" and t != \"3\":\n        print(\"Type a path from 1 to 3.\")\n        return move\n    d = plain(input(\"point it at (doc-a, doc-b or doc-c)> \"))\n    if d == \"\":\n        print(\"Cancelled.\")\n        return move\n    if d != \"doc-a\" and d != \"doc-b\" and d != \"doc-c\":\n        print(\"Type doc-a, doc-b or doc-c.\")\n        return move\n    name = share.paths()[int(t) - 1][0]\n    print(\"The move is now: \" + name + \" pointed at \" + d + \", \" + share.owner(d) + \"'s document.\")\n    return [name, d]\n\nmove = [\"/reports/q3\", \"doc-b\"]\nprint(\"== File share ==\")\nprint(\"Documents reached by path, saved by a handler that looks the path up twice and one that resolves it once.\")\nrunning = True\nwhile running:\n    print(\"\")\n    print(\"1) the share  2) a save  3) every save  4) choose the move  5) quit\")\n    choice = plain(input(\"choice> \"))\n    if choice == \"1\":\n        show(move)\n    elif choice == \"2\":\n        one_save(move)\n    elif choice == \"3\":\n        every(move)\n    elif choice == \"4\":\n        move = set_move(move)\n    elif choice == \"5\":\n        running = False\n    else:\n        print(\"Pick a number from 1 to 5.\")\nprint(\"Bye.\")\n"
    },
    {
      "name": "share.eml",
      "eml": "# P155 file share - documents reached by path, saved by two handlers. As in\n# the corpus case path-resolved-twice, the handler that looks the path up\n# to check the caller owns the document, and looks it up again to write,\n# can check one document and write another when a path is moved between\n# the two lookups: the value did not change under the write, the subject\n# did. The handler that resolves the path once and uses the document for\n# both cannot. Paths and owners are lists of pairs.\n\ndef paths():\n    return [[\"/reports/q3\", \"doc-a\"], [\"/reports/archive\", \"doc-b\"], [\"/notes/bob\", \"doc-c\"]]\n\ndef owners():\n    return [[\"doc-a\", \"ann\"], [\"doc-b\", \"root\"], [\"doc-c\", \"bob\"]]\n\ndef users():\n    return [\"ann\", \"bob\"]\n\ndef points():\n    return [\"never\", \"before\", \"between\", \"after\"]\n\ndef resolve(ps, name):\n    for p in ps:\n        if p[0] == name:\n            return p[1]\n    return \"\"\n\ndef owner(doc):\n    for o in owners():\n        if o[0] == doc:\n            return o[1]\n    return \"\"\n\ndef moved(ps, name, doc):\n    # the paths with name pointed at doc: an ordinary admin action\n    [] => out\n    for p in ps:\n        if p[0] == name:\n            out + [[name, doc]] => out\n        else:\n            out + [p] => out\n    return out\n\ndef save(handler, user, name, at, move):\n    # [outcome, the document written or \"\"]; move is [path, document] and\n    # at the point it happens: never, before, between (after the check,\n    # before the write) or after\n    paths() => ps\n    if at == \"before\":\n        moved(ps, move[0], move[1]) => ps\n    resolve(ps, name) => checked\n    if owner(checked) != user:\n        return [\"refused\", \"\"]\n    if at == \"between\":\n        moved(ps, move[0], move[1]) => ps\n    if handler == \"twice\":\n        resolve(ps, name) => written\n    else:\n        checked => written\n    return [\"written\", written]\n",
      "python": "def paths():\n    return [[\"/reports/q3\", \"doc-a\"], [\"/reports/archive\", \"doc-b\"], [\"/notes/bob\", \"doc-c\"]]\n\ndef owners():\n    return [[\"doc-a\", \"ann\"], [\"doc-b\", \"root\"], [\"doc-c\", \"bob\"]]\n\ndef users():\n    return [\"ann\", \"bob\"]\n\ndef points():\n    return [\"never\", \"before\", \"between\", \"after\"]\n\ndef resolve(ps, name):\n    for p in ps:\n        if p[0] == name:\n            return p[1]\n    return \"\"\n\ndef owner(doc):\n    for o in owners():\n        if o[0] == doc:\n            return o[1]\n    return \"\"\n\ndef moved(ps, name, doc):\n    out = []\n    for p in ps:\n        if p[0] == name:\n            out = out + [[name, doc]]\n        else:\n            out = out + [p]\n    return out\n\ndef save(handler, user, name, at, move):\n    ps = paths()\n    if at == \"before\":\n        ps = moved(ps, move[0], move[1])\n    checked = resolve(ps, name)\n    if owner(checked) != user:\n        return [\"refused\", \"\"]\n    if at == \"between\":\n        ps = moved(ps, move[0], move[1])\n    if handler == \"twice\":\n        written = resolve(ps, name)\n    else:\n        written = checked\n    return [\"written\", written]\n"
    }
  ],
  "sessions": [
    {
      "name": "bad-input",
      "input": "0\nx\n2\n\n2\ncy\n2\nann\n\n2\nann\n4\n2\nann\n1\n\n2\nann\n1\nlater\n4\n\n4\n9\n4\n2\n\n4\n2\ndoc-z\n4\n2\ndoc-b\n3\n5\n",
      "screen": "== File share ==\nDocuments reached by path, saved by a handler that looks the path up twice and one that resolves it once.\n\n1) the share  2) a save  3) every save  4) choose the move  5) quit\nchoice> 0\nPick a number from 1 to 5.\n\n1) the share  2) a save  3) every save  4) choose the move  5) quit\nchoice> x\nPick a number from 1 to 5.\n\n1) the share  2) a save  3) every save  4) choose the move  5) quit\nchoice> 2\nuser (ann or bob)> \nCancelled.\n\n1) the share  2) a save  3) every save  4) choose the move  5) quit\nchoice> 2\nuser (ann or bob)> cy\nType ann or bob.\n\n1) the share  2) a save  3) every save  4) choose the move  5) quit\nchoice> 2\nuser (ann or bob)> ann\npath (1 to 3)> \nCancelled.\n\n1) the share  2) a save  3) every save  4) choose the move  5) quit\nchoice> 2\nuser (ann or bob)> ann\npath (1 to 3)> 4\nType a path from 1 to 3.\n\n1) the share  2) a save  3) every save  4) choose the move  5) quit\nchoice> 2\nuser (ann or bob)> ann\npath (1 to 3)> 1\nthe move happens (never, before, between, after)> \nCancelled.\n\n1) the share  2) a save  3) every save  4) choose the move  5) quit\nchoice> 2\nuser (ann or bob)> ann\npath (1 to 3)> 1\nthe move happens (never, before, between, after)> later\nType never, before, between or after.\n\n1) the share  2) a save  3) every save  4) choose the move  5) quit\nchoice> 4\npath to move (1 to 3)> \nCancelled.\n\n1) the share  2) a save  3) every save  4) choose the move  5) quit\nchoice> 4\npath to move (1 to 3)> 9\nType a path from 1 to 3.\n\n1) the share  2) a save  3) every save  4) choose the move  5) quit\nchoice> 4\npath to move (1 to 3)> 2\npoint it at (doc-a, doc-b or doc-c)> \nCancelled.\n\n1) the share  2) a save  3) every save  4) choose the move  5) quit\nchoice> 4\npath to move (1 to 3)> 2\npoint it at (doc-a, doc-b or doc-c)> doc-z\nType doc-a, doc-b or doc-c.\n\n1) the share  2) a save  3) every save  4) choose the move  5) quit\nchoice> 4\npath to move (1 to 3)> 2\npoint it at (doc-a, doc-b or doc-c)> doc-b\nThe move is now: /reports/archive pointed at doc-b, root's document.\n\n1) the share  2) a save  3) every save  4) choose the move  5) quit\nchoice> 3\nEvery user, path and point of the move (24 saves):\n  looks the path up twice:  8 writes, 0 to a document the caller does not own\n  resolves it once:         8 writes, 0 to a document the caller does not own\n\n1) the share  2) a save  3) every save  4) choose the move  5) quit\nchoice> 5\nBye.\n",
      "interpreter": "equal"
    },
    {
      "name": "basic",
      "input": "1\n2\nann\n1\nbetween\n2\nann\n1\nbefore\n2\nann\n1\nafter\n2\nbob\n1\nnever\n3\n4\n3\ndoc-a\n3\n2\nbob\n3\nbetween\n5\n",
      "screen": "== File share ==\nDocuments reached by path, saved by a handler that looks the path up twice and one that resolves it once.\n\n1) the share  2) a save  3) every save  4) choose the move  5) quit\nchoice> 1\nThe paths and the documents they point at:\n  /reports/q3       doc-a  owned by ann\n  /reports/archive  doc-b  owned by root\n  /notes/bob        doc-c  owned by bob\nThe move an admin makes: /reports/q3 is pointed at doc-b, root's document.\n\n1) the share  2) a save  3) every save  4) choose the move  5) quit\nchoice> 2\nuser (ann or bob)> ann\npath (1 to 3)> 1\nthe move happens (never, before, between, after)> between\nann saves /reports/q3, the move between:\n  looks the path up twice:  wrote doc-b, root's document - not ann's\n  resolves it once:         wrote doc-a, ann's document\nThe check passed on doc-a and the write went to doc-b: between the two lookups the path was moved.\n\n1) the share  2) a save  3) every save  4) choose the move  5) quit\nchoice> 2\nuser (ann or bob)> ann\npath (1 to 3)> 1\nthe move happens (never, before, between, after)> before\nann saves /reports/q3, the move before:\n  looks the path up twice:  refused: not ann's document\n  resolves it once:         refused: not ann's document\n\n1) the share  2) a save  3) every save  4) choose the move  5) quit\nchoice> 2\nuser (ann or bob)> ann\npath (1 to 3)> 1\nthe move happens (never, before, between, after)> after\nann saves /reports/q3, the move after:\n  looks the path up twice:  wrote doc-a, ann's document\n  resolves it once:         wrote doc-a, ann's document\n\n1) the share  2) a save  3) every save  4) choose the move  5) quit\nchoice> 2\nuser (ann or bob)> bob\npath (1 to 3)> 1\nthe move happens (never, before, between, after)> never\nbob saves /reports/q3, the move never:\n  looks the path up twice:  refused: not bob's document\n  resolves it once:         refused: not bob's document\n\n1) the share  2) a save  3) every save  4) choose the move  5) quit\nchoice> 3\nEvery user, path and point of the move (24 saves):\n  looks the path up twice:  7 writes, 1 to a document the caller does not own\n  resolves it once:         7 writes, 0 to a document the caller does not own\n  ann saving /reports/q3 with the move between wrote doc-b, root's\nOnly a move between the check and the write does it, which is why no test that moves a path first or last can see it.\n\n1) the share  2) a save  3) every save  4) choose the move  5) quit\nchoice> 4\npath to move (1 to 3)> 3\npoint it at (doc-a, doc-b or doc-c)> doc-a\nThe move is now: /notes/bob pointed at doc-a, ann's document.\n\n1) the share  2) a save  3) every save  4) choose the move  5) quit\nchoice> 3\nEvery user, path and point of the move (24 saves):\n  looks the path up twice:  8 writes, 1 to a document the caller does not own\n  resolves it once:         8 writes, 0 to a document the caller does not own\n  bob saving /notes/bob with the move between wrote doc-a, ann's\nOnly a move between the check and the write does it, which is why no test that moves a path first or last can see it.\n\n1) the share  2) a save  3) every save  4) choose the move  5) quit\nchoice> 2\nuser (ann or bob)> bob\npath (1 to 3)> 3\nthe move happens (never, before, between, after)> between\nbob saves /notes/bob, the move between:\n  looks the path up twice:  wrote doc-a, ann's document - not bob's\n  resolves it once:         wrote doc-c, bob's document\nThe check passed on doc-c and the write went to doc-a: between the two lookups the path was moved.\n\n1) the share  2) a save  3) every save  4) choose the move  5) quit\nchoice> 5\nBye.\n",
      "interpreter": "equal"
    }
  ],
  "builtOn": [
    {
      "slug": "path-resolved-twice",
      "caseId": "302-path-resolved-twice",
      "title": "Path resolved twice — the check and the write agreed on the name, not the document"
    }
  ],
  "updated": "2026-10-18"
}
