//! The block-device wire protocol — what a filesystem (the FAT server) says to a //! block driver (usb-storage) over `/protocol/block`. Defined through the //! envelope, so every packet begins with the folded `Header`. //! //! **`Header.target` is always 0 here**: a block driver instance serves exactly //! one device over its own endpoint, so there is no object within the peer to //! address. A driver that later fronts several volumes gives them target ids and //! `enumerate` lists them; nothing else about the protocol changes. //! //! Data path: read and write move whole blocks to or from a **caller-owned DMA //! buffer**, named by its physical address — the same physical-address handoff //! usb-storage already uses toward the controller, one layer up. So a 512-byte //! sector never has to cross the packet floor; only the small request / reply //! parts do. Under an enforcing IOMMU the buffer's physical addresses are only //! reachable by the device once the filesystem has `attach`ed the buffer's //! capability (the block server forwards it to the controller); see //! docs/driver-model.md. const envelope = @import("envelope"); /// The answer to `geometry()`. pub const Geometry = extern struct { block_size: u32, // bytes per block (512) _padding: u32 = 0, block_count: u64, // total blocks }; /// `read(lba, count, physical)` / `write(...)`: move `count` blocks between the /// device and the caller's DMA buffer at `physical`. pub const Transfer = extern struct { lba: u64, count: u32, _padding: u32 = 0, physical: u64, // caller's DMA buffer physical address }; /// How many blocks a transfer actually moved. pub const Transferred = extern struct { count: u32 }; /// `define_range(badge, base_lba, block_count)`: confine the sender identified by /// `badge` to blocks `[base_lba, base_lba + block_count)`. The volume manager /// calls this for each filesystem process it hands a channel to — the badge is /// the filesystem's kernel-stamped task id, and the range is the partition it /// mounts. A confined sender's read/write LBAs are then volume-relative (the /// driver adds `base_lba`) and a transfer past `block_count` is refused. A /// sender with no range is unconfined (the whole device), the default until the /// volume manager defines one. The clamp lives at the provider because a channel /// must carry exactly the authority it grants (storage-architecture.md): handing /// a filesystem the whole disk plus a base offset would let it reach the /// neighbouring partition. **A confined caller may not call this** — a filesystem /// cannot redefine its own range and escape; only an unconfined party (the /// volume manager) confines others. pub const DefineRange = extern struct { badge: u32, _padding: u32 = 0, base_lba: u64, block_count: u64, }; /// The `medium_changed` event payload: whether a medium is now present, and a /// monotonic counter so a subscriber that missed an edge still sees that /// SOMETHING changed. Pushed by a driver whose transport can tell medium from /// device (a card reader, an ATAPI tray): the device stays, the medium comes and /// goes. The volume manager consumes it into the same unmount/remount path it /// runs on device death — one lifecycle, two triggers /// (docs/file-system-development/storage-architecture.md). Presence only, never /// content: the driver reports that the medium changed, not what is on it. pub const MediumChanged = extern struct { present: u8, // 1 present, 0 absent _padding: u8 = 0, _padding2: u16 = 0, change_count: u32, }; pub const Protocol = envelope.Define(.{ .name = "block", .version = 1, .events = &.{ .{ .name = "medium_changed", .payload = MediumChanged }, }, .operations = &.{ .{ .name = "geometry", .reply = Geometry }, .{ .name = "read", .request = Transfer, .reply = Transferred }, .{ .name = "write", .request = Transfer, .reply = Transferred }, // flush(): commit any device write cache to stable media (no data // transfer). A filesystem calls this to make prior writes durable — // before power-off, so a shutdown-time write isn't lost in the USB flash // controller's cache. .{ .name = "flush" }, // attach(): the caller's DMA-region capability rides the call's cap // slot; the block server forwards it to the controller so the buffer's // physical addresses (named in later read/write) are reachable by the // device under an enforcing IOMMU. Call once per buffer before using it. .{ .name = "attach" }, // detach(): the reverse — the same region capability rides the cap slot // (the caller still holds its handle; the kernel matches the region) and // the buffer leaves the device's domain. Every grant a live process // makes is revocable by the granter while alive; death remains the // mechanical backstop (storage-architecture.md, the lifecycle rule). .{ .name = "detach" }, // define_range(): confine a sender to a block sub-range — the partition // it mounts. See `DefineRange`. Appended, so every verb above keeps its // number. .{ .name = "define_range", .request = DefineRange }, }, }); pub const Operation = Protocol.Operation; pub const Event = Protocol.Event; pub const message_maximum: usize = Protocol.message_maximum;