From d8ecc510108426fba8f9a53e0d5fa54d5942e75f Mon Sep 17 00:00:00 2001
From: Helmut Grohne <helmut@subdivi.de>
Date: Thu, 15 Feb 2024 22:31:51 +0100
Subject: MountFlags: support conversion to and from a textual representation

The textual representation matches util-linux. Not all flag values can
be represented textually.
---
 linuxnamespaces/syscalls.py | 117 ++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 117 insertions(+)

(limited to 'linuxnamespaces')

diff --git a/linuxnamespaces/syscalls.py b/linuxnamespaces/syscalls.py
index 0e33a44..338e602 100644
--- a/linuxnamespaces/syscalls.py
+++ b/linuxnamespaces/syscalls.py
@@ -112,6 +112,123 @@ class MountFlags(enum.IntFlag):
 
     PROPAGATION_FLAGS = UNBINDABLE | PRIVATE | SLAVE | SHARED
 
+    # Map each flag to:
+    # * The flag value
+    # * Whether the flag value is negated
+    # * Whether the flag must be negated
+    # * Whether the flag can be negated
+    __flagstrmap = {
+        "acl": (POSIXACL, False, False, False),
+        "async": (SYNCHRONOUS, True, False, False),
+        "atime": (NOATIME, True, False, True),
+        "bind": (BIND, False, False, False),
+        "dev": (NODEV, True, False, True),
+        "diratime": (NODIRATIME, True, False, True),
+        "dirsync": (DIRSYNC, False, False, False),
+        "exec": (NOEXEC, True, False, True),
+        "iversion": (I_VERSION, False, False, True),
+        "lazytime": (LAZYTIME, False, False, True),
+        "loud": (SILENT, True, False, False),
+        "mand": (MANDLOCK, False, False, True),
+        "private": (PRIVATE, False, False, False),
+        "rbind": (BIND | REC, False, False, False),
+        "relatime": (RELATIME, False, False, True),
+        "remount": (REMOUNT, False, False, True),
+        "ro": (RDONLY, False, False, False),
+        "rprivate": (PRIVATE | REC, False, False, False),
+        "rshared": (SHARED | REC, False, False, False),
+        "rslave": (SLAVE | REC, False, False, False),
+        "runbindable": (UNBINDABLE | REC, False, False, False),
+        "rw": (RDONLY, True, False, False),
+        "shared": (SHARED, False, False, False),
+        "silent": (SILENT, False, False, False),
+        "slave": (SLAVE, False, False, False),
+        "strictatime": (STRICTATIME, False, False, True),
+        "suid": (NOSUID, True, False, True),
+        "symfollow": (NOSYMFOLLOW, True, False, True),
+        "sync": (SYNCHRONOUS, False, False, False),
+        "unbindable": (UNBINDABLE, False, False, False),
+    }
+
+    def change(self, flagsstr: str) -> "MountFlags":
+        """Return modified mount flags after applying comma-separated mount
+        flags represented as a str. Raise a ValueError if any given flag
+        does not correspond to a textual mount flag.
+        """
+        ret = self
+        for flagstr in flagsstr.split(","):
+            if not flagstr:
+                continue
+            flag, negated, mustnegate, cannegate = self.__flagstrmap.get(
+                flagstr.removeprefix("no"),
+                (MountFlags.NONE, False, True, False),
+            )
+            if mustnegate <= flagstr.startswith("no") <= cannegate:
+                if negated ^ flagstr.startswith("no"):
+                    ret &= ~flag
+                else:
+                    if flag & MountFlags.PROPAGATION_FLAGS:
+                        ret &= ~MountFlags.PROPAGATION_FLAGS
+                    ret |= flag
+            else:
+                raise ValueError(f"not a valid mount flag: {flagstr!r}")
+        return ret
+
+    @staticmethod
+    def fromstr(flagsstr: str) -> "MountFlags":
+        """Construct mount flags by changing flags according to the passed
+        flagsstr using the change method on an initial value with all flags
+        cleared.
+        """
+        return MountFlags.NONE.change(flagsstr)
+
+    __flagvals: list[tuple[int, str]] = sorted(
+        [
+            (RDONLY, "ro"),
+            (NOSUID, "nosuid"),
+            (NODEV, "nodev"),
+            (NOEXEC, "noexec"),
+            (SYNCHRONOUS, "sync"),
+            (REMOUNT, "remount"),
+            (MANDLOCK, "mand"),
+            (DIRSYNC, "dirsync"),
+            (NOSYMFOLLOW, "nosymfollow"),
+            (NOATIME, "noatime"),
+            (NODIRATIME, "nodiratime"),
+            (BIND, "bind"),
+            (BIND | REC, "rbind"),
+            (SILENT, "silent"),
+            (POSIXACL, "acl"),
+            (UNBINDABLE, "unbindable"),
+            (UNBINDABLE | REC, "runbindable"),
+            (PRIVATE, "private"),
+            (PRIVATE | REC, "rprivate"),
+            (SLAVE, "slave"),
+            (SLAVE | REC, "rslave"),
+            (SHARED, "shared"),
+            (SHARED | REC, "rshared"),
+            (RELATIME, "relatime"),
+            (I_VERSION, "iversion"),
+            (STRICTATIME, "strictatime"),
+            (LAZYTIME, "lazytime"),
+        ],
+        reverse=True
+    )
+
+    def tostr(self) -> str:
+        """Attempt to represent the flags in a comma-separated, textual way."""
+        if (self & MountFlags.PROPAGATION_FLAGS).bit_count() > 1:
+            raise ValueError("cannot represent conflicting propagtion flags")
+        parts: list[str] = []
+        remain = self
+        for val, text in MountFlags.__flagvals:
+            if remain & val == val:
+                parts.insert(0, text)
+                remain &= ~val
+        if remain:
+            raise ValueError("cannot represent flags {remain}")
+        return ",".join(parts)
+
 
 class MountSetattrFlags(enum.IntFlag):
     """This value may be supplied as flags to mount_setattr(2)."""
-- 
cgit v1.2.3