Bläddra i källkod

refactor id <-> name lookup for monkeypatching

we can't monkeypatch stuff in Cython/C code, so we
go over python module attribute lookup.

that way, we can more easily test some functions that
internally do id<->name lookups.
Thomas Waldmann 1 månad sedan
förälder
incheckning
798bd9ed0d

+ 34 - 6
src/borg/platform/__init__.py

@@ -4,12 +4,16 @@ Platform-specific APIs.
 Public APIs are documented in platform.base.
 """
 
+from types import ModuleType
+
 from ..platformflags import is_win32, is_linux, is_freebsd, is_netbsd, is_darwin, is_cygwin
 
 from .base import ENOATTR, API_VERSION
 from .base import SaveFile, sync_dir, fdatasync, safe_fadvise
 from .base import get_process_id, fqdn, hostname, hostid
 
+platform_ug: ModuleType | None = None  # make mypy happy
+
 if is_linux:  # pragma: linux only
     from .linux import API_VERSION as OS_API_VERSION
     from .linux import listxattr, getxattr, setxattr
@@ -19,7 +23,8 @@ if is_linux:  # pragma: linux only
     from .posix import process_alive, local_pid_alive
     from .posix import swidth
     from .posix import get_errno
-    from .posix import uid2user, user2uid, gid2group, group2gid, getosusername
+    from .posix import getosusername
+    from . import posix_ug as platform_ug
 elif is_freebsd:  # pragma: freebsd only
     from .freebsd import API_VERSION as OS_API_VERSION
     from .freebsd import listxattr, getxattr, setxattr
@@ -30,7 +35,8 @@ elif is_freebsd:  # pragma: freebsd only
     from .posix import process_alive, local_pid_alive
     from .posix import swidth
     from .posix import get_errno
-    from .posix import uid2user, user2uid, gid2group, group2gid, getosusername
+    from .posix import getosusername
+    from . import posix_ug as platform_ug
 elif is_netbsd:  # pragma: netbsd only
     from .netbsd import API_VERSION as OS_API_VERSION
     from .netbsd import listxattr, getxattr, setxattr
@@ -40,7 +46,8 @@ elif is_netbsd:  # pragma: netbsd only
     from .posix import process_alive, local_pid_alive
     from .posix import swidth
     from .posix import get_errno
-    from .posix import uid2user, user2uid, gid2group, group2gid, getosusername
+    from .posix import getosusername
+    from . import posix_ug as platform_ug
 elif is_darwin:  # pragma: darwin only
     from .darwin import API_VERSION as OS_API_VERSION
     from .darwin import listxattr, getxattr, setxattr
@@ -52,7 +59,8 @@ elif is_darwin:  # pragma: darwin only
     from .posix import process_alive, local_pid_alive
     from .posix import swidth
     from .posix import get_errno
-    from .posix import uid2user, user2uid, gid2group, group2gid, getosusername
+    from .posix import getosusername
+    from . import posix_ug as platform_ug
 elif not is_win32:  # pragma: posix only
     # Generic code for all other POSIX OSes
     OS_API_VERSION = API_VERSION
@@ -63,7 +71,8 @@ elif not is_win32:  # pragma: posix only
     from .posix import process_alive, local_pid_alive
     from .posix import swidth
     from .posix import get_errno
-    from .posix import uid2user, user2uid, gid2group, group2gid, getosusername
+    from .posix import getosusername
+    from . import posix_ug as platform_ug
 else:  # pragma: win32 only
     # Win32-specific stuff
     OS_API_VERSION = API_VERSION
@@ -73,7 +82,8 @@ else:  # pragma: win32 only
     from .base import SyncFile
     from .windows import process_alive, local_pid_alive
     from .base import swidth
-    from .windows import uid2user, user2uid, gid2group, group2gid, getosusername
+    from .windows import getosusername
+    from . import windows_ug as platform_ug
 
 
 def get_birthtime_ns(st, path, fd=None):
@@ -86,3 +96,21 @@ def get_birthtime_ns(st, path, fd=None):
         return int(st.st_birthtime * 10**9)
     else:
         return None
+
+
+# have some wrapper functions, so we can monkeypatch the functions in platform_ug.
+# for normal usage from outside the platform package, always import these:
+def uid2user(uid, default=None):
+    return platform_ug._uid2user(uid, default)
+
+
+def gid2group(gid, default=None):
+    return platform_ug._gid2group(gid, default)
+
+
+def user2uid(user, default=None):
+    return platform_ug._user2uid(user, default)
+
+
+def group2gid(group, default=None):
+    return platform_ug._group2gid(group, default)

+ 3 - 3
src/borg/platform/darwin.pyx

@@ -4,7 +4,7 @@ from libc.stdint cimport uint32_t
 from libc cimport errno
 from posix.time cimport timespec
 
-from .posix import user2uid, group2gid
+from . import posix_ug
 from ..helpers import safe_decode, safe_encode
 from .xattr import _listxattr_inner, _getxattr_inner, _setxattr_inner, split_string0
 
@@ -108,10 +108,10 @@ def _remove_numeric_id_if_possible(acl):
         if entry:
             fields = entry.split(':')
             if fields[0] == 'user':
-                if user2uid(fields[2]) is not None:
+                if posix_ug._user2uid(fields[2]) is not None:
                     fields[1] = fields[3] = ''
             elif fields[0] == 'group':
-                if group2gid(fields[2]) is not None:
+                if posix_ug._group2gid(fields[2]) is not None:
                     fields[1] = fields[3] = ''
             entries.append(':'.join(fields))
     return safe_encode('\n'.join(entries))

+ 9 - 9
src/borg/platform/linux.pyx

@@ -3,7 +3,7 @@ import re
 import stat
 
 from .posix import posix_acl_use_stored_uid_gid
-from .posix import user2uid, group2gid, uid2user, gid2group
+from . import posix_ug
 from ..helpers import workarounds
 from ..helpers import safe_decode, safe_encode
 from .base import SyncFile as BaseSyncFile
@@ -99,14 +99,14 @@ def _acl_from_numeric_to_named_with_id(acl):
                     uid = int(name)
                 except ValueError:
                     uid = None
-                uname = uid2user(uid, name) if uid is not None else name
+                uname = posix_ug._uid2user(uid, name) if uid is not None else name
                 entries.append(':'.join([typ, uname, perm, str(uid if uid is not None else name)]))
             elif name and typ == 'group':
                 try:
                     gid = int(name)
                 except ValueError:
                     gid = None
-                gname = gid2group(gid, name) if gid is not None else name
+                gname = posix_ug._gid2group(gid, name) if gid is not None else name
                 entries.append(':'.join([typ, gname, perm, str(gid if gid is not None else name)]))
             else:
                 # owner, group_obj, mask, other (empty name field) stay as-is
@@ -261,9 +261,9 @@ def acl_use_local_uid_gid(acl):
         if entry:
             fields = entry.split(':')
             if fields[0] == 'user' and fields[1]:
-                fields[1] = str(user2uid(fields[1], fields[3]))
+                fields[1] = str(posix_ug._user2uid(fields[1], fields[3]))
             elif fields[0] == 'group' and fields[1]:
-                fields[1] = str(group2gid(fields[1], fields[3]))
+                fields[1] = str(posix_ug._group2gid(fields[1], fields[3]))
             entries.append(':'.join(fields[:3]))
     return safe_encode('\n'.join(entries))
 
@@ -277,9 +277,9 @@ cdef acl_append_numeric_ids(acl):
         if entry:
             type, name, permission = entry.split(':')
             if name and type == 'user':
-                entries.append(':'.join([type, name, permission, str(user2uid(name, name))]))
+                entries.append(':'.join([type, name, permission, str(posix_ug._user2uid(name, name))]))
             elif name and type == 'group':
-                entries.append(':'.join([type, name, permission, str(group2gid(name, name))]))
+                entries.append(':'.join([type, name, permission, str(posix_ug._group2gid(name, name))]))
             else:
                 entries.append(entry)
     return safe_encode('\n'.join(entries))
@@ -294,10 +294,10 @@ cdef acl_numeric_ids(acl):
         if entry:
             type, name, permission = entry.split(':')
             if name and type == 'user':
-                uid = str(user2uid(name, name))
+                uid = str(posix_ug._user2uid(name, name))
                 entries.append(':'.join([type, uid, permission, uid]))
             elif name and type == 'group':
-                gid = str(group2gid(name, name))
+                gid = str(posix_ug._group2gid(name, name))
                 entries.append(':'.join([type, gid, permission, gid]))
             else:
                 entries.append(entry)

+ 3 - 40
src/borg/platform/posix.pyx

@@ -1,8 +1,7 @@
 import errno
 import os
-import grp
-import pwd
-from functools import lru_cache
+
+from . import posix_ug
 
 from libc.errno cimport errno as c_errno
 
@@ -77,42 +76,6 @@ def local_pid_alive(pid):
         return True
 
 
-@lru_cache(maxsize=None)
-def uid2user(uid, default=None):
-    try:
-        return pwd.getpwuid(uid).pw_name
-    except KeyError:
-        return default
-
-
-@lru_cache(maxsize=None)
-def user2uid(user, default=None):
-    if not user:
-        return default
-    try:
-        return pwd.getpwnam(user).pw_uid
-    except KeyError:
-        return default
-
-
-@lru_cache(maxsize=None)
-def gid2group(gid, default=None):
-    try:
-        return grp.getgrgid(gid).gr_name
-    except KeyError:
-        return default
-
-
-@lru_cache(maxsize=None)
-def group2gid(group, default=None):
-    if not group:
-        return default
-    try:
-        return grp.getgrnam(group).gr_gid
-    except KeyError:
-        return default
-
-
 def posix_acl_use_stored_uid_gid(acl):
     """Replace the user/group field with the stored uid/gid."""
     assert isinstance(acl, bytes)
@@ -131,4 +94,4 @@ def posix_acl_use_stored_uid_gid(acl):
 def getosusername():
     """Return the OS username."""
     uid = os.getuid()
-    return uid2user(uid, uid)
+    return posix_ug._uid2user(uid, uid)

+ 39 - 0
src/borg/platform/posix_ug.py

@@ -0,0 +1,39 @@
+import grp
+import pwd
+from functools import lru_cache
+
+
+@lru_cache(maxsize=None)
+def _uid2user(uid, default=None):
+    try:
+        return pwd.getpwuid(uid).pw_name
+    except KeyError:
+        return default
+
+
+@lru_cache(maxsize=None)
+def _user2uid(user, default=None):
+    if not user:
+        return default
+    try:
+        return pwd.getpwnam(user).pw_uid
+    except KeyError:
+        return default
+
+
+@lru_cache(maxsize=None)
+def _gid2group(gid, default=None):
+    try:
+        return grp.getgrgid(gid).gr_name
+    except KeyError:
+        return default
+
+
+@lru_cache(maxsize=None)
+def _group2gid(group, default=None):
+    if not group:
+        return default
+    try:
+        return grp.getgrnam(group).gr_gid
+    except KeyError:
+        return default

+ 0 - 27
src/borg/platform/windows.pyx

@@ -1,6 +1,5 @@
 import os
 import platform
-from functools import lru_cache
 
 
 cdef extern from 'windows.h':
@@ -14,32 +13,6 @@ cdef extern from 'windows.h':
     cdef extern int PROCESS_QUERY_INFORMATION
 
 
-@lru_cache(maxsize=None)
-def uid2user(uid, default=None):
-    return "root"
-
-
-@lru_cache(maxsize=None)
-def user2uid(user, default=None):
-    if not user:
-        # user is either None or the empty string
-        return default
-    return 0
-
-
-@lru_cache(maxsize=None)
-def gid2group(gid, default=None):
-    return "root"
-
-
-@lru_cache(maxsize=None)
-def group2gid(group, default=None):
-    if not group:
-        # group is either None or the empty string
-        return default
-    return 0
-
-
 def getosusername():
     """Return the OS username."""
     return os.getlogin()

+ 33 - 0
src/borg/platform/windows_ug.py

@@ -0,0 +1,33 @@
+from functools import lru_cache
+
+
+@lru_cache(maxsize=None)
+def _uid2user(uid, default=None):
+    # On Windows, Borg uses a simplified mapping for ownership fields.
+    # Return a stable placeholder name.
+    return "root"
+
+
+@lru_cache(maxsize=None)
+def _user2uid(user, default=None):
+    if not user:
+        # user is either None or the empty string
+        return default
+    # Use 0 as the canonical uid placeholder on Windows.
+    return 0
+
+
+@lru_cache(maxsize=None)
+def _gid2group(gid, default=None):
+    # On Windows, Borg uses a simplified mapping for ownership fields.
+    # Return a stable placeholder name.
+    return "root"
+
+
+@lru_cache(maxsize=None)
+def _group2gid(group, default=None):
+    if not group:
+        # group is either None or the empty string
+        return default
+    # Use 0 as the canonical gid placeholder on Windows.
+    return 0

+ 1 - 1
src/borg/testsuite/archive_test.py

@@ -379,7 +379,7 @@ def test_get_item_uid_gid():
     assert gid == 8
 
     if not is_win32:
-        # Due to the hack in borg.platform.windows, user2uid/group2gid always return 0
+        # Due to the hack in borg.platform.windows_ug, user2uid/group2gid always return 0
         # (no matter which username we ask for), and they never raise a KeyError (e.g., for
         # a non-existing user/group name). Thus, these tests can currently not succeed on win32.
 

+ 6 - 6
src/borg/testsuite/platform/linux_test.py

@@ -129,7 +129,7 @@ def test_numeric_to_named_with_id_simple(monkeypatch):
     from ...platform.linux import _acl_from_numeric_to_named_with_id
 
     # Pretend uid 1000 -> 'alice', gid 100 -> 'staff'
-    from ...platform import posix
+    from ...platform import platform_ug
 
     def _uid2user(uid, default=None):
         if uid == 1000:
@@ -141,8 +141,8 @@ def test_numeric_to_named_with_id_simple(monkeypatch):
             return "staff"
         return default
 
-    monkeypatch.setattr(posix, "uid2user", _uid2user)
-    monkeypatch.setattr(posix, "gid2group", _gid2group)
+    monkeypatch.setattr(platform_ug, "_uid2user", _uid2user)
+    monkeypatch.setattr(platform_ug, "_gid2group", _gid2group)
 
     src = b"\n".join([b"user::rwx", b"user:1000:r-x", b"group::r--", b"group:100:r--", b"mask::r-x", b"other::r--"])
     out = _acl_from_numeric_to_named_with_id(src)
@@ -159,7 +159,7 @@ def test_numeric_to_named_with_id_nonexistent_ids(monkeypatch):
     from ...platform.linux import _acl_from_numeric_to_named_with_id
 
     # Map functions return default (the given fallback), so names stay numeric but still append the fourth field
-    from ...platform import posix
+    from ...platform import platform_ug
 
     def _uid2user(uid, default=None):
         return default
@@ -167,8 +167,8 @@ def test_numeric_to_named_with_id_nonexistent_ids(monkeypatch):
     def _gid2group(gid, default=None):
         return default
 
-    monkeypatch.setattr(posix, "uid2user", _uid2user)
-    monkeypatch.setattr(posix, "gid2group", _gid2group)
+    monkeypatch.setattr(platform_ug, "_uid2user", _uid2user)
+    monkeypatch.setattr(platform_ug, "_gid2group", _gid2group)
 
     src = b"user:9999:r--\ngroup:8888:r--\n"
     out = _acl_from_numeric_to_named_with_id(src)