Files
T
orrisroot a2bc9bb642 feat: build access level labels from their bits and project names
Labels came from a fixed table matched by subset, so Storage (no bits)
matched any level the table lacked, and a level was named after the first
entry it contained: the server's Brain/MINDS 2.0 folders (0x10000) showed
as Storage and 0x10104 as CBS or PW Open.

Labels are now composed from the bits. Projects (bit 16 and up) take the
names the server reports to their members through users/current/, and
otherwise Project<n>; restrict slot 1 is CBS and any other Restrict<n>.
A level with no meaning is shown as Unknown (0x...). chacl reads keys
built the same way, refuses public and storage before any request, and
lists the user's projects with their keys when given an invalid level.
2026-11-13 16:53:25 +09:00

150 lines
5.4 KiB
Python

import re
from typing import Any, Final, Mapping
from unicodedata import normalize
from pydantic.dataclasses import dataclass
from mdrsclient.models.utils import iso8601_to_user_friendly
class FolderAccessLevel:
# Bit Mask
# - bit 0: Is Private
# - bit 1: Is Public
# - bit 2: With Password
# - bit 3-7: (Reserved)
# - bit 8-15: Restricted Open, one bit per restrict network slot (slot 1 is bit 8)
# - bit 16-: Project Open, one bit per project bit_position (bit_position 0 is bit 16)
STORAGE: Final[int] = 0x0000
PRIVATE: Final[int] = 0x0001
PUBLIC: Final[int] = 0x0002
PASSWORD: Final[int] = 0x0004
RESERVED: Final[int] = 0x00F8
RESTRICT_SHIFT: Final[int] = 8
RESTRICT_SLOTS: Final[int] = 8
PROJECT_SHIFT: Final[int] = 16
# The server keeps no names for its restrict networks, so the one in use is named here.
# Any other slot is named after its number.
RESTRICT_NAMES: Final[dict[int, str]] = {1: "CBS"}
LEVEL_NAMES: Final[dict[int, str]] = {STORAGE: "Storage", PRIVATE: "Private", PUBLIC: "Public"}
@staticmethod
def restrict_name(slot: int) -> str:
return FolderAccessLevel.RESTRICT_NAMES.get(slot, f"Restrict{slot}")
@staticmethod
def project_name(bit_position: int, projects: Mapping[int, str] | None = None) -> str:
name = (projects or {}).get(bit_position)
return name if name else f"Project{bit_position}"
@staticmethod
def id2label(id: int, projects: Mapping[int, str] | None = None) -> str | None:
"""
Label an access level from its bits, e.g. 0x0104 is "CBS or PW Open".
`projects` names the projects by bit_position, as the server tells their members;
any other project gets a name made from its bit_position. None for a level that
has no meaning: Private or Public with any other bit, or a reserved bit.
"""
if id in FolderAccessLevel.LEVEL_NAMES:
return FolderAccessLevel.LEVEL_NAMES[id]
if id < 0 or id & (FolderAccessLevel.PRIVATE | FolderAccessLevel.PUBLIC | FolderAccessLevel.RESERVED):
return None
names = [
FolderAccessLevel.restrict_name(slot)
for slot in range(1, FolderAccessLevel.RESTRICT_SLOTS + 1)
if id & (1 << (FolderAccessLevel.RESTRICT_SHIFT + slot - 1))
]
project_bits = id >> FolderAccessLevel.PROJECT_SHIFT
names += [
FolderAccessLevel.project_name(bit, projects)
for bit in range(project_bits.bit_length())
if project_bits & (1 << bit)
]
if id & FolderAccessLevel.PASSWORD:
names.append("PW")
return " or ".join(names) + " Open"
@staticmethod
def key2id(key: str) -> int | None:
"""
The access level a key names: `private`, `public`, `storage`, or the parts of an
open level joined by `_or_` and ending in `_open`, e.g. `cbs_or_pw_open`. A part
is `pw`, `cbs`, `restrict<slot>` or `project<bit_position>`.
"""
fixed = {label.lower(): id for id, label in FolderAccessLevel.LEVEL_NAMES.items()}
if key in fixed:
return fixed[key]
if not key.endswith("_open"):
return None
id = 0
for part in key.removesuffix("_open").split("_or_"):
bit = FolderAccessLevel._part2bit(part)
if bit is None:
return None
id |= bit
return id
@staticmethod
def _part2bit(part: str) -> int | None:
if part == "pw":
return FolderAccessLevel.PASSWORD
slots = {name.lower(): slot for slot, name in FolderAccessLevel.RESTRICT_NAMES.items()}
match = re.fullmatch(r"restrict([1-9][0-9]*)", part)
slot = slots.get(part, int(match.group(1)) if match else None)
if slot is not None:
if slot > FolderAccessLevel.RESTRICT_SLOTS:
return None
return 1 << (FolderAccessLevel.RESTRICT_SHIFT + slot - 1)
match = re.fullmatch(r"project(0|[1-9][0-9]*)", part)
if match:
return 1 << (FolderAccessLevel.PROJECT_SHIFT + int(match.group(1)))
return None
@dataclass(frozen=True)
class FolderSimple:
id: str
pid: str | None
name: str
access_level: int
lock: bool
size: int
laboratory_id: int
description: str
created_at: str
updated_at: str
restrict_opened_at: str | None
def access_level_label(self, projects: Mapping[int, str] | None = None) -> str:
"""The access level as shown to a user, naming the projects given by bit_position."""
label = FolderAccessLevel.id2label(self.access_level, projects)
return label if label is not None else f"Unknown (0x{self.access_level:x})"
@property
def access_level_name(self) -> str:
return self.access_level_label()
@property
def lock_name(self) -> str:
return "locked" if self.lock else "unlocked"
@property
def created_at_name(self) -> str:
return iso8601_to_user_friendly(self.created_at)
@property
def updated_at_name(self) -> str:
return iso8601_to_user_friendly(self.updated_at)
@dataclass(frozen=True)
class Folder(FolderSimple):
metadata: list[dict[str, Any]]
sub_folders: list[FolderSimple]
path: str
def find_sub_folder(self, name: str) -> FolderSimple | None:
_name = normalize("NFC", name).lower()
return next((x for x in self.sub_folders if x.name.lower() == _name), None)