From 430affbeda3c9147d7e2221ca2b4ca74ccb0fb2c Mon Sep 17 00:00:00 2001 From: LunaStev Date: Sun, 27 Sep 2026 17:00:24 +0900 Subject: [PATCH 1/2] Fix native filesystem, memory, environment and CLI boundaries Signed-off-by: LunaStev --- .github/workflows/rust.yml | 2 +- src/cli.rs | 12 +- src/main.rs | 6 +- src/runner.rs | 71 ++++- std/env/consts.wave | 3 + std/env/environ.wave | 60 +++- std/sys/linux/amd64/env.wave | 28 +- std/sys/linux/arm64/env.wave | 28 +- std/sys/linux/riscv64/env.wave | 28 +- std/sys/windows/fs.wave | 87 +++++- std/sys/windows/memory.wave | 16 +- .../native_boundaries/environment.wave | 16 + .../native_boundaries/environment_mock.c | 48 +++ .../native_boundaries/environment_mock.wave | 22 ++ .../linux_env_read_mock.wave | 14 + tests/fixtures/native_boundaries/windows.wave | 85 +++++ .../fixtures/native_boundaries/windows_mock.c | 76 +++++ .../native_boundaries/windows_mock.wave | 46 +++ tests/native_boundaries.rs | 293 ++++++++++++++++++ 19 files changed, 871 insertions(+), 70 deletions(-) create mode 100644 tests/fixtures/native_boundaries/environment.wave create mode 100644 tests/fixtures/native_boundaries/environment_mock.c create mode 100644 tests/fixtures/native_boundaries/environment_mock.wave create mode 100644 tests/fixtures/native_boundaries/linux_env_read_mock.wave create mode 100644 tests/fixtures/native_boundaries/windows.wave create mode 100644 tests/fixtures/native_boundaries/windows_mock.c create mode 100644 tests/fixtures/native_boundaries/windows_mock.wave create mode 100644 tests/native_boundaries.rs diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 2f9c18b8..a63f6622 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -861,7 +861,7 @@ jobs: env: WAVE_RUNTIME_ARTIFACT_DIR: ${{ runner.temp }} run: >- - cargo test --locked --no-fail-fast --test runtime_regressions --test std_io_regressions --test native_providers --test stabilization_17 --target aarch64-pc-windows-msvc + cargo test --locked --no-fail-fast --test runtime_regressions --test std_io_regressions --test native_providers --test stabilization_17 --test native_boundaries --target aarch64-pc-windows-msvc --no-default-features --features llvm-target-aarch64 --jobs 2 - name: Save failed native runtime compiler and fixture diff --git a/src/cli.rs b/src/cli.rs index bb91dde7..5844c59a 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -216,7 +216,17 @@ struct BuildPlan { } pub fn run() -> Result<(), CliError> { - let args: Vec = env::args().skip(1).collect(); + // The CLI grammar currently uses UTF-8 strings. Reject unrepresentable + // arguments before planning any outputs; args() panics and lossy decoding + // can alias distinct filesystem names. + let args: Vec = env::args_os() + .skip(1) + .map(|arg| { + arg.into_string().map_err(|arg| { + CliError::usage(format!("command-line argument is not valid UTF-8: {arg:?}")) + }) + }) + .collect::>()?; if args.is_empty() { return Err(CliError::usage("not enough arguments")); } diff --git a/src/main.rs b/src/main.rs index ae53e8ad..87ebc349 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,7 +19,11 @@ use std::process; fn main() { - let json_errors = wavec::cli::args_request_json_errors(std::env::args().skip(1)); + let json_errors = wavec::cli::args_request_json_errors( + std::env::args_os() + .skip(1) + .map(|arg| arg.to_string_lossy().into_owned()), + ); if let Err(e) = wavec::cli::run() { if json_errors { diff --git a/src/runner.rs b/src/runner.rs index a273c397..db391c98 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -410,6 +410,48 @@ fn expand_imports_for_codegen( }) } +#[allow(dead_code)] +fn default_output_path(file_path: &Path, directory: &Path, extension: Option<&str>) -> PathBuf { + let Some(stem) = file_path.file_stem().filter(|stem| !stem.is_empty()) else { + WaveError::new( + WaveErrorKind::FileWriteError(file_path.display().to_string()), + "cannot derive an output name from the source path", + file_path.display().to_string(), + 0, + 0, + ) + .with_code("E1005") + .with_help("pass an explicit output path with -o ") + .display_auto(); + process::exit(1); + }; + let mut name = stem.to_os_string(); + if let Some(extension) = extension { + name.push("."); + name.push(extension); + } + directory.join(name) +} + +fn utf8_output_path(path: &Path, file_path: &Path) -> String { + match path.to_str() { + Some(value) => value.to_owned(), + None => { + WaveError::new( + WaveErrorKind::FileWriteError(path.display().to_string()), + "the backend requires a UTF-8 output path", + file_path.display().to_string(), + 0, + 0, + ) + .with_code("E1005") + .with_help("pass a UTF-8 output path with -o ") + .display_auto(); + process::exit(1); + } + } +} + #[allow(dead_code)] fn resolve_output_target( default_output: &str, @@ -462,7 +504,7 @@ fn resolve_output_target( } } - output.display().to_string() + utf8_output_path(output, file_path) } fn build_backend_options(llvm: &LlvmFlags) -> BackendOptions { @@ -796,8 +838,10 @@ pub(crate) unsafe fn run_wave_file( let hir = lower_wave_hir_or_exit(file_path, &code, ast); - let file_stem = file_path.file_stem().unwrap().to_str().unwrap(); - let object_patch = format!("{}.o", file_stem); + let object_patch = utf8_output_path( + &default_output_path(file_path, Path::new(""), Some("o")), + file_path, + ); emit_wave_codegen_file_from_hir( file_path, &code, @@ -826,7 +870,10 @@ pub(crate) unsafe fn run_wave_file( println!(); } - let exe_patch = format!("target/{}", file_stem); + let exe_patch = utf8_output_path( + &default_output_path(file_path, Path::new("target"), None), + file_path, + ); let backend_opts = build_backend_options(llvm); match run_panic_guarded(|| { @@ -927,9 +974,10 @@ pub(crate) unsafe fn object_build_wave_file( let hir = lower_wave_hir_or_exit(file_path, &code, ast); - let file_stem = file_path.file_stem().unwrap().to_str().unwrap(); - let default_object_path = PathBuf::from(format!("{}.o", file_stem)); - let output_path = output.unwrap_or(default_object_path.as_path()); + let output_path = output + .map(Path::to_path_buf) + .unwrap_or_else(|| default_output_path(file_path, Path::new(""), Some("o"))); + let object_path = utf8_output_path(&output_path, file_path); emit_wave_codegen_file_from_hir( file_path, &code, @@ -937,10 +985,9 @@ pub(crate) unsafe fn object_build_wave_file( opt_flag, debug, llvm, - output_path, + &output_path, CodegenFileKind::Object, ); - let object_path = output_path.to_string_lossy().to_string(); if debug.mc { println!("\n===== MACHINE CODE PATH ====="); @@ -974,8 +1021,10 @@ pub(crate) unsafe fn build_wave_file( ) { let object_path = object_build_wave_file(file_path, opt_flag, debug, dep, llvm, None); - let file_stem = file_path.file_stem().unwrap().to_str().unwrap(); - let default_exe_path = format!("target/{}", file_stem); + let exe_path = output + .map(Path::to_path_buf) + .unwrap_or_else(|| default_output_path(file_path, Path::new("target"), None)); + let default_exe_path = utf8_output_path(&exe_path, file_path); let source = fs::read_to_string(file_path).unwrap_or_default(); let exe_path = resolve_output_target(&default_exe_path, output, file_path, &source, "native-link"); diff --git a/std/env/consts.wave b/std/env/consts.wave index ac35507d..c576c59c 100644 --- a/std/env/consts.wave +++ b/std/env/consts.wave @@ -19,6 +19,9 @@ pub const ENV_ERR_NOT_FOUND: i64 = -2; pub const ENV_ERR_NO_SPACE: i64 = -3; pub const ENV_ERR_INVALID_KEY: i64 = -4; +pub const ENV_ERR_READ: i64 = -5; +pub const ENV_ERR_SOURCE_INCOMPLETE: i64 = -6; +pub const ENV_ERR_NO_MEMORY: i64 = -7; pub const ENV_O_RDONLY: i32 = 0; pub const ENV_SCAN_CAP: i64 = 32768; diff --git a/std/env/environ.wave b/std/env/environ.wave index 6cd8fce0..59a55d39 100644 --- a/std/env/environ.wave +++ b/std/env/environ.wave @@ -26,7 +26,24 @@ import("std::env::parse")::{ _env_parse_i64, }; -import("std::env::consts")::{ENV_ERR_NO_SPACE}; +import("std::env::consts")::{ + ENV_ERR_NO_SPACE, ENV_ERR_NOT_FOUND, ENV_ERR_INVALID_KEY, + ENV_ERR_READ, ENV_ERR_SOURCE_INCOMPLETE, ENV_ERR_NO_MEMORY +}; +import("std::sys::memory")::{sys_alloc, sys_free}; + +// Raw OS error domains differ. Only Linux's complete-read contract reserves +// -ENOSPC for a source buffer that needs to grow. +#[target(os="linux")] +fun _env_source_needs_space(status: i64) -> bool { return status == -28; } +#[target(os="windows")] +fun _env_source_needs_space(status: i64) -> bool { return false; } +#[target(os="macos")] +fun _env_source_needs_space(status: i64) -> bool { return false; } +#[target(os="freebsd")] +fun _env_source_needs_space(status: i64) -> bool { return false; } +#[target(os="wasi")] +fun _env_source_needs_space(status: i64) -> bool { return false; } pub struct EnvResult { ok: bool; @@ -57,21 +74,36 @@ pub fun env_unwrap_or(result: EnvResult, default_value: T) -> T { pub fun env_get(name: str, dst: ptr, dst_cap: i64) -> i64 { var key_len: i64 = _env_key_len(name); - - if (key_len <= 0) { - return -4; + if (key_len <= 0) { return ENV_ERR_INVALID_KEY; } + if (dst == null || dst_cap <= 0) { return ENV_ERR_NO_SPACE; } + + var initial: array; + var raw: ptr = &initial[0]; + var capacity: i64 = 32768; + var allocated: bool = false; + var n: i64 = env_read(raw, capacity); + while (_env_source_needs_space(n)) { + if (allocated) { sys_free(raw, capacity); } + if (capacity > 4611686018427387903) { return ENV_ERR_NO_MEMORY; } + capacity *= 2; + raw = sys_alloc(capacity); + if (raw == null) { return ENV_ERR_NO_MEMORY; } + allocated = true; + n = env_read(raw, capacity); } - - if (dst_cap <= 0) { - return -3; + var result: i64 = ENV_ERR_READ; + if (n >= 0 && n <= capacity) { + result = _env_lookup(raw, n, name, key_len, dst, dst_cap); } + if (allocated) { sys_free(raw, capacity); } + return result; +} - var raw: array; - var n: i64 = env_read(&raw[0], 32768); - - if (n <= 0) { - return -2; - } +fun _env_lookup(raw: ptr, n: i64, name: str, key_len: i64, dst: ptr, dst_cap: i64) -> i64 { + if (n == 0) { return ENV_ERR_NOT_FOUND; } + // An unterminated source entry is neither a complete value nor proof that + // an absent key does not exist beyond the buffer boundary. + if (raw[n - 1] != 0) { return ENV_ERR_SOURCE_INCOMPLETE; } var i: i64 = 0; while (i < n) { @@ -106,7 +138,7 @@ pub fun env_get(name: str, dst: ptr, dst_cap: i64) -> i64 { return _env_copy_value(&raw[0], eq_pos + 1, entry_end, dst, dst_cap); } - return -2; + return ENV_ERR_NOT_FOUND; } pub fun env_exists(name: str) -> bool { diff --git a/std/sys/linux/amd64/env.wave b/std/sys/linux/amd64/env.wave index 0ca3663d..14e34ff9 100644 --- a/std/sys/linux/amd64/env.wave +++ b/std/sys/linux/amd64/env.wave @@ -55,17 +55,27 @@ import("std::sys::linux::amd64::fs")::{ fstat, }; +// A positive result always covers the whole source through EOF. -28 means +// the caller must retry with more space; partial entries are never success. pub fun env_read(buf: ptr, cap: i64) -> i64 { - if (cap <= 0) { - return -22; - } - + if (buf == null || cap <= 0) { return -22; } var fd: i64 = open("/proc/self/environ", 0, 0); - if (fd < 0) { - return fd; + if (fd < 0) { return fd; } + var total: i64 = 0; + while (total < cap) { + var count: i64 = read(fd, &buf[total], cap - total); + if (count == -4) { continue; } + if (count < 0) { close(fd); return count; } + if (count == 0) { close(fd); return total; } + total += count; } - - var n: i64 = read(fd, buf, cap); + // Distinguish an exact fit from a truncated source, including when the + // last available byte happens to be a complete entry's NUL terminator. + var extra: u8 = 0; + var count: i64 = read(fd, &extra, 1); + while (count == -4) { count = read(fd, &extra, 1); } close(fd); - return n; + if (count < 0) { return count; } + if (count == 0) { return total; } + return -28; } diff --git a/std/sys/linux/arm64/env.wave b/std/sys/linux/arm64/env.wave index 29e24eb0..57594fa7 100644 --- a/std/sys/linux/arm64/env.wave +++ b/std/sys/linux/arm64/env.wave @@ -55,17 +55,27 @@ import("std::sys::linux::arm64::fs")::{ fstat, }; +// A positive result always covers the whole source through EOF. -28 means +// the caller must retry with more space; partial entries are never success. pub fun env_read(buf: ptr, cap: i64) -> i64 { - if (cap <= 0) { - return -22; - } - + if (buf == null || cap <= 0) { return -22; } var fd: i64 = open("/proc/self/environ", 0, 0); - if (fd < 0) { - return fd; + if (fd < 0) { return fd; } + var total: i64 = 0; + while (total < cap) { + var count: i64 = read(fd, &buf[total], cap - total); + if (count == -4) { continue; } + if (count < 0) { close(fd); return count; } + if (count == 0) { close(fd); return total; } + total += count; } - - var n: i64 = read(fd, buf, cap); + // Distinguish an exact fit from a truncated source, including when the + // last available byte happens to be a complete entry's NUL terminator. + var extra: u8 = 0; + var count: i64 = read(fd, &extra, 1); + while (count == -4) { count = read(fd, &extra, 1); } close(fd); - return n; + if (count < 0) { return count; } + if (count == 0) { return total; } + return -28; } diff --git a/std/sys/linux/riscv64/env.wave b/std/sys/linux/riscv64/env.wave index 9d8cd73e..6cadfc9d 100644 --- a/std/sys/linux/riscv64/env.wave +++ b/std/sys/linux/riscv64/env.wave @@ -55,17 +55,27 @@ import("std::sys::linux::fs")::{ fstat, }; +// A positive result always covers the whole source through EOF. -28 means +// the caller must retry with more space; partial entries are never success. pub fun env_read(buf: ptr, cap: i64) -> i64 { - if (cap <= 0) { - return -22; - } - + if (buf == null || cap <= 0) { return -22; } var fd: i64 = open("/proc/self/environ", 0, 0); - if (fd < 0) { - return fd; + if (fd < 0) { return fd; } + var total: i64 = 0; + while (total < cap) { + var count: i64 = read(fd, &buf[total], cap - total); + if (count == -4) { continue; } + if (count < 0) { close(fd); return count; } + if (count == 0) { close(fd); return total; } + total += count; } - - var n: i64 = read(fd, buf, cap); + // Distinguish an exact fit from a truncated source, including when the + // last available byte happens to be a complete entry's NUL terminator. + var extra: u8 = 0; + var count: i64 = read(fd, &extra, 1); + while (count == -4) { count = read(fd, &extra, 1); } close(fd); - return n; + if (count < 0) { return count; } + if (count == 0) { return total; } + return -28; } diff --git a/std/sys/windows/fs.wave b/std/sys/windows/fs.wave index d449a878..261b51ac 100644 --- a/std/sys/windows/fs.wave +++ b/std/sys/windows/fs.wave @@ -26,7 +26,10 @@ extern(system, "FlushFileBuffers") fun win_flush_file_buffers(handle: ptr) - extern(system, "SetFilePointerEx") fun win_set_file_pointer( handle: ptr, distance: i64, new_position: ptr, method: u32 ) -> i32; -extern(system, "GetFileSizeEx") fun win_get_file_size(handle: ptr, size: ptr) -> i32; +extern(system, "GetFileType") fun win_get_file_type(handle: ptr) -> u32; +extern(system, "GetFileInformationByHandle") fun win_get_file_information( + handle: ptr, data: ptr +) -> i32; extern(system, "GetFileAttributesW") fun win_get_file_attributes(path: ptr) -> u32; extern(system, "GetFileAttributesExW") fun win_get_file_attributes_ex( path: ptr, info_level: i32, data: ptr @@ -65,11 +68,14 @@ pub const FS_F_SETFL: i32 = 4; const GENERIC_READ: u32 = 2147483648; const GENERIC_WRITE: u32 = 1073741824; const FILE_APPEND_DATA: u32 = 4; +const FILE_READ_ATTRIBUTES: u32 = 128; +const SYNCHRONIZE: u32 = 1048576; const FILE_SHARE_ALL: u32 = 7; const CREATE_NEW: u32 = 1; const CREATE_ALWAYS: u32 = 2; const OPEN_EXISTING: u32 = 3; const OPEN_ALWAYS: u32 = 4; +const TRUNCATE_EXISTING: u32 = 5; const FILE_ATTRIBUTE_NORMAL: u32 = 128; const FILE_ATTRIBUTE_DIRECTORY: u32 = 16; const INVALID_FILE_ATTRIBUTES: u32 = 4294967295; @@ -98,6 +104,24 @@ struct WinFileAttributeData { size_low: u32; } +// BY_HANDLE_FILE_INFORMATION uses DWORD fields, including both halves of +// FILETIME. Its size is 52 bytes with 4-byte alignment on amd64 and ARM64. +struct WinHandleFileInformation { + attributes: u32; + creation_low: u32; + creation_high: u32; + access_low: u32; + access_high: u32; + write_low: u32; + write_high: u32; + volume: u32; + size_high: u32; + size_low: u32; + links: u32; + index_high: u32; + index_low: u32; +} + fun win_handle_from_fd(fd: i64) -> ptr { if (fd == 0) { return win_get_std_handle(STD_INPUT_HANDLE); } if (fd == 1) { return win_get_std_handle(STD_OUTPUT_HANDLE); } @@ -106,6 +130,12 @@ fun win_handle_from_fd(fd: i64) -> ptr { } pub fun open(path: str, flags: i32, mode: i32) -> i64 { + var access_mode: i32 = flags & 3; + if (access_mode == 3 || (access_mode == FS_O_RDONLY && (flags & FS_O_TRUNC) != 0)) { + return -22; + } + var append: bool = access_mode != FS_O_RDONLY && (flags & FS_O_APPEND) != 0; + var truncate: bool = (flags & FS_O_TRUNC) != 0; var native_path: WidePath = wide_path(path); if (native_path.error != 0) { return native_path.error; @@ -116,32 +146,51 @@ pub fun open(path: str, flags: i32, mode: i32) -> i64 { } else if ((flags & FS_O_WRONLY) != 0) { desired = GENERIC_WRITE; } - if ((flags & FS_O_APPEND) != 0) { - desired = desired | FILE_APPEND_DATA; + if (append) { + // FILE_WRITE_DATA (also granted by GENERIC_WRITE) would allow writes + // before EOF. Let the kernel append each write, even after lseek/dup. + desired = FILE_APPEND_DATA | FILE_READ_ATTRIBUTES | SYNCHRONIZE; + if (access_mode == FS_O_RDWR) { desired = desired | GENERIC_READ; } } var creation: u32 = OPEN_EXISTING; if ((flags & FS_O_CREAT) != 0 && (flags & FS_O_EXCL) != 0) { creation = CREATE_NEW; - } else if ((flags & FS_O_CREAT) != 0 && (flags & FS_O_TRUNC) != 0) { + } else if ((flags & FS_O_CREAT) != 0 && truncate && !append) { creation = CREATE_ALWAYS; } else if ((flags & FS_O_CREAT) != 0) { creation = OPEN_ALWAYS; + } else if (truncate && !append) { + creation = TRUNCATE_EXISTING; } + // Append+truncate needs a temporary write-capable handle. Obtain the + // restricted final handle before truncating so duplication failure leaves + // existing data intact. Both handles refer to the same opened file. + var open_access: u32 = desired; + if (append && truncate) { open_access = open_access | GENERIC_WRITE; } + var handle: ptr = win_create_file( - native_path.data, desired, FILE_SHARE_ALL, null, creation, FILE_ATTRIBUTE_NORMAL, null + native_path.data, open_access, FILE_SHARE_ALL, null, creation, FILE_ATTRIBUTE_NORMAL, null ); if ((handle as i64) == -1) { return release_path(native_path, windows_fs_error(win_file_last_error())); } - if ((flags & FS_O_APPEND) != 0) { - var ignored: i64 = 0; - if (win_set_file_pointer(handle, 0, &ignored, FS_SEEK_END as u32) == 0) { + if (append && truncate) { + var current: ptr = win_get_current_process(); + var restricted: ptr = null; + if (win_duplicate_handle(current, handle, current, &restricted, desired, 0, 0) == 0) { var error_code: u32 = win_file_last_error(); win_close_handle(handle); return release_path(native_path, windows_fs_error(error_code)); } + var status: i64 = ftruncate(handle as i64, 0); + win_close_handle(handle); + if (status != 0) { + win_close_handle(restricted); + return release_path(native_path, status); + } + handle = restricted; } return release_path(native_path, handle as i64); } @@ -301,11 +350,23 @@ pub fun rename_path(old_path: str, new_path: str) -> i64 { pub fun fstat(fd: i64, st: ptr) -> i64 { if (st == null) { return -22; } - var native_size: i64 = 0; - if (win_get_file_size(win_handle_from_fd(fd), &native_size) == 0) { return windows_fs_error(win_file_last_error()); } - deref st.mode = 32768 as i16; - deref st.size = native_size; - deref st.mtime = 0; + var handle: ptr = win_handle_from_fd(fd); + var kind: u32 = win_get_file_type(handle); + // FILE_TYPE_DISK includes directories. Pipes/consoles do not supply this + // metadata contract; never invent a regular-file mode or timestamp. + if (kind == 0) { return windows_fs_error(win_file_last_error()); } + if (kind != 1) { return -95; } + var native: WinHandleFileInformation; + if (win_get_file_information(handle, &native) == 0) { return windows_fs_error(win_file_last_error()); } + deref st.size = ((native.size_high as u64 << 32) | native.size_low as u64) as i64; + var write_ticks: u64 = (native.write_high as u64 << 32) | native.write_low as u64; + deref st.mtime = (write_ticks / 10000000) as i64 - 11644473600; + if ((native.attributes & FILE_ATTRIBUTE_DIRECTORY) != 0) { + deref st.mode = 16384 as i16; + deref st.size = 0; + } else { + deref st.mode = 32768 as i16; + } return 0; } diff --git a/std/sys/windows/memory.wave b/std/sys/windows/memory.wave index 5174ddb7..80e62eda 100644 --- a/std/sys/windows/memory.wave +++ b/std/sys/windows/memory.wave @@ -33,6 +33,8 @@ pub const MAP_ANONYMOUS: i32 = 32; const MEM_COMMIT_RESERVE: u32 = 12288; const MEM_RELEASE: u32 = 32768; +const PAGE_NOACCESS: u32 = 1; +const PAGE_READONLY: u32 = 2; const PAGE_READWRITE: u32 = 4; const SYS_ERR_INVALID: i64 = -22; @@ -44,10 +46,20 @@ pub fun mmap( fd: i64, offset: i64 ) -> ptr { - if (length <= 0 || fd != -1 || offset != 0) { + if (length <= 0 || fd != -1 || offset != 0 || flags != (MAP_PRIVATE | MAP_ANONYMOUS)) { return null; } - return win_virtual_alloc(addr, length, MEM_COMMIT_RESERVE, PAGE_READWRITE); + // There is no write-only VirtualAlloc protection. Reject it instead of + // silently granting read access, along with unknown/execute protection bits. + var protection: u32 = PAGE_NOACCESS; + if (prot == PROT_READ) { + protection = PAGE_READONLY; + } else if (prot == (PROT_READ | PROT_WRITE)) { + protection = PAGE_READWRITE; + } else if (prot != 0) { + return null; + } + return win_virtual_alloc(addr, length, MEM_COMMIT_RESERVE, protection); } pub fun munmap(addr: ptr, length: i64) -> i64 { diff --git a/tests/fixtures/native_boundaries/environment.wave b/tests/fixtures/native_boundaries/environment.wave new file mode 100644 index 00000000..cdb487f3 --- /dev/null +++ b/tests/fixtures/native_boundaries/environment.wave @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MPL-2.0 +import("std::env::environ")::{env_get}; +import("std::env::consts")::{ENV_ERR_NOT_FOUND, ENV_ERR_NO_SPACE}; +fun main() -> i32 { + var data: array; + var count: i64 = env_get("WAVE_VALUE", &data[0], 70000); + if (count <= 0) { return 1; } + var i: i64 = 0; + while (i < count) { if (data[i] != 120) { return 2; } i += 1; } + if (data[count] != 0) { return 3; } + var short: array = [85, 86]; + if (env_get("WAVE_VALUE", &short[0], 1) != ENV_ERR_NO_SPACE || short[0] != 85 || short[1] != 86) { return 4; } + if (env_get("NOT_PRESENT", &data[0], 70000) != ENV_ERR_NOT_FOUND) { return 5; } + if (env_get("EMPTY", &data[0], 70000) != 0 || data[0] != 0) { return 6; } + return 0; +} diff --git a/tests/fixtures/native_boundaries/environment_mock.c b/tests/fixtures/native_boundaries/environment_mock.c new file mode 100644 index 00000000..8fb99946 --- /dev/null +++ b/tests/fixtures/native_boundaries/environment_mock.c @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MPL-2.0 +#include +#include +static int mode, reads, allocated, freed; +static unsigned char pool[131072]; +void choose(int n) { mode = n; reads = allocated = freed = 0; } +int live_allocations(void) { return allocated - freed; } +void *mock_alloc(int64_t size) { + if (mode == 3 || size > (int64_t)sizeof(pool)) return 0; + allocated++; + return pool; +} +int64_t mock_free(void *p, int64_t size) { + (void)size; + if (p == pool) freed++; + return 0; +} +int64_t mock_env_read(unsigned char *buf, int64_t cap) { + reads++; + if (mode == 0) return -5; + if (mode == 1) { memcpy(buf, "KEY=x", 5); return 5; } + if (mode == 2) return cap + 1; + if (mode == 3) return -28; + if (mode == 4) return reads == 1 ? -28 : -5; + if (mode == 5 && reads < 3) return -28; + if (mode == 6) return 0; + memcpy(buf, "KEY=value", 10); + return 10; +} +static int offset, interrupted, closed; +void select_read(int n) { mode = n; offset = interrupted = closed = 0; } +int close_count(void) { return closed; } +int64_t mock_open(const char *p, int flags, int perms) { + (void)p; (void)flags; (void)perms; return 10; +} +int64_t mock_close(int64_t fd) { (void)fd; closed++; return 0; } +int64_t mock_read(int64_t fd, unsigned char *dst, int64_t cap) { + (void)fd; + if (!interrupted++) return -4; + if (mode == 2 && offset == 2) return -5; + int length = mode == 1 ? 5 : 4; + if (offset == length) return 0; + if (cap > 2) cap = 2; + if (cap > length - offset) cap = length - offset; + memcpy(dst, "K=x\0y" + offset, (size_t)cap); + offset += (int)cap; + return cap; +} diff --git a/tests/fixtures/native_boundaries/environment_mock.wave b/tests/fixtures/native_boundaries/environment_mock.wave new file mode 100644 index 00000000..dc071b95 --- /dev/null +++ b/tests/fixtures/native_boundaries/environment_mock.wave @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MPL-2.0 +extern(c) fun choose(n: i32); +extern(c) fun live_allocations() -> i32; +fun main() -> i32 { + var buffer: array; + var mode: i32 = 0; + while (mode <= 6) { + choose(mode); + buffer[0] = 85; + var result: i64 = env_get("KEY", &buffer[0], 16); + var expected: i64 = -5; + if (mode == 1) { expected = -6; } + if (mode == 3) { expected = -7; } + if (mode == 5) { expected = 5; } + if (mode == 6) { expected = -2; } + if (result != expected || live_allocations() != 0) { return mode + 1; } + if (mode != 5 && buffer[0] != 85) { return mode + 20; } + if (mode == 5 && (buffer[0] != 118 || buffer[5] != 0)) { return 40; } + mode += 1; + } + return 0; +} diff --git a/tests/fixtures/native_boundaries/linux_env_read_mock.wave b/tests/fixtures/native_boundaries/linux_env_read_mock.wave new file mode 100644 index 00000000..a93201e5 --- /dev/null +++ b/tests/fixtures/native_boundaries/linux_env_read_mock.wave @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MPL-2.0 +extern(c) fun select_read(n: i32); +extern(c) fun close_count() -> i32; +fun main() -> i32 { + var buf: array; + select_read(0); + if (env_read(&buf[0], 4) != 4 || buf[0] != 75 || buf[3] != 0 || close_count() != 1) { return 1; } + select_read(1); + if (env_read(&buf[0], 4) != -28 || close_count() != 1) { return 2; } + select_read(2); + if (env_read(&buf[0], 4) != -5 || close_count() != 1) { return 3; } + if (env_read(null, 1) != -22) { return 4; } + return 0; +} diff --git a/tests/fixtures/native_boundaries/windows.wave b/tests/fixtures/native_boundaries/windows.wave new file mode 100644 index 00000000..4ec1837d --- /dev/null +++ b/tests/fixtures/native_boundaries/windows.wave @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: MPL-2.0 +import("std::sys::windows::fs")::{ + Stat, open, close, write, read, stat, fstat, lseek, dup, unlink, mkdir, rmdir, + FS_O_CREAT, FS_O_EXCL, FS_O_TRUNC, FS_O_APPEND, FS_O_RDWR, FS_O_WRONLY, FS_O_RDONLY +}; +import("std::sys::windows::memory")::{mmap, munmap, sys_alloc, sys_free, PROT_READ, PROT_WRITE, MAP_PRIVATE, MAP_ANONYMOUS}; + +extern(system, "CreateFileW") fun directory_handle(name: ptr, access: u32, share: u32, + security: ptr, creation: u32, flags: u32, template: ptr) -> ptr; +extern(system, "VirtualQuery") fun query_memory(address: ptr, info: ptr, length: u64) -> u64; +struct MemoryInfo { + base: ptr; allocation_base: ptr; allocation_protect: u32; + partition: u16; reserved: u16; region_size: u64; + state: u32; protect: u32; kind: u32; +} +fun same_metadata(first: Stat, second: Stat) -> bool { + return first.mode == second.mode && first.size == second.size && first.mtime == second.mtime; +} +fun main() -> i32 { + var bytes: array = [10, 20, 30, 40]; + var fd: i64 = open("boundary.bin", FS_O_CREAT | FS_O_EXCL | FS_O_RDWR, 0); + if (fd < 0 || write(fd, &bytes[0], 2) != 2 || close(fd) != 0) { return 1; } + var first: i64 = open("boundary.bin", FS_O_WRONLY | FS_O_APPEND, 0); + var second: i64 = open("boundary.bin", FS_O_RDWR | FS_O_APPEND, 0); + if (first < 0 || second < 0) { return 2; } + if (lseek(first, 0, 0) != 0 || write(first, &bytes[2], 1) != 1) { return 3; } + var duplicate: i64 = dup(second); + if (duplicate < 0 || lseek(duplicate, 0, 0) != 0 || write(duplicate, &bytes[3], 1) != 1) { return 4; } + if (close(duplicate) != 0 || close(first) != 0 || close(second) != 0) { return 5; } + if (open("boundary.bin", FS_O_CREAT | FS_O_EXCL | FS_O_TRUNC | FS_O_WRONLY, 0) != -17) { return 6; } + fd = open("boundary.bin", FS_O_RDONLY, 0); + var got: array; + if (fd < 0 || read(fd, &got[0], 4) != 4) { return 7; } + var i: i32 = 0; + while (i < 4) { if (got[i] != bytes[i]) { return 8; } i += 1; } + var by_path: Stat; + var by_handle: Stat; + if (stat("boundary.bin", &by_path) != 0 || fstat(fd, &by_handle) != 0 + || !same_metadata(by_path, by_handle) || by_handle.mtime <= 0 || by_handle.size != 4) { return 9; } + close(fd); + fd = open("boundary.bin", FS_O_WRONLY | FS_O_TRUNC, 0); + if (fd < 0 || close(fd) != 0 || stat("boundary.bin", &by_path) != 0 || by_path.size != 0) { return 10; } + if (open("missing.bin", FS_O_WRONLY | FS_O_TRUNC, 0) != -2) { return 11; } + if (open("boundary.bin", FS_O_RDONLY | FS_O_TRUNC, 0) != -22) { return 12; } + fd = open("boundary.bin", FS_O_WRONLY, 0); + if (fd < 0 || write(fd, &bytes[0], 2) != 2 || close(fd) != 0) { return 26; } + fd = open("boundary.bin", FS_O_RDWR | FS_O_APPEND | FS_O_TRUNC, 0); + if (fd < 0 || write(fd, &bytes[0], 2) != 2 || lseek(fd, 0, 0) != 0 + || write(fd, &bytes[2], 2) != 2 || close(fd) != 0) { return 13; } + if (stat("boundary.bin", &by_path) != 0 || by_path.size != 4) { return 14; } + if (mkdir("folder", 0) != 0) { return 15; } + var name: array = [102, 111, 108, 100, 101, 114, 0]; + var directory: ptr = directory_handle(&name[0], 128, 7, null, 3, 33554432, null); + if ((directory as i64) == -1 || fstat(directory as i64, &by_handle) != 0 + || stat("folder", &by_path) != 0 || !same_metadata(by_path, by_handle) + || by_handle.mode != (16384 as i16) || by_handle.size != 0) { return 16; } + close(directory as i64); + fd = open("NUL", FS_O_RDONLY, 0); + by_handle.size = 99; + if (fd < 0 || fstat(fd, &by_handle) != -95 || by_handle.size != 99) { return 17; } + close(fd); + if (fstat(-1, &by_handle) != -9 || by_handle.size != 99) { return 18; } + var prot: i32 = 0; + while (prot <= 3) { + var p: ptr = mmap(null, 4096, prot, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (prot == PROT_WRITE) { + if (p != null) { return 19; } + } else { + if (p == null) { return 20; } + var info: MemoryInfo; + if (query_memory(p, &info, 48) == 0) { return 21; } + var expected: u32 = 1; + if (prot == PROT_READ) { expected = 2; } + if (prot == (PROT_READ | PROT_WRITE)) { expected = 4; deref p = 42; } + if (info.protect != expected || munmap(p, 4096) != 0) { return 22; } + } + prot += 1; + } + if (mmap(null, 4096, 4, 34, -1, 0) != null || mmap(null, 4096, 3, 35, -1, 0) != null) { return 23; } + var allocation: ptr = sys_alloc(16); + if (allocation == null) { return 24; } + deref allocation = 42; + if (sys_free(allocation, 16) != 0 || unlink("boundary.bin") != 0 || rmdir("folder") != 0) { return 25; } + return 0; +} diff --git a/tests/fixtures/native_boundaries/windows_mock.c b/tests/fixtures/native_boundaries/windows_mock.c new file mode 100644 index 00000000..18fb6ec5 --- /dev/null +++ b/tests/fixtures/native_boundaries/windows_mock.c @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: MPL-2.0 +#include +#include +#include +static int scenario, failed, resized, closed; +static uint32_t protection, allocations; +void choose(int n) { scenario = n; resized = closed = 0; } +int failures(void) { return failed; } +int truncations(void) { return resized; } +int closed_handles(void) { return closed; } +uint32_t last_protection(void) { return protection; } +uint32_t allocation_count(void) { return allocations; } +void *CreateFileW(const uint16_t *name, uint32_t access, uint32_t share, + void *security, uint32_t creation, uint32_t attrs, void *template) { + (void)name; (void)security; (void)attrs; (void)template; + if (share != 7) failed++; + if (scenario == 0 && (access != 0x40000000 || creation != 5)) failed++; + if (scenario == 1 && ((access & (0x40000000 | 2)) || !(access & 4) || creation != 3)) failed++; + if (scenario >= 2 && scenario <= 4 && (!(access & 0x40000000) || creation != 3)) failed++; + return (void *)(uintptr_t)4096; +} +void *GetCurrentProcess(void) { return (void *)(intptr_t)-1; } +int DuplicateHandle(void *p, void *h, void *p2, void **out, uint32_t access, int inherit, uint32_t flags) { + (void)p; (void)h; (void)p2; (void)inherit; + if (flags || (access & (0x40000000 | 2)) || !(access & 4) || resized) failed++; + if (scenario == 3) return 0; + *out = (void *)(uintptr_t)8192; + return 1; +} +int SetFileInformationByHandle(void *h, int kind, const int64_t *end, uint32_t size) { + if (h != (void *)(uintptr_t)4096 || kind != 6 || size != 8 || *end) failed++; + resized++; + return scenario != 4; +} +int CloseHandle(void *h) { (void)h; closed++; return 1; } +uint32_t GetLastError(void) { return scenario == 12 ? 6 : 5; } +uint32_t GetFileType(void *h) { (void)h; return scenario == 11 ? 3 : scenario == 12 ? 0 : 1; } +struct info { uint32_t attr, cl, ch, al, ah, wl, wh, volume, sh, sl, links, ih, il; }; +_Static_assert(sizeof(struct info) == 52, "Windows metadata ABI"); +int GetFileInformationByHandle(void *h, struct info *info) { + (void)h; + memset(info, 0, sizeof(*info)); + if (scenario == 13) return 0; + uint64_t ticks = UINT64_C(133444736000000000); // Unix 1700000000 seconds. + info->attr = scenario == 10 ? 16 : 128; + info->sh = 2; info->sl = 17; + info->wl = (uint32_t)ticks; info->wh = (uint32_t)(ticks >> 32); + return 1; +} +void *VirtualAlloc(void *addr, uint64_t size, uint32_t flags, uint32_t prot) { + (void)addr; + if (!size || flags != 12288) failed++; + allocations++; protection = prot; + return (void *)(uintptr_t)16384; +} +int VirtualFree(void *addr, uint64_t size, uint32_t flags) { + if (addr != (void *)(uintptr_t)16384 || size || flags != 32768) failed++; + return 1; +} +// Unused exported provider functions still have to link at O0. +void *GetStdHandle(uint32_t n) { (void)n; return 0; } +int closesocket(int64_t fd) { (void)fd; return 0; } +int ReadFile(void *h, void *b, uint32_t n, uint32_t *r, void *o) { return 0; } +int WriteFile(void *h, void *b, uint32_t n, uint32_t *r, void *o) { return 0; } +int FlushFileBuffers(void *h) { return 0; } +int SetFilePointerEx(void *h, int64_t d, int64_t *p, uint32_t m) { failed++; return 0; } +uint32_t GetFileAttributesW(const uint16_t *p) { return 0; } +int GetFileAttributesExW(const uint16_t *p, int k, void *out) { return 0; } +uint32_t GetCurrentDirectoryW(uint32_t n, uint16_t *b) { return 0; } +int SetCurrentDirectoryW(const uint16_t *p) { return 0; } +int DeleteFileW(const uint16_t *p) { return 0; } +int CreateDirectoryW(const uint16_t *p, void *s) { return 0; } +int RemoveDirectoryW(const uint16_t *p) { return 0; } +int MoveFileExW(const uint16_t *a, const uint16_t *b, uint32_t f) { return 0; } +int GetFileInformationByHandleEx(void *h, int k, void *out, uint32_t n) { return 0; } +void GetSystemInfo(void *out) { memset(out, 0, 48); } diff --git a/tests/fixtures/native_boundaries/windows_mock.wave b/tests/fixtures/native_boundaries/windows_mock.wave new file mode 100644 index 00000000..1106b433 --- /dev/null +++ b/tests/fixtures/native_boundaries/windows_mock.wave @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MPL-2.0 +import("./provider")::{open, close, fstat, Stat, FS_O_WRONLY, FS_O_APPEND, FS_O_TRUNC}; +import("./memory")::{mmap, munmap, sys_alloc}; +extern(c) fun choose(n: i32); +extern(c) fun failures() -> i32; +extern(c) fun truncations() -> i32; +extern(c) fun closed_handles() -> i32; +extern(c) fun last_protection() -> u32; +extern(c) fun allocation_count() -> u32; +fun main() -> i32 { + choose(0); + if (open("file", FS_O_WRONLY | FS_O_TRUNC, 0) != 4096) { return 1; } + choose(1); + if (open("file", FS_O_WRONLY | FS_O_APPEND, 0) != 4096) { return 2; } + choose(2); + if (open("file", FS_O_WRONLY | FS_O_APPEND | FS_O_TRUNC, 0) != 8192 || truncations() != 1 || closed_handles() != 1) { return 3; } + choose(3); + if (open("file", FS_O_WRONLY | FS_O_APPEND | FS_O_TRUNC, 0) != -13 || truncations() != 0 || closed_handles() != 1) { return 4; } + choose(4); + if (open("file", FS_O_WRONLY | FS_O_APPEND | FS_O_TRUNC, 0) != -13 || closed_handles() != 2) { return 5; } + choose(9); + var info: Stat; + if (fstat(4096, &info) != 0 || info.size != 8589934609 || info.mtime != 1700000000 || info.mode != (32768 as i16)) { return 6; } + choose(10); + if (fstat(4096, &info) != 0 || info.size != 0 || info.mode != (16384 as i16) || info.mtime != 1700000000) { return 7; } + info.size = 99; + choose(11); + if (fstat(4096, &info) != -95 || info.size != 99) { return 8; } + choose(12); + if (fstat(4096, &info) != -9 || info.size != 99) { return 9; } + choose(13); + if (fstat(4096, &info) != -13 || info.size != 99) { return 10; } + var p: ptr = mmap(null, 16, 1, 34, -1, 0); + if (p == null || last_protection() != 2 || munmap(p, 16) != 0) { return 11; } + p = mmap(null, 16, 0, 34, -1, 0); + if (p == null || last_protection() != 1 || munmap(p, 16) != 0) { return 12; } + p = mmap(null, 16, 3, 34, -1, 0); + if (p == null || last_protection() != 4 || munmap(p, 16) != 0) { return 13; } + var count: u32 = allocation_count(); + if (mmap(null, 16, 2, 34, -1, 0) != null || mmap(null, 16, 4, 34, -1, 0) != null + || mmap(null, 16, 3, 35, -1, 0) != null || mmap(null, 16, 3, 2, -1, 0) != null + || mmap(null, 16, 3, 34, 0, 0) != null || mmap(null, 16, 3, 34, -1, 1) != null + || mmap(null, 0, 3, 34, -1, 0) != null || allocation_count() != count) { return 14; } + if (sys_alloc(16) == null || last_protection() != 4) { return 15; } + return failures(); +} diff --git a/tests/native_boundaries.rs b/tests/native_boundaries.rs new file mode 100644 index 00000000..163d9b11 --- /dev/null +++ b/tests/native_boundaries.rs @@ -0,0 +1,293 @@ +// SPDX-License-Identifier: MPL-2.0 +use std::{ + fs, + path::{Path, PathBuf}, + process::{Command, Output}, + sync::atomic::{AtomicU64, Ordering}, +}; +static NEXT: AtomicU64 = AtomicU64::new(0); +struct Case(PathBuf); +impl Case { + fn new() -> Self { + let path = std::env::temp_dir().join(format!( + "wave-native-boundaries-{}-{}", + std::process::id(), + NEXT.fetch_add(1, Ordering::Relaxed) + )); + fs::create_dir_all(&path).unwrap(); + Self(path) + } + fn command(&self) -> Command { + let mut c = Command::new(env!("CARGO_BIN_EXE_wavec")); + c.current_dir(&self.0).arg("--std-root").arg(repo("std")); + c + } +} +impl Drop for Case { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } +} +fn repo(path: &str) -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join(path) +} +fn success(out: Output) { + assert!( + out.status.success(), + "{}\n{}\n{}", + out.status, + String::from_utf8_lossy(&out.stdout), + String::from_utf8_lossy(&out.stderr) + ); +} +#[test] +fn windows_file_and_memory_boundaries() { + let case = Case::new(); + for target in ["x86_64-pc-windows-msvc", "aarch64-pc-windows-msvc"] { + if llvm::codegen::target::target_spec_for_triple(target).is_none() { + continue; + } + for opt in ["-O0", "-O2"] { + success( + case.command() + .arg("build") + .arg(repo("tests/fixtures/native_boundaries/windows.wave")) + .args(["--target", target, "--emit=ir,obj", opt, "--out-dir"]) + .arg(case.0.join(format!("{target}-{opt}"))) + .output() + .unwrap(), + ); + } + } + #[cfg(target_os = "windows")] + for opt in ["-O0", "-O2"] { + success( + case.command() + .arg("build") + .arg(repo("tests/fixtures/native_boundaries/windows.wave")) + .args([opt, "-o", "case.exe"]) + .output() + .unwrap(), + ); + success( + Command::new(case.0.join("case.exe")) + .current_dir(&case.0) + .output() + .unwrap(), + ); + } +} +#[cfg(target_os = "linux")] +#[test] +fn windows_boundary_native_api_mocks() { + let case = Case::new(); + let mut provider = fs::read_to_string(repo("std/sys/windows/fs.wave")) + .unwrap() + .replace("\r\n", "\n"); + let path_import = "import(\"std::sys::windows::path_encoding\")::{WidePath, wide_path, release_path, wide_path_to_utf8};"; + assert!(provider.contains(path_import)); + provider = provider.replace(path_import, r#" +struct WidePath { data: ptr; error: i64; } +fun wide_path(path: str) -> WidePath { var result: WidePath; result.data = null; result.error = 0; return result; } +fun release_path(path: WidePath, status: i64) -> i64 { return status; } +fun wide_path_to_utf8(path: ptr, n: i32, dst: ptr, cap: i64) -> i64 { return -38; } +"#).replace("import(\"std::sys::windows::memory\")", "import(\"./memory\")").replace("extern(system,", "extern(c,"); + fs::write(case.0.join("provider.wave"), provider).unwrap(); + fs::write( + case.0.join("memory.wave"), + fs::read_to_string(repo("std/sys/windows/memory.wave")) + .unwrap() + .replace("extern(system,", "extern(c,"), + ) + .unwrap(); + fs::copy( + repo("tests/fixtures/native_boundaries/windows_mock.wave"), + case.0.join("case.wave"), + ) + .unwrap(); + success( + Command::new("cc") + .args(["-c", "-O2"]) + .arg(repo("tests/fixtures/native_boundaries/windows_mock.c")) + .arg("-o") + .arg(case.0.join("mock.o")) + .output() + .unwrap(), + ); + for opt in ["-O0", "-O2"] { + success( + case.command() + .args(["build", "case.wave", "mock.o", opt, "-o", "case.exe"]) + .output() + .unwrap(), + ); + success(Command::new(case.0.join("case.exe")).output().unwrap()); + } +} +#[cfg(unix)] +#[test] +fn non_utf8_cli_paths_report_errors_without_colliding_outputs() { + use std::os::unix::ffi::OsStringExt; + let case = Case::new(); + // Both names have the same lossy Unicode rendering. + for byte in [0xfe, 0xff] { + let name = std::ffi::OsString::from_vec(vec![b'x', byte, b'.', b'w', b'a', b'v', b'e']); + fs::write(case.0.join(&name), "fun main() -> i32 { return 0; }").unwrap(); + for args in [ + vec!["check"], + vec!["build", "--emit=obj", "-o", "output.o"], + vec!["build", "--out-dir", "out"], + ] { + let out = case + .command() + .args(args) + .arg(&name) + .arg("--error-format=json") + .output() + .unwrap(); + assert_eq!(out.status.code(), Some(2), "{out:?}"); + let error = utils::json::parse(String::from_utf8_lossy(&out.stderr).trim()).unwrap(); + assert!(error + .get("error") + .unwrap() + .get_str("message") + .unwrap() + .contains("not valid UTF-8")); + assert!(!case.0.join("output.o").exists()); + assert!(!case.0.join("out").exists()); + } + } + fs::write( + case.0.join("normal.wave"), + "fun main() -> i32 { return 0; }", + ) + .unwrap(); + success( + case.command() + .args(["build", "normal.wave", "--emit=obj", "-o", "explicit.o"]) + .output() + .unwrap(), + ); + assert!(case.0.join("explicit.o").is_file()); +} + +#[cfg(target_os = "linux")] +#[test] +fn linux_environment_reads_complete_large_and_exact_boundary_sources() { + let case = Case::new(); + for opt in ["-O0", "-O2"] { + success( + case.command() + .arg("build") + .arg(repo("tests/fixtures/native_boundaries/environment.wave")) + .args([opt, "-o", "env.exe"]) + .output() + .unwrap(), + ); + // WAVE_VALUE= (11 bytes), terminating NUL, and EMPTY= plus NUL. + for length in [1, 32768 - 19, 32768 - 18, 40000, 65536 - 19, 69000] { + success( + Command::new(case.0.join("env.exe")) + .env_clear() + .env("WAVE_VALUE", "x".repeat(length)) + .env("EMPTY", "") + .output() + .unwrap(), + ); + } + // The queried key occurs after a different entry beyond the initial read. + success( + Command::new(case.0.join("env.exe")) + .env_clear() + .env("AAA_PADDING", "y".repeat(40000)) + .env("WAVE_VALUE", "xxx") + .env("EMPTY", "") + .output() + .unwrap(), + ); + } +} + +#[cfg(target_os = "linux")] +#[test] +fn environment_short_reads_interruptions_and_failure_cleanup() { + let case = Case::new(); + success( + Command::new("cc") + .args(["-c", "-O2"]) + .arg(repo("tests/fixtures/native_boundaries/environment_mock.c")) + .arg("-o") + .arg(case.0.join("mock.o")) + .output() + .unwrap(), + ); + let high = fs::read_to_string(repo("std/env/environ.wave")) + .unwrap() + .replace("\r\n", "\n"); + let high = high.replace("import(\"std::sys::env\")::{\n env_read,\n};", "extern(c, \"mock_env_read\") fun env_read(buf: ptr, cap: i64) -> i64;") + .replace("import(\"std::sys::memory\")::{sys_alloc, sys_free};", "extern(c, \"mock_alloc\") fun sys_alloc(size: i64) -> ptr; extern(c, \"mock_free\") fun sys_free(p: ptr, size: i64) -> i64;"); + assert!(!high.contains("std::sys::env") && !high.contains("std::sys::memory")); + let low = fs::read_to_string(repo("std/sys/linux/amd64/env.wave")).unwrap(); + let low = format!( + r#" +extern(c, "mock_open") fun open(path: str, flags: i32, mode: i32) -> i64; +extern(c, "mock_read") fun read(fd: i64, buf: ptr, count: i64) -> i64; +extern(c, "mock_close") fun close(fd: i64) -> i64; +{}"#, + &low[low.find("pub fun env_read(").unwrap()..] + ); + for (provider, fixture) in [(high, "environment_mock"), (low, "linux_env_read_mock")] { + fs::write( + case.0.join("case.wave"), + format!( + "{provider}\n{}", + fs::read_to_string(repo(&format!( + "tests/fixtures/native_boundaries/{fixture}.wave" + ))) + .unwrap() + ), + ) + .unwrap(); + for opt in ["-O0", "-O2"] { + success( + case.command() + .args(["build", "case.wave", "mock.o", opt, "-o", "case.exe"]) + .output() + .unwrap(), + ); + success(Command::new(case.0.join("case.exe")).output().unwrap()); + } + } +} + +#[test] +fn environment_provider_cross_target_objects() { + let case = Case::new(); + for target in [ + "x86_64-unknown-linux-gnu", + "aarch64-unknown-linux-gnu", + "riscv64-unknown-linux-gnu", + "loongarch64-unknown-linux-gnu", + "x86_64-apple-darwin", + "aarch64-apple-darwin", + "x86_64-pc-windows-msvc", + "aarch64-pc-windows-msvc", + "x86_64-unknown-freebsd", + "wasm32-wasip1", + ] { + if llvm::codegen::target::target_spec_for_triple(target).is_none() { + continue; + } + for opt in ["-O0", "-O2"] { + success( + case.command() + .arg("build") + .arg(repo("tests/fixtures/native_boundaries/environment.wave")) + .args(["--target", target, "--emit=obj", opt, "--out-dir"]) + .arg(case.0.join(format!("{target}-{opt}"))) + .output() + .unwrap(), + ); + } + } +} From e2a62ee0b4fd275ea88348399b119576f60ff390 Mon Sep 17 00:00:00 2001 From: LunaStev Date: Sun, 27 Sep 2026 17:14:25 +0900 Subject: [PATCH 2/2] Make non-UTF-8 CLI regression independent of filesystem encoding Signed-off-by: LunaStev --- tests/native_boundaries.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/native_boundaries.rs b/tests/native_boundaries.rs index 163d9b11..2a83096f 100644 --- a/tests/native_boundaries.rs +++ b/tests/native_boundaries.rs @@ -129,10 +129,16 @@ fun wide_path_to_utf8(path: ptr, n: i32, dst: ptr, cap: i64) -> i64 { r fn non_utf8_cli_paths_report_errors_without_colliding_outputs() { use std::os::unix::ffi::OsStringExt; let case = Case::new(); - // Both names have the same lossy Unicode rendering. + // Validate argv before filesystem access: macOS CI rejects file creation + // with these byte sequences. A valid file at their shared lossy rendering + // also catches accidental compilation of a different source via decoding. + fs::write( + case.0.join("x\u{fffd}.wave"), + "fun main() -> i32 { return 0; }", + ) + .unwrap(); for byte in [0xfe, 0xff] { let name = std::ffi::OsString::from_vec(vec![b'x', byte, b'.', b'w', b'a', b'v', b'e']); - fs::write(case.0.join(&name), "fun main() -> i32 { return 0; }").unwrap(); for args in [ vec!["check"], vec!["build", "--emit=obj", "-o", "output.o"],