diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3de86d0..7ff9eaa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -224,10 +224,11 @@ jobs: runs-on: ubuntu-24.04 timeout-minutes: 120 env: - # openkal 0.23.0, which this release is written against, is the one - # openkal-platform 0.23.3's version group names; mcpp 2026.11.3.1 and - # later pin it for their openkal rows. - MCPP_PLATFORM: openkal-platform@0.24.0 + # openkal 0.24.0, which this release is written against, in the group of + # openkal-platform 0.24.2, whose C++ run-time keeps the volume of a + # normalized name (openkal-llvm-runtime 0.19.2); the long-spelling tests + # of 0.3.1 need it on Windows. + MCPP_PLATFORM: openkal-platform@0.24.2 defaults: run: shell: bash @@ -276,10 +277,11 @@ jobs: runs-on: ${{ matrix.os }} timeout-minutes: 45 env: - # openkal 0.23.0, which this release is written against, is the one - # openkal-platform 0.23.3's version group names; mcpp 2026.11.3.1 and - # later pin it for their openkal rows. - MCPP_PLATFORM: openkal-platform@0.24.0 + # openkal 0.24.0, which this release is written against, in the group of + # openkal-platform 0.24.2, whose C++ run-time keeps the volume of a + # normalized name (openkal-llvm-runtime 0.19.2); the long-spelling tests + # of 0.3.1 need it on Windows. + MCPP_PLATFORM: openkal-platform@0.24.2 strategy: fail-fast: false matrix: diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ce535d..be00d97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # Changelog +## 0.3.1 + +- **A name in the long spelling is the name it spells.** `fs::extended` writes + `\\?\C:\...` and `\\?\UNC\server\share\...` on Windows. `locate` compared that + spelling as written: the leading `//` matched the supplied directory of shares + and the remainder kept `?/C:`, whose colon openkal refuses, so `read`, `write`, + `rename`, `stamp_of`, `identity_of` and every other located operation upon such a + name failed with `errc::invalid` (mcpp-community/mcpp#889). The long spelling is + now read as the plain name it denotes, before the name is normalized (a C++ + library with POSIX's rules reads the leading `//` as `/`). `fs::extended` makes a + name absolute by this library's own reading of a Windows name: over the hosted + arrangement `std::filesystem::absolute` took `C:\Users\...` for a relative name + and put the working directory before it. The Windows test writes, reads, + stamps, identifies and renames a file through both spellings, natively and over + the hosted arrangement (openkal-platform 0.24.2, whose C++ runtime keeps the + volume of a normalized name, openkal-llvm-runtime#68). +- **The kernel layer is a compatible range.** openkal `^0.24.0` and its + implementations `^0.25.0` (Linux), `^0.22.0` (macOS), `^0.20.0` (Windows) and + `^0.13.0` (Emscripten). A target that selects openkal-platform receives the + versions of its version group; any other build receives the newest compatible + releases, recorded in `mcpp.lock`. + ## 0.2.7 - **openkal 0.21.** The implementations of openkal 0.21 (openkal-linux 0.22.0, diff --git a/README.md b/README.md index 469704b..cb7ff0f 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ with compile-time platform constants and a few platform modules. ```toml [dependencies] -os = { namespace = "mcpplibs", version = "0.3.0" } +os = { namespace = "mcpplibs", version = "0.3.1" } ``` ```cpp @@ -199,5 +199,5 @@ The Linux backend is openkal-linux 0.24.1. It requires Landlock ABI 3 before claiming confinement, which protects read-only grants against raw file truncation. Older ABIs refuse a confinement request before starting a program. The backend's ABI probe cache uses atomic accesses during concurrent first -queries. Hosted and image CI select the matching openkal-platform 0.23.3 group. +queries. Hosted and image CI select the openkal-platform 0.24.2 group. The library's own implementation is unchanged. diff --git a/checks/hosted/mcpp.toml b/checks/hosted/mcpp.toml index 5f5f3ba..f0f0705 100644 --- a/checks/hosted/mcpp.toml +++ b/checks/hosted/mcpp.toml @@ -13,7 +13,7 @@ version = "0.1.0" os = { path = "../.." } [target.'cfg(any(linux, macos, windows))'] -platform = "openkal-platform@0.24.0" +platform = "openkal-platform@0.24.2" # The tests are the package's own, each reached by an include from # tests/.cpp here (a test program is a file of the package that runs it). diff --git a/mcpp.toml b/mcpp.toml index ebc4091..bf627a8 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "os" -version = "0.3.0" +version = "0.3.1" description = "mcpplibs.os: a C++ operating-system library over openkal --- processes, files, environment, terminals, directories and local IPC, with compile-time platform constants and a few platform modules." license = "Apache-2.0" authors = ["mcpplibs"] @@ -12,23 +12,23 @@ repo = "https://github.com/mcpplibs/os" # own C and C++ runtime. A program that wants the standalone arrangement, or # the image arrangement of `-openkal`, selects it in its own manifest. [dependencies] -openkal = "0.24.0" +openkal = "^0.24.0" [target.linux.dependencies] -openkal-linux = "0.25.0" +openkal-linux = "^0.25.0" [target.macos.dependencies] -openkal-macos = "0.22.0" +openkal-macos = "^0.22.0" [target.windows.dependencies] -openkal-windows = "0.20.0" +openkal-windows = "^0.20.0" # Emscripten (`wasm32-emscripten`): openkal written above the C library, with # twelve of openkal's fifteen interfaces. `openkal.process', `openkal.exec' and # `openkal.space' are absent, so a program that starts one fails at link # naming the symbol; `openkal.task' is present in a `-pthread' link only. [target.'cfg(os = "emscripten")'.dependencies] -openkal-emscripten = "0.13.0" +openkal-emscripten = "^0.13.0" [targets.os] kind = "lib" diff --git a/src/fs.cpp b/src/fs.cpp index f20dbe5..b760dd6 100644 --- a/src/fs.cpp +++ b/src/fs.cpp @@ -212,8 +212,27 @@ bool names_a_place(std::string_view name) { return name.find('/') != std::string_view::npos; } +// A NAME IN THE LONG SPELLING IS THE NAME IT SPELLS (0.3.1). `extended' writes +// `\\?\C:\...' and `\\?\UNC\server\share\...'. Compared as written, the leading +// `//' matched the supplied directory of shares and the remainder kept `?/C:', +// whose colon openkal refuses as a name: every operation upon such a name +// failed with errc::invalid (mcpp-community/mcpp#889). The long spelling only +// lifts Windows' length limit; the file it names is the plain one. +namespace { +std::string plain_spelling(std::string path) { + if (host() != platform::windows || !path.starts_with("//?/")) return path; + if (path.size() > 8 && (path.compare(4, 4, "UNC/") == 0 || path.compare(4, 4, "unc/") == 0)) + return "//" + path.substr(8); + if (path.size() >= 6 && std::isalpha(static_cast(path[4])) && path[5] == ':') + return path.substr(4); + return path; +} +} // namespace + std::optional locate(const std::filesystem::path& name) { - const auto whole = mcpplibs::os::fs::absolute(name); + // The plain spelling first: a C++ library with POSIX's rules reads the + // leading `//' of `//?/C:/...' as `/' when it normalizes the name. + const auto whole = mcpplibs::os::fs::absolute(from_utf8(plain_spelling(slashed(generic_utf8(name))))); const std::string path = slashed(generic_utf8(whole.lexically_normal())); const bool fold = conventions().case_insensitive_names; std::optional best; @@ -598,9 +617,12 @@ std::string windows_extended(std::string_view in) { std::filesystem::path extended(const std::filesystem::path& name) { if (host() != platform::windows || name.empty()) return name; - std::error_code ec; - const auto abs = std::filesystem::absolute(name, ec); - if (ec) return name; + // This library's own reading of a Windows name (0.3.1): `C:\...' is + // absolute however the C++ library beneath reads it. Over the hosted + // arrangement std::filesystem takes `\' as an ordinary character, so + // `std::filesystem::absolute' put the working directory before the drive + // (`Z:/cwd/C:\Users\...'), and the long spelling named no file. + const auto abs = mcpplibs::os::fs::absolute(name); const auto u8 = abs.generic_u8string(); const auto spelled = windows_extended(std::string(reinterpret_cast(u8.data()), u8.size())); return std::filesystem::path(std::u8string(reinterpret_cast(spelled.data()), spelled.size())); diff --git a/tests/fs_test.cpp b/tests/fs_test.cpp index 4291e1d..8441e71 100644 --- a/tests/fs_test.cpp +++ b/tests/fs_test.cpp @@ -201,5 +201,25 @@ int main() { check(os::fs::windows_extended("C:/..") == "\\\\?\\C:\\", "and `..' never climbs above the drive"); check(os::fs::windows_extended("rel/x") == "rel/x", "a relative name is not this rule's"); if (os::host() != os::platform::windows) check(os::fs::extended("/a/b") == "/a/b", "elsewhere the name is itself"); + + // THE LONG SPELLING NAMES THE SAME FILE (0.3.1). Every operation that + // locates a name accepts the name `extended' produces. + if (os::host() == os::platform::windows) { + const auto base = stdfs::temp_directory_path() / std::format("mcpplibs-os-long-{}", run_tag()); + std::error_code ec; + stdfs::create_directories(base, ec); + const auto plain = base / "a.txt"; + const auto longer = os::fs::extended(plain); + check(longer != plain, "the long spelling is produced"); + const auto at = os::fs::locate(longer); + check(at && at->relative.find(':') == std::string::npos, "it is located beneath a drive, not beneath the shares"); + check(os::fs::write(longer, "one").has_value(), "a file is written through it"); + check(os::fs::read(longer).value_or("") == "one" && os::fs::read(plain).value_or("") == "one", + "and read through either spelling"); + check(os::fs::stamp_of(longer).has_value() && os::fs::identity_of(longer).has_value(), "its stamp and identity are read"); + const auto moved = os::fs::extended(base / "b.txt"); + check(os::fs::rename(longer, moved, true).has_value() && stdfs::exists(base / "b.txt"), "and it is renamed"); + stdfs::remove_all(base, ec); + } return g_failures; }