rokdocs
v0.1

3.8. path:: - UTF-8 Path (Python 식)

rok::std::path 모듈은 경로 표현을 정의합니다. 여기서 핵심은 UTF-8 newtype 을 우선시한다는 점인데, 이는 Rust 의 Path 와 OsStr 분리가 가져오는 ergonomic 부담을 회피하면서, 동시에 Python pathlib.Path 의 직관성을 채택하기 위함입니다.

3.8.1. 두 타입 - Path / RawPath

rok 의 경로 표현은 두 개의 타입으로 나뉩니다.

타입의미사용 빈도
PathUTF-8 경로 (95%+)일반 코드 default
RawPathOS native bytes (Unix bytes / Windows WTF-16)1% (non-UTF-8 파일명)

대부분의 코드는 Path 만 사용하면 충분합니다. RawPath 가 필요해지는 경우는 legacy bytes 가 정말로 요구되는 자리, 예컨대 오래된 unix 시스템과 마주칠 때에 한정됩니다.

3.8.2. Path - UTF-8

Path 는 불변 값입니다. join, /, with_* 는 모두 새 Path 를 돌려주며, 경로를 제자리에서 바꾸는 PathBuf / push 는 없습니다. p /= q 는 일반 복합 대입 규칙대로 p = p / q 입니다.

Path 의 인터페이스는 다음과 같이 구성됩니다.

struct Path { /* UTF-8 str newtype, GC heap */ }

impl Path {
    fn new() -> Path;
    fn from(s: str) -> Path;
    fn from_components<I: Iterator<Item = str>>(components: I) -> Path;

    // 분해
    fn parent(self) -> Path | none;
    fn file_name(self) -> str | none;
    fn file_stem(self) -> str | none;
    fn extension(self) -> str | none;
    fn components(self) -> Components;   // Iterator<Item = Component> 구현
    fn ancestors(self) -> Ancestors;     // Iterator<Item = Path> 구현

    // 조작
    fn join(self, other: Path) -> Path;
    fn with_file_name(self, name: str) -> Path;
    fn with_extension(self, ext: str) -> Path;
    fn strip_prefix(self, base: Path) throws (StripPrefixError) -> Path;

    fn is_absolute(self) -> bool;
    fn is_relative(self) -> bool;
    fn has_root(self) -> bool;
    fn starts_with(self, base: Path) -> bool;
    fn ends_with(self, suffix: Path) -> bool;
    fn normalize(self) -> Path;             // resolve "." / ".." (lexical)

    // OS 상호작용 (편의 - 내부적으로 fs::*)
    fn exists(self) -> bool;
    fn is_file(self) -> bool;
    fn is_dir(self) -> bool;
    fn is_symlink(self) -> bool;
    fn metadata(self) throws (IoError) -> Metadata;
    fn canonicalize(self) throws (IoError) -> Path;     // realpath (filesystem 검증)

    fn as_str(self) -> str;
    fn to_raw(self) -> RawPath;
}

enum Component {
    RootDir,                    // "/"
    Prefix(str),                // "C:" (Windows)
    CurDir,                     // "."
    ParentDir,                  // ".."
    Normal(str),                // "data"
}

3.8.3. / 연산자 - Python pathlib 식

경로 결합의 가독성을 한층 끌어올리기 위해 / 연산자를 도입합니다.

impl Div<str> for Path {
    type Output = Path;
    #[infix("/")]
    fn div(self, rhs: str) -> Path { self.join(Path::from(rhs)) }
}

impl Div<Path> for Path { ... }

p /= "x" 와 p /= q (q: Path) 도 쓸 수 있으며, p = p / .. 와 같습니다.

효과 는 다음 예시에서 분명히 드러납니다.

let p = Path::from("/etc") / "nginx" / "nginx.conf";
                                          //→ Path("/etc/nginx/nginx.conf")

let home = env::home_dir()? / ".config" / "myapp" / "settings.toml";

이는 Python 의 Path("/etc") / "nginx" / "nginx.conf" 와 동일한 ergonomics 를 제공합니다. 그 결과 Rust PathBuf::join 과 push 의 verbose 한 표현을 자연스럽게 회피합니다.

3.8.3a. AsPath - 경로 인자

trait AsPath {
    fn as_path(self) -> Path;
}
impl AsPath for str { ... }
impl AsPath for Path { ... }

fs 의 경로 인자는 AsPath 로 받으므로 str 과 Path 를 모두 넘길 수 있습니다 (fs::read("a.txt"), fs::read(dir / "a.txt")).

3.8.4. UTF-8 invariant

Path 는 항상 UTF-8 입니다. 따라서 Path::from(s) 시점에 s 가 이미 UTF-8 이므로 별도 검증이 필요하지 않습니다.

다만 OS 가 non-UTF-8 파일명을 반환하는 드문 상황도 존재합니다.

  • Unix 의 경우 arbitrary bytes 가 가능하므로 Path::from(...) 은 lossy 변환 (replacement char U+FFFD) 을 수행합니다.
  • Windows 의 경우 WTF-16 이며, surrogate pair 처리가 별도로 이루어집니다.

엄격한 변환이 필요하다면 다음과 같이 작성합니다.

let raw: RawPath = ...;
let p: Path = try Path::try_from(raw);    // UTF-8 이 아니면 NonUtf8Error 를 throw
let q: Path = try raw.try_into();          // 같음

이 변환은 impl TryFrom<RawPath> for Path { type Error = NonUtf8Error; ... } 입니다 (2.11.4).

3.8.5. RawPath - bytes / WTF-16

OS 의 native 표현이 그대로 필요한 자리를 위해 RawPath 가 마련됩니다.

struct RawPath { /* unix: Vec<u8>, windows: Vec<u16> */ }

impl RawPath {
    fn from_bytes(bytes: List<u8>) -> RawPath;        // unix
    fn from_wide(wide: List<u16>) -> RawPath;          // windows

    fn as_bytes(self) -> Slice<u8>;                    // unix 만
    fn as_wide(self) -> Slice<u16>;                    // windows 만

    fn try_to_path(self) throws (NonUtf8Error) -> Path;
    fn to_path_lossy(self) -> Path;
}

Windows 에서 NonUtf8Error.valid_up_to 는 UTF-16 단위를 셉니다. to_path_lossy 는 잘못된 maximal subsequence (Unix) / 짝 없는 surrogate (Windows) 하나하나를 U+FFFD 하나로 바꿉니다.

대부분의 코드는 이 타입을 마주칠 일이 없습니다. OS 와 직접 syscall 을 주고받는 자리에서만 등장한다고 보면 됩니다.

3.8.6. canonicalize / normalize 차이

이 두 함수는 비슷해 보이지만, 동작 방식과 비용 측면에서 분명히 구분됩니다.

함수의미파일시스템 접근
normalize()순수 lexical 정리 (".." 해석)하지 않음
canonicalize()symlink 해석 + 절대 경로 변환함 (realpath)
Path::from("/a/b/../c").normalize();              //→ "/a/c" (lexical)
try Path::from("/a/b/../c").canonicalize();       //→ symlink 해석된 절대 경로

normalize 는 빠르고 syscall 을 동반하지 않으므로 90% 의 자리에서 충분히 제 역할을 해냅니다.

3.8.7. WASI 호환

Path 는 WASI Preview 2 의 capability path 와도 자연스럽게 통합됩니다.

let dir: Dir = try Dir::root().open_dir("data");
let f = try dir.open(Path::from("config.toml"));
                                          // Path 가 dir 에 상대적 - escape 거부

자세한 capability 모델은 3.1 fs 에서 다룹니다.

3.8.8. 다른 언어와의 비교

설계 결정의 좌표를 명확히 하기 위해, 인접 언어들과 비교해 보자.

측면RustPython pathlibGo filepathrok
타입Path / PathBufPathstringPath
owned vs viewborrowed &Path / owned불변불변불변 (/ 는 새 Path)
/ 연산자없음 (.join() 명시)/없음/
non-UTF-8OsStr (혼합)bytes (별도)string (혼란)RawPath 분리
canonicalize.canonicalize().resolve()filepath.Abs.canonicalize()
메서드 chainOK (대부분)excellentOKexcellent

요약하자면 rok 의 가장 큰 결정 은 UTF-8 default 와 / 연산자의 결합입니다. 이로써 Python pathlib 의 ergonomics 와 Rust 수준의 performance 를 함께 가져갑니다.

GitHub에서 이 페이지 고치기