diff --git a/AGENTS.md b/AGENTS.md index 2f561f8..98367f6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,7 +8,7 @@ This file is binding for AI coding agents working in this repository. - Confirm the task, acceptance criteria, target branch, and files in scope. - Do not guess kernel API, license text, or release intent. - Do not work directly on `main` unless the owner explicitly authorizes an emergency fix. -- Userspace (`src/`, `tests/`) is the current product. Kernel / Rust-for-Linux work stays out of this tree until a later phase. +- Userspace lives in `userspace/`. The loadable distro module is `kernel/rust_null.c`. `kernel/rfl/rust_null.rs` is RfL source for a `CONFIG_RUST=y` kernel and is not built by `make -C kernel`. ## During editing @@ -21,7 +21,7 @@ This file is binding for AI coding agents working in this repository. ## Before commit / push / merge - Review the complete diff and scan for secrets, conflict markers, TODO/FIXME, debug output, and accidental files. -- Run `cargo fmt --check`, `cargo clippy --all-targets --locked -- -D warnings`, and `cargo test --locked`. +- Run `cargo fmt --check`, `cargo clippy --all-targets --locked -- -D warnings`, and `cargo test --locked` from the repository root. - New failures block the commit. Do not hide failures as warnings. - Use a focused Conventional Commit: `type(scope): imperative summary`. - Push a branch and use a PR. Do not bypass required checks. @@ -30,4 +30,6 @@ This file is binding for AI coding agents working in this repository. ## Stop conditions -Stop and ask when scope, destructive action, public behavior, credentials, or factual copy is unclear. Block when security, correctness, required checks, or release prerequisites fail. Do not `insmod` / `rmmod` / build against a live kernel without an explicit owner order. +Stop and ask when scope, destructive action, public behavior, credentials, or factual copy is unclear. Block when security, correctness, required checks, or release prerequisites fail. + +Do not `insmod` / `rmmod` / `make -C kernel load` without an explicit owner order. Do not compile or boot a replacement kernel, and do not treat distro `linux-headers` as sufficient to build `kernel/rfl/*.rs`. diff --git a/CHANGELOG.md b/CHANGELOG.md index 50240e8..34db33d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,3 +11,11 @@ Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), versio - Userspace `NullDevice` matching Linux `/dev/null` (`read` EOF, `write` discard, `lseek` always 0). - Contract tests against `drivers/char/mem.c` helpers `read_null`, `write_null`, and `null_lseek`. - `examples/discard` stdin sink. +- C misc module `kernel/rust_null.c` (`misc_register`, `/dev/rust-null`) with the same fops contract. +- Rust-for-Linux `MiscDevice` source in `kernel/rfl/` (not built against distro headers). +- `/dev/null` vs `/dev/rust-null` tests (`compare_devices`); skip when the node is missing. + +### Changed + +- Crate lives under `userspace/`; repository root is a Cargo workspace. +- README states this tree is work in progress while learning Rust for Linux. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ed30bef..5144a02 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,6 +39,7 @@ cargo fmt --check cargo clippy --all-targets --locked -- -D warnings cargo test --locked ./scripts/governance-check.sh +# optional, needs linux-headers: make -C kernel ``` ## Release diff --git a/Cargo.toml b/Cargo.toml index bd8aedf..694dc70 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,15 +1,3 @@ -[package] -name = "rust-null" -version = "0.1.0" -edition = "2021" -rust-version = "1.80" -authors = ["evie "] -license = "GPL-2.0-only" -description = "Userspace model of Linux /dev/null, heading toward a Rust-for-Linux misc device." -repository = "https://github.com/hareai/rust-null" -readme = "README.md" -keywords = ["linux", "null", "kernel", "device"] -categories = ["os"] -publish = false - -[dependencies] +[workspace] +members = ["userspace"] +resolver = "2" diff --git a/README.md b/README.md index 72849b3..7b95088 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # rust-null -A userspace model of Linux `/dev/null`, on the way to a Rust-for-Linux `/dev/rust-null` device. +A userspace model of Linux `/dev/null`, plus a misc device `/dev/rust-null` with the same I/O contract. Work in progress, written while learning [Rust for Linux](https://rust-for-linux.com). [简体中文](README.zh-CN.md) @@ -10,10 +10,10 @@ A userspace model of Linux `/dev/null`, on the way to a Rust-for-Linux `/dev/rus git clone https://github.com/hareai/rust-null.git cd rust-null cargo test -echo hello | cargo run --example discard +echo hello | cargo run -p rust-null --example discard ``` -Rust 1.80 or newer. `cargo test` is the userspace contract; it does not load a kernel module. +Rust 1.80 or newer. `cargo test` is the userspace contract. It does not load a kernel module. `/dev/null` vs `/dev/rust-null` tests skip unless the device node exists. ## Usage @@ -37,11 +37,22 @@ assert_eq!(n.read(&mut buf).unwrap(), 0); // EOF, buffer untouched That last row is the surprising one: `lseek` on `/dev/null` always returns `0`, even for `SEEK_END`. -## Roadmap +## Tree -1. **Userspace** (this tree) — `NullDevice` + contract tests. -2. **Rust for Linux** — out-of-tree module against a `CONFIG_RUST=y` kernel. -3. **`/dev/rust-null`** — misc device with the same read/write/llseek contract. +```text +userspace/ NullDevice crate and contract tests +kernel/ loadable misc device (C) and RfL source (not built here) +``` + +The C module registers a misc device named `rust-null` (`misc_register`, major 10, dynamic minor) whose `file_operations` copy `null_fops`. That is the same VFS path Rust-for-Linux `MiscDevice` wraps. + +```bash +make -C kernel # needs linux-headers for the running kernel +# make -C kernel load # insmod; only on a machine you intend to test +RUST_NULL_REQUIRE_DEV=1 cargo test -p rust-null --test compare_devices --locked +``` + +A distro kernel without `CONFIG_RUST=y` cannot compile `kernel/rfl/rust_null.rs`. That file is the out-of-tree module for a self-built Rust-enabled kernel (`make LLVM=1`). ## License diff --git a/README.zh-CN.md b/README.zh-CN.md index e2786dd..1a3791a 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -1,6 +1,6 @@ # rust-null -Linux `/dev/null` 的用户态模型,目标是做成 Rust-for-Linux 的 `/dev/rust-null`。 +Linux `/dev/null` 的用户态模型,以及合同相同的 misc 设备 `/dev/rust-null`。边学 [Rust for Linux](https://rust-for-linux.com) 边写。 [English](README.md) @@ -10,10 +10,10 @@ Linux `/dev/null` 的用户态模型,目标是做成 Rust-for-Linux 的 `/dev/ git clone https://github.com/hareai/rust-null.git cd rust-null cargo test -echo hello | cargo run --example discard +echo hello | cargo run -p rust-null --example discard ``` -需要 Rust 1.80 或更新。`cargo test` 只验证用户态合同,不会加载内核模块。 +需要 Rust 1.80 或更新。`cargo test` 只验证用户态合同,不会加载内核模块。没有 `/dev/rust-null` 时,设备对比测试会跳过。 ## 用法 @@ -37,11 +37,22 @@ assert_eq!(n.read(&mut buf).unwrap(), 0); // EOF,缓冲区不动 最后一行容易踩:`/dev/null` 上的 `lseek` 永远返回 `0`,包括 `SEEK_END`。 -## 路线 +## 目录 -1. **用户态**(当前树)— `NullDevice` + 合同测试。 -2. **Rust for Linux** — 对着 `CONFIG_RUST=y` 的内核做 out-of-tree 模块。 -3. **`/dev/rust-null`** — 同样 read/write/llseek 合同的 misc 设备。 +```text +userspace/ NullDevice crate 和合同测试 +kernel/ 可加载的 misc 设备(C)和 RfL 源码(这里不编) +``` + +C 模块用 `misc_register` 注册名为 `rust-null` 的 misc 设备(主设备号 10,动态次设备号),`file_operations` 抄 `null_fops`。这就是 Rust-for-Linux `MiscDevice` 封装的那条 VFS 路径。 + +```bash +make -C kernel # 需要正在运行的内核的 linux-headers +# make -C kernel load # insmod;只在打算测试的机器上 +RUST_NULL_REQUIRE_DEV=1 cargo test -p rust-null --test compare_devices --locked +``` + +发行核没有 `CONFIG_RUST=y` 时编不了 `kernel/rfl/rust_null.rs`。那是给自己编的 Rust 内核用的 out-of-tree 模块(`make LLVM=1`)。 ## 许可证 diff --git a/docs/contract.md b/docs/contract.md index c0f56fd..6d03fa1 100644 --- a/docs/contract.md +++ b/docs/contract.md @@ -1,6 +1,6 @@ # `/dev/null` contract -Userspace `NullDevice` copies three helpers from Linux `drivers/char/mem.c`. It is not a file, not a buffer, and not `/dev/zero`. +Userspace `NullDevice` and the `rust-null` misc device copy three helpers from Linux `drivers/char/mem.c`. They are not a file, not a buffer, and not `/dev/zero`. ## Kernel @@ -25,6 +25,8 @@ static loff_t null_lseek(struct file *file, loff_t offset, int orig) `ppos` / `offset` / `orig` are unused. A successful write does not move the file position, because the next seek (and the implicit position) is always zero. +`null_fops` also sets `read_iter` / `write_iter` to the same idea (`return 0` / `return count` after `iov_iter_advance`). + ## Userspace mapping | Kernel | `NullDevice` | `std::io` | @@ -33,6 +35,20 @@ static loff_t null_lseek(struct file *file, loff_t offset, int orig) | `write_null` | `write_null` | `Write::write` → `Ok(count)` | | `null_lseek` | `null_lseek` | `Seek::seek` → `Ok(0)` | +## `/dev/null` vs `/dev/rust-null` + +| | `/dev/null` | `/dev/rust-null` | +|---|---|---| +| Class | mem char `1:3` | misc, major 10, dynamic minor | +| Register | `chr_dev_init` / `devlist[3]` | `misc_register` (`name = "rust-null"`) | +| Contract | `null_fops` | same read / write / llseek / `*_iter` | + +`userspace/tests/compare_devices.rs` opens both and checks the table. Without the device node the tests skip. After `make -C kernel load`: + +```bash +RUST_NULL_REQUIRE_DEV=1 cargo test -p rust-null --test compare_devices --locked +``` + ## Not this device - `/dev/zero` fills reads with `0x00`. @@ -40,7 +56,3 @@ static loff_t null_lseek(struct file *file, loff_t offset, int orig) - A regular file of length 0 still has a moving offset after `SEEK_SET`. Live Linux `/dev/null` (`1:3`) matches the table above: `lseek(SEEK_SET, 100)` returns `0`. - -## Next - -The out-of-tree Rust-for-Linux module will register a misc device named `rust-null` and implement the same three operations on `kernel::file` types. The contract tests here stay the oracle. diff --git a/docs/development.md b/docs/development.md index 353abc5..1fb0210 100644 --- a/docs/development.md +++ b/docs/development.md @@ -15,6 +15,14 @@ cargo clippy --all-targets --locked -- -D warnings cargo test --locked ``` +Optional, when `linux-headers-$(uname -r)` is installed: + +```bash +make -C kernel +``` + +Do not `make -C kernel load` unless the owner asked to test `/dev/rust-null` on that machine. + Public GitHub repos: `git config hermes.public true`. Do not commit `.env`, live keys, or household/private domains (history counts). Private repos are exempt. ## PR gate diff --git a/kernel/.gitignore b/kernel/.gitignore new file mode 100644 index 0000000..40f4228 --- /dev/null +++ b/kernel/.gitignore @@ -0,0 +1,12 @@ +*.o +*.ko +*.mod +*.mod.c +*.mod.o +.*.cmd +.tmp_versions/ +Module.symvers +modules.order +*.dwo +.rust_null.o.d +rust_null.o.d diff --git a/kernel/Kbuild b/kernel/Kbuild new file mode 100644 index 0000000..24c358e --- /dev/null +++ b/kernel/Kbuild @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: GPL-2.0 + +obj-m := rust_null.o diff --git a/kernel/Makefile b/kernel/Makefile new file mode 100644 index 0000000..5b0993a --- /dev/null +++ b/kernel/Makefile @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: GPL-2.0 + +KDIR ?= /lib/modules/$(shell uname -r)/build + +default: + $(MAKE) -C $(KDIR) M=$(CURDIR) modules + +clean: + $(MAKE) -C $(KDIR) M=$(CURDIR) clean + +# Opt-in. Do not run on a machine you are not prepared to test. +load: default + @if lsmod | grep -q '^rust_null '; then rmmod rust_null; fi + insmod $(CURDIR)/rust_null.ko + @test -e /dev/rust-null + @ls -l /dev/rust-null + +unload: + @if lsmod | grep -q '^rust_null '; then rmmod rust_null; fi + @test ! -e /dev/rust-null diff --git a/kernel/README.md b/kernel/README.md new file mode 100644 index 0000000..c9b7114 --- /dev/null +++ b/kernel/README.md @@ -0,0 +1,35 @@ +# kernel + +Two sources, one device name: `rust-null`. + +| File | What it is | When it builds | +|---|---|---| +| `rust_null.c` | `misc_register` + `file_operations` copied from `null_fops` | Distro kernels with `CONFIG_MODULES=y` and matching `linux-headers` | +| `rfl/rust_null.rs` | Rust-for-Linux `MiscDevice` | A self-built kernel with `CONFIG_RUST=y` (`make LLVM=1`) | + +`make` in this directory builds **only** the C module (`obj-m := rust_null.o`). It does not compile the `.rs`. Distro headers have no Rust metadata. + +## VFS + +`/dev/null` is mem `1:3` (`drivers/char/mem.c`). This module is a *misc* device (major 10, `MISC_DYNAMIC_MINOR`). The I/O contract is the same: + +```c +read → 0 +write → count +llseek → f_pos = 0 +read_iter → 0 +write_iter → count (iov_iter_advance) +``` + +`kernel::miscdevice::MiscDevice` on mainline currently wires `read_iter` / `write_iter` / `ioctl` / `mmap`, not `.llseek`. The C module implements `llseek` so a loaded `/dev/rust-null` can match `/dev/null` on seek. A future RfL hook should do the same. + +## Commands + +```bash +make # rust_null.ko +make load # insmod; creates /dev/rust-null +make unload # rmmod +make clean +``` + +`load` / `unload` are opt-in. Do not run them on a machine you are not prepared to test. diff --git a/kernel/rfl/rust_null.rs b/kernel/rfl/rust_null.rs new file mode 100644 index 0000000..3e63346 --- /dev/null +++ b/kernel/rfl/rust_null.rs @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: GPL-2.0 + +//! Rust-for-Linux `MiscDevice` with the `/dev/null` contract. +//! +//! This file is the out-of-tree module you would build against a +//! `CONFIG_RUST=y` kernel (`make LLVM=1` in that tree, `M=` this +//! directory, `obj-m := rust_null.o` compiling *this* `.rs`). +//! +//! Distro kernels often ship `CONFIG_HAVE_RUST=y` but not `CONFIG_RUST`. +//! They cannot compile this file. The loadable module in that case is +//! [`rust_null.c`](../rust_null.c), which registers the same +//! `struct miscdevice` name (`rust-null`) and the same +//! `file_operations` (`read` / `write` / `read_iter` / `write_iter` / +//! `llseek`). +//! +//! `kernel::miscdevice::MiscDevice` (mainline) currently hooks +//! `read_iter` / `write_iter` / `ioctl` / `mmap`. It does **not** +//! expose `.llseek`. `/dev/null` uses `null_lseek` (`f_pos = 0`). +//! The C module implements that; a future RfL `llseek` hook should +//! match it. Until then, a pure `MiscDevice` would not pass the seek +//! half of `tests/compare_devices.rs`. + +use kernel::{ + fs::{File, Kiocb}, + iov::{IovIterDest, IovIterSource}, + miscdevice::{MiscDevice, MiscDeviceOptions, MiscDeviceRegistration}, + prelude::*, +}; + +module! { + type: RustNullModule, + name: "rust_null", + authors: ["evie"], + description: "misc device with the /dev/null I/O contract", + license: "GPL", +} + +#[pin_data] +struct RustNullModule { + #[pin] + _miscdev: MiscDeviceRegistration, +} + +impl kernel::InPlaceModule for RustNullModule { + fn init(_module: &'static ThisModule) -> impl PinInit { + pr_info!("rust_null: registering misc device rust-null\n"); + + let options = MiscDeviceOptions { name: c"rust-null" }; + + try_pin_init!(Self { + _miscdev <- MiscDeviceRegistration::register(options), + }) + } +} + +/// Per-open state. `/dev/null` has none; neither do we. +struct RustNull; + +#[vtable] +impl MiscDevice for RustNull { + type Ptr = (); + + fn open(_file: &File, _misc: &MiscDeviceRegistration) -> Result { + Ok(()) + } + + /// `read_null` / `read_iter_null`: always EOF. Do not fill `iov`. + fn read_iter(_kiocb: Kiocb<'_, Self::Ptr>, _iov: &mut IovIterDest<'_>) -> Result { + Ok(0) + } + + /// `write_null` / `write_iter_null`: accept every byte, discard it. + fn write_iter(_kiocb: Kiocb<'_, Self::Ptr>, iov: &mut IovIterSource<'_>) -> Result { + let count = iov.len(); + iov.advance(count); + Ok(count) + } +} diff --git a/kernel/rust_null.c b/kernel/rust_null.c new file mode 100644 index 0000000..15ca65d --- /dev/null +++ b/kernel/rust_null.c @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * rust_null — misc device with the Linux /dev/null I/O contract. + * + * /dev/null itself is mem 1:3 (drivers/char/mem.c, null_fops). This + * module registers a *misc* device named "rust-null" (major 10, dynamic + * minor) whose file_operations copy that contract: + * + * read_null → return 0 + * write_null → return count (do not touch the user buffer) + * null_lseek → file->f_pos = 0; return 0 + * read_iter_null / write_iter_null + * + * The VFS hook is struct file_operations, hung off struct miscdevice + * via misc_register(). That is the same path Rust-for-Linux + * kernel::miscdevice::MiscDevice uses (see rfl/rust_null.rs). Distro + * kernels without CONFIG_RUST cannot compile the .rs; this .c is the + * loadable module. + */ + +#include +#include +#include +#include + +static ssize_t rust_null_read(struct file *file, char __user *buf, + size_t count, loff_t *ppos) +{ + return 0; +} + +static ssize_t rust_null_write(struct file *file, const char __user *buf, + size_t count, loff_t *ppos) +{ + return count; +} + +static loff_t rust_null_llseek(struct file *file, loff_t offset, int orig) +{ + return file->f_pos = 0; +} + +static ssize_t rust_null_read_iter(struct kiocb *iocb, struct iov_iter *to) +{ + return 0; +} + +static ssize_t rust_null_write_iter(struct kiocb *iocb, struct iov_iter *from) +{ + size_t count = iov_iter_count(from); + + iov_iter_advance(from, count); + return count; +} + +static const struct file_operations rust_null_fops = { + .owner = THIS_MODULE, + .llseek = rust_null_llseek, + .read = rust_null_read, + .write = rust_null_write, + .read_iter = rust_null_read_iter, + .write_iter = rust_null_write_iter, +}; + +static struct miscdevice rust_null_misc = { + .minor = MISC_DYNAMIC_MINOR, + .name = "rust-null", + .fops = &rust_null_fops, + .mode = 0666, +}; + +static int __init rust_null_init(void) +{ + int err; + + err = misc_register(&rust_null_misc); + if (err) + return err; + + pr_info("rust_null: /dev/rust-null registered (misc dynamic minor)\n"); + return 0; +} + +static void __exit rust_null_exit(void) +{ + misc_deregister(&rust_null_misc); + pr_info("rust_null: /dev/rust-null unregistered\n"); +} + +module_init(rust_null_init); +module_exit(rust_null_exit); + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("evie "); +MODULE_DESCRIPTION("misc device with the /dev/null I/O contract"); +MODULE_VERSION("0.1.0"); diff --git a/userspace/Cargo.toml b/userspace/Cargo.toml new file mode 100644 index 0000000..61f6f86 --- /dev/null +++ b/userspace/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "rust-null" +version = "0.1.0" +edition = "2021" +rust-version = "1.80" +authors = ["evie "] +license = "GPL-2.0-only" +description = "Userspace model of Linux /dev/null, plus a misc device /dev/rust-null." +repository = "https://github.com/hareai/rust-null" +readme = "../README.md" +keywords = ["linux", "null", "kernel", "device"] +categories = ["os"] +publish = false + +[dependencies] diff --git a/examples/discard.rs b/userspace/examples/discard.rs similarity index 88% rename from examples/discard.rs rename to userspace/examples/discard.rs index 6d8267b..62e3345 100644 --- a/examples/discard.rs +++ b/userspace/examples/discard.rs @@ -1,7 +1,7 @@ //! Read stdin, discard it the same way `/dev/null` would. //! //! ```bash -//! echo hello | cargo run --example discard +//! echo hello | cargo run -p rust-null --example discard //! ``` use std::io::{self, Read, Write}; diff --git a/src/lib.rs b/userspace/src/lib.rs similarity index 91% rename from src/lib.rs rename to userspace/src/lib.rs index 48e6b2f..7367c47 100644 --- a/src/lib.rs +++ b/userspace/src/lib.rs @@ -8,10 +8,10 @@ //! | `write_null` | return `count`. Discard the bytes. | //! | `null_lseek` | `file->f_pos = 0`; return `0`. Ignore whence. | //! -//! This crate is the first step toward an out-of-tree Rust-for-Linux -//! misc device registered as `/dev/rust-null`. The I/O contract stays -//! the same when that module lands; only the transport changes -//! (`std::io` here, `kernel::file` / `MiscDevice` there). +//! This crate is the userspace model (`userspace/`). The matching misc +//! device is `kernel/rust_null.c` (`/dev/rust-null`). The I/O contract +//! stays the same; only the transport changes (`std::io` here, +//! `file_operations` / RfL `MiscDevice` there). use std::io::{self, Read, Seek, SeekFrom, Write}; diff --git a/tests/against_linux_null.rs b/userspace/tests/against_linux_null.rs similarity index 100% rename from tests/against_linux_null.rs rename to userspace/tests/against_linux_null.rs diff --git a/userspace/tests/compare_devices.rs b/userspace/tests/compare_devices.rs new file mode 100644 index 0000000..0e68187 --- /dev/null +++ b/userspace/tests/compare_devices.rs @@ -0,0 +1,129 @@ +//! `/dev/null` vs `/dev/rust-null`. +//! +//! Same I/O contract, different devices: mem char `1:3` versus a misc +//! device named `rust-null`. +//! +//! Without `/dev/rust-null` the tests skip, so `cargo test` stays +//! green in CI. After `make -C kernel load`, run: +//! +//! ```bash +//! RUST_NULL_REQUIRE_DEV=1 cargo test -p rust-null --test compare_devices --locked +//! ``` + +#![cfg(unix)] + +use std::fs::{File, OpenOptions}; +use std::io::{self, Read, Seek, SeekFrom, Write}; +use std::path::Path; + +const LINUX_NULL: &str = "/dev/null"; +const RUST_NULL: &str = "/dev/rust-null"; + +fn require_dev() -> bool { + std::env::var_os("RUST_NULL_REQUIRE_DEV").is_some() +} + +fn open_rw(path: &str) -> io::Result { + OpenOptions::new().read(true).write(true).open(path) +} + +fn rust_null() -> Option { + match open_rw(RUST_NULL) { + Ok(f) => Some(f), + Err(err) => { + if require_dev() { + panic!("{RUST_NULL} is required ({err}); insmod kernel/rust_null.ko"); + } + eprintln!("skip: {RUST_NULL} ({err})"); + None + } + } +} + +#[test] +fn rust_null_node_exists() { + if Path::new(RUST_NULL).exists() { + return; + } + if require_dev() { + panic!("RED: {RUST_NULL} missing; the misc device is not registered"); + } + eprintln!("skip: {RUST_NULL} is not registered"); +} + +#[test] +fn read_is_eof_on_both_and_does_not_fill() { + let Some(mut rust) = rust_null() else { + return; + }; + let mut linux = open_rw(LINUX_NULL).unwrap(); + let mut a = [0xAAu8; 16]; + let mut b = [0xAAu8; 16]; + assert_eq!(linux.read(&mut a).unwrap(), 0); + assert_eq!(rust.read(&mut b).unwrap(), 0); + assert_eq!(a, [0xAA; 16]); + assert_eq!(b, [0xAA; 16]); +} + +#[test] +fn write_returns_count_on_both() { + let Some(mut rust) = rust_null() else { + return; + }; + let mut linux = open_rw(LINUX_NULL).unwrap(); + assert_eq!( + linux.write(b"hello").unwrap(), + rust.write(b"hello").unwrap() + ); + assert_eq!( + linux.write(&[0u8; 65_536]).unwrap(), + rust.write(&[0u8; 65_536]).unwrap() + ); +} + +#[test] +fn written_bytes_cannot_be_read_back_on_either() { + let Some(mut rust) = rust_null() else { + return; + }; + let mut linux = open_rw(LINUX_NULL).unwrap(); + linux.write_all(b"secret").unwrap(); + rust.write_all(b"secret").unwrap(); + let mut a = [1u8; 4]; + let mut b = [1u8; 4]; + assert_eq!(linux.read(&mut a).unwrap(), 0); + assert_eq!(rust.read(&mut b).unwrap(), 0); + assert_eq!(a, [1, 1, 1, 1]); + assert_eq!(b, [1, 1, 1, 1]); +} + +#[test] +fn seek_always_returns_zero_on_both() { + let Some(mut rust) = rust_null() else { + return; + }; + let mut linux = open_rw(LINUX_NULL).unwrap(); + for pos in [ + SeekFrom::Start(100), + SeekFrom::Current(5), + SeekFrom::Current(-10), + SeekFrom::End(7), + SeekFrom::End(-1), + ] { + assert_eq!(linux.seek(pos).unwrap(), rust.seek(pos).unwrap(), "{pos:?}"); + assert_eq!(linux.seek(pos).unwrap(), 0, "linux {pos:?}"); + assert_eq!(rust.seek(pos).unwrap(), 0, "rust {pos:?}"); + } +} + +#[test] +fn write_does_not_move_position_on_either() { + let Some(mut rust) = rust_null() else { + return; + }; + let mut linux = open_rw(LINUX_NULL).unwrap(); + linux.write_all(b"abc").unwrap(); + rust.write_all(b"abc").unwrap(); + assert_eq!(linux.stream_position().unwrap(), 0); + assert_eq!(rust.stream_position().unwrap(), 0); +} diff --git a/tests/null_contract.rs b/userspace/tests/null_contract.rs similarity index 100% rename from tests/null_contract.rs rename to userspace/tests/null_contract.rs