diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index affdb02..2fd7a1a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -189,8 +189,30 @@ jobs: # # A check that reads source files must say what they are encoded in, or # it reports on the runner's locale. - declared = set(re.findall(r'OKW_IMPORT\s+\w+\s+OKW_API\s+(\w+)\s*\(', - open("src/win32.h", encoding="utf-8").read())) + # ⚠️⚠️ EVERY HEADER, AND EVERY SPELLING. THIS CHECK MISSED THE SECOND + # OF EACH AND THE DEFECT IT WAS WRITTEN FOR HAPPENED AGAIN. + # + # It read `src/win32.h' alone and matched `OKW_IMPORT ... OKW_API' + # alone. `src/win.h' declares the object manager's entries in the + # plain form, `__declspec(dllimport) long __stdcall Nt...', so four of + # them were outside what this looked at --- and the failure appeared + # exactly where the comment above says it appeared last time, in + # openkal-llvm-runtime's cross-build, one repository away: + # + # ld.lld: error: undefined symbol: + # __declspec(dllimport) NtQueryVolumeInformationFile + # + # ⭐ THE CHECK WAS RIGHT AND ITS SCOPE WAS WRONG, which is the harder + # kind to notice: it reported a number, the number was of the names it + # knew about, and nothing said the set was partial. So the headers are + # globbed and both forms are matched. + declared = set() + headers = sorted(glob.glob("src/*.h")) + for h in headers: + text = open(h, encoding="utf-8").read() + declared |= set(re.findall(r'OKW_IMPORT\s+\w+\s+OKW_API\s+(\w+)\s*\(', text)) + declared |= set(re.findall( + r'__declspec\(dllimport\)[\w\s\*]*?(\w+)\s*\(', text)) exported = set() for f in glob.glob("port/*.def"): body = open(f, encoding="utf-8").read().split("EXPORTS", 1) @@ -203,10 +225,10 @@ jobs: print(f"::error::declared={len(declared)} exported={len(exported)}; nothing was compared") sys.exit(1) missing = sorted(declared - exported) - print(f" {len(declared)} declared, {len(exported)} exported across " - f"{len(glob.glob('port/*.def'))} .def files") + print(f" {len(declared)} declared across {len(headers)} headers, " + f"{len(exported)} exported across {len(glob.glob('port/*.def'))} .def files") if missing: - print("::error::declared in src/win32.h and exported by no .def:") + print("::error::declared in src/*.h and exported by no .def:") for m in missing: print(f" {m}") sys.exit(1) print(" ok every declared name is exported") @@ -266,6 +288,40 @@ jobs: git -C .spec checkout --quiet "origin/$branch" fi + # ⚠️⚠️ CLONING THE SPECIFICATION IS NOT THE SAME AS BUILDING AGAINST IT, + # AND THIS JOB DID THE FIRST WHILE BELIEVING IT DID THE SECOND. + # + # `.spec` above is consumed by `run-conformance.sh`, which substitutes the + # 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 the steps AFTER it call `mcpp build` directly, and + # by then the manifest names `openkal` by version again, so those steps + # 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 JOB, NOT THE REPOSITORY. Measured 2026-08-28 across + # the eight repositories of this ecosystem: seven jobs in three of them + # had this shape, and each of those repositories ALSO had a job doing it + # correctly --- which is what made the gap invisible to a check done a + # repository at a time. 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 + - name: Install wine run: | sudo dpkg --add-architecture i386 diff --git a/.gitignore b/.gitignore index ff39a22..51060a7 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,7 @@ compile_commands.json # What a system leaves behind. .DS_Store Thumbs.db + +# A working tree of the specification, cloned by continuous integration. +# No trailing slash: the pattern must match a symbolic link too. +.spec diff --git a/README.md b/README.md index 8811ec6..b3bb09a 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,10 @@ An implementation of [openkal](https://github.com/mcpplibs/openkal) for Windows. ```toml [dependencies] -openkal = "0.8.0" +openkal = "0.9.0" [target.'cfg(windows)'.dependencies] -openkal-windows = "0.3.0" +openkal-windows = "0.4.0" ``` Its purpose is as much to test the specification as to be used. openkal was diff --git a/mcpp.toml b/mcpp.toml index aec11c6..4181ba2 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-windows" -version = "0.3.0" +version = "0.4.0" description = "An implementation of openkal for Windows, written on the Win32 interfaces and the object manager beneath them, using no C runtime symbol." license = "Apache-2.0" @@ -18,7 +18,7 @@ authors = ["mcpplibs"] repo = "https://github.com/mcpplibs/openkal-windows" [dependencies] -openkal = "0.8.0" +openkal = "0.9.0" # The package contributes definitions and no modules. The interface it # implements is declared by the specification package, which this package diff --git a/port/kernel32.def b/port/kernel32.def index 93380a3..f109af7 100644 --- a/port/kernel32.def +++ b/port/kernel32.def @@ -27,6 +27,7 @@ GetLogicalDriveStringsW GetProcAddress GetProcessHeap GetStdHandle +GetSystemInfo GetSystemTimePreciseAsFileTime HeapAlloc HeapFree diff --git a/port/ntdll.def b/port/ntdll.def index 4358c82..7078316 100644 --- a/port/ntdll.def +++ b/port/ntdll.def @@ -7,7 +7,11 @@ LIBRARY ntdll.dll EXPORTS NtClose NtCreateFile +NtFlushBuffersFile NtQueryDirectoryFile NtQueryInformationFile +NtQueryVolumeInformationFile +NtReadFile NtSetInformationFile +NtWriteFile RtlNtStatusToDosError diff --git a/src/datagram.cpp b/src/datagram.cpp index aab9b1d..c150f04 100644 --- a/src/datagram.cpp +++ b/src/datagram.cpp @@ -77,22 +77,22 @@ int kal_datagram_local(kal_datagram d, kal_endpoint* out) { return okw::from_system(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 SOCKET s = socket_of(d); - if (bad(s) || to == nullptr) return { 0, kal_err_invalid }; - if (len > kMaxOne) return { 0, kal_err_invalid }; + if (bad(s) || to == nullptr) return -kal_err_invalid; + if (len > kMaxOne) return -kal_err_invalid; auto* n = net(); - if (n == nullptr) return { 0, kal_err_io }; + if (n == nullptr) return -kal_err_io; ksockaddr_storage ss{}; int addrlen = 0; if (const int rc = okw::to_system(*to, ss, addrlen); rc != kal_ok) - return { 0, rc }; + return -rc; const int r = n->send_to(s, static_cast(buf), static_cast(len), 0, &ss, addrlen); - if (r < 0) return { 0, okw::last_socket_error() }; + if (r < 0) return -okw::last_socket_error(); // A MESSAGE IS SENT WHOLE OR NOT AT ALL, which is what this interface // states. The system reports a count anyway; a count short of the length @@ -101,17 +101,17 @@ kal_io_result kal_datagram_send_to(kal_datagram d, const void* buf, kal_uintptr // partial send this interface says cannot occur, so it is reported as a // failure of the medium instead. const kal_uintptr sent = static_cast(r); - return { sent, sent == len ? kal_ok : kal_err_io }; + return sent == len ? static_cast(sent) : -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 SOCKET s = socket_of(d); - if (bad(s)) return { 0, kal_err_invalid }; + if (bad(s)) return -kal_err_invalid; if (len > kMaxOne) len = kMaxOne; auto* n = net(); - if (n == nullptr) return { 0, kal_err_io }; + if (n == nullptr) return -kal_err_io; ksockaddr_storage ss{}; int addrlen = static_cast(sizeof ss); @@ -137,9 +137,12 @@ kal_io_result kal_datagram_recv_from(kal_datagram d, void* buf, kal_uintptr len, from->addr_len = 0; from->port = 0; } - return { len, kal_ok }; + // A message longer than the buffer: the bytes placed are the + // buffer's length, and reporting the count is reporting what the + // caller may read. + return static_cast(len); } - return { 0, okw::last_socket_error() }; + return -okw::last_socket_error(); } if (from != nullptr) { @@ -152,7 +155,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); } void kal_datagram_close(kal_datagram d) { @@ -166,6 +169,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 9b17f25..05e7acd 100644 --- a/src/env.cpp +++ b/src/env.cpp @@ -101,15 +101,30 @@ extern "C" { kal_uintptr kal_env_arg_count(void) { prepare(); return static_cast(g_argc); } -const char* kal_env_arg(kal_uintptr index, kal_uintptr* len) { +// EVERY VALUE IS COPIED INTO THE CALLER'S BUFFER, and each reports the length +// the value HAS. These answered with a pointer into this implementation's own +// storage, which is meaningful only while the implementation shares the +// caller's address space. +namespace { +kal_intptr give(const char* v, kal_uintptr n, char* out, kal_uintptr cap) { + if (out != nullptr && cap != 0) { + const kal_uintptr room = n < cap ? n : cap; + for (kal_uintptr i = 0; i < room; ++i) out[i] = v[i]; + } + return static_cast(n); +} +} // namespace + +kal_intptr kal_env_arg(kal_uintptr index, char* out, kal_uintptr cap) { prepare(); - if (index >= static_cast(g_argc)) { if (len) *len = 0; return nullptr; } - if (len) *len = g_argv_len[index]; - return g_argv[index]; + if (index >= static_cast(g_argc)) return -kal_err_not_found; + return give(g_argv[index], g_argv_len[index], 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) { prepare(); + if (name == nullptr) return -kal_err_invalid; for (int i = 0; i < g_varc; ++i) { // Names are compared without regard to case, because that is how this // environment compares them. A program that set PATH and asked for Path @@ -124,23 +139,19 @@ const char* kal_env_var(const char* name, kal_uintptr name_len, kal_uintptr* val if (a != b) { equal = false; break; } } if (!equal) continue; - if (value_len) *value_len = g_value_len[i]; - return g_value[i]; + return give(g_value[i], g_value_len[i], 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) { prepare(); return static_cast(g_varc); } -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. +kal_intptr kal_env_var_at(kal_uintptr index, char* out, kal_uintptr cap) { prepare(); - if (index >= static_cast(g_varc)) return nullptr; - if (name_len) *name_len = g_name_len[index]; - if (value) *value = g_value[index]; - if (value_len) *value_len = g_value_len[index]; - return g_entry[index]; + if (index >= static_cast(g_varc)) return -kal_err_not_found; + return give(g_entry[index], g_name_len[index], out, cap); } } diff --git a/src/exec.cpp b/src/exec.cpp index 2ce0472..3c17737 100644 --- a/src/exec.cpp +++ b/src/exec.cpp @@ -66,6 +66,10 @@ void kal_exec_free(void* p, kal_uintptr size) { // protection call is not one-way, and a second `VirtualProtect' to // PAGE_READWRITE succeeds. The position is set accordingly, and a caller that // must change published bytes need not abandon the region. -const kal_uintptr kal_exec_props = KAL_EXEC_PROP_REPUBLISH; +// This system grants executable memory to every program; nothing here is +// withheld from an artifact for the way it was produced. +kal_uintptr kal_exec_props(void) { + return KAL_EXEC_PROP_REPUBLISH | KAL_EXEC_PROP_AVAILABLE; +} } // extern "C" diff --git a/src/fs.cpp b/src/fs.cpp index 67a3624..0b0a816 100644 --- a/src/fs.cpp +++ b/src/fs.cpp @@ -121,7 +121,29 @@ long open_relative(void* root, const char* name, kal_uintptr len, nullptr, 0); } -int fill(void* h, kal_node_info* out) { +// 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; +} + +void put_bytes(void* dst, const void* src, kal_uintptr n) { + auto* d = static_cast(dst); + const auto* s = static_cast(src); + for (kal_uintptr i = 0; i < n; ++i) d[i] = s[i]; +} + +// 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) put_bytes(out, src, n < cap ? n : cap); + if (len) *len = n; + return n; +} + +// Writes no more of the structure than the caller says exists on its side, and +// reports which fields it filled. +int fill(void* h, kal_u32 wanted, kal_node_info* out) { + (void)wanted; okw::io_status_block s{}; okw::file_basic_information basic{}; okw::file_standard_information standard{}; @@ -132,15 +154,76 @@ int fill(void* h, kal_node_info* out) { okw::file_standard_information_class); if (!okw::ok(r)) return okw::translate_nt(r); - out->size = static_cast(standard.end_of_file); - out->modified_ns = to_nanoseconds(basic.last_write_time); - out->kind = (basic.attributes & FILE_ATTRIBUTE_REPARSE_POINT) ? kal_node_link - : (basic.attributes & FILE_ATTRIBUTE_DIRECTORY) ? kal_node_directory - : kal_node_file; - out->writable = (basic.attributes & FILE_ATTRIBUTE_READONLY) ? 0 : 1; + const kal_u32 self = out->self_size; + kal_node_info v{}; + v.self_size = self; + v.present = KAL_INFO_KIND | KAL_INFO_SIZE | KAL_INFO_MODIFIED + | KAL_INFO_WRITABLE; + v.size = static_cast(standard.end_of_file); + v.modified_ns = to_nanoseconds(basic.last_write_time); + v.kind = (basic.attributes & FILE_ATTRIBUTE_REPARSE_POINT) ? kal_node_link + : (basic.attributes & FILE_ATTRIBUTE_DIRECTORY) ? kal_node_directory + : kal_node_file; + v.writable = (basic.attributes & FILE_ATTRIBUTE_READONLY) ? 0 : 1; + + // ⭐ THE IDENTITY IS TWO WORDS BECAUSE ONE IS NOT ENOUGH, AND THIS + // ENVIRONMENT SAYS SO ITSELF: the index it keeps for a file is unique + // WITHIN A VOLUME, so two files on two volumes can share one. The volume's + // serial number is the other word. Where either enquiry is refused --- a + // handle to something that is not on a volume --- the position is left + // clear and a caller is told that this is not known, rather than being told + // that two different nodes are the same. + // ⚠️⚠️ THE VOLUME ENQUIRY REPORTS AN OVERFLOW AND ANSWERS ANYWAY, AND + // TREATING THE OVERFLOW AS A FAILURE THREW THE ANSWER AWAY. + // + // FILE_FS_VOLUME_INFORMATION ends in the volume's LABEL, which is as long + // as the label is. A buffer holding the fixed part and one character of it + // is enough for every field this reads --- the serial number precedes the + // label --- and the object manager still reports STATUS_BUFFER_OVERFLOW, + // because the label did not fit. That value is 0x80000005: negative, so + // `okw::ok' said no, so the position was left clear. + // + // ⭐ WHICH IS A CORRECT REPORT OF SOMETHING THAT WAS NOT TRUE. The + // implementation was saying "this node's identity is not known here", a + // caller was believing it, and the identity was sitting in the buffer. It + // surfaced two packages away, in openkal-musl's probe: `two different files + // have different identities' did not hold on Windows, because both had been + // given the zero this branch leaves behind. + // + // Room for a label is given so the ordinary case SUCCEEDS, and the overflow + // is accepted so the extraordinary one still answers. Both are checked + // rather than one, because a label longer than this is a volume nobody + // tests with and the buffer would be back to reporting an overflow. + struct { + okw::file_fs_volume_information info; + wchar_t label_tail[128]; + } volume{}; + okw::file_internal_information index{}; + const long ri = okw::NtQueryInformationFile(h, &s, &index, sizeof index, + okw::file_internal_information_class); + const long rv = okw::NtQueryVolumeInformationFile(h, &s, &volume, sizeof volume, + okw::fs_volume_information_class); + if (okw::ok(ri) && (okw::ok(rv) || rv == okw::status_buffer_overflow)) { + v.identity[0] = static_cast(volume.info.serial_number); + v.identity[1] = static_cast(index.index_number); + v.present |= KAL_INFO_IDENTITY; + } + + const kal_u32 n = self < sizeof v ? self : (kal_u32)sizeof v; + put_bytes(out, &v, n); return kal_ok; } +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; + put_bytes(out, &v, n); +} + // Enumeration holds a buffer and a handle of its own, obtained by opening the // directory through itself: an enumeration that shared the caller's handle // would share its position, and two enumerations of one directory would consume @@ -171,14 +254,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; } @@ -233,14 +316,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) { void* h = dir_handle(d); if (h) { okw::retire(d.h); okw::NtClose(h); } @@ -254,11 +329,14 @@ void kal_fs_close_file(kal_file f) { // A file's stream is the file. The environment's handle is what openkal.stream // holds here, so no conversion is required and none is performed --- which is a // property of this implementation rather than of the specification. -kal_uintptr kal_fs_stream(kal_file f) { +kal_stream kal_fs_stream(kal_file f) { void* h = file_handle(f); - return h ? reinterpret_cast(h) : 0u; + return kal_stream{ h ? reinterpret_cast(h) : 0u }; } +// The greatest length of a name this implementation accepts. +kal_uintptr kal_fs_max_name(void) { return okw::kMaxName - 1; } + int kal_fs_seek(kal_file f, kal_i64 offset, int whence, kal_u64* result) { void* h = file_handle(f); if (!h) return kal_err_invalid; @@ -286,32 +364,40 @@ int kal_fs_truncate(kal_file f, kal_u64 size) { return okw::ok(r) ? kal_ok : okw::translate_nt(r); } -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) { void* root = dir_handle(base); - if (!root || out == nullptr || !okw::acceptable(name, len)) return kal_err_invalid; + if (!root || !info_ok(out) || !okw::acceptable(name, len)) return kal_err_invalid; void* h = nullptr; + // RESOLVES BY DEFAULT, SO THAT ASKING AND OPENING ANSWER THE SAME QUESTION. + // Without FILE_OPEN_REPARSE_POINT this environment follows the node to what + // it finally refers to, which is what `kal_fs_open' does; with it, the node + // itself is opened and reported. + const unsigned long options = okw::file_open_for_backup_intent + | ((flags & KAL_FS_NO_RESOLVE) ? okw::file_open_reparse_point : 0u); const long r = open_relative(root, name, len, FILE_READ_ATTRIBUTES, - okw::file_open, okw::file_open_for_backup_intent, &h); + okw::file_open, options, &h); if (!okw::ok(r)) { // Clause 7.7: a name that does not exist is an answer, not a failure. A // caller that asks what a name refers to has been answered when told - // that it refers to nothing. + // that it refers to nothing --- and so is a node whose content names + // something absent, when the enquiry resolves. const int e = okw::translate_nt(r); if (e == kal_err_not_found || e == kal_err_not_directory) { - *out = kal_node_info{ 0, 0, kal_node_absent, 0 }; + fill_absent(out); return kal_ok; } return e; } - const int e = fill(h, out); + const int e = fill(h, wanted, out); okw::NtClose(h); return e; } -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) { void* h = file_handle(f); - if (!h || out == nullptr) return kal_err_invalid; - return fill(h, out); + if (!h || !info_ok(out)) return kal_err_invalid; + return fill(h, wanted, out); } int kal_fs_set_modified(kal_file f, kal_u64 modified_ns) { @@ -421,8 +507,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); @@ -438,8 +525,7 @@ int kal_fs_list_next(kal_dir, kal_uintptr* iter, const char** name, okw::NtClose(s->handle); kal_free(s, sizeof(listing), alignof(listing)); *iter = 0; - if (name) *name = nullptr; - if (len) *len = 0; + if (name_len) *name_len = 0; // The end of a directory is reported here as a distinct status // and is the ordinary outcome, not a failure. return (static_cast(r) == 0x80000006u) ? kal_ok @@ -462,8 +548,7 @@ int kal_fs_list_next(kal_dir, kal_uintptr* iter, const char** name, if (chars == 2 && e->file_name[0] == L'.' && e->file_name[1] == L'.') continue; const okw_uptr n = okw::narrow(e->file_name, chars, s->reported, sizeof s->reported); - if (name) *name = s->reported; - if (len) *len = n; + put_name(s->reported, n, name_out, name_cap, name_len); if (kind) *kind = (e->file_attributes & FILE_ATTRIBUTE_REPARSE_POINT) ? kal_node_link : (e->file_attributes & FILE_ATTRIBUTE_DIRECTORY) ? kal_node_directory : kal_node_file; @@ -471,11 +556,57 @@ int kal_fs_list_next(kal_dir, kal_uintptr* iter, const char** name, } } -// Names on this system are compared without regard to case, and a program that -// created two names differing only in case would succeed on one implementation -// and not on this one. 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. This environment reports the volume's abilities itself, so nothing is +// guessed: names on the volume this system is ordinarily installed on are +// compared without regard to case, and a volume attached to the same machine +// may be otherwise --- and a word per implementation could state neither. +kal_uintptr kal_fs_props(kal_dir d) { + void* h = dir_handle(d); + const kal_uintptr conservative = + KAL_FS_PROP_MODIFIED_TIME | KAL_FS_PROP_ATOMIC_RENAME; + if (!h) return 0; + + okw::io_status_block s{}; + struct { okw::file_fs_attribute_information info; wchar_t rest[64]; } a{}; + const long r = okw::NtQueryVolumeInformationFile(h, &s, &a, sizeof a, + okw::fs_attribute_information_class); + if (!okw::ok(r)) return conservative; + + kal_uintptr p = conservative; + if (a.info.attributes & okw::fs_case_sensitive_search) p |= KAL_FS_PROP_CASE_SENSITIVE; + + // ⚠️ LINKS ARE REPORTED AND ARE NOT MADE, AND THE ASYMMETRY IS THIS + // IMPLEMENTATION'S RATHER THAN THE SPECIFICATION'S. + // + // A volume that supports reparse points holds nodes whose content is + // another name, and `kal_fs_info' reports one when it meets it --- so the + // position for meeting them is claimed. Creating one on this system + // requires a privilege an ordinary program does not hold, or the developer + // mode of the system; and reading one requires a control code this + // implementation does not yet issue. Neither is claimed, so a caller asks + // and is told before it tries, which is what the enquiry is for. + if (a.info.attributes & okw::fs_supports_reparse_points) p |= KAL_FS_PROP_LINKS; + return p; +} + +// Nodes whose content is another name. +// +// Refused, and the enquiry above says so in advance. Creating one on this +// system requires SeCreateSymbolicLinkPrivilege or the system's developer mode, +// and reading one requires a file-system control code this implementation does +// not issue. A caller reads KAL_FS_PROP_MAKE_LINKS --- which is not claimed +// here --- rather than discovering it by the attempt. +int kal_fs_link_create(kal_dir, const char*, kal_uintptr, + const char*, kal_uintptr, kal_uintptr) { + return kal_err_not_supported; +} + +kal_intptr kal_fs_link_read(kal_dir, const char*, kal_uintptr, + char*, kal_uintptr) { + return -kal_err_not_supported; +} } diff --git a/src/memory.cpp b/src/memory.cpp index 597bccd..b25c212 100644 --- a/src/memory.cpp +++ b/src/memory.cpp @@ -61,4 +61,24 @@ void kal_free(void* p, kal_uintptr size, kal_uintptr align) { HeapFree(h, 0, reinterpret_cast(p)[-1]); } + +// The quantum this environment allocates and protects memory in. +// +// ⭐⭐ THIS SYSTEM HAS TWO, AND THE COARSER IS REPORTED. It protects memory in +// pages of four kilobytes and RESERVES it in units of sixty-four --- so a value +// taken from either alone is wrong for the other, and a specification that +// derived one number from the page size of one family of systems would be wrong +// here. What the operation promises is that an address and a length that are +// multiples of the reported value are acceptable to every operation of the +// interface, so the coarser of the two is the only answer that keeps the +// promise. +kal_uintptr kal_memory_granularity(void) { + SYSTEM_INFO info{}; + GetSystemInfo(&info); + const kal_uintptr page = info.dwPageSize ? info.dwPageSize : 4096u; + const kal_uintptr grain = info.dwAllocationGranularity + ? info.dwAllocationGranularity : 65536u; + return page > grain ? page : grain; +} + } diff --git a/src/net.cpp b/src/net.cpp index d4daba6..2bcaca9 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -139,12 +139,12 @@ int kal_net_accept(kal_net_listener l, kal_net_conn* out) { return kal_ok; } -kal_uintptr kal_net_stream(kal_net_conn c) { +kal_stream kal_net_stream(kal_net_conn c) { // The socket itself, for the reason kal_fs_stream gives: openkal.stream's // operations take whatever this system's transfer calls take, and a packed // word is not that. const SOCKET s = socket_of(c); - return bad(s) ? 0u : static_cast(s); + return kal_stream{ bad(s) ? 0u : static_cast(s) }; } 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 system: 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 8af7600..c2404db 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -152,14 +152,14 @@ int kal_process_spawn(kal_dir base, STARTUPINFOW startup{}; startup.cb = sizeof startup; bool inherit = false; - if (streams && (streams->in || streams->out || streams->err)) { + if (streams && (streams->in.h || streams->out.h || streams->err.h)) { startup.dwFlags = STARTF_USESTDHANDLES; - startup.hStdInput = streams->in ? reinterpret_cast(streams->in) - : GetStdHandle(STD_INPUT_HANDLE); - startup.hStdOutput = streams->out ? reinterpret_cast(streams->out) - : GetStdHandle(STD_OUTPUT_HANDLE); - startup.hStdError = streams->err ? reinterpret_cast(streams->err) - : GetStdHandle(STD_ERROR_HANDLE); + startup.hStdInput = streams->in.h ? reinterpret_cast(streams->in.h) + : GetStdHandle(STD_INPUT_HANDLE); + startup.hStdOutput = streams->out.h ? reinterpret_cast(streams->out.h) + : GetStdHandle(STD_OUTPUT_HANDLE); + startup.hStdError = streams->err.h ? reinterpret_cast(streams->err.h) + : GetStdHandle(STD_ERROR_HANDLE); SetHandleInformation(startup.hStdInput, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT); SetHandleInformation(startup.hStdOutput, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT); SetHandleInformation(startup.hStdError, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT); @@ -290,9 +290,9 @@ void kal_process_close(kal_process p) { // KAL_PROCESS_PROP_GRANT_DIR is deliberately absent: kal_process_spawn_with // refuses a non-empty set of grants here, and a word claiming a facility the // next call refuses is the disagreement clause 6.2 exists to prevent. -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_CHANNEL; } } diff --git a/src/random.cpp b/src/random.cpp index 3882060..7f9d547 100644 --- a/src/random.cpp +++ b/src/random.cpp @@ -83,6 +83,6 @@ int kal_random_fill(void* out, kal_uintptr len) { // Neither blocking nor hardware. The system'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. -const kal_uintptr kal_random_props = 0; +kal_uintptr kal_random_props(void) { return 0; } } // extern "C" diff --git a/src/stream.cpp b/src/stream.cpp index 05a57cb..b62b0c2 100644 --- a/src/stream.cpp +++ b/src/stream.cpp @@ -17,9 +17,10 @@ kal_stream kal_stdin (void) { return kal_stream{ reinterpret_cast(G kal_stream kal_stdout(void) { return kal_stream{ reinterpret_cast(GetStdHandle(STD_OUTPUT_HANDLE)) }; } kal_stream kal_stderr(void) { return kal_stream{ reinterpret_cast(GetStdHandle(STD_ERROR_HANDLE)) }; } -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) { void* h = handle_of(s); - if (!valid(h)) return { 0, kal_err_invalid }; + if (!valid(h)) return -kal_err_invalid; const auto* p = static_cast(buf); kal_uintptr done = 0; while (done < len) { @@ -30,17 +31,19 @@ kal_io_result kal_stream_write(kal_stream s, const void* buf, kal_uintptr len) { const kal_uintptr want = len - done; const DWORD chunk = want > 0x7fffffffu ? 0x7fffffffu : static_cast(want); DWORD written = 0; - if (!WriteFile(h, p + done, chunk, &written, nullptr)) - return { done, okw::translate_win32(GetLastError()) }; + if (!WriteFile(h, p + done, chunk, &written, nullptr)) { + if (done != 0) return static_cast(done); + return -okw::translate_win32(GetLastError()); + } if (written == 0) break; done += written; } - 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) { void* h = handle_of(s); - if (!valid(h)) return { 0, kal_err_invalid }; + if (!valid(h)) return -kal_err_invalid; const DWORD want = len > 0x7fffffffu ? 0x7fffffffu : static_cast(len); DWORD got = 0; if (!ReadFile(h, buf, want, &got, nullptr)) { @@ -48,12 +51,12 @@ kal_io_result kal_stream_read(kal_stream s, void* buf, kal_uintptr len) { // The end of a pipe whose other side has gone is the end of input, and // this environment reports it as a failure. A caller that could not // tell the two apart would treat every completed transfer as broken. - if (e == ERROR_BROKEN_PIPE || e == ERROR_HANDLE_EOF) return { 0, kal_ok }; - return { 0, okw::translate_win32(e) }; + if (e == ERROR_BROKEN_PIPE || e == ERROR_HANDLE_EOF) return 0; + return -okw::translate_win32(e); } // A short read is reported as it occurred: unlike a short write it carries // information the caller requires, and zero denotes the end of input. - return { got, kal_ok }; + return static_cast(got); } int kal_stream_flush(kal_stream s) { diff --git a/src/task.cpp b/src/task.cpp index 1604007..3873088 100644 --- a/src/task.cpp +++ b/src/task.cpp @@ -119,8 +119,8 @@ int kal_task_wake(const kal_u32* word, kal_uintptr count, kal_uintptr* woken) { // that compiled the program: this environment's loader establishes it for every // thread it creates, which is why the position can be reported here 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 a22fce6..61f2095 100644 --- a/src/time.cpp +++ b/src/time.cpp @@ -73,7 +73,7 @@ void kal_time_sleep(kal_duration ns) { // The counter this environment supplies continues while the machine is // suspended, which the corresponding position records. That is the opposite of // the Linux implementation, and the difference is why the position exists. -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 d678801..0692694 100644 --- a/src/timeout.cpp +++ b/src/timeout.cpp @@ -186,21 +186,21 @@ int await_stream(kal_stream s, 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; if (const int rc = await_stream(s, POLLRDNORM_, 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; if (const int rc = await_stream(s, POLLWRNORM_, ns); rc != kal_ok) - return { 0, rc }; + return -rc; return kal_stream_write(s, buf, len); } @@ -213,12 +213,12 @@ 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 SOCKET s = okw::unpack_socket(d.h); - if (s == INVALID_SOCKET) return { 0, kal_err_invalid }; + if (s == INVALID_SOCKET) return -kal_err_invalid; - if (const int rc = await(s, POLLRDNORM_, ns); rc != kal_ok) return { 0, rc }; + if (const int rc = await(s, POLLRDNORM_, ns); rc != kal_ok) return -rc; return kal_datagram_recv_from(d, buf, len, from); } @@ -248,6 +248,6 @@ int kal_timeout_wait_process(kal_process p, kal_u64 ns, int* status, int* termin // The bound this system distinguishes. Both `WSAPoll' and the wait upon an // object state theirs in milliseconds, and there is no call here that takes // less --- so a millisecond is what an implementation can honestly report. -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..7e4febf --- /dev/null +++ b/src/version.cpp @@ -0,0 +1,22 @@ +#include "win.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) { + // ⚠️ `openkal.space' IS ABSENT AND THE WORD SAYS SO. This system starts a + // NAMED PROGRAM and has no primitive that copies an address space, so the + // interface is not provided at all --- a consumer that is linked learns that + // from the linker, and one bound otherwise learns it here. + 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_TIMEOUT; +} + +} diff --git a/src/win.h b/src/win.h index 714fee7..9e05382 100644 --- a/src/win.h +++ b/src/win.h @@ -113,8 +113,35 @@ struct file_directory_information { wchar_t file_name[1]; }; +// The index this environment keeps for a file, which is unique within a volume. +struct file_internal_information { okw_i64 index_number; }; + +// What the object manager reports about the volume a handle is on. Only the +// serial number is read; the label follows it and is not. +struct file_fs_volume_information { + okw_i64 creation_time; + unsigned long serial_number; + unsigned long label_length; + unsigned char supports_objects; + wchar_t label[1]; +}; + +// What the object manager reports about the volume's abilities. +struct file_fs_attribute_information { + unsigned long attributes; + long maximum_component_name_length; + unsigned long file_system_name_length; + wchar_t file_system_name[1]; +}; + +enum : unsigned long { + fs_case_sensitive_search = 0x00000001u, + fs_supports_reparse_points = 0x00000080u, +}; + enum : int { file_directory_information_class = 1, + file_internal_information_class = 6, file_basic_information_class = 4, file_standard_information_class = 5, file_position_information_class = 14, @@ -123,6 +150,12 @@ enum : int { file_rename_information_class = 10, }; +// The classes NtQueryVolumeInformationFile takes. +enum : int { + fs_volume_information_class = 1, + fs_attribute_information_class = 5, +}; + // The dispositions NtCreateFile takes. They are the whole of what // kal_fs_open's flags become: an intent stated once and carried out once, // rather than an open followed by a truncation that a program could stop @@ -138,6 +171,8 @@ enum : unsigned long { file_synchronous_io_nonalert = 0x00000020, file_non_directory_file = 0x00000040, file_open_for_backup_intent = 0x00004000, + // Opens the node itself rather than what its content names. + file_open_reparse_point = 0x00200000, obj_case_insensitive = 0x00000040, }; @@ -166,6 +201,8 @@ __declspec(dllimport) long __stdcall NtWriteFile(void* handle, void* event, void okw_i64* offset, unsigned long* key); __declspec(dllimport) long __stdcall NtQueryInformationFile(void* handle, io_status_block* status, void* info, unsigned long length, int cls); +__declspec(dllimport) long __stdcall NtQueryVolumeInformationFile(void* handle, io_status_block* status, + void* info, unsigned long length, int cls); __declspec(dllimport) long __stdcall NtSetInformationFile(void* handle, io_status_block* status, void* info, unsigned long length, int cls); __declspec(dllimport) long __stdcall NtQueryDirectoryFile(void* handle, void* event, void* apc, void* apc_context, @@ -178,6 +215,15 @@ __declspec(dllimport) unsigned long __stdcall RtlNtStatusToDosError(long status) inline bool ok(long status) { return status >= 0; } +// ⚠️ AN ENQUIRY THAT REPORTS AN OVERFLOW HAS STILL ANSWERED. STATUS_BUFFER_OVERFLOW +// is a warning rather than an error: the fixed part of the structure was written +// and a variable-length tail was cut. `ok' correctly says no to it --- its sign +// bit is set --- so a caller that reads only fields preceding the tail names it +// here. Measured: `src/fs.cpp' read a volume serial number that the object +// manager had written and discarded it, and two files two packages away were +// reported to have the same identity. +inline constexpr long status_buffer_overflow = static_cast(0x80000005ul); + // --- translation ------------------------------------------------------------- // // The environment's error values are mapped onto the closed set the diff --git a/src/win32.h b/src/win32.h index 565a96c..818bd68 100644 --- a/src/win32.h +++ b/src/win32.h @@ -332,6 +332,22 @@ OKW_IMPORT int OKW_API WideCharToMultiByte(UINT, DWORD, LPCWSTR, int, LPSTR, // specification targets, and the third is the one that matters on a processor // whose instruction path does not see the data path's writes. OKW_IMPORT LPVOID OKW_API VirtualAlloc(LPVOID, unsigned long long, DWORD, DWORD); + +// What this system reports about itself. Only two fields are read and the rest +// are named so that the record has the layout the system writes. +struct SYSTEM_INFO { + DWORD dwOemId; + DWORD dwPageSize; + LPVOID lpMinimumApplicationAddress; + LPVOID lpMaximumApplicationAddress; + unsigned long long dwActiveProcessorMask; + DWORD dwNumberOfProcessors; + DWORD dwProcessorType; + DWORD dwAllocationGranularity; + unsigned short wProcessorLevel; + unsigned short wProcessorRevision; +}; +OKW_IMPORT void OKW_API GetSystemInfo(SYSTEM_INFO*); OKW_IMPORT BOOL OKW_API VirtualProtect(LPVOID, unsigned long long, DWORD, DWORD*); OKW_IMPORT BOOL OKW_API VirtualFree(LPVOID, unsigned long long, DWORD); OKW_IMPORT BOOL OKW_API FlushInstructionCache(HANDLE, LPCVOID, unsigned long long);