diff --git a/AGENTS.md b/AGENTS.md index a917554..d12d701 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -124,6 +124,12 @@ The workspace opts into strict linting: Clippy's `pedantic` group plus restricti - **Voice:** Use the passive voice or neutral descriptive language. Instead of "We initialize the buffer," use "The buffer is initialized." Instead of "Your vertex shader needs this," use "The vertex shader requires this." - **Focus:** Describe the code's behavior, the system's state, or technical invariants. - **Struct Documentation:** Every field in a public or internal struct must have a doc comment (`///`) explaining its purpose and any invariants. +- **Function documentation sections:** Function doc comments follow the [Rust API Guidelines](https://rust-lang.github.io/api-guidelines/documentation.html) standard sections, in this fixed order after the summary and any extended description: `# Errors`, then `# Panics`, then `# Safety`. The sections apply to **all** functions, public and private (clippy only enforces the public ones; the same standard is expected on private helpers by hand). + - **`# Errors`** is mandatory on every function returning `Result`, and states the conditions under which each error variant is returned. `fn main` is exempt. + - **`# Panics`** is mandatory on any function that can panic (an `expect`/`unwrap`/`panic!`/`assert!`/indexing/arithmetic that can trip), and states the condition that triggers the panic. + - **`# Safety`** is mandatory on every `unsafe fn`, and states the invariants the caller must uphold. + - Test functions (`#[test]`, and helpers inside `#[cfg(test)]`) are exempt from all three; they are not part of the documented surface. + - Enforcement: `missing_errors_doc`, `missing_panics_doc`, and `missing_safety_doc` are warnings in the workspace lint set, so a missing section on a public item fails CI. - **Stability:** Treat the documentation as a technical specification for the engine. - **Line breaks:** Do not insert line returns inside a comment unless necessary. A comment that fits on a single line stays on a single line; do not pre-wrap at ~80 chars for aesthetics. Only break across lines when the comment is genuinely long (multi-sentence prose, enumerated invariants) or when a hard break carries meaning (separating an intro line from a bullet list, for instance). diff --git a/Cargo.toml b/Cargo.toml index 9690c18..e763305 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,5 +41,4 @@ unimplemented = "warn" # Pedantic exceptions (too noisy) module_name_repetitions = "allow" -must_use_candidate = "allow" -missing_errors_doc = "allow" \ No newline at end of file +must_use_candidate = "allow" \ No newline at end of file diff --git a/crates/net/src/codec.rs b/crates/net/src/codec.rs index b38d8f3..9270a0c 100644 --- a/crates/net/src/codec.rs +++ b/crates/net/src/codec.rs @@ -33,6 +33,10 @@ fn write_varint(value: u64, buf: &mut Vec) { } /// Reads an unsigned LEB128 varint from the front of `bytes`, returning the decoded value and the number of bytes consumed. +/// +/// # Errors +/// +/// Returns [`NetError::MalformedVarint`] if the encoding exceeds the ten bytes a `u64` may occupy, or [`NetError::UnexpectedEof`] if the buffer ends while the continuation bit is still set. fn read_varint(bytes: &[u8]) -> Result<(u64, usize), NetError> { let mut value: u64 = 0; let mut shift: u32 = 0; @@ -52,6 +56,10 @@ fn read_varint(bytes: &[u8]) -> Result<(u64, usize), NetError> { } /// Encodes `msg` as a single length-prefixed `postcard` frame into a freshly allocated buffer. +/// +/// # Errors +/// +/// Returns [`NetError::Postcard`] if `msg` fails to serialize. fn encode_frame(msg: &T) -> Result, NetError> { let payload = postcard::to_stdvec(msg)?; let mut frame = Vec::new(); @@ -61,6 +69,10 @@ fn encode_frame(msg: &T) -> Result, NetError> { } /// Writes one length-prefixed `postcard` frame to a quinn send stream. +/// +/// # Errors +/// +/// Returns [`NetError::Postcard`] if `msg` fails to serialize, or [`NetError::Write`] if the send stream rejects the bytes. pub async fn write_frame( stream: &mut quinn::SendStream, msg: &T, @@ -71,6 +83,10 @@ pub async fn write_frame( } /// Reads one length-prefixed `postcard` frame from a quinn recv stream and decodes it. +/// +/// # Errors +/// +/// Returns [`NetError::MalformedVarint`] if the length prefix is overlong, [`NetError::FrameTooLarge`] if the declared length exceeds `max_len`, [`NetError::Read`] if the stream ends before the frame is complete, or [`NetError::Postcard`] if the payload fails to deserialize. pub async fn read_frame( stream: &mut quinn::RecvStream, max_len: usize, @@ -98,6 +114,10 @@ pub async fn read_frame( } /// Validates a declared frame length against `max_len`. +/// +/// # Errors +/// +/// Returns [`NetError::FrameTooLarge`] if `len` exceeds `max_len`. fn check_frame_len(len: u64, max_len: usize) -> Result { if len > max_len as u64 { return Err(NetError::FrameTooLarge { len, max: max_len }); diff --git a/crates/net/src/endpoint.rs b/crates/net/src/endpoint.rs index fe865e7..2acc0c6 100644 --- a/crates/net/src/endpoint.rs +++ b/crates/net/src/endpoint.rs @@ -31,6 +31,10 @@ fn ensure_crypto_provider() { } /// Builds a QUIC server endpoint bound to `bind`, using a freshly generated self-signed certificate and the `synvael` ALPN. +/// +/// # Errors +/// +/// Returns [`NetError::Rcgen`] if certificate generation fails, [`NetError::Rustls`] if the TLS configuration cannot be built, [`NetError::NoInitialCipherSuite`] if the configuration lacks a TLS 1.3 cipher suite, or [`NetError::Io`] if the UDP socket cannot be bound. pub fn server_endpoint(bind: SocketAddr) -> Result { ensure_crypto_provider(); @@ -51,6 +55,10 @@ pub fn server_endpoint(bind: SocketAddr) -> Result { } /// Builds a QUIC client endpoint bound to an ephemeral local address, configured with the `synvael` ALPN and a permissive certificate verifier. +/// +/// # Errors +/// +/// Returns [`NetError::NoInitialCipherSuite`] if the TLS configuration lacks a TLS 1.3 cipher suite, or [`NetError::Io`] if the local UDP socket cannot be bound. pub fn client_endpoint() -> Result { ensure_crypto_provider(); diff --git a/crates/renderer/src/device.rs b/crates/renderer/src/device.rs index a4d43d8..6fd461a 100644 --- a/crates/renderer/src/device.rs +++ b/crates/renderer/src/device.rs @@ -6,6 +6,10 @@ use crate::error::RendererError; use ash::{Device, Instance, khr, vk}; /// Picks a physical device (GPU) that supports the required features and extensions. +/// +/// # Errors +/// +/// Returns [`RendererError::VulkanError`] if physical devices cannot be enumerated, or [`RendererError::NoSuitableGpu`] if none meets the requirements. pub fn pick_physical_device( instance: &Instance, surface_loader: &khr::surface::Instance, @@ -27,6 +31,10 @@ pub fn pick_physical_device( } /// Creates a logical device and retrieves the graphics queue. +/// +/// # Errors +/// +/// Returns [`RendererError::VulkanError`] if the device cannot be created. pub fn create_logical_device( instance: &Instance, physical_device: vk::PhysicalDevice, @@ -58,6 +66,10 @@ pub fn create_logical_device( } /// Finds a queue family that supports both graphics commands and presentation. +/// +/// # Errors +/// +/// Returns [`RendererError::VulkanError`] if surface-support queries fail, or [`RendererError::NoSuitableGpu`] if no family supports both graphics and presentation. pub fn find_graphics_queue_family( instance: &Instance, physical_device: vk::PhysicalDevice, diff --git a/crates/renderer/src/instance.rs b/crates/renderer/src/instance.rs index 7a9a654..16bbd51 100644 --- a/crates/renderer/src/instance.rs +++ b/crates/renderer/src/instance.rs @@ -6,6 +6,10 @@ use std::ffi::{CStr, c_char}; use tracing::{debug, error, info, warn}; /// Creates a Vulkan instance and optionally a debug messenger. +/// +/// # Errors +/// +/// Returns [`RendererError::VulkanError`] if instance creation fails, or if the debug messenger cannot be created in debug builds. pub fn create_instance( entry: &Entry, required_extensions: &[*const c_char], @@ -62,6 +66,10 @@ pub fn create_instance( } /// The callback function invoked by Vulkan's validation layers. +/// +/// # Safety +/// +/// Invoked by the Vulkan loader, which must pass a valid `p_callback_data` pointer whose `p_message` is either null or a valid NUL-terminated C string. Not to be called directly. unsafe extern "system" fn vulkan_debug_callback( message_severity: vk::DebugUtilsMessageSeverityFlagsEXT, _message_type: vk::DebugUtilsMessageTypeFlagsEXT, diff --git a/crates/renderer/src/lib.rs b/crates/renderer/src/lib.rs index e9d5442..6bb5f49 100644 --- a/crates/renderer/src/lib.rs +++ b/crates/renderer/src/lib.rs @@ -36,6 +36,10 @@ impl Renderer { /// This function loads the Vulkan library, creates an instance, selects a GPU, /// and initializes a logical device with a graphics queue. /// + /// # Errors + /// + /// Returns [`RendererError`] if any initialization step fails: loading Vulkan, creating the instance, surface, device, swapchain, pipeline, allocator, or initial geometry. + /// /// # Panics /// /// Panics if `MAX_FRAMES_IN_FLIGHT` or vertex data sizes exceed `u32`/`u64` limits. @@ -166,6 +170,10 @@ impl Renderer { } /// Creates a GPU memory allocator. +/// +/// # Errors +/// +/// Returns [`RendererError::AllocationError`] if the allocator cannot be initialized. fn create_allocator( instance: &ash::Instance, device: &ash::Device, @@ -186,6 +194,10 @@ fn create_allocator( } /// Creates the 3D geometry buffers (vertex and index) for a cube. +/// +/// # Errors +/// +/// Returns [`RendererError::AllocationError`] if GPU memory cannot be allocated, or [`RendererError::VulkanError`] if a buffer cannot be created. fn create_geometry( device: &ash::Device, allocator: &mut Allocator, @@ -260,6 +272,10 @@ fn create_geometry( } /// Creates the depth buffer resources (image, memory, and view). +/// +/// # Errors +/// +/// Returns [`RendererError::AllocationError`] if GPU memory cannot be allocated, or [`RendererError::VulkanError`] if the depth image or its view cannot be created. fn create_depth_resources( device: &ash::Device, allocator: &mut Allocator, @@ -321,6 +337,10 @@ fn create_depth_resources( } /// Helper function to create and populate a GPU buffer. +/// +/// # Errors +/// +/// Returns [`RendererError::AllocationError`] if GPU memory cannot be allocated, or [`RendererError::VulkanError`] if the buffer cannot be created or bound. fn create_gpu_buffer( device: &ash::Device, allocator: &mut Allocator, diff --git a/crates/renderer/src/pipeline.rs b/crates/renderer/src/pipeline.rs index 4668ab5..0995dfe 100644 --- a/crates/renderer/src/pipeline.rs +++ b/crates/renderer/src/pipeline.rs @@ -10,6 +10,10 @@ use std::io::Cursor; /// Helper to load SPIR-V bytes and create a Vulkan Shader Module. /// /// Vulkan expects shader code to be 32-bit aligned; `ash::util::read_spv` is used to correctly interpret the raw bytes as a slice of `u32`. +/// +/// # Errors +/// +/// Returns [`RendererError::IoError`] if `bytes` is not valid, 32-bit-aligned SPIR-V, or [`RendererError::VulkanError`] if module creation fails on the device. pub fn create_shader_module( device: &Device, bytes: &[u8], @@ -27,6 +31,10 @@ pub fn create_shader_module( /// Defines the 'interface' of the pipeline (what data we can pass to the shaders). /// /// This layout defines any push constants or descriptor sets (textures/UBOs) accessed by the shaders during execution. +/// +/// # Errors +/// +/// Returns [`RendererError::VulkanError`] if the device fails to create the pipeline layout. pub fn create_pipeline_layout(device: &Device) -> Result { // A single push constant range is defined for the MVP matrix, allowing it to be updated for every draw call with high efficiency. #[expect( @@ -50,6 +58,10 @@ pub fn create_pipeline_layout(device: &Device) -> Result Result<(vk::ShaderModule, vk::ShaderModule), RendererError> { diff --git a/crates/renderer/src/renderer.rs b/crates/renderer/src/renderer.rs index b8bf83e..ac5efee 100644 --- a/crates/renderer/src/renderer.rs +++ b/crates/renderer/src/renderer.rs @@ -79,6 +79,10 @@ pub struct Renderer { impl Renderer { /// Renders a single frame. + /// + /// # Errors + /// + /// Returns [`RendererError::SyncPrimitivesMissing`] if the synchronization primitives have been torn down, or [`RendererError::VulkanError`] if any device operation (fence wait, image acquire, command recording, submit, or present) fails. pub fn draw_frame(&mut self, camera_view: glam::Mat4) -> Result<(), RendererError> { let sync = self .sync @@ -157,6 +161,10 @@ impl Renderer { } /// Records the drawing commands into the given command buffer. + /// + /// # Errors + /// + /// Returns [`RendererError::VulkanError`] if beginning or ending command-buffer recording fails. fn record_commands( &self, cmd: vk::CommandBuffer, @@ -322,6 +330,10 @@ impl Renderer { } /// Transitions the swapchain image back to the presentation layout. + /// + /// # Errors + /// + /// Returns [`RendererError::VulkanError`] if the pipeline barrier command cannot be recorded. fn transition_to_present_layout( &self, cmd: vk::CommandBuffer, @@ -359,7 +371,8 @@ impl Renderer { /// Replaces the currently rendering mesh with a new set of vertices and indices. /// /// # Errors - /// Returns a `RendererError` if new Vulkan buffers cannot be allocated or created. + /// + /// Returns [`RendererError::AllocationError`] if GPU memory cannot be allocated, or [`RendererError::VulkanError`] if the vertex or index buffers cannot be created. #[expect( clippy::cast_possible_truncation, reason = "a chunk mesh's index count never approaches u32::MAX" diff --git a/crates/renderer/src/surface.rs b/crates/renderer/src/surface.rs index c904f53..94dc3a0 100644 --- a/crates/renderer/src/surface.rs +++ b/crates/renderer/src/surface.rs @@ -5,6 +5,10 @@ use ash::{Entry, Instance, khr, vk}; use raw_window_handle::{RawDisplayHandle, RawWindowHandle}; /// Creates a Vulkan surface for the given window. +/// +/// # Errors +/// +/// Returns [`RendererError::VulkanError`] if the platform surface cannot be created for the given display and window handles. pub fn create_surface( entry: &Entry, instance: &Instance, diff --git a/crates/renderer/src/swapchain.rs b/crates/renderer/src/swapchain.rs index 718de42..159c272 100644 --- a/crates/renderer/src/swapchain.rs +++ b/crates/renderer/src/swapchain.rs @@ -4,6 +4,14 @@ use crate::error::RendererError; use ash::{Device, Instance, khr, vk}; /// Creates a swapchain and retrieves its images. +/// +/// # Errors +/// +/// Returns [`RendererError::VulkanError`] if a surface query fails or the swapchain and its images cannot be created. +/// +/// # Panics +/// +/// Panics if the driver reports zero surface formats, which the Vulkan specification forbids for a supported surface. pub fn create_swapchain( instance: &Instance, physical_device: vk::PhysicalDevice, @@ -87,6 +95,10 @@ pub fn create_swapchain( } /// Creates image views for the swapchain images. +/// +/// # Errors +/// +/// Returns [`RendererError::VulkanError`] if the device fails to create an image view. pub fn create_image_views( device: &Device, images: &[vk::Image], diff --git a/crates/renderer/src/sync.rs b/crates/renderer/src/sync.rs index d864f03..c9da30c 100644 --- a/crates/renderer/src/sync.rs +++ b/crates/renderer/src/sync.rs @@ -14,6 +14,10 @@ pub struct SyncPrimitives { } /// Creates all synchronization primitives for the given number of frames and images. +/// +/// # Errors +/// +/// Returns [`RendererError::VulkanError`] if the device fails to create a semaphore or fence. pub fn create_sync_primitives( device: &Device, max_frames_in_flight: usize, @@ -43,6 +47,10 @@ pub fn create_sync_primitives( } /// Destroys all synchronization primitives. +/// +/// # Safety +/// +/// The caller must ensure every primitive in `sync` was created from `device`, is no longer in use by any in-flight GPU work, and is not destroyed again. pub unsafe fn destroy_sync_primitives(device: &Device, sync: SyncPrimitives) { unsafe { for semaphore in sync.image_available { diff --git a/crates/server/src/save/region_actor.rs b/crates/server/src/save/region_actor.rs index 07430d7..0a96da7 100644 --- a/crates/server/src/save/region_actor.rs +++ b/crates/server/src/save/region_actor.rs @@ -108,6 +108,10 @@ fn actor_loop(region_dir: &Path, request_rx: &Receiver) { } /// Flushes every dirty region to disk, returning the first error while still attempting the rest. +/// +/// # Errors +/// +/// Returns the first [`SaveError`] produced by [`RegionFile::save`]; remaining dirty regions are still flushed. fn flush_dirty(regions: &mut HashMap<(i32, i32), RegionFile>) -> Result<(), SaveError> { let mut result = Ok(()); for region in regions.values_mut() { @@ -127,6 +131,10 @@ fn flush_dirty(regions: &mut HashMap<(i32, i32), RegionFile>) -> Result<(), Save } /// Returns the region file covering `pos`, opening and caching it on first access. +/// +/// # Errors +/// +/// Returns a [`SaveError`] from [`RegionFile::open`] if the region file exists but cannot be read or decoded. fn region_mut<'a>( regions: &'a mut HashMap<(i32, i32), RegionFile>, region_dir: &Path, @@ -143,6 +151,10 @@ fn region_mut<'a>( } /// Reads the stored chunk at `pos`, opening and caching its region file on first access. +/// +/// # Errors +/// +/// Returns a [`SaveError`] if the region file cannot be opened or the stored record cannot be decoded. fn read_chunk( regions: &mut HashMap<(i32, i32), RegionFile>, region_dir: &Path, diff --git a/crates/server/src/save/region_file.rs b/crates/server/src/save/region_file.rs index 72bec93..87b45a4 100644 --- a/crates/server/src/save/region_file.rs +++ b/crates/server/src/save/region_file.rs @@ -43,6 +43,10 @@ pub struct RegionFile { impl RegionFile { /// Opens the region file at `path`, or yields an empty region if the file does not yet exist. + /// + /// # Errors + /// + /// Returns [`SaveError::Io`] if the file cannot be read, a decoding error from [`RegionIndex::decode`] if the index is malformed, or [`SaveError::PayloadTooLarge`] / [`SaveError::Truncated`] if a header entry's span falls outside the file. pub fn open(path: PathBuf) -> Result { if !path.exists() { return Ok(Self { @@ -107,6 +111,10 @@ impl RegionFile { } /// Decodes and returns the chunk at `pos`, or `None` if the region holds no record for it. + /// + /// # Errors + /// + /// Returns a decoding error from [`record::decode`] if the stored record is malformed. pub fn read_chunk(&self, pos: ChunkPos) -> Result, SaveError> { match self.records.get(&pos) { Some(bytes) => { @@ -118,6 +126,10 @@ impl RegionFile { } /// Encodes `data` into a `SYNC` record stamped with `last_modified` and stores it under `pos`. + /// + /// # Errors + /// + /// Returns an encoding error from [`record::encode`] if serialization fails, or [`SaveError::PayloadTooLarge`] if the encoded record exceeds `u32::MAX` bytes. pub fn write_chunk( &mut self, pos: ChunkPos, @@ -150,6 +162,10 @@ impl RegionFile { } /// Flushes the region to disk with a crash-safe whole-file atomic rewrite, clearing the dirty flag. + /// + /// # Errors + /// + /// Returns [`SaveError::PayloadTooLarge`] if a record's length exceeds `u32::MAX`, or [`SaveError::Io`] if the atomic write to disk fails. pub fn save(&mut self) -> Result<(), SaveError> { let image = self.serialize()?; atomic_write(&self.path, &image)?; @@ -158,6 +174,10 @@ impl RegionFile { } /// Builds the complete on-disk file image: the encoded index followed by every record. + /// + /// # Errors + /// + /// Returns [`SaveError::PayloadTooLarge`] if the index or any record length exceeds `u32::MAX` bytes. // * NOTE: this is a whole-file rewrite. The right way to do it for large saves is to append changed records into free space and rewriting only the header table, so save cost scales with chunks modified rather than total file size. The free list and absolute offsets already on disk support that switch without a format change. // TODO: incremental save. fn serialize(&mut self) -> Result, SaveError> { @@ -188,6 +208,10 @@ impl RegionFile { } /// Writes `bytes` to `path` via the POSIX atomic-write pattern: `.tmp` + fsync + rename. +/// +/// # Errors +/// +/// Returns [`SaveError::Io`] if the parent directory cannot be created, or if writing, syncing, or renaming the temporary file fails. fn atomic_write(path: &Path, bytes: &[u8]) -> Result<(), SaveError> { // The region directory is created on demand so the first write to a fresh world succeeds. if let Some(parent) = path.parent() { diff --git a/crates/server/src/world_server.rs b/crates/server/src/world_server.rs index 09a7aec..4cff871 100644 --- a/crates/server/src/world_server.rs +++ b/crates/server/src/world_server.rs @@ -230,6 +230,10 @@ fn load_chunk( } /// Sends a read request to the save actor and blocks for its reply, mapping a departed actor to an absent record so generation can still proceed. +/// +/// # Errors +/// +/// Returns the [`SaveError`] reported by the save actor if reading the stored chunk fails. A departed actor yields `Ok(None)` rather than an error. fn request_saved_chunk( save_tx: &Sender, pos: ChunkPos, diff --git a/crates/shared/src/save/cursor.rs b/crates/shared/src/save/cursor.rs index eb6d480..3fa5d35 100644 --- a/crates/shared/src/save/cursor.rs +++ b/crates/shared/src/save/cursor.rs @@ -17,7 +17,11 @@ impl<'a> Reader<'a> { Self { bytes, offset: 0 } } - /// Returns the next `n` bytes and advances the cursor, or [`SaveError::Truncated`] if fewer remain. + /// Returns the next `n` bytes and advances the cursor. + /// + /// # Errors + /// + /// Returns [`SaveError::Truncated`] if fewer than `n` bytes remain, or if the offset addition overflows. pub(crate) fn take(&mut self, n: usize) -> Result<&'a [u8], SaveError> { let end = self.offset.checked_add(n).ok_or(SaveError::Truncated { offset: self.offset, @@ -37,6 +41,10 @@ impl<'a> Reader<'a> { } /// Returns the next `N` bytes as a fixed-size array and advances the cursor. + /// + /// # Errors + /// + /// Returns [`SaveError::Truncated`] if fewer than `N` bytes remain. pub(crate) fn take_array(&mut self) -> Result<[u8; N], SaveError> { let mut array = [0u8; N]; array.copy_from_slice(self.take(N)?); diff --git a/crates/shared/src/save/record.rs b/crates/shared/src/save/record.rs index 5d43e48..aa13e49 100644 --- a/crates/shared/src/save/record.rs +++ b/crates/shared/src/save/record.rs @@ -32,6 +32,10 @@ pub struct RecordMeta { } /// Encodes `data` into a `SYNC` record, stamping it with `last_modified` (unix-ms). +/// +/// # Errors +/// +/// Returns [`SaveError::Postcard`] if serialization fails, [`SaveError::Io`] if zstd compression fails, or [`SaveError::PayloadTooLarge`] if either the uncompressed or compressed length exceeds `u32::MAX`. pub fn encode(data: &ChunkData, last_modified: u64) -> Result, SaveError> { let uncompressed = postcard::to_stdvec(data)?; let compressed = zstd::encode_all(uncompressed.as_slice(), ZSTD_LEVEL)?; @@ -61,6 +65,10 @@ pub fn encode(data: &ChunkData, last_modified: u64) -> Result, SaveError /// /// `bytes` is untrusted on-disk input, so every field is bounds-checked and the decompressed /// payload length is validated against the header before deserialization is attempted. +/// +/// # Errors +/// +/// Returns [`SaveError::Truncated`] if the buffer ends mid-field, [`SaveError::BadMagic`] if the leading tag is not `SYNC`, [`SaveError::Io`] if zstd decompression fails, [`SaveError::LengthMismatch`] if the decompressed length disagrees with the header, or [`SaveError::Postcard`] if the payload fails to deserialize. pub fn decode(bytes: &[u8]) -> Result<(RecordMeta, ChunkData), SaveError> { let mut reader = Reader::new(bytes); diff --git a/crates/shared/src/save/region.rs b/crates/shared/src/save/region.rs index ae7b78c..1c64fa6 100644 --- a/crates/shared/src/save/region.rs +++ b/crates/shared/src/save/region.rs @@ -134,6 +134,10 @@ impl RegionIndex { } /// Serializes the index to its on-disk framing bytes. + /// + /// # Errors + /// + /// Returns [`SaveError::PayloadTooLarge`] if the header, free-list, or stamp table holds more than `u32::MAX` entries. pub fn encode(&self) -> Result, SaveError> { let mut out = Vec::new(); out.extend_from_slice(&MAGIC); @@ -171,6 +175,10 @@ impl RegionIndex { } /// Parses a region index from its framing bytes, ignoring any chunk records that follow it. + /// + /// # Errors + /// + /// Returns [`SaveError::Truncated`] if the buffer ends mid-field, [`SaveError::BadMagic`] if the leading tag is not the region magic, or [`SaveError::UnsupportedVersion`] if the format version is not recognised. pub fn decode(bytes: &[u8]) -> Result { let mut reader = Reader::new(bytes); @@ -232,6 +240,10 @@ impl RegionIndex { } /// Reads a chunk position as three little-endian `i32`s. +/// +/// # Errors +/// +/// Returns [`SaveError::Truncated`] if fewer than twelve bytes remain. fn read_pos(reader: &mut Reader) -> Result { let x = i32::from_le_bytes(reader.take_array()?); let y = i32::from_le_bytes(reader.take_array()?); @@ -240,6 +252,10 @@ fn read_pos(reader: &mut Reader) -> Result { } /// Narrows a table length to the `u32` the framing uses, failing loudly rather than truncating. +/// +/// # Errors +/// +/// Returns [`SaveError::PayloadTooLarge`] if `len` exceeds `u32::MAX`. fn len_u32(len: usize) -> Result { u32::try_from(len).map_err(|_| SaveError::PayloadTooLarge { len }) }