Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/bindings-c.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ on:
types: [ published ]
schedule:
- cron: '0 0 * * *'
workflow_dispatch:

name: bindings-c

Expand Down
1 change: 1 addition & 0 deletions .github/workflows/bindings-go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ on:
types: [ published ]
schedule:
- cron: '0 0 * * *'
workflow_dispatch:

name: bindings-go

Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/bindings-python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ on:
types: [ published ]
schedule:
- cron: '0 0 * * *'
workflow_dispatch:

env:
PYTHON_DIST: ${{ github.workspace }}/.tmp/python3-pathrs-${{ github.run_id }}-${{ github.run_attempt }}
Expand Down Expand Up @@ -60,7 +61,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.x"]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.x"]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
Expand Down Expand Up @@ -105,7 +106,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.x"]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.x"]
needs:
- build-pyproject
runs-on: ubuntu-latest
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ on:
types: [ published ]
schedule:
- cron: '0 0 * * *'
workflow_dispatch:

name: e2e-tests

Expand All @@ -41,7 +42,7 @@ jobs:
lang-desc: [""]
include:
# Test minimum python version.
# TODO: Switch to python 3.9 (pathrs bindings version).
# TODO: Switch to python 3.10 (pathrs bindings version).
# typing.Self: python >= 3.11
# match: python >= 3.10
- lang: python
Expand Down
23 changes: 21 additions & 2 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ on:
types: [ published ]
schedule:
- cron: '0 0 * * *'
workflow_dispatch:

name: rust-ci

Expand Down Expand Up @@ -382,9 +383,18 @@ jobs:
# FIXME: llvm-cov appears to have some kind of bug with
# --nextest-archive-file as they do not strip the "target" prefix from
# the nextest archive. As a workaround, we just extract it ourselves.
#
# FIXME: Cargo's v2 build-dir layout (enabled since nightly-2026-07-30)
# causes issues because while both nextest and cargo-llvm-cov support it,
# cargo-llvm-cov depends on a magic "fingerprint" directory being present
# to detect v2 build-dir usage but nextest does not include in its
# archives. We can workaround it for now by creating the directory
# ourselves. See <https://github.com/taiki-e/cargo-llvm-cov/issues/520>.
- name: extract nextest archive
run: >-
run: |-
tar xv -f nextest-pathrs-${{ matrix.run-as }}.tar.zst -C target/llvm-cov-target/ --strip-components=1
# mkdir -p ./target/llvm-cov-target/debug/build/<crate>/<hash>/fingerprint
find target/llvm-cov-target/debug/build -mindepth 2 -maxdepth 2 -type d -exec mkdir -p '{}/fingerprint' ';'
# Upload to CodeCov.
- name: generate codecov-friendly coverage
id: codecov-coverage
Expand Down Expand Up @@ -562,9 +572,18 @@ jobs:
# FIXME: llvm-cov appears to have some kind of bug with
# --nextest-archive-file as they do not strip the "target" prefix from
# the nextest archive. As a workaround, we just extract it ourselves.
#
# FIXME: Cargo's v2 build-dir layout (enabled since nightly-2026-07-30)
# causes issues because while both nextest and cargo-llvm-cov support it,
# cargo-llvm-cov depends on a magic "fingerprint" directory being present
# to detect v2 build-dir usage but nextest does not include in its
# archives. We can workaround it for now by creating the directory
# ourselves. See <https://github.com/taiki-e/cargo-llvm-cov/issues/520>.
- name: extract nextest archive
run: >-
run: |-
tar xv -f nextest-pathrs-root.tar.zst -C target/llvm-cov-target/ --strip-components=1
# mkdir -p ./target/llvm-cov-target/debug/build/<crate>/<hash>/fingerprint
find target/llvm-cov-target/debug/build -mindepth 2 -maxdepth 2 -type d -exec mkdir -p '{}/fingerprint' ';'

- name: calculate coverage
run: cargo llvm-cov report
Expand Down
2 changes: 1 addition & 1 deletion .rustfmt.toml
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ match_block_trailing_comma = false
blank_lines_upper_bound = 1
blank_lines_lower_bound = 0
edition = "2021"
version = "One"
style_edition = "2021"
inline_attribute_width = 0
merge_derives = true
use_try_shorthand = false
Expand Down
8 changes: 4 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -111,18 +111,18 @@ RUN CARGO_BINSTALL_VERSION="$CARGO_BINSTALL_VERSION" \
curl -L --proto '=https' --tlsv1.2 -sSf \
"https://raw.githubusercontent.com/cargo-bins/cargo-binstall/v$CARGO_BINSTALL_VERSION/install-from-binstall-release.sh" | bash

ARG CARGO_LLVM_COV_VERSION=0.8.7
ARG CARGO_LLVM_COV_VERSION=0.9.0
ARG CARGO_HACK_VERSION=0.6.45
ARG CARGO_NEXTEST_VERSION=0.9.137
ARG CARGO_NEXTEST_VERSION=0.9.143
RUN cargo binstall --no-confirm \
"cargo-llvm-cov@$CARGO_LLVM_COV_VERSION" \
"cargo-hack@$CARGO_HACK_VERSION" \
"cargo-nextest@$CARGO_NEXTEST_VERSION"

ARG RUST_NIGHTLY=nightly-2026-06-03
RUN rustup toolchain install "$RUST_NIGHTLY" && \
rustup component add llvm-tools llvm-tools-preview && \
rustup component add --toolchain "$RUST_NIGHTLY" llvm-tools llvm-tools-preview
rustup component add llvm-tools && \
rustup component add --toolchain "$RUST_NIGHTLY" llvm-tools
ENV CARGO_NIGHTLY="cargo +$RUST_NIGHTLY"

# We want the installed libpathrs library for the Python and Go tests.
Expand Down
37 changes: 19 additions & 18 deletions build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@ fn main() {
};
if is_cdylib {
let name = "pathrs";
// TODO: Since we use symbol versioning, it seems quite unlikely that we
// would ever bump the major version in the SONAME, so we should
// probably hard-code this or define it elsewhere.
// TODO: Since we use symbol versioning, it seems quite unlikely
// that we would ever bump the major version in the SONAME, so we
// should probably hard-code this or define it elsewhere.
let major = env::var("CARGO_PKG_VERSION_MAJOR").unwrap();
println!("cargo:rustc-cdylib-link-arg=-Wl,-soname,lib{name}.so.{major}");

Expand All @@ -76,9 +76,9 @@ fn main() {
.expect("mktemp should be utf-8 safe string");
writeln!(
version_script_file,
// All of the symbol versions are done with in-line .symver entries.
// This version script is only needed to define the version nodes
// (and their dependencies).
// All of the symbol versions are done with in-line .symver
// entries. This version script is only needed to define the
// version nodes (and their dependencies).
// FIXME: "local" doesn't appear to actually hide symbols in the
// output .so. For more information about getting all of this to
// work nicely, see <https://internals.rust-lang.org/t/23626>.
Expand All @@ -91,20 +91,21 @@ fn main() {
.expect("write version script");
println!("cargo:rustc-cdylib-link-arg=-Wl,--version-script={version_script_path}");

// The above version script (and our .symver setup) conflicts with the
// version script and options used by Rust when linking with GNU ld.
// Thankfully, lld Just Works(TM) out of the box so we can use it.
// The above version script (and our .symver setup) conflicts with
// the version script and options used by Rust when linking with GNU
// ld. Thankfully, lld Just Works(TM) out of the box so we can use
// it.
//
// Rust 1.90 switched to lld by default for x86, but for older versions
// and other architectures it is necessary to specify the linker as lld
// (there was also a rustflag for this but it was unstable until Rust
// 1.90).
// Rust 1.90 switched to lld by default for x86, but for older
// versions and other architectures it is necessary to specify the
// linker as lld (there was also a rustflag for this but it was
// unstable until Rust 1.90).
//
// Unfortunately, while there are some clever tricks you could use for
// GNU ld (such as writing an ld wrapper and executing it with "cc -B"),
// doing so produces useless symbol versions so it's better to just
// require lld. Debian bullseye and later all have lld, so this is a
// non-issue for packagers.
// Unfortunately, while there are some clever tricks you could use
// for GNU ld (such as writing an ld wrapper and executing it with
// "cc -B"), doing so produces useless symbol versions so it's
// better to just require lld. Debian bullseye and later all have
// lld, so this is a non-issue for packagers.
println!("cargo:rustc-cdylib-link-arg=-fuse-ld=lld");
}
}
Expand Down
6 changes: 3 additions & 3 deletions contrib/bindings/python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@ RENAME_EXCHANGE = 0x2

with pathrs.Root("/path/to/rootfs") as root:
# symlink
root.symlink("foo", "bar") # foo -> bar
root.symlink("foo", "bar") # foo -> bar
# link
root.hardlink("a", "b") # a -> b
root.hardlink("a", "b") # a -> b
# rename(at2)
root.rename("foo", "b", flags=RENAME_EXCHANGE) # foo <-> b
root.rename("foo", "b", flags=RENAME_EXCHANGE) # foo <-> b
# open(O_CREAT)
with root.creat("newfile", "w+") as f:
f.write("Some contents.")
Expand Down
3 changes: 1 addition & 2 deletions contrib/bindings/python/pathrs/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
#!/usr/bin/python3
# SPDX-License-Identifier: MPL-2.0
#
# libpathrs: safe path resolution on Linux
Expand All @@ -13,7 +12,7 @@
import importlib.metadata

from . import _pathrs
from ._pathrs import * # noqa: F403 # We just re-export everything.
from ._pathrs import * # We just re-export everything.

# In order get pydoc to include the documentation for the re-exported code from
# _pathrs, we need to include all of the members in __all__. Rather than
Expand Down
52 changes: 25 additions & 27 deletions contrib/bindings/python/pathrs/_internal.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
#!/usr/bin/python3
# SPDX-License-Identifier: MPL-2.0
#
# libpathrs: safe path resolution on Linux
Expand All @@ -9,19 +8,18 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.

import io
import os
import sys
import copy
import errno
import fcntl

import io
import os
import sys
import typing
from types import TracebackType
from typing import Any, Dict, IO, Optional, TextIO, Type, TypeVar, Union
from typing import IO, Any, ClassVar, TextIO, TypeAlias, TypeVar

# TODO: Remove this once we only support Python >= 3.11.
from typing_extensions import Self, TypeAlias
from typing_extensions import Self

from ._libpathrs_cffi import lib as libpathrs_so

Expand Down Expand Up @@ -52,7 +50,7 @@ def _pystr(cstr: CString) -> str:


def _cbuffer(size: int) -> CBuffer:
return ffi.new("char[%d]" % (size,))
return ffi.new(f"char[{size}]")


def _is_pathrs_err(ret: int) -> bool:
Expand All @@ -68,10 +66,10 @@ class PathrsError(Exception):
"""

message: str
errno: Optional[int]
strerror: Optional[str]
errno: int | None
strerror: str | None

def __init__(self, message: str, /, *, errno: Optional[int] = None):
def __init__(self, message: str, /, *, errno: int | None = None):
# Construct Exception.
super().__init__(message)

Expand All @@ -88,7 +86,7 @@ def __init__(self, message: str, /, *, errno: Optional[int] = None):
self.strerror = str(errno)

@classmethod
def _fetch(cls, err_id: int, /) -> Optional[Self]:
def _fetch(cls, err_id: int, /) -> Self | None:
if err_id >= 0:
return None

Expand All @@ -109,19 +107,19 @@ def __str__(self) -> str:
if self.errno is None:
return self.message
else:
return "%s (%s)" % (self.message, self.strerror)
return f"{self.message} ({self.strerror})"

def __repr__(self) -> str:
return "Error(%r, errno=%r)" % (self.message, self.errno)
return f"Error({self.message!r}, errno={self.errno!r})"

def pprint(self, out: TextIO = sys.stdout) -> None:
"Pretty-print the error to the given @out file."
# Basic error information.
if self.errno is None:
print("pathrs error:", file=out)
else:
print("pathrs error [%s]:" % (self.strerror,), file=out)
print(" %s" % (self.message,), file=out)
print(f"pathrs error [{self.strerror}]:", file=out)
print(f" {self.message}", file=out)


INTERNAL_ERROR = PathrsError("tried to fetch libpathrs error but no error found")
Expand All @@ -131,7 +129,7 @@ class FilenoFile(typing.Protocol):
def fileno(self) -> int: ...


FileLike = Union[FilenoFile, int]
FileLike = FilenoFile | int


def _fileno(file: FileLike) -> int:
Expand All @@ -151,7 +149,7 @@ def _clonefile(file: FileLike) -> int:
Fd = TypeVar("Fd", bound="WrappedFd")


class WrappedFd(object):
class WrappedFd:
"""
Represents a file descriptor that allows for manual lifetime management,
unlike os.fdopen() which are tracked by the GC with no way of "leaking" the
Expand All @@ -160,7 +158,7 @@ class WrappedFd(object):
pathrs will return WrappedFds for most operations that return an fd.
"""

_fd: Optional[int]
_fd: int | None

def __init__(self, file: FileLike, /):
"""
Expand Down Expand Up @@ -233,12 +231,12 @@ def fdopen(self, mode: str = "r") -> IO[Any]:
raise

@classmethod
def from_raw_fd(cls: Type[Fd], fd: int, /) -> Fd:
def from_raw_fd(cls, fd: int, /) -> Self:
"Shorthand for WrappedFd(fd)."
return cls(fd)

@classmethod
def from_file(cls: Type[Fd], file: FileLike, /) -> Fd:
def from_file(cls, file: FileLike, /) -> Self:
"Shorthand for WrappedFd(file)."
return cls(file)

Expand Down Expand Up @@ -288,7 +286,7 @@ def __copy__(self) -> Self:
# A "shallow copy" of a file is the same as a deep copy.
return copy.deepcopy(self)

def __deepcopy__(self, memo: Dict[int, Any]) -> Self:
def __deepcopy__(self, memo: dict[int, Any]) -> Self:
"Identical to WrappedFd.clone()"
return self.clone()

Expand All @@ -301,9 +299,9 @@ def __enter__(self) -> Self:

def __exit__(
self,
exc_type: Optional[Type[BaseException]],
exc_value: Optional[BaseException],
exc_traceback: Optional[TracebackType],
exc_type: type[BaseException] | None,
exc_value: BaseException | None,
exc_traceback: TracebackType | None,
) -> None:
self.close()

Expand Down Expand Up @@ -349,9 +347,9 @@ def _convert_mode(mode: str) -> int:
class SingletonClass(type):
"""Metaclass used to create singleton classes."""

_instances: dict[type, Type[Any]] = {}
_instances: ClassVar[dict[type, type[Any]]] = {}

def __call__(cls, *args, **kwargs): # type: ignore[no-untyped-def] # TODO: Not clear what annotations to use, and mypy appears to be confused by metaclasses.
if cls not in cls._instances:
cls._instances[cls] = super(SingletonClass, cls).__call__(*args, **kwargs)
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
Loading
Loading