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.
This commit is contained in:
Thomas Waldmann 2025-11-04 03:05:13 +01:00
parent 6808809818
commit 798bd9ed0d
No known key found for this signature in database
GPG key ID: 243ACFA951F78E01
9 changed files with 128 additions and 92 deletions

View file

@ -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)

View file

@ -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))

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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()

View file

@ -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

View file

@ -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.

View file

@ -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)