From 798bd9ed0d8c7ed1d91c154fab6503bd1c7722fc Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 4 Nov 2025 03:05:13 +0100 Subject: [PATCH] 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. --- src/borg/platform/__init__.py | 40 +++++++++++++++++---- src/borg/platform/darwin.pyx | 6 ++-- src/borg/platform/linux.pyx | 18 +++++----- src/borg/platform/posix.pyx | 43 ++--------------------- src/borg/platform/posix_ug.py | 39 ++++++++++++++++++++ src/borg/platform/windows.pyx | 27 -------------- src/borg/platform/windows_ug.py | 33 +++++++++++++++++ src/borg/testsuite/archive_test.py | 2 +- src/borg/testsuite/platform/linux_test.py | 12 +++---- 9 files changed, 128 insertions(+), 92 deletions(-) create mode 100644 src/borg/platform/posix_ug.py create mode 100644 src/borg/platform/windows_ug.py diff --git a/src/borg/platform/__init__.py b/src/borg/platform/__init__.py index f73f616d3..d436b67dc 100644 --- a/src/borg/platform/__init__.py +++ b/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) diff --git a/src/borg/platform/darwin.pyx b/src/borg/platform/darwin.pyx index a1af12582..4f63607c3 100644 --- a/src/borg/platform/darwin.pyx +++ b/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)) diff --git a/src/borg/platform/linux.pyx b/src/borg/platform/linux.pyx index 6a129a6b5..3e4cec014 100644 --- a/src/borg/platform/linux.pyx +++ b/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) diff --git a/src/borg/platform/posix.pyx b/src/borg/platform/posix.pyx index 6ffa6e78e..835c15c1d 100644 --- a/src/borg/platform/posix.pyx +++ b/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) diff --git a/src/borg/platform/posix_ug.py b/src/borg/platform/posix_ug.py new file mode 100644 index 000000000..3260e96ba --- /dev/null +++ b/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 diff --git a/src/borg/platform/windows.pyx b/src/borg/platform/windows.pyx index 74ed8a343..714b8b952 100644 --- a/src/borg/platform/windows.pyx +++ b/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() diff --git a/src/borg/platform/windows_ug.py b/src/borg/platform/windows_ug.py new file mode 100644 index 000000000..c94e55ac8 --- /dev/null +++ b/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 diff --git a/src/borg/testsuite/archive_test.py b/src/borg/testsuite/archive_test.py index 97a2ef71c..f05b22dd6 100644 --- a/src/borg/testsuite/archive_test.py +++ b/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. diff --git a/src/borg/testsuite/platform/linux_test.py b/src/borg/testsuite/platform/linux_test.py index b7be372a6..3d7de871c 100644 --- a/src/borg/testsuite/platform/linux_test.py +++ b/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)