Skip to content

File entries (entry)

FileEntry records and the functions that build, resolve and apply them.

pkgforge.entry.FileType

Bases: str, Enum

The three on-disk kinds pkgforge stages/records: File, Directory, Symlink. :meth:from_path looks one up from a file on disk.

Directory = 'directory' class-attribute instance-attribute

File = 'file' class-attribute instance-attribute

from_path(path) classmethod

Source code in src/pkgforge/entry.py
103
104
105
106
107
108
109
110
111
112
@classmethod
def from_path(cls, path: Path) -> FileType:
    try:
        st = path.lstat()
    except FileNotFoundError:
        raise TypeError(
            f"{path}: not a regular file, directory or symlink "
            "(missing or special file)"
        ) from None
    return _file_type(path, st)

pkgforge.entry.FileKind

Bases: str, Enum

How packaging treats an entry, whatever the package format.

  • File: an ordinary payload file (the default);
  • Config: a configuration file the package manager keeps when the administrator edited it and the package ships a new one;
  • ConfigNoReplace: a configuration file whose edited copy stays in place, the package's new one is written beside it;
  • Doc: documentation, which a minimal install may leave out;
  • License: a license text;
  • Ghost: a path the package owns but does not ship, created at run time.

Config, ConfigNoReplace, Doc and License need a regular file; File and Ghost fit every type.

Config = 'config' class-attribute instance-attribute

ConfigNoReplace = 'config-noreplace' class-attribute instance-attribute

Doc = 'doc' class-attribute instance-attribute

File = 'file' class-attribute instance-attribute

Ghost = 'ghost' class-attribute instance-attribute

License = 'license' class-attribute instance-attribute

pkgforge.entry.FileEntry

Bases: TypedDict

One DB record: mode (an octal permission string, e.g. "644", never a raw st_mode int), owner, group, type, kind (a :class:FileKind value; "-" only before it is resolved), caps (one setcap clause, or "-" for none), package (the package the entry ships in; "-" only when the configuration has no [package]) and a free-form meta string map. A plain dict at runtime (a TypedDict carries no methods) -- use the module functions below.

caps instance-attribute

group instance-attribute

kind instance-attribute

meta instance-attribute

mode instance-attribute

owner instance-attribute

package instance-attribute

type instance-attribute

pkgforge.entry.entry_from_path(path, meta=None)

Build a :class:FileEntry by lstat-ing a real path on disk.

Source code in src/pkgforge/entry.py
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
def entry_from_path(
    path: Path, meta: typing.Optional[typing.Dict[str, str]] = None
) -> FileEntry:
    """Build a :class:`FileEntry` by ``lstat``-ing a real path on disk."""
    base: FileEntry = {
        "mode": AUTO,
        "owner": AUTO,
        "group": AUTO,
        "type": AUTO,
        "kind": FileKind.File.value,
        "caps": DEFAULT,
        "package": DEFAULT,
        "meta": {} if meta is None else meta,
    }
    return _resolve_stat(base, path, path.lstat(), lookupval=AUTO)

pkgforge.entry.resolve_entry(entry, path, lookupval=AUTO, **overwrite)

Replace every field of entry equal to lookupval with the on-disk value -- a single lstat, and a pwd/grp name lookup only for owner/group fields that actually equal lookupval.

Source code in src/pkgforge/entry.py
431
432
433
434
435
436
437
438
439
def resolve_entry(
    entry: FileEntry, path: Path, lookupval: str = AUTO, **overwrite: typing.Any
) -> FileEntry:
    """Replace every field of ``entry`` equal to ``lookupval`` with the
    on-disk value -- a single ``lstat``, and a ``pwd``/``grp`` name lookup
    only for ``owner``/``group`` fields that actually equal ``lookupval``."""
    resolved = _resolve_stat(entry, path, path.lstat(), lookupval)
    resolved.update(overwrite)
    return resolved

pkgforge.entry.apply_entry(entry, path, chown=False, *, logger=None, usedefault=DEFAULT)

Apply entry's owner/group (if chown) and then mode to path.

Order matters: on Linux, chown() of a regular file clears any setuid/setgid bit even when the owner doesn't change and even as root, so chown runs first and mode is applied last (or, when the entry leaves mode at usedefault, the pre-chown special bits are restored). A symlink's mode is never set on disk -- Linux ignores it -- but it is still recorded in entry.

Source code in src/pkgforge/entry.py
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
def apply_entry(
    entry: FileEntry,
    path: Path,
    chown: bool = False,
    *,
    logger: typing.Optional[logging.Logger] = None,
    usedefault: str = DEFAULT,
) -> None:
    """Apply ``entry``'s owner/group (if ``chown``) and then mode to ``path``.

    Order matters: on Linux, ``chown()`` of a regular file clears any
    setuid/setgid bit even when the owner doesn't change and even as root, so
    chown runs first and mode is applied last (or, when the entry leaves mode
    at ``usedefault``, the pre-chown special bits are restored). A symlink's
    mode is never set on disk -- Linux ignores it -- but it is still recorded
    in ``entry``.
    """
    mode = _or_default(entry["mode"])
    owner = _or_default(entry["owner"])
    group = _or_default(entry["group"])

    if mode == AUTO:
        raise UsageError(
            "entry mode is unresolved (AUTO); call resolve_entry before apply_entry"
        )

    is_symlink = stat.S_ISLNK(os.lstat(path).st_mode)
    restore_mode = None

    if chown and (owner != usedefault or group != usedefault):
        if pwd is None or grp is None:
            raise RuntimeError("chown requires the Unix pwd/grp modules")
        try:
            uid = -1 if owner == usedefault else pwd.getpwnam(owner).pw_uid
        except KeyError:
            raise UsageError(f"unknown owner {owner!r}") from None
        try:
            gid = -1 if group == usedefault else grp.getgrnam(group).gr_gid
        except KeyError:
            raise UsageError(f"unknown group {group!r}") from None
        if not is_symlink and mode == usedefault:
            # The entry isn't setting an explicit mode, so chown() below
            # would otherwise silently drop any setuid/setgid/sticky bit
            # already on disk; capture it here to restore below.
            current = stat.S_IMODE(os.lstat(path).st_mode)
            if current & (stat.S_ISUID | stat.S_ISGID | stat.S_ISVTX):
                restore_mode = current
        if logger:
            logger.debug("Setting owner/group for %s to %s:%s", path, uid, gid)
        os.chown(path, uid, gid, follow_symlinks=False)

    if mode != usedefault:
        if is_symlink:
            if logger:
                logger.debug("Not setting mode on symlink %s (Linux ignores it)", path)
        else:
            mode_int = int(mode, 8)
            if logger:
                logger.debug("Setting mode for %s to %o", path, mode_int)
            if os.chmod in os.supports_follow_symlinks:
                os.chmod(path, mode_int, follow_symlinks=False)
            else:
                # Plain chmod(2): where CPython does not advertise
                # follow_symlinks for chmod (Linux), passing it raises
                # NotImplementedError for every path.
                os.chmod(path, mode_int)
    elif restore_mode is not None:
        if logger:
            logger.debug("Restoring mode %o for %s after chown", restore_mode, path)
        os.chmod(path, restore_mode)

pkgforge.entry.normalize_mode(value)

Normalize a mode value to an octal permission string.

-/"" mean :data:DEFAULT; -- and auto (mode only -- owner/group get no such alias, since auto can be a real account name) mean :data:AUTO. A string of 1-4 octal digits is normalized ("0644" -> "644"). Anything else raises :class:UsageError.

Source code in src/pkgforge/entry.py
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
def normalize_mode(value: str) -> str:
    """Normalize a ``mode`` value to an octal permission string.

    ``-``/``""`` mean
    :data:`DEFAULT`; ``--`` and ``auto`` (mode only -- ``owner``/``group`` get
    no such alias, since ``auto`` can be a real account name) mean
    :data:`AUTO`. A string of 1-4 octal digits is
    normalized (``"0644"`` -> ``"644"``). Anything else raises
    :class:`UsageError`.
    """
    value = _normalize_field(value)
    if value == AUTO or value == "auto":
        return AUTO
    if value == DEFAULT:
        return DEFAULT
    if isinstance(value, str) and re.fullmatch(r"[0-7]{1,4}", value):
        return mode_to_octal(int(value, 8))
    raise UsageError(
        f"invalid mode {value!r}: expected 1-4 octal digits, '-', '--' or 'auto'"
    )

pkgforge.entry.normalize_kind(value)

Normalize a kind value: -/"" mean :data:DEFAULT (unset), one of the :class:FileKind values is returned as text. Anything else raises :class:UsageError.

Source code in src/pkgforge/entry.py
239
240
241
242
243
244
245
246
247
248
def normalize_kind(value: str) -> str:
    """Normalize a ``kind`` value: ``-``/``""`` mean :data:`DEFAULT` (unset),
    one of the :class:`FileKind` values is returned as text.
    Anything else raises :class:`UsageError`."""
    if isinstance(value, FileKind):
        return value.value
    value = _normalize_field(value)
    if value == DEFAULT or value in _KINDS:
        return value
    raise UsageError(f"invalid kind {value!r}: choose from {', '.join(_KINDS)}")

pkgforge.entry.normalize_caps(value)

Normalize a caps value: -/"" mean :data:DEFAULT (none), otherwise one clause of libcap's text form, lowercase with no space (cap_net_bind_service=ep, cap_net_raw,cap_net_admin+ep). Anything else raises :class:UsageError.

Source code in src/pkgforge/entry.py
251
252
253
254
255
256
257
258
259
260
261
262
263
264
def normalize_caps(value: str) -> str:
    """Normalize a ``caps`` value: ``-``/``""`` mean :data:`DEFAULT` (none),
    otherwise one clause of libcap's text form, lowercase with no space
    (``cap_net_bind_service=ep``, ``cap_net_raw,cap_net_admin+ep``).
    Anything else raises :class:`UsageError`."""
    value = _normalize_field(value)
    if value == DEFAULT:
        return value
    if isinstance(value, str) and _CAPS_RE.fullmatch(value):
        return value
    raise UsageError(
        f"invalid caps {value!r}: expected one lowercase setcap clause "
        "without spaces, such as cap_net_bind_service=ep"
    )

pkgforge.entry.normalize_package(value)

Normalize a package value: -/"" mean :data:DEFAULT (unset), otherwise a package name ([a-z0-9][a-z0-9+.-]+). Anything else raises :class:UsageError.

Source code in src/pkgforge/entry.py
267
268
269
270
271
272
273
274
275
276
277
def normalize_package(value: str) -> str:
    """Normalize a ``package`` value: ``-``/``""`` mean :data:`DEFAULT` (unset),
    otherwise a package name (``[a-z0-9][a-z0-9+.-]+``).
    Anything else raises :class:`UsageError`."""
    value = _normalize_field(value)
    if isinstance(value, str) and _PACKAGE_RE.fullmatch(value):
        return value
    raise UsageError(
        f"invalid package {value!r}: expected a package name "
        "([a-z0-9][a-z0-9+.-]+) or '-'"
    )

pkgforge.entry.mode_to_octal(mode)

Render a stat st_mode as a bare octal permission string (e.g. "644").

Source code in src/pkgforge/entry.py
192
193
194
def mode_to_octal(mode: int) -> str:
    """Render a stat ``st_mode`` as a bare octal permission string (e.g. ``"644"``)."""
    return format(mode & 0o7777, "o")

pkgforge.entry.BASE_USERS = frozenset('root daemon bin sys sync games man lp mail news uucp proxy www-data backup list irc _apt nobody'.split()) module-attribute

pkgforge.entry.BASE_GROUPS = frozenset('root daemon bin sys adm tty disk lp mail news uucp man proxy kmem dialout fax voice cdrom floppy tape sudo audio dip www-data backup operator list irc src shadow utmp video sasl plugdev staff games users nogroup'.split()) module-attribute

pkgforge.entry.AUTO = '--' module-attribute

pkgforge.entry.DEFAULT = '-' module-attribute