diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e6206d1..7b9e062 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -182,6 +182,39 @@ jobs: run: | bash .spec/tools/run-conformance.sh openkal-macos . full,optional + # ⚠️⚠️ CLONING THE SPECIFICATION IS NOT THE SAME AS BUILDING AGAINST IT. + # + # `.spec` is cloned at the top of this job and consumed by the script + # above, which substitutes this manifest itself and RESTORES IT ON EXIT + # --- correctly, since a script that rewrote a checked-in file and walked + # away would leave the tree holding a path. But every step BELOW calls + # `mcpp build` directly, and by then the manifest names `openkal` by + # version again, so they resolved the PUBLISHED specification: + # + # E_NOT_FOUND: package 'compat.openkal@0.9.0' not found in the synced + # index ... the index is current, so this name is either wrong or not + # published yet + # + # ⭐⭐ THE UNIT IS THE STEP, NOT THE JOB, AND NOT THE REPOSITORY. Measured + # 2026-08-28 across the eight repositories of this ecosystem: eight jobs + # in four of them had this shape. An audit that asked "does this job + # substitute?" passed this one, because it does --- and then gives it + # back. These steps are green on `main` and can only be green there, + # because there the published version IS the one under test. + - name: Point at the specification's working tree + run: | + set -euo pipefail + # ⚠️ NOT `sed -i'. This step runs on macOS and on Windows too, and + # BSD sed requires an argument to -i that GNU sed refuses. A temporary + # file is the spelling that holds on all three. + subst() { # subst + sed "s|^openkal = .*$|openkal = { path = \"$2\" }|" "$1" > "$1.next" + mv "$1.next" "$1" + grep -q "path = \"$2\"" "$1" \ + || { echo "::error::$1 was not substituted"; exit 1; } + } + subst mcpp.toml .spec + # The other architecture, as far as this system allows it to be reached. # # The system-call numbers agree between the two --- measured, in the diff --git a/README.md b/README.md index 7929863..e209a2c 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,10 @@ written on the kernel's own calls. ```toml [dependencies] -openkal = "0.8.0" +openkal = "0.9.0" [target.'cfg(os = "macos")'.dependencies] -openkal-macos = "0.5.0" +openkal-macos = "0.6.0" ``` Its purpose is as much to test the specification as to be used. A specification diff --git a/mcpp.toml b/mcpp.toml index fc8a9c1..d2dbc96 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-macos" -version = "0.5.0" +version = "0.6.0" description = "An implementation of openkal for macOS, written on the kernel's own calls. Its purpose is as much to test the specification as to be used." license = "Apache-2.0" @@ -18,7 +18,7 @@ authors = ["mcpplibs"] repo = "https://github.com/mcpplibs/openkal-macos" [dependencies] -openkal = "0.8.0" +openkal = "0.9.0" [build] # The flags are attached to this package's own sources rather than to the whole diff --git a/src/datagram.cpp b/src/datagram.cpp index 1074ebc..3443122 100644 --- a/src/datagram.cpp +++ b/src/datagram.cpp @@ -72,15 +72,15 @@ int kal_datagram_local(kal_datagram d, kal_endpoint* out) { return okm::from_kernel(ss, *out); } -kal_io_result kal_datagram_send_to(kal_datagram d, const void* buf, kal_uintptr len, - const kal_endpoint* to) { +kal_intptr kal_datagram_send_to(kal_datagram d, const void* buf, kal_uintptr len, + const kal_endpoint* to) { const int fd = fd_of(d); - if (fd < 0 || to == nullptr) return { 0, kal_err_invalid }; + if (fd < 0 || to == nullptr) return -kal_err_invalid; okm::ksockaddr_storage ss{}; okm_u32 addrlen = 0; if (const int rc = okm::to_kernel(*to, ss, addrlen); rc != kal_ok) - return { 0, rc }; + return -rc; for (;;) { const okm_long r = okm::sys(okm::nr_sendto, fd, @@ -89,7 +89,7 @@ kal_io_result kal_datagram_send_to(kal_datagram d, const void* buf, kal_uintptr reinterpret_cast(&ss), static_cast(addrlen)); if (okm::interrupted(r)) continue; - if (okm::failed(r)) return { 0, okm::translate(r) }; + if (okm::failed(r)) return -okm::translate(r); // A MESSAGE IS SENT WHOLE OR NOT AT ALL, which is what this interface // states. The kernel reports a count anyway; a count short of the length @@ -98,14 +98,14 @@ kal_io_result kal_datagram_send_to(kal_datagram d, const void* buf, kal_uintptr // a caller a partial send this interface says cannot occur, so it is // reported as a failure of the medium instead. const kal_uintptr n = static_cast(r); - return { n, n == len ? kal_ok : kal_err_io }; + return n == len ? static_cast(n) : -kal_err_io; } } -kal_io_result kal_datagram_recv_from(kal_datagram d, void* buf, kal_uintptr len, - kal_endpoint* from) { +kal_intptr kal_datagram_recv_from(kal_datagram d, void* buf, kal_uintptr len, + kal_endpoint* from) { const int fd = fd_of(d); - if (fd < 0) return { 0, kal_err_invalid }; + if (fd < 0) return -kal_err_invalid; okm::ksockaddr_storage ss{}; okm_u32 addrlen = static_cast(sizeof ss); @@ -117,7 +117,7 @@ kal_io_result kal_datagram_recv_from(kal_datagram d, void* buf, kal_uintptr len, reinterpret_cast(&ss), reinterpret_cast(&addrlen)); if (okm::interrupted(r)) continue; - if (okm::failed(r)) return { 0, okm::translate(r) }; + if (okm::failed(r)) return -okm::translate(r); // THE COUNT REPORTED IS WHAT WAS PLACED IN THE BUFFER, not what was // sent. Without MSG_TRUNC the kernel already reports the former, which @@ -133,7 +133,7 @@ kal_io_result kal_datagram_recv_from(kal_datagram d, void* buf, kal_uintptr len, from->port = 0; } } - return { static_cast(r), kal_ok }; + return static_cast(r); } } @@ -148,6 +148,6 @@ void kal_datagram_close(kal_datagram d) { // been set, and this interface has no operation that would set it; a word // claiming a facility no operation reaches is the disagreement clause 6.2 exists // to prevent. -const kal_uintptr kal_datagram_props = KAL_DGRAM_PROP_IPV6; +kal_uintptr kal_datagram_props(void) { return KAL_DGRAM_PROP_IPV6; } } // extern "C" diff --git a/src/env.cpp b/src/env.cpp index 224e0c1..87d7a6a 100644 --- a/src/env.cpp +++ b/src/env.cpp @@ -27,46 +27,56 @@ extern "C" { kal_uintptr kal_env_arg_count(void) { return static_cast(okm::g_argc); } -const char* kal_env_arg(kal_uintptr index, kal_uintptr* len) { - if (index >= static_cast(okm::g_argc)) { if (len) *len = 0; return nullptr; } +// EVERY VALUE IS COPIED INTO THE CALLER'S BUFFER. These answered with a pointer +// into this implementation's own storage, which is meaningful only while the +// implementation shares the caller's address space. Each reports the length the +// value HAS, so a caller with a large enough buffer is done in one call and one +// that wants to size first passes a capacity of zero. +namespace { +kal_intptr give(const char* v, kal_uintptr n, char* out, kal_uintptr cap) { + if (out != nullptr && cap != 0) okm::copy(out, v, n < cap ? n : cap); + return static_cast(n); +} +} // namespace + +kal_intptr kal_env_arg(kal_uintptr index, char* out, kal_uintptr cap) { + if (index >= static_cast(okm::g_argc)) return -kal_err_not_found; const char* s = okm::g_argv[index]; - if (len) *len = okm::length(s); - return s; + return give(s, okm::length(s), out, cap); } -const char* kal_env_var(const char* name, kal_uintptr name_len, kal_uintptr* value_len) { +kal_intptr kal_env_var(const char* name, kal_uintptr name_len, + char* out, kal_uintptr cap) { + if (name == nullptr) return -kal_err_invalid; for (char** e = okm::g_envp; e && *e; ++e) { const char* entry = *e; kal_uintptr i = 0; while (i < name_len && entry[i] != '\0' && entry[i] == name[i]) ++i; if (i == name_len && entry[i] == '=') { const char* v = entry + name_len + 1; - if (value_len) *value_len = okm::length(v); - return v; + return give(v, okm::length(v), out, cap); } } - if (value_len) *value_len = 0; - return nullptr; + // A name that is not there is distinct from one whose value is empty. + return -kal_err_not_found; } kal_uintptr kal_env_var_count(void) { kal_uintptr n = 0; for (char** e = okm::g_envp; e && *e; ++e) ++n; return n; } -const char* kal_env_var_at(kal_uintptr index, kal_uintptr* name_len, - const char** value, kal_uintptr* value_len) { +// The NAME at a position. The value is then obtained by kal_env_var: an +// operation answering both needs two buffers, two capacities and two lengths, +// and its second half is kal_env_var written again. +kal_intptr kal_env_var_at(kal_uintptr index, char* out, kal_uintptr cap) { kal_uintptr n = 0; for (char** e = okm::g_envp; e && *e; ++e, ++n) { if (n != index) continue; const char* entry = *e; kal_uintptr i = 0; while (entry[i] != '\0' && entry[i] != '=') ++i; - if (name_len) *name_len = i; - const char* v = entry[i] == '=' ? entry + i + 1 : entry + i; - if (value) *value = v; - if (value_len) *value_len = okm::length(v); - return entry; + return give(entry, i, out, cap); } - return nullptr; + return -kal_err_not_found; } } diff --git a/src/exec.cpp b/src/exec.cpp index 994d3c3..9cdb886 100644 --- a/src/exec.cpp +++ b/src/exec.cpp @@ -1,5 +1,6 @@ #include "sys.h" #include +#include // openkal.exec on this system. // @@ -74,22 +75,83 @@ namespace { -constexpr okm_uptr kPage = 4096; - -// ⚠️ THE PAGE IS 16384 BYTES ON ONE OF THIS SYSTEM'S TWO ARCHITECTURES. Rounding -// to the smaller number is still correct --- the kernel rounds up to its own -// granularity, and a region reserved as 4096 occupies a whole page of whatever -// size --- but a caller freeing with the size it reserved must reach the same -// number, which it does because both go through the same rounding. +// ⚠️ THE GRANULARITY IS ASKED FOR RATHER THAN ASSUMED. This file held +// `constexpr okm_uptr kPage = 4096' and a comment arguing that rounding to the +// smaller of this system's two page sizes was still correct because the kernel +// rounds up. The argument holds for the reservation and fails for the release: +// `munmap' with a length shorter than the mapping unmaps less than was mapped. +// It is the same defect `src/memory.cpp' records having measured, in the same +// system, one file away --- so the number now comes from the one operation +// that answers it. okm_uptr round_up(okm_uptr n, okm_uptr to) { return (n + to - 1) & ~(to - 1); } +okm_uptr granularity() { + return static_cast(kal_memory_granularity()); +} + +// ⭐⭐ WHETHER THIS SYSTEM GRANTS EXECUTABLE MEMORY IS MEASURED, NOT ARGUED. +// +// This file previously carried both answers. One comment reasoned that the +// write-then-publish order is the case an entitlement is NOT needed for and +// concluded the interface is provided unconditionally; another, thirty lines +// below it, reasoned that executable memory is granted only to an artifact +// produced with an entitlement and returned zero. The operations behaved as +// the first said and the capability word said the second. +// +// ⚠️ AND THE DISAGREEMENT WAS INVISIBLE UNTIL A CONSUMER COULD READ THE WORD. +// A statically-linked caller never asked: it linked the operations and used +// them. The word became load-bearing when the specification made an +// implementation's own account of itself part of the ABI, and the conformance +// suite then reported what had been true all along --- `an implementation that +// does not claim availability reserves nothing' DID NOT HOLD, because this one +// claimed nothing and reserved anyway. +// +// The remedy is not to pick the more likely of the two readings. It is that +// neither this file nor any comment in it is the party that knows: the answer +// depends on how the artifact was signed, which is settled after this code is +// compiled and can differ between two runs of the same binary. So the enquiry +// performs the thing it is being asked about --- one reservation, one publish, +// one release --- and reports what the kernel said. +// +// The probe is the operation's own path, so an environment where publishing +// fails is one where this reports unavailable and `kal_exec_alloc' declines, +// and the two can no longer disagree. +int probe() { + const okm_uptr bytes = granularity(); + const okm_long m = okm::sys(okm::nr_mmap, 0, static_cast(bytes), + okm::prot_read | okm::prot_write, + okm::map_private | okm::map_anon, -1, 0); + if (okm::failed(m)) return 2; + const okm_long p = okm::sys(okm::nr_mprotect, m, + static_cast(bytes), + okm::prot_read | okm::prot_exec); + okm::sys(okm::nr_munmap, m, static_cast(bytes)); + return okm::failed(p) ? 2 : 1; +} + +// Asked once. Constant-initialised, so no guard variable is emitted and this +// file acquires no dependency upon the runtime --- the property the +// independence check in this package exists to hold. Two contexts racing here +// perform the probe twice and store the same answer. +bool available() { + static int cached = 0; + int v = __atomic_load_n(&cached, __ATOMIC_RELAXED); + if (v == 0) { v = probe(); __atomic_store_n(&cached, v, __ATOMIC_RELAXED); } + return v == 1; +} + } // namespace extern "C" { void* kal_exec_alloc(kal_uintptr size) { if (size == 0) return nullptr; - const okm_uptr bytes = round_up(static_cast(size), kPage); + // An implementation that does not claim availability reserves nothing. + // Otherwise the word is advice a caller cannot act upon: it would report + // unavailable and then hand back memory, and a caller that believed the + // word would have declined memory it could have had. + if (!available()) return nullptr; + const okm_uptr bytes = round_up(static_cast(size), granularity()); const okm_long r = okm::sys(okm::nr_mmap, 0, static_cast(bytes), okm::prot_read | okm::prot_write, okm::map_private | okm::map_anon, -1, 0); @@ -99,7 +161,7 @@ void* kal_exec_alloc(kal_uintptr size) { int kal_exec_publish(void* p, kal_uintptr size) { if (p == nullptr || size == 0) return kal_err_invalid; - const okm_uptr bytes = round_up(static_cast(size), kPage); + const okm_uptr bytes = round_up(static_cast(size), granularity()); const okm_long r = okm::sys(okm::nr_mprotect, reinterpret_cast(p), static_cast(bytes), okm::prot_read | okm::prot_exec); @@ -109,17 +171,19 @@ int kal_exec_publish(void* p, kal_uintptr size) { void kal_exec_free(void* p, kal_uintptr size) { if (p == nullptr || size == 0) return; - const okm_uptr bytes = round_up(static_cast(size), kPage); + const okm_uptr bytes = round_up(static_cast(size), granularity()); okm::sys(okm::nr_munmap, reinterpret_cast(p), static_cast(bytes)); } // A published region may NOT be reserved for writing again on this system, and -// the position is withheld accordingly. Asking this kernel to make an executable -// mapping writable is the case it refuses, which is the whole reason the -// interface separates the two states; a caller that must change published bytes -// reserves a second region and abandons the first, which is what the header -// says a zero here means. -const kal_uintptr kal_exec_props = 0; +// the position is withheld accordingly. Asking this kernel to make an +// executable mapping writable is the case it refuses, which is the whole reason +// the interface separates the two states; a caller that must change published +// bytes reserves a second region and abandons the first, which is what the +// header says a zero in that position means. +kal_uintptr kal_exec_props(void) { + return available() ? KAL_EXEC_PROP_AVAILABLE : 0; +} } // extern "C" diff --git a/src/fs.cpp b/src/fs.cpp index e9e3959..b5ea7c6 100644 --- a/src/fs.cpp +++ b/src/fs.cpp @@ -64,14 +64,50 @@ int kind_of(okm_u32 mode) { } } -void fill_info(const okm::kstat64& st, kal_node_info* out) { - *out = kal_node_info{ - static_cast(st.size), - static_cast(st.mtime_sec) * 1000000000u - + static_cast(st.mtime_nsec), - kind_of(okm::stat_mode(st)), - (okm::stat_mode(st) & 0200u) != 0 ? 1 : 0, - }; +// Writes no more of the structure than the caller says exists on its side, and +// reports which fields it filled. `wanted' is ignored and every field is +// filled: one call answers all of them on this kernel, so selecting would cost +// a branch and save nothing. +void fill_info(const okm::kstat64& st, kal_u32 wanted, kal_node_info* out) { + (void)wanted; + const kal_u32 self = out->self_size; + kal_node_info v{}; + v.self_size = self; + v.present = KAL_INFO_ALL; + v.size = static_cast(st.size); + v.modified_ns = static_cast(st.mtime_sec) * 1000000000u + + static_cast(st.mtime_nsec); + // Opaque to a caller, which may compare it and may not read it. The device + // and the inode are this kernel's answer and not the interface's shape. + v.identity[0] = st.dev; + v.identity[1] = st.ino; + v.kind = kind_of(okm::stat_mode(st)); + v.writable = (okm::stat_mode(st) & 0200u) != 0 ? 1 : 0; + const kal_u32 n = self < sizeof v ? self : (kal_u32)sizeof v; + okm::copy(out, &v, n); +} + +void fill_absent(kal_node_info* out) { + const kal_u32 self = out->self_size; + kal_node_info v{}; + v.self_size = self; + v.present = KAL_INFO_KIND; + v.kind = kal_node_absent; + const kal_u32 n = self < sizeof v ? self : (kal_u32)sizeof v; + okm::copy(out, &v, n); +} + +// The caller must state how much of the structure exists on its side. +bool info_ok(const kal_node_info* out) { + return out != nullptr && out->self_size >= sizeof(kal_u32) * 2; +} + +// Copies a name into a caller's buffer and reports the length it HAS. +kal_uintptr put_name(const char* src, kal_uintptr n, + char* out, kal_uintptr cap, kal_uintptr* len) { + if (out != nullptr && cap != 0) okm::copy(out, src, n < cap ? n : cap); + if (len) *len = n; + return n; } // Enumeration reads the kernel's own directory records. It holds a descriptor @@ -92,14 +128,14 @@ extern "C" { kal_uintptr kal_fs_preopen_count(void) { kal_uintptr n = 0; table(&n); return n; } -int kal_fs_preopen(kal_uintptr index, kal_dir* out, const char** name, kal_uintptr* len) { +int kal_fs_preopen(kal_uintptr index, kal_dir* out, + char* name_out, kal_uintptr name_cap, kal_uintptr* name_len) { kal_uintptr n = 0; preopen* t = table(&n); if (index >= n || out == nullptr) return kal_err_invalid; if (t[index].handle == 0) return kal_err_permission; *out = kal_dir{ t[index].handle }; - if (name) *name = t[index].name; - if (len) *len = t[index].len; + put_name(t[index].name, t[index].len, name_out, name_cap, name_len); return kal_ok; } @@ -135,14 +171,6 @@ int kal_fs_open(kal_dir base, const char* name, kal_uintptr len, return kal_ok; } -int kal_fs_open_file(kal_dir base, const char* name, kal_uintptr len, - int write, int create, kal_file* out) { - kal_uintptr flags = KAL_OPEN_READ; - if (write) flags |= KAL_OPEN_WRITE; - if (create) flags |= KAL_OPEN_WRITE | KAL_OPEN_CREATE | KAL_OPEN_TRUNCATE; - return kal_fs_open(base, name, len, flags, out); -} - void kal_fs_close_dir(kal_dir d) { const int fd = okm::unpack(d.h); if (fd >= 0) { okm::retire(d.h); okm::sys(okm::nr_close, fd); } @@ -153,9 +181,11 @@ void kal_fs_close_file(kal_file f) { if (fd >= 0) { okm::retire(f.h); okm::sys(okm::nr_close, fd); } } -kal_uintptr kal_fs_stream(kal_file f) { +kal_uintptr kal_fs_max_name(void) { return okm::max_name; } + +kal_stream kal_fs_stream(kal_file f) { const int fd = okm::unpack(f.h); - return fd < 0 ? 0u : static_cast(fd); + return kal_stream{ fd < 0 ? 0u : static_cast(fd) }; } int kal_fs_seek(kal_file f, kal_i64 offset, int whence, kal_u64* result) { @@ -177,34 +207,41 @@ int kal_fs_truncate(kal_file f, kal_u64 size) { return okm::failed(r) ? okm::translate(r) : kal_ok; } -int kal_fs_info(kal_dir base, const char* name, kal_uintptr len, kal_node_info* out) { +int kal_fs_info(kal_dir base, const char* name, kal_uintptr len, + kal_uintptr flags, kal_u32 wanted, kal_node_info* out) { const int b = okm::unpack(base.h); - if (b < 0 || out == nullptr || !okm::acceptable(name, len)) return kal_err_invalid; + if (b < 0 || !info_ok(out) || !okm::acceptable(name, len)) return kal_err_invalid; okm::terminated t(name, len); if (!t.ok) return kal_err_invalid; okm::kstat64 st{}; + // RESOLVES BY DEFAULT, SO THAT ASKING AND OPENING ANSWER THE SAME QUESTION. + // This implementation asked with AT_SYMLINK_NOFOLLOW always while + // `kal_fs_open' resolved, so a name referring to a node whose content is + // another name was reported as that node while opening it reached a file. + const okm_long at = (flags & KAL_FS_NO_RESOLVE) ? okm::at_symlink_nofollow : 0; const okm_long r = okm::sys(okm::nr_fstatat64, b, reinterpret_cast(t.buf), - reinterpret_cast(&st), okm::at_symlink_nofollow); + reinterpret_cast(&st), at); if (okm::failed(r)) { // Clause 7.7: enquiry about a name that does not exist is answered, not // refused. A component of the name that is not a directory is the same - // answer, because the name still refers to nothing. - if (r == -okm::e_noent || r == -okm::e_notdir) { - *out = kal_node_info{ 0, 0, kal_node_absent, 0 }; + // answer, and so is a node whose content names something absent when + // the enquiry resolves. + if (r == -okm::e_noent || r == -okm::e_notdir || r == -okm::e_loop) { + fill_absent(out); return kal_ok; } return okm::translate(r); } - fill_info(st, out); + fill_info(st, wanted, out); return kal_ok; } -int kal_fs_file_info(kal_file f, kal_node_info* out) { +int kal_fs_file_info(kal_file f, kal_u32 wanted, kal_node_info* out) { const int fd = okm::unpack(f.h); - if (fd < 0 || out == nullptr) return kal_err_invalid; + if (fd < 0 || !info_ok(out)) return kal_err_invalid; okm::kstat64 st{}; const okm_long r = okm::sys(okm::nr_fstat64, fd, reinterpret_cast(&st)); if (okm::failed(r)) return okm::translate(r); - fill_info(st, out); + fill_info(st, wanted, out); return kal_ok; } @@ -288,8 +325,9 @@ int kal_fs_list_begin(kal_dir d, kal_uintptr* iter) { return kal_ok; } -int kal_fs_list_next(kal_dir, kal_uintptr* iter, const char** name, - kal_uintptr* len, int* kind) { +int kal_fs_list_next(kal_dir, kal_uintptr* iter, + char* name_out, kal_uintptr name_cap, + kal_uintptr* name_len, int* kind) { if (iter == nullptr || *iter == 0) return kal_err_invalid; auto* s = reinterpret_cast(*iter); for (;;) { @@ -303,8 +341,7 @@ int kal_fs_list_next(kal_dir, kal_uintptr* iter, const char** name, okm::sys(okm::nr_close, s->fd); kal_free(s, sizeof(listing), alignof(listing)); *iter = 0; - if (name) *name = nullptr; - if (len) *len = 0; + if (name_len) *name_len = 0; return okm::failed(r) ? okm::translate(r) : kal_ok; } s->used = static_cast(r); @@ -316,8 +353,7 @@ int kal_fs_list_next(kal_dir, kal_uintptr* iter, const char** name, // They exist to support ascent, which this interface does not offer. if (e->name[0] == '.' && (e->name[1] == '\0' || (e->name[1] == '.' && e->name[2] == '\0'))) continue; - if (name) *name = e->name; - if (len) *len = e->namlen; + put_name(e->name, e->namlen, name_out, name_cap, name_len); if (kind) *kind = e->type == okm::dt_dir ? kal_node_directory : e->type == okm::dt_reg ? kal_node_file : e->type == okm::dt_lnk ? kal_node_link : kal_node_other; @@ -329,7 +365,96 @@ int kal_fs_list_next(kal_dir, kal_uintptr* iter, const char** name, // it is ordinarily installed on. A program that creates two names differing // only in case succeeds on the Linux implementation and not on this one, and // the position reports it in advance, which no operation could. -const kal_uintptr kal_fs_props = - KAL_FS_PROP_MODIFIED_TIME | KAL_FS_PROP_ATOMIC_RENAME; +// The properties of the volume a directory is on. +// +// AN ENQUIRY TAKING THE RESOURCE, BECAUSE EVERY POSITION IS A PROPERTY OF THE +// FORMAT. A word per implementation could state none of them honestly here: the +// volume this system is ordinarily installed on compares names without regard +// to case, and a volume attached to the same machine may not --- and this +// implementation offers the whole filesystem as a preopen, so both are +// reachable through it. +// +// This kernel names the format in words rather than by a number, so that is +// what is consulted. For a format it does not recognise, what is claimed is the +// set that cannot be wrong: a modification time is reported for every volume it +// mounts, and `renameat' within one directory is atomic by POSIX. +kal_uintptr kal_fs_props(kal_dir d) { + const int fd = okm::unpack(d.h); + const kal_uintptr conservative = + KAL_FS_PROP_MODIFIED_TIME | KAL_FS_PROP_ATOMIC_RENAME; + if (fd < 0) return 0; + + okm::kstatfs64 sf{}; + const okm_long r = okm::sys(okm::nr_fstatfs64, fd, reinterpret_cast(&sf)); + if (okm::failed(r)) return conservative; + + const auto named = [&](const char* w) { + for (int i = 0; i < 16; ++i) { + if (sf.f_fstypename[i] != w[i]) return false; + if (w[i] == '\0') return true; + } + return false; + }; + + // Nodes whose content is another name, without a case distinction. This is + // the ordinary volume of this system. + if (named("apfs") || named("hfs") || named("autofs")) + return conservative | KAL_FS_PROP_LINKS | KAL_FS_PROP_MAKE_LINKS; + + // A case-sensitive volume with such nodes: a disk image formatted that way, + // or a network volume presenting one. + if (named("nfs") || named("smbfs") || named("webdav")) + return conservative | KAL_FS_PROP_LINKS | KAL_FS_PROP_MAKE_LINKS; + + // The FAT family stores neither a case distinction nor a node that names + // another. `symlinkat' on such a volume reports a refusal, and this is + // where a caller learns it before it tries. + if (named("msdos") || named("exfat")) return conservative; + + return conservative; +} + +// Nodes whose content is another name. +int kal_fs_link_create(kal_dir base, const char* name, kal_uintptr len, + const char* target, kal_uintptr target_len, + kal_uintptr flags) { + // The target is content rather than a name this interface resolves, so it + // is not passed through `acceptable' --- which would refuse one that + // ascends, and one that ascends is the ordinary case for a relative target. + (void)flags; // this kernel does not distinguish a link to a directory + const int b = okm::unpack(base.h); + if (b < 0 || !okm::acceptable(name, len) || target == nullptr) return kal_err_invalid; + okm::terminated n(name, len); if (!n.ok) return kal_err_invalid; + okm::terminated tgt(target, target_len); if (!tgt.ok) return kal_err_invalid; + const okm_long r = okm::sys(okm::nr_symlinkat, reinterpret_cast(tgt.buf), + b, reinterpret_cast(n.buf)); + return okm::failed(r) ? okm::translate(r) : kal_ok; +} + +kal_intptr kal_fs_link_read(kal_dir base, const char* name, kal_uintptr len, + char* out, kal_uintptr cap) { + const int b = okm::unpack(base.h); + if (b < 0 || !okm::acceptable(name, len)) return -kal_err_invalid; + okm::terminated t(name, len); if (!t.ok) return -kal_err_invalid; + + // The kernel truncates into the buffer it is given and does not report the + // length the content has, so a caller asking for the length --- a capacity + // of zero --- is served from a buffer of this implementation's own. + char own[okm::max_name + 1]; + char* dst = (out != nullptr && cap != 0) ? out : own; + okm_uptr room = (out != nullptr && cap != 0) ? cap : sizeof own; + okm_long r = okm::sys(okm::nr_readlinkat, b, reinterpret_cast(t.buf), + reinterpret_cast(dst), static_cast(room)); + if (okm::failed(r)) return -okm::translate(r); + + if (static_cast(r) == room && room < sizeof own) { + const okm_long full = okm::sys(okm::nr_readlinkat, b, + reinterpret_cast(t.buf), + reinterpret_cast(own), + static_cast(sizeof own)); + if (!okm::failed(full)) r = full; + } + return static_cast(r); +} } diff --git a/src/memory.cpp b/src/memory.cpp index 38a3adb..b9a20d0 100644 --- a/src/memory.cpp +++ b/src/memory.cpp @@ -21,7 +21,41 @@ namespace { -constexpr okm_uptr kPage = 4096; +// ⚠️⚠️ THE PAGE IS ASKED FOR, NOT ASSUMED. It was the constant 4096, and this +// system's own hardware has two page sizes: four kilobytes on one architecture +// and SIXTEEN on the other. A mapping rounded to four kilobytes on a machine +// whose page is sixteen is rounded up again by the kernel --- so allocation +// appeared to work --- while `kal_free' unmapped a range SHORTER than the one +// that was mapped, and the remainder was never returned. +// +// It is asked once. The value cannot change while the program runs, and the +// enquiry is a system call this allocator is otherwise not on the path of. +okm_uptr page_size() { + static okm_uptr cached = 0; + if (cached != 0) return cached; + + // hw.pagesize, by the numeric name the kernel takes: { CTL_HW, HW_PAGESIZE }. + int mib[2] = { 6, 7 }; + okm_u32 value = 0; + okm_uptr length = sizeof value; + const okm_long r = okm::sys(okm::nr_sysctl, + reinterpret_cast(mib), 2, + reinterpret_cast(&value), + reinterpret_cast(&length), 0, 0); + // The fallback is the architecture's own page rather than a number that is + // right on one of the two: a value smaller than the truth is what the + // defect above was made of. + if (okm::failed(r) || value == 0) { +#if defined(__aarch64__) + cached = 16384; +#else + cached = 4096; +#endif + } else { + cached = static_cast(value); + } + return cached; +} constexpr okm_uptr kMinBlock = 16; constexpr okm_uptr kMaxSmall = 32768; constexpr okm_uptr kChunk = 1u << 20; @@ -98,8 +132,9 @@ void* kal_alloc(kal_uintptr size, kal_uintptr align) { return b; } - const okm_uptr bytes = round_up(size, kPage); - if (align <= kPage) return map(bytes); + const okm_uptr page = page_size(); + const okm_uptr bytes = round_up(size, page); + if (align <= page) return map(bytes); // An alignment wider than a page is satisfied by mapping more and // recording, immediately before the region returned, what must be @@ -135,7 +170,9 @@ void kal_free(void* p, kal_uintptr size, kal_uintptr align) { return; } - if (align <= kPage) { unmap(p, round_up(size, kPage)); return; } + if (const okm_uptr page = page_size(); align <= page) { + unmap(p, round_up(size, page)); return; + } auto* user = static_cast(p); const okm_uptr total = reinterpret_cast(user)[-1]; @@ -143,4 +180,12 @@ void kal_free(void* p, kal_uintptr size, kal_uintptr align) { unmap(base, total); } + +// The quantum this environment allocates and protects memory in. This kernel +// allocates and protects in the same unit, so the coarsest of the two is that +// unit; a caller that rounds to it is never wrong. +kal_uintptr kal_memory_granularity(void) { + return static_cast(page_size()); +} + } diff --git a/src/net.cpp b/src/net.cpp index c1ed06f..4a3a619 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -140,12 +140,12 @@ int kal_net_accept(kal_net_listener l, kal_net_conn* out) { } } -kal_uintptr kal_net_stream(kal_net_conn c) { +kal_stream kal_net_stream(kal_net_conn c) { // The bare descriptor, for the reason kal_fs_stream gives: openkal.stream's // operations take whatever the environment's transfer calls take, and a // packed word is not that. const int fd = fd_of(c); - return fd < 0 ? 0u : static_cast(fd); + return kal_stream{ fd < 0 ? 0u : static_cast(fd) }; } int kal_net_peer(kal_net_conn c, kal_endpoint* out) { @@ -197,6 +197,6 @@ void kal_net_close_listener(kal_net_listener l) { // Both positions hold on this kernel: it speaks IPv6 and its `shutdown' ends // transfer in one direction while the other continues. -const kal_uintptr kal_net_props = KAL_NET_PROP_IPV6 | KAL_NET_PROP_HALFCLOSE; +kal_uintptr kal_net_props(void) { return KAL_NET_PROP_IPV6 | KAL_NET_PROP_HALFCLOSE; } } // extern "C" diff --git a/src/process.cpp b/src/process.cpp index 54716a9..fac60c2 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -81,9 +81,9 @@ int kal_process_spawn(kal_dir base, if (!args.build(argv, argv_lens, argc)) return kal_err_no_memory; if (!envs.build(envp, envp_lens, envc)) return kal_err_no_memory; - const okm_long in = streams ? static_cast(streams->in) : 0; - const okm_long ou = streams ? static_cast(streams->out) : 0; - const okm_long er = streams ? static_cast(streams->err) : 0; + const okm_long in = streams ? static_cast(streams->in.h) : 0; + const okm_long ou = streams ? static_cast(streams->out.h) : 0; + const okm_long er = streams ? static_cast(streams->err.h) : 0; bool is_duplicate = false; const okm_long child = okm::duplicate(is_duplicate); @@ -182,9 +182,9 @@ int kal_process_spawn_with(kal_dir base, if (granted[i] < 0) return kal_err_invalid; } - const okm_long in = streams ? static_cast(streams->in) : 0; - const okm_long ou = streams ? static_cast(streams->out) : 0; - const okm_long er = streams ? static_cast(streams->err) : 0; + const okm_long in = streams ? static_cast(streams->in.h) : 0; + const okm_long ou = streams ? static_cast(streams->out.h) : 0; + const okm_long er = streams ? static_cast(streams->err.h) : 0; bool is_duplicate = false; const okm_long child = okm::duplicate(is_duplicate); @@ -247,9 +247,9 @@ int kal_process_terminate(kal_process h) { // waited for continues, and this environment collects it when the caller exits. void kal_process_close(kal_process) { } -const kal_uintptr kal_process_props = +kal_uintptr kal_process_props(void) { return KAL_PROCESS_PROP_TERMINATE | KAL_PROCESS_PROP_STREAM_PASSING | KAL_PROCESS_PROP_EXIT_STATUS - | KAL_PROCESS_PROP_CHANNEL | KAL_PROCESS_PROP_GRANT_DIR; + | KAL_PROCESS_PROP_CHANNEL | KAL_PROCESS_PROP_GRANT_DIR; } } diff --git a/src/random.cpp b/src/random.cpp index 2532d74..844568f 100644 --- a/src/random.cpp +++ b/src/random.cpp @@ -42,4 +42,4 @@ extern "C" int kal_random_fill(void* out, kal_uintptr len) { // Neither blocking nor hardware. This kernel's generator is seeded before a // process runs, so there is no wait to report; and whether the seed came from a // hardware source is not something this backend can observe. -extern "C" const kal_uintptr kal_random_props = 0; +extern "C" kal_uintptr kal_random_props(void) { return 0; } diff --git a/src/space.cpp b/src/space.cpp index 1204033..99a3f19 100644 --- a/src/space.cpp +++ b/src/space.cpp @@ -62,7 +62,7 @@ int kal_space_start(void (*entry)(void*), void* arg, void* stack_top, // already reported success. An implementation cannot undefer that, and stating // it is what lets a program that cannot tolerate it know which environment it // is in. -const kal_uintptr kal_space_props = - KAL_SPACE_PROP_CLONE_HANDLES | KAL_SPACE_PROP_DEFERRED_COPY; +kal_uintptr kal_space_props(void) { return + KAL_SPACE_PROP_CLONE_HANDLES | KAL_SPACE_PROP_DEFERRED_COPY; } } // extern "C" diff --git a/src/stream.cpp b/src/stream.cpp index 8f3bd9b..3337de2 100644 --- a/src/stream.cpp +++ b/src/stream.cpp @@ -7,7 +7,8 @@ kal_stream kal_stdin (void) { return kal_stream{0}; } kal_stream kal_stdout(void) { return kal_stream{1}; } kal_stream kal_stderr(void) { return kal_stream{2}; } -kal_io_result kal_stream_write(kal_stream s, const void* buf, kal_uintptr len) { +// ONE SIGNED WORD: the count, or the negated condition when no byte moved. +kal_intptr kal_stream_write(kal_stream s, const void* buf, kal_uintptr len) { const auto* p = static_cast(buf); kal_uintptr done = 0; while (done < len) { @@ -20,23 +21,26 @@ kal_io_result kal_stream_write(kal_stream s, const void* buf, kal_uintptr len) { // reports it produces short writes on any system that delivers // signals --- a failure a test suite is unlikely to reproduce. if (okm::interrupted(r)) continue; - if (okm::failed(r)) return { done, okm::translate(r) }; + if (okm::failed(r)) { + if (done != 0) return static_cast(done); + return -okm::translate(r); + } if (r == 0) break; done += static_cast(r); } - return { done, done == len ? kal_ok : kal_err_io }; + return static_cast(done); } -kal_io_result kal_stream_read(kal_stream s, void* buf, kal_uintptr len) { +kal_intptr kal_stream_read(kal_stream s, void* buf, kal_uintptr len) { for (;;) { const okm_long r = okm::sys(okm::nr_read, static_cast(s.h), reinterpret_cast(buf), static_cast(len)); if (okm::interrupted(r)) continue; - if (okm::failed(r)) return { 0, okm::translate(r) }; + if (okm::failed(r)) return -okm::translate(r); // A short read is reported as it occurred. Unlike a short write it // carries information the caller requires: zero denotes end of input. - return { static_cast(r), kal_ok }; + return static_cast(r); } } diff --git a/src/sys.h b/src/sys.h index e248d4a..9a70117 100644 --- a/src/sys.h +++ b/src/sys.h @@ -179,6 +179,7 @@ enum : okm_long { nr_thread_selfid = 372, nr_openat = 463, nr_renameat = 465, nr_faccessat = 466, nr_fstatat64 = 470, nr_unlinkat = 472, nr_readlinkat = 473, + nr_symlinkat = 474, nr_fstatfs64 = 346, nr_sysctl = 202, nr_mkdirat = 475, nr_ulock_wait = 515, nr_ulock_wake = 516, @@ -302,6 +303,27 @@ inline int translate(okm_long r) { // --- this kernel's structure layouts ----------------------------------------- +// What this kernel reports about the volume a descriptor is on. It names the +// format in words rather than by a number, which is what this implementation +// consults --- a property that varies between the RESOURCES of an interface is +// answered by an enquiry taking the resource, and here the resource's format is +// what the enquiry has to look at. +struct kstatfs64 { + okm_u32 f_bsize; + okm_u32 f_iosize; // int32; the pair fills the first eight bytes + okm_u64 f_blocks, f_bfree, f_bavail, f_files, f_ffree; + okm_u32 f_fsid[2]; + okm_u32 f_owner; + okm_u32 f_type; + okm_u32 f_flags; + okm_u32 f_fssubtype; + char f_fstypename[16]; + char f_mntonname[1024]; + char f_mntfromname[1024]; + okm_u32 f_flags_ext; + okm_u32 f_reserved[7]; +}; + struct kstat64 { okm_u32 dev; okm_u32 mode_pad; // st_mode is 16 bits followed by 16 of nlink @@ -423,8 +445,14 @@ inline bool acceptable(const char* name, okm_uptr len) { return true; } +// The greatest length of a name this implementation accepts, which is the +// buffer below less the terminator it adds. A bound a caller cannot learn +// produces a failure it cannot attribute: a longer name was refused as +// kal_err_invalid, which is also the answer for a name that ascends. +inline constexpr okm_uptr max_name = 1023; + struct terminated { - char buf[1024]; + char buf[max_name + 1]; bool ok; terminated(const char* s, okm_uptr n) : ok(n < sizeof buf) { if (ok) { copy(buf, s, n); buf[n] = '\0'; } diff --git a/src/task.cpp b/src/task.cpp index 6696e38..b090b33 100644 --- a/src/task.cpp +++ b/src/task.cpp @@ -188,8 +188,8 @@ int kal_task_wake(const kal_u32* word, kal_uintptr count, kal_uintptr* woken) { // that compiled the program: this system's thread library establishes it for // every context it creates, which is why the position can be reported without // this implementation doing anything to earn it. -const kal_uintptr kal_task_props = +kal_uintptr kal_task_props(void) { return KAL_TASK_PROP_PREEMPTIVE | KAL_TASK_PROP_PARALLEL - | KAL_TASK_PROP_WAIT_TIMEOUT | KAL_TASK_PROP_THREAD_LOCAL; + | KAL_TASK_PROP_WAIT_TIMEOUT | KAL_TASK_PROP_THREAD_LOCAL; } } diff --git a/src/time.cpp b/src/time.cpp index f523edd..7649b18 100644 --- a/src/time.cpp +++ b/src/time.cpp @@ -79,7 +79,7 @@ void kal_time_sleep(kal_duration ns) { // exactly this divergence: a program measuring an interval across a suspension // obtains different answers from the two, and no operation could report which // it is dealing with. -const kal_uintptr kal_time_props = - KAL_TIME_PROP_WALL_AVAILABLE | KAL_TIME_PROP_SLEEP_PRECISE; +kal_uintptr kal_time_props(void) { return + KAL_TIME_PROP_WALL_AVAILABLE | KAL_TIME_PROP_SLEEP_PRECISE; } } diff --git a/src/timeout.cpp b/src/timeout.cpp index fec8b7b..439f2ef 100644 --- a/src/timeout.cpp +++ b/src/timeout.cpp @@ -63,10 +63,10 @@ int await(int fd, short events, kal_u64 ns) { extern "C" { -kal_io_result kal_timeout_read(kal_stream s, void* buf, kal_uintptr len, kal_u64 ns) { +kal_intptr kal_timeout_read(kal_stream s, void* buf, kal_uintptr len, kal_u64 ns) { // A transfer of zero bytes does not wait and is not bounded. Waiting first // would turn a call that always succeeds into one that can expire. - if (len == 0) return { 0, kal_ok }; + if (len == 0) return 0; const int fd = okm::unpack(s.h); // THE STANDARD STREAMS ARE NOT PACKED HANDLES. openkal.stream reports them @@ -75,18 +75,18 @@ kal_io_result kal_timeout_read(kal_stream s, void* buf, kal_uintptr len, kal_u64 const int use = (fd >= 0) ? fd : static_cast(s.h); if (const int rc = await(use, static_cast(okm::poll_in), ns); rc != kal_ok) - return { 0, rc }; + return -rc; return kal_stream_read(s, buf, len); } -kal_io_result kal_timeout_write(kal_stream s, const void* buf, kal_uintptr len, kal_u64 ns) { - if (len == 0) return { 0, kal_ok }; +kal_intptr kal_timeout_write(kal_stream s, const void* buf, kal_uintptr len, kal_u64 ns) { + if (len == 0) return 0; const int fd = okm::unpack(s.h); const int use = (fd >= 0) ? fd : static_cast(s.h); if (const int rc = await(use, static_cast(okm::poll_out), ns); rc != kal_ok) - return { 0, rc }; + return -rc; return kal_stream_write(s, buf, len); } @@ -100,13 +100,13 @@ int kal_timeout_accept(kal_net_listener l, kal_u64 ns, kal_net_conn* out) { return kal_net_accept(l, out); } -kal_io_result kal_timeout_recv_from(kal_datagram d, void* buf, kal_uintptr len, - kal_endpoint* from, kal_u64 ns) { +kal_intptr kal_timeout_recv_from(kal_datagram d, void* buf, kal_uintptr len, + kal_endpoint* from, kal_u64 ns) { const int fd = okm::unpack(d.h); - if (fd < 0) return { 0, kal_err_invalid }; + if (fd < 0) return -kal_err_invalid; if (const int rc = await(fd, static_cast(okm::poll_in), ns); rc != kal_ok) - return { 0, rc }; + return -rc; return kal_datagram_recv_from(d, buf, len, from); } @@ -160,6 +160,6 @@ int kal_timeout_wait_process(kal_process p, kal_u64 ns, int* status, int* termin // honestly report --- and it is also the interval the child-waiting loop above // polls at, so a caller asking for less is not told a number one of the // operations cannot meet. -const kal_uintptr kal_timeout_granularity_ns = 1000000u; +kal_u64 kal_timeout_granularity(void) { return 1000000u; } } // extern "C" diff --git a/src/version.cpp b/src/version.cpp new file mode 100644 index 0000000..519a4f4 --- /dev/null +++ b/src/version.cpp @@ -0,0 +1,21 @@ +#include "sys.h" +#include + +// What this implementation says about itself before it is used. Both answers are +// constants; openkal/version.h states why they belong to no interface. +extern "C" { + +kal_u64 kal_version(void) { return KAL_VERSION; } + +kal_u64 kal_interfaces(void) { + // Written out rather than derived: a word derived from what happens to be + // linked would report a facility as present when the linker had merely + // kept it. + return KAL_IFACE_ABORT | KAL_IFACE_STREAM | KAL_IFACE_MEMORY + | KAL_IFACE_ENV | KAL_IFACE_TIME | KAL_IFACE_RANDOM + | KAL_IFACE_FS | KAL_IFACE_PROCESS | KAL_IFACE_TASK + | KAL_IFACE_EXEC | KAL_IFACE_TERMINAL | KAL_IFACE_NET + | KAL_IFACE_DATAGRAM | KAL_IFACE_SPACE | KAL_IFACE_TIMEOUT; +} + +} diff --git a/tests/conformance_env_time.cpp b/tests/conformance_env_time.cpp index 68ed75f..fc56909 100644 --- a/tests/conformance_env_time.cpp +++ b/tests/conformance_env_time.cpp @@ -20,16 +20,19 @@ int main() { // A program always receives the name it was started with, even where the // environment has none, in which case it is empty rather than absent. check(kal::env::arg_count() >= 1, "at least the program name is present"); - kal_uintptr n = 0; - check(kal::env::arg(0, &n) != nullptr, "argument zero is readable"); + // ⭐ THE VALUE IS COPIED AND THE LENGTH REPORTED IS THE VALUE'S OWN, so a + // capacity of zero asks for the length without writing. + char buf[1024]; + check(kal::env::arg(0, buf, sizeof buf) >= 0, "argument zero is readable"); + check(kal::env::arg(0, nullptr, 0) == kal::env::arg(0, buf, sizeof buf), + "a capacity of zero reports the same length as a copy"); // A variable that is certain to exist under the harness, and one that is // certain not to. Both halves are asserted, because a lookup that always // succeeded and one that always failed would each satisfy only one. - kal_uintptr vlen = 0; - const char* path = kal::env::var("PATH", 4, &vlen); - check(path != nullptr && vlen > 0, "an existing variable is found"); - check(kal::env::var("OPENKAL_ABSENT_VARIABLE", 23, &vlen) == nullptr, + check(kal::env::var("PATH", 4, buf, sizeof buf) > 0, "an existing variable is found"); + check(kal::env::var("OPENKAL_ABSENT_VARIABLE", 23, buf, sizeof buf) + == -kal_err_not_found, "an absent variable is reported absent"); check(kal_env_var_count() > 0, "the set can be enumerated"); diff --git a/tests/conformance_fs.cpp b/tests/conformance_fs.cpp index 4255d02..f490e0d 100644 --- a/tests/conformance_fs.cpp +++ b/tests/conformance_fs.cpp @@ -25,38 +25,41 @@ int main() { check(root.h != 0, "the working directory is the first entry"); // Each supplied directory carries the name the environment gives it. - kal_dir d0{}; const char* n0 = nullptr; kal_uintptr l0 = 0; - check(kal_fs_preopen(0, &d0, &n0, &l0) == kal_ok && n0 != nullptr && l0 > 0, + kal_dir d0{}; char n0[512]; kal_uintptr l0 = 0; + check(kal_fs_preopen(0, &d0, n0, sizeof n0, &l0) == kal_ok && l0 > 0, "a supplied directory carries a name"); kal_dir beyond{}; - check(kal_fs_preopen(kal::fs::preopen_count(), &beyond, nullptr, nullptr) != kal_ok, + check(kal_fs_preopen(kal::fs::preopen_count(), &beyond, nullptr, 0, nullptr) != kal_ok, "an index beyond the set is refused"); // Creation, writing, reading back, and removal. kal_file f{}; - check(kal_fs_open_file(root, "okl_probe.txt", 13, 1, 1, &f) == kal_ok, + check(kal_fs_open(root, "okl_probe.txt", 13, + (kal::fs::open::read | kal::fs::open::write + | kal::fs::open::create | kal::fs::open::truncate).bits, &f) == kal_ok, "a file is created"); - const kal_stream s{ kal_fs_stream(f) }; + const kal_stream s = kal_fs_stream(f); const char payload[] = "conformance"; - check(kal::write(s, payload, sizeof(payload) - 1).e == kal_ok, "the file is written"); + check(kal::write(s, payload, sizeof(payload) - 1) + == static_cast(sizeof(payload) - 1), "the file is written"); __UINT64_TYPE__ pos = 0; check(kal_fs_seek(f, 0, kal::fs::seek_set, &pos) == kal_ok && pos == 0, "the file is repositioned"); char back[32] = {}; - const auto r = kal::read(s, back, sizeof(back)); - check(r.e == kal_ok && r.n == sizeof(payload) - 1, "the file reads back"); + const kal_intptr r = kal::read(s, back, sizeof(back)); + check(r == static_cast(sizeof(payload) - 1), "the file reads back"); for (kal_uintptr i = 0; i < sizeof(payload) - 1; ++i) check(back[i] == payload[i], "the contents match"); kal_fs_close_file(f); // Enquiry reports what was written, and reports absence without failing. - kal_node_info info{}; - check(kal_fs_info(root, "okl_probe.txt", 13, &info) == kal_ok, "enquiry succeeds"); + kal_node_info info{}; info.self_size = sizeof info; + check(kal_fs_info(root, "okl_probe.txt", 13, 0, kal::fs::field::all, &info) == kal_ok, "enquiry succeeds"); check(info.kind == kal_node_file, "the node is a file"); check(info.size == sizeof(payload) - 1, "the size is reported"); - kal_node_info absent{}; - check(kal_fs_info(root, "okl_absent", 10, &absent) == kal_ok + kal_node_info absent{}; absent.self_size = sizeof absent; + check(kal_fs_info(root, "okl_absent", 10, 0, kal::fs::field::all, &absent) == kal_ok && absent.kind == kal_node_absent, "an absent name is reported absent rather than as a failure"); @@ -65,14 +68,17 @@ int main() { kal_dir d{}; check(kal_fs_open_dir(root, "okl_dir", 7, &d) == kal_ok, "the directory opens"); kal_file inner{}; - check(kal_fs_open_file(d, "inner", 5, 1, 1, &inner) == kal_ok, "a file is created within"); + check(kal_fs_open(d, "inner", 5, + (kal::fs::open::read | kal::fs::open::write + | kal::fs::open::create | kal::fs::open::truncate).bits, &inner) == kal_ok, "a file is created within"); kal_fs_close_file(inner); kal_uintptr iter = 0; bool found = false; check(kal_fs_list_begin(d, &iter) == kal_ok, "enumeration begins"); for (;;) { - const char* name = nullptr; kal_uintptr len = 0; int kind = 0; - if (kal_fs_list_next(d, &iter, &name, &len, &kind) != kal_ok) break; - if (name == nullptr) break; + char name[512]; kal_uintptr len = 0; int kind = 0; + if (kal_fs_list_next(d, &iter, name, sizeof name, &len, &kind) != kal_ok) break; + // The iterator becoming zero is how the end is reported now. + if (iter == 0) break; if (len == 5 && name[0] == 'i') found = true; } check(found, "enumeration finds the entry"); @@ -87,7 +93,7 @@ int main() { // A released handle is not valid, which the specification requires. kal_fs_close_dir(d); kal_file after{}; - check(kal_fs_open_file(d, "inner", 5, 0, 0, &after) != kal_ok, + check(kal_fs_open(d, "inner", 5, kal::fs::open::read.bits, &after) != kal_ok, "a released handle is not treated as valid"); check(kal_fs_remove(root, "okl_dir/inner", 13) == kal_ok, "the inner file is removed"); diff --git a/tests/conformance_process_task.cpp b/tests/conformance_process_task.cpp index 0bbaca8..5353253 100644 --- a/tests/conformance_process_task.cpp +++ b/tests/conformance_process_task.cpp @@ -37,12 +37,14 @@ int main() { // The program to start is reached through a directory the environment // supplied, which is the whole reason the set exists: a program and the // program it starts are commonly not beneath one root. - kal_dir slash{}; const char* nm = nullptr; kal_uintptr nl = 0; + kal_dir slash{}; char nm[512] = {}; kal_uintptr nl = 0; bool have_root = false; for (kal_uintptr i = 0; i < kal::fs::preopen_count(); ++i) { - kal_dir d{}; const char* n = nullptr; kal_uintptr l = 0; - if (kal_fs_preopen(i, &d, &n, &l) != kal_ok) continue; - if (l == 1 && n[0] == '/') { slash = d; nm = n; nl = l; have_root = true; } + kal_dir d{}; char n[512]; kal_uintptr l = 0; + if (kal_fs_preopen(i, &d, n, sizeof n, &l) != kal_ok) continue; + if (l == 1 && n[0] == '/') { + slash = d; nm[0] = '/'; nm[1] = '\0'; nl = l; have_root = true; + } } check(have_root, "a directory covering the file system is supplied"); diff --git a/tests/conformance_stream.cpp b/tests/conformance_stream.cpp index 307fbf7..85d9ed9 100644 --- a/tests/conformance_stream.cpp +++ b/tests/conformance_stream.cpp @@ -33,13 +33,16 @@ int main() { // specification excludes a successful partial transfer, so a conforming // result reports either the full count or a non-zero error. const char msg[] = "openkal-linux: conformance\n"; - const auto r = kal::write(kal::out(), msg, sizeof(msg) - 1); - check(r.e == kal_ok, "write reports success"); - check(r.n == sizeof(msg) - 1, "write transfers the whole buffer"); + // ⭐ ONE SIGNED WORD: the count, or the negated condition when no byte + // moved. A caller never inspects two things to learn one thing. + const kal_intptr r = kal::write(kal::out(), msg, sizeof(msg) - 1); + check(r >= 0, "write reports success"); + check(r == static_cast(sizeof(msg) - 1), + "write transfers the whole buffer"); // An invalid handle is reported rather than accepted. - const auto bad = kal::write(kal::stream{ 0x7fffffff }, msg, 1); - check(bad.e != kal_ok, "an invalid handle is rejected"); + const kal_intptr bad = kal::write(kal::stream{ 0x7fffffff }, msg, 1); + check(bad < 0, "an invalid handle is rejected"); // Flushing an unbuffered stream succeeds. check(kal::flush(kal::out()) == kal_ok, "flush succeeds");