From c3e6d3cc0c40f83fcf3542b08ff5552898f76819 Mon Sep 17 00:00:00 2001 From: Robert M1 <50460704+githubrobbi@users.noreply.github.com> Date: Tue, 7 Jul 2026 09:45:32 -0700 Subject: [PATCH 1/5] refactor(uffs-mft): migrate chunks_exact(2) to as_chunks::<2>() and drop dead index suppressions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migrate all 35 `chunks_exact(2)` call sites in the NTFS parser to `as_chunks::<2>()`, ahead of the upcoming-nightly `chunks_exact`-with- constant-size clippy lint (nightly-canary #492, item 2). Verified on the pinned toolchain, not blind: the floating nightly can't build locally (ring 0.17.14 aarch64-apple const-eval, #492 item 1), but `as_chunks` is stable on our pin and clears the lint by construction. This is a net reduction in unsafe-shaped code, not just lint churn: each `&[u8]` chunk becomes a `&[u8; 2]` array, so the mapping closures drop their fallible `try_into().ok()` / `map_or(0, ..)` paths and their `[c[0], c[1]]` reindexing in favour of `u16::from_le_bytes(*c)`. As a result 7 lint suppressions become unnecessary and are removed: * 2× `#[expect(missing_asserts_for_indexing)]` whose reason cited `chunks_exact(2)` (attribute_helpers) * 5× now-unfulfilled `missing_asserts_for_indexing` expectations (module-level in direct_index/direct_index_extension; trimmed from the combined `indexing_slicing` attrs in io/parser index/ index_extension/fragment, keeping the still-live indexing_slicing). Verified: native clippy + xwin (windows-msvc) clippy clean with zero unfulfilled expectations, `cargo nextest run -p uffs-mft` 264 pass, rustdoc -D warnings clean. Addresses the lint half of #492. Co-Authored-By: Claude Opus 4.8 --- crates/uffs-mft/src/io/parser/fragment.rs | 13 ++++--- .../src/io/parser/fragment_extension.rs | 12 ++++--- crates/uffs-mft/src/io/parser/index.rs | 19 +++++++---- .../uffs-mft/src/io/parser/index_extension.rs | 19 +++++++---- crates/uffs-mft/src/ntfs/records.rs | 6 ++-- .../uffs-mft/src/parse/attribute_helpers.rs | 20 +++++------ crates/uffs-mft/src/parse/direct_index.rs | 34 +++++++++++-------- .../src/parse/direct_index_extension.rs | 34 +++++++++++-------- crates/uffs-mft/src/parse/forensic/base.rs | 24 ++++++------- .../uffs-mft/src/parse/forensic/extension.rs | 24 ++++++------- crates/uffs-mft/src/parse/full.rs | 24 ++++++------- .../uffs-mft/src/platform/metafile_decode.rs | 6 ++-- crates/uffs-mft/src/platform/metafile_read.rs | 6 ++-- crates/uffs-mft/src/usn/windows.rs | 11 +++--- 14 files changed, 140 insertions(+), 112 deletions(-) diff --git a/crates/uffs-mft/src/io/parser/fragment.rs b/crates/uffs-mft/src/io/parser/fragment.rs index 3c74aa5db..fea418cf7 100644 --- a/crates/uffs-mft/src/io/parser/fragment.rs +++ b/crates/uffs-mft/src/io/parser/fragment.rs @@ -50,7 +50,6 @@ use crate::ntfs::{ )] #[expect( clippy::indexing_slicing, - clippy::missing_asserts_for_indexing, reason = "the only `[]` indexing that remains is into internal arena vectors \ (fragment.links / fragment.streams / fragment.records / fragment.frs_to_idx) \ whose indices are produced by this code, not by untrusted on-disk bytes. \ @@ -175,8 +174,10 @@ pub fn parse_record_to_fragment( .and_then(|end| data.get(name_bytes_offset..end)) { let name_u16: Vec = name_bytes - .chunks_exact(2) - .map(|pair| u16::from_le_bytes([pair[0], pair[1]])) + .as_chunks::<2>() + .0 + .iter() + .map(|pair| u16::from_le_bytes(*pair)) .collect(); let name = crate::io::parser::unified::decode_name_u16(&name_u16).0; let parent_frs = file_reference_to_frs(fn_attr.parent_directory); @@ -272,8 +273,10 @@ pub fn parse_record_to_fragment( .and_then(|end| data.get(name_offset..end)) { let name_u16: SmallVec<[u16; 64]> = name_bytes - .chunks_exact(2) - .map(|pair| u16::from_le_bytes([pair[0], pair[1]])) + .as_chunks::<2>() + .0 + .iter() + .map(|pair| u16::from_le_bytes(*pair)) .collect(); let stream_name = crate::io::parser::unified::decode_name_u16(&name_u16).0; // ALL named $DATA streams create regular diff --git a/crates/uffs-mft/src/io/parser/fragment_extension.rs b/crates/uffs-mft/src/io/parser/fragment_extension.rs index debc8f3c5..444f4efa3 100644 --- a/crates/uffs-mft/src/io/parser/fragment_extension.rs +++ b/crates/uffs-mft/src/io/parser/fragment_extension.rs @@ -118,8 +118,10 @@ pub(super) fn parse_extension_to_fragment( .and_then(|end| data.get(name_start..end)) { let name_u16: SmallVec<[u16; 64]> = name_bytes - .chunks_exact(2) - .map(|pair| <[u8; 2]>::try_from(pair).map_or(0, u16::from_le_bytes)) + .as_chunks::<2>() + .0 + .iter() + .map(|pair| u16::from_le_bytes(*pair)) .collect(); let name = crate::io::parser::unified::decode_name_u16(&name_u16).0; let parent_frs = fn_attr.parent_directory & 0x0000_FFFF_FFFF_FFFF; @@ -188,8 +190,10 @@ pub(super) fn parse_extension_to_fragment( .and_then(|end| data.get(name_offset..end)) { let name_u16: SmallVec<[u16; 64]> = name_bytes - .chunks_exact(2) - .map(|pair| <[u8; 2]>::try_from(pair).map_or(0, u16::from_le_bytes)) + .as_chunks::<2>() + .0 + .iter() + .map(|pair| u16::from_le_bytes(*pair)) .collect(); let stream_name = crate::io::parser::unified::decode_name_u16(&name_u16).0; // ALL named $DATA streams create regular diff --git a/crates/uffs-mft/src/io/parser/index.rs b/crates/uffs-mft/src/io/parser/index.rs index aabaa714c..78f45ef79 100644 --- a/crates/uffs-mft/src/io/parser/index.rs +++ b/crates/uffs-mft/src/io/parser/index.rs @@ -77,7 +77,6 @@ use crate::parse::index_helpers::{ )] #[expect( clippy::indexing_slicing, - clippy::missing_asserts_for_indexing, reason = "remaining [] are internal arena indices (index.records[..]/stream_indices[..]/\ link_indices[..]) keyed by indices minted by this fn; not attacker-controlled. \ All untrusted-`data` reads go through .get()/rd_u* (WI-5.2)." @@ -235,8 +234,10 @@ pub fn parse_record_to_index(data: &[u8], frs: u64, index: &mut crate::index::Mf { // SmallVec avoids heap allocation for typical filenames (<= 64 chars) let name_u16: SmallVec<[u16; 64]> = name_bytes - .chunks_exact(2) - .map(|pair| u16::from_le_bytes([pair[0], pair[1]])) + .as_chunks::<2>() + .0 + .iter() + .map(|pair| u16::from_le_bytes(*pair)) .collect(); let name = crate::io::parser::unified::decode_name_u16(&name_u16).0; let parent_frs = file_reference_to_frs(fn_attr.parent_directory); @@ -358,8 +359,10 @@ pub fn parse_record_to_index(data: &[u8], frs: u64, index: &mut crate::index::Mf .and_then(|name_end| data.get(name_offset..name_end)) { let name_u16: SmallVec<[u16; 64]> = name_bytes - .chunks_exact(2) - .map(|pair| u16::from_le_bytes([pair[0], pair[1]])) + .as_chunks::<2>() + .0 + .iter() + .map(|pair| u16::from_le_bytes(*pair)) .collect(); let stream_name = crate::io::parser::unified::decode_name_u16(&name_u16).0; @@ -452,8 +455,10 @@ pub fn parse_record_to_index(data: &[u8], frs: u64, index: &mut crate::index::Mf String::new() } else { let name_u16: SmallVec<[u16; 64]> = name_bytes - .chunks_exact(2) - .map(|pair| u16::from_le_bytes([pair[0], pair[1]])) + .as_chunks::<2>() + .0 + .iter() + .map(|pair| u16::from_le_bytes(*pair)) .collect(); crate::io::parser::unified::decode_name_u16(&name_u16).0 }; diff --git a/crates/uffs-mft/src/io/parser/index_extension.rs b/crates/uffs-mft/src/io/parser/index_extension.rs index a646a23cc..1167f0539 100644 --- a/crates/uffs-mft/src/io/parser/index_extension.rs +++ b/crates/uffs-mft/src/io/parser/index_extension.rs @@ -62,7 +62,6 @@ use crate::index::{frs_to_usize, len_to_u16, len_to_u32, u32_as_usize}; )] #[expect( clippy::indexing_slicing, - clippy::missing_asserts_for_indexing, reason = "the only `[]` indexing that remains is into internal arena vectors \ (index.records / index.links / index.streams / index.internal_streams / \ index.frs_to_idx) whose indices are produced by this code, not by untrusted \ @@ -158,8 +157,10 @@ pub(super) fn parse_extension_to_index( .and_then(|end| data.get(name_start..end)) { let name_u16: SmallVec<[u16; 64]> = name_bytes - .chunks_exact(2) - .map(|pair| u16::from_le_bytes([pair[0], pair[1]])) + .as_chunks::<2>() + .0 + .iter() + .map(|pair| u16::from_le_bytes(*pair)) .collect(); let name = crate::io::parser::unified::decode_name_u16(&name_u16).0; let parent_frs = fn_attr.parent_directory & 0x0000_FFFF_FFFF_FFFF; @@ -245,8 +246,10 @@ pub(super) fn parse_extension_to_index( let name_offset = offset.saturating_add(usize::from(attr_header.name_offset)); if let Some(name_bytes) = read_name_bytes(data, name_offset, name_len) { let name_u16: SmallVec<[u16; 64]> = name_bytes - .chunks_exact(2) - .map(|pair| u16::from_le_bytes([pair[0], pair[1]])) + .as_chunks::<2>() + .0 + .iter() + .map(|pair| u16::from_le_bytes(*pair)) .collect(); let stream_name = crate::io::parser::unified::decode_name_u16(&name_u16).0; // ALL named $DATA streams create regular @@ -278,8 +281,10 @@ pub(super) fn parse_extension_to_index( String::new() } else { let name_u16: SmallVec<[u16; 64]> = name_bytes - .chunks_exact(2) - .map(|pair| u16::from_le_bytes([pair[0], pair[1]])) + .as_chunks::<2>() + .0 + .iter() + .map(|pair| u16::from_le_bytes(*pair)) .collect(); crate::io::parser::unified::decode_name_u16(&name_u16).0 }; diff --git a/crates/uffs-mft/src/ntfs/records.rs b/crates/uffs-mft/src/ntfs/records.rs index 156eb8c0e..94449d35e 100644 --- a/crates/uffs-mft/src/ntfs/records.rs +++ b/crates/uffs-mft/src/ntfs/records.rs @@ -540,8 +540,10 @@ impl<'a> AttributeRef<'a> { Some( name_bytes - .chunks_exact(2) - .filter_map(|chunk| chunk.try_into().ok().map(u16::from_le_bytes)) + .as_chunks::<2>() + .0 + .iter() + .map(|chunk| u16::from_le_bytes(*chunk)) .collect(), ) } diff --git a/crates/uffs-mft/src/parse/attribute_helpers.rs b/crates/uffs-mft/src/parse/attribute_helpers.rs index abc4f24bd..7f97b869f 100644 --- a/crates/uffs-mft/src/parse/attribute_helpers.rs +++ b/crates/uffs-mft/src/parse/attribute_helpers.rs @@ -101,13 +101,11 @@ pub(super) fn parse_file_name_full( } let name_bytes = &data[name_offset..name_offset + name_len * 2]; - #[expect( - clippy::missing_asserts_for_indexing, - reason = "chunks_exact(2) guarantees chunk.len() == 2" - )] let name_u16: SmallVec<[u16; 128]> = name_bytes - .chunks_exact(2) - .map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]])) + .as_chunks::<2>() + .0 + .iter() + .map(|chunk| u16::from_le_bytes(*chunk)) .collect(); let name = String::from_utf16(&name_u16).ok()?; @@ -149,13 +147,11 @@ pub(super) fn parse_data_attribute_full( return None; } let name_bytes = &data[name_offset..name_offset + name_len * 2]; - #[expect( - clippy::missing_asserts_for_indexing, - reason = "chunks_exact(2) guarantees chunk.len() == 2" - )] let name_u16: SmallVec<[u16; 64]> = name_bytes - .chunks_exact(2) - .map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]])) + .as_chunks::<2>() + .0 + .iter() + .map(|chunk| u16::from_le_bytes(*chunk)) .collect(); String::from_utf16(&name_u16).unwrap_or_default() } else { diff --git a/crates/uffs-mft/src/parse/direct_index.rs b/crates/uffs-mft/src/parse/direct_index.rs index ad2dc120f..77cce4960 100644 --- a/crates/uffs-mft/src/parse/direct_index.rs +++ b/crates/uffs-mft/src/parse/direct_index.rs @@ -20,10 +20,6 @@ clippy::manual_let_else, reason = "explicit match is clearer in NTFS attribute dispatch" )] -#![expect( - clippy::missing_asserts_for_indexing, - reason = "bounds are verified by size checks before all index access" -)] #![expect( clippy::single_match_else, reason = "explicit match arms are clearer for attribute type dispatch" @@ -193,8 +189,10 @@ pub fn parse_record_to_index(data: &[u8], frs: u64, index: &mut crate::index::Mf &data[name_bytes_offset..name_bytes_offset + name_len * 2]; // SmallVec avoids heap allocation for typical filenames (<= 64 chars) let name_u16: SmallVec<[u16; 64]> = name_bytes - .chunks_exact(2) - .map(|c| u16::from_le_bytes([c[0], c[1]])) + .as_chunks::<2>() + .0 + .iter() + .map(|c| u16::from_le_bytes(*c)) .collect(); let name = crate::io::parser::unified::decode_name_u16(&name_u16).0; let parent_frs = file_reference_to_frs(fn_attr.parent_directory); @@ -301,8 +299,10 @@ pub fn parse_record_to_index(data: &[u8], frs: u64, index: &mut crate::index::Mf if name_offset + name_len * 2 <= data.len() { let name_bytes = &data[name_offset..name_offset + name_len * 2]; let name_u16: SmallVec<[u16; 64]> = name_bytes - .chunks_exact(2) - .map(|c| u16::from_le_bytes([c[0], c[1]])) + .as_chunks::<2>() + .0 + .iter() + .map(|c| u16::from_le_bytes(*c)) .collect(); let stream_name = crate::io::parser::unified::decode_name_u16(&name_u16).0; // ALL named $DATA streams create regular stream entries. @@ -373,8 +373,10 @@ pub fn parse_record_to_index(data: &[u8], frs: u64, index: &mut crate::index::Mf String::new() } else { let name_u16: SmallVec<[u16; 64]> = name_bytes - .chunks_exact(2) - .map(|c| u16::from_le_bytes([c[0], c[1]])) + .as_chunks::<2>() + .0 + .iter() + .map(|c| u16::from_le_bytes(*c)) .collect(); crate::io::parser::unified::decode_name_u16(&name_u16).0 }; @@ -497,8 +499,10 @@ pub fn parse_record_to_index(data: &[u8], frs: u64, index: &mut crate::index::Mf if name_offset + name_len * 2 <= data.len() { let name_bytes = &data[name_offset..name_offset + name_len * 2]; let name_u16: SmallVec<[u16; 64]> = name_bytes - .chunks_exact(2) - .map(|c| u16::from_le_bytes([c[0], c[1]])) + .as_chunks::<2>() + .0 + .iter() + .map(|c| u16::from_le_bytes(*c)) .collect(); crate::io::parser::unified::decode_name_u16(&name_u16).0 } else { @@ -582,8 +586,10 @@ pub fn parse_record_to_index(data: &[u8], frs: u64, index: &mut crate::index::Mf if name_offset + name_len * 2 <= data.len() { let name_bytes = &data[name_offset..name_offset + name_len * 2]; let name_u16: SmallVec<[u16; 64]> = name_bytes - .chunks_exact(2) - .map(|c| u16::from_le_bytes([c[0], c[1]])) + .as_chunks::<2>() + .0 + .iter() + .map(|c| u16::from_le_bytes(*c)) .collect(); crate::io::parser::unified::decode_name_u16(&name_u16).0 } else { diff --git a/crates/uffs-mft/src/parse/direct_index_extension.rs b/crates/uffs-mft/src/parse/direct_index_extension.rs index adb6bcf8f..e0d513808 100644 --- a/crates/uffs-mft/src/parse/direct_index_extension.rs +++ b/crates/uffs-mft/src/parse/direct_index_extension.rs @@ -15,10 +15,6 @@ clippy::manual_let_else, reason = "explicit match is clearer in NTFS attribute dispatch" )] -#![expect( - clippy::missing_asserts_for_indexing, - reason = "bounds are verified by size checks before all index access" -)] #![expect( clippy::shadow_unrelated, reason = "reusing common names like 'record' in nested scopes is idiomatic here" @@ -151,8 +147,10 @@ pub(super) fn parse_extension_to_index( if name_start + name_len * 2 <= data.len() { let name_bytes = &data[name_start..name_start + name_len * 2]; let name_u16: SmallVec<[u16; 64]> = name_bytes - .chunks_exact(2) - .map(|c| u16::from_le_bytes([c[0], c[1]])) + .as_chunks::<2>() + .0 + .iter() + .map(|c| u16::from_le_bytes(*c)) .collect(); let name = crate::io::parser::unified::decode_name_u16(&name_u16).0; let parent_frs = fn_attr.parent_directory & 0x0000_FFFF_FFFF_FFFF; @@ -230,8 +228,10 @@ pub(super) fn parse_extension_to_index( if name_offset + name_len * 2 <= data.len() { let name_bytes = &data[name_offset..name_offset + name_len * 2]; let name_u16: SmallVec<[u16; 64]> = name_bytes - .chunks_exact(2) - .map(|c| u16::from_le_bytes([c[0], c[1]])) + .as_chunks::<2>() + .0 + .iter() + .map(|c| u16::from_le_bytes(*c)) .collect(); let stream_name = crate::io::parser::unified::decode_name_u16(&name_u16).0; // ALL named $DATA streams create regular @@ -279,8 +279,10 @@ pub(super) fn parse_extension_to_index( String::new() } else { let name_u16: SmallVec<[u16; 64]> = name_bytes - .chunks_exact(2) - .map(|c| u16::from_le_bytes([c[0], c[1]])) + .as_chunks::<2>() + .0 + .iter() + .map(|c| u16::from_le_bytes(*c)) .collect(); crate::io::parser::unified::decode_name_u16(&name_u16).0 }; @@ -400,8 +402,10 @@ pub(super) fn parse_extension_to_index( if name_offset + name_len * 2 <= data.len() { let name_bytes = &data[name_offset..name_offset + name_len * 2]; let name_u16: SmallVec<[u16; 64]> = name_bytes - .chunks_exact(2) - .map(|c| u16::from_le_bytes([c[0], c[1]])) + .as_chunks::<2>() + .0 + .iter() + .map(|c| u16::from_le_bytes(*c)) .collect(); crate::io::parser::unified::decode_name_u16(&name_u16).0 } else { @@ -485,8 +489,10 @@ pub(super) fn parse_extension_to_index( if name_offset + name_len * 2 <= data.len() { let name_bytes = &data[name_offset..name_offset + name_len * 2]; let name_u16: SmallVec<[u16; 64]> = name_bytes - .chunks_exact(2) - .map(|c| u16::from_le_bytes([c[0], c[1]])) + .as_chunks::<2>() + .0 + .iter() + .map(|c| u16::from_le_bytes(*c)) .collect(); crate::io::parser::unified::decode_name_u16(&name_u16).0 } else { diff --git a/crates/uffs-mft/src/parse/forensic/base.rs b/crates/uffs-mft/src/parse/forensic/base.rs index d143853b6..e0e66556d 100644 --- a/crates/uffs-mft/src/parse/forensic/base.rs +++ b/crates/uffs-mft/src/parse/forensic/base.rs @@ -176,10 +176,10 @@ pub(super) fn parse_base_record( String::new() } else { let name_u16: smallvec::SmallVec<[u16; 64]> = name_bytes - .chunks_exact(2) - .filter_map(|chunk| { - <[u8; 2]>::try_from(chunk).ok().map(u16::from_le_bytes) - }) + .as_chunks::<2>() + .0 + .iter() + .map(|chunk| u16::from_le_bytes(*chunk)) .collect(); String::from_utf16(&name_u16).unwrap_or_default() }; @@ -327,10 +327,10 @@ pub(super) fn parse_base_record( if name_offset + name_len * 2 <= data.len() { let name_bytes = &data[name_offset..name_offset + name_len * 2]; let name_u16: smallvec::SmallVec<[u16; 64]> = name_bytes - .chunks_exact(2) - .filter_map(|chunk| { - <[u8; 2]>::try_from(chunk).ok().map(u16::from_le_bytes) - }) + .as_chunks::<2>() + .0 + .iter() + .map(|chunk| u16::from_le_bytes(*chunk)) .collect(); String::from_utf16(&name_u16).unwrap_or_default() } else { @@ -435,10 +435,10 @@ pub(super) fn parse_base_record( if name_offset + name_len * 2 <= data.len() { let name_bytes = &data[name_offset..name_offset + name_len * 2]; let name_u16: smallvec::SmallVec<[u16; 64]> = name_bytes - .chunks_exact(2) - .filter_map(|chunk| { - <[u8; 2]>::try_from(chunk).ok().map(u16::from_le_bytes) - }) + .as_chunks::<2>() + .0 + .iter() + .map(|chunk| u16::from_le_bytes(*chunk)) .collect(); String::from_utf16(&name_u16).unwrap_or_default() } else { diff --git a/crates/uffs-mft/src/parse/forensic/extension.rs b/crates/uffs-mft/src/parse/forensic/extension.rs index 66b5f7a4c..8db540252 100644 --- a/crates/uffs-mft/src/parse/forensic/extension.rs +++ b/crates/uffs-mft/src/parse/forensic/extension.rs @@ -124,10 +124,10 @@ pub(super) fn parse_extension_record( String::new() } else { let name_u16: smallvec::SmallVec<[u16; 64]> = name_bytes - .chunks_exact(2) - .filter_map(|chunk| { - <[u8; 2]>::try_from(chunk).ok().map(u16::from_le_bytes) - }) + .as_chunks::<2>() + .0 + .iter() + .map(|chunk| u16::from_le_bytes(*chunk)) .collect(); String::from_utf16(&name_u16).unwrap_or_default() }; @@ -245,10 +245,10 @@ pub(super) fn parse_extension_record( if name_offset + name_len * 2 <= data.len() { let name_bytes = &data[name_offset..name_offset + name_len * 2]; let name_u16: smallvec::SmallVec<[u16; 64]> = name_bytes - .chunks_exact(2) - .filter_map(|chunk| { - <[u8; 2]>::try_from(chunk).ok().map(u16::from_le_bytes) - }) + .as_chunks::<2>() + .0 + .iter() + .map(|chunk| u16::from_le_bytes(*chunk)) .collect(); String::from_utf16(&name_u16).unwrap_or_default() } else { @@ -325,10 +325,10 @@ pub(super) fn parse_extension_record( if name_offset + name_len * 2 <= data.len() { let name_bytes = &data[name_offset..name_offset + name_len * 2]; let name_u16: smallvec::SmallVec<[u16; 64]> = name_bytes - .chunks_exact(2) - .filter_map(|chunk| { - <[u8; 2]>::try_from(chunk).ok().map(u16::from_le_bytes) - }) + .as_chunks::<2>() + .0 + .iter() + .map(|chunk| u16::from_le_bytes(*chunk)) .collect(); String::from_utf16(&name_u16).unwrap_or_default() } else { diff --git a/crates/uffs-mft/src/parse/full.rs b/crates/uffs-mft/src/parse/full.rs index b6e64780f..874056531 100644 --- a/crates/uffs-mft/src/parse/full.rs +++ b/crates/uffs-mft/src/parse/full.rs @@ -226,10 +226,10 @@ pub fn parse_record_full(data: &[u8], frs: u64) -> ParseResult { String::new() } else { let name_u16: smallvec::SmallVec<[u16; 64]> = name_bytes - .chunks_exact(2) - .filter_map(|chunk| { - <[u8; 2]>::try_from(chunk).ok().map(u16::from_le_bytes) - }) + .as_chunks::<2>() + .0 + .iter() + .map(|chunk| u16::from_le_bytes(*chunk)) .collect(); String::from_utf16(&name_u16).unwrap_or_default() }; @@ -380,10 +380,10 @@ pub fn parse_record_full(data: &[u8], frs: u64) -> ParseResult { if name_offset + name_len * 2 <= data.len() { let name_bytes = &data[name_offset..name_offset + name_len * 2]; let name_u16: smallvec::SmallVec<[u16; 64]> = name_bytes - .chunks_exact(2) - .filter_map(|chunk| { - <[u8; 2]>::try_from(chunk).ok().map(u16::from_le_bytes) - }) + .as_chunks::<2>() + .0 + .iter() + .map(|chunk| u16::from_le_bytes(*chunk)) .collect(); String::from_utf16(&name_u16).unwrap_or_default() } else { @@ -501,10 +501,10 @@ pub fn parse_record_full(data: &[u8], frs: u64) -> ParseResult { if name_offset + name_len * 2 <= data.len() { let name_bytes = &data[name_offset..name_offset + name_len * 2]; let name_u16: smallvec::SmallVec<[u16; 64]> = name_bytes - .chunks_exact(2) - .filter_map(|chunk| { - <[u8; 2]>::try_from(chunk).ok().map(u16::from_le_bytes) - }) + .as_chunks::<2>() + .0 + .iter() + .map(|chunk| u16::from_le_bytes(*chunk)) .collect(); String::from_utf16(&name_u16).unwrap_or_default() } else { diff --git a/crates/uffs-mft/src/platform/metafile_decode.rs b/crates/uffs-mft/src/platform/metafile_decode.rs index f0edd2003..8b9e93440 100644 --- a/crates/uffs-mft/src/platform/metafile_decode.rs +++ b/crates/uffs-mft/src/platform/metafile_decode.rs @@ -161,8 +161,10 @@ pub fn attribute_list_data_frs(list: &[u8], stream_name: &str) -> Vec { /// Decode UTF-16LE bytes to a lossy UTF-8 string. fn decode_utf16_lossy(bytes: &[u8]) -> String { let units: Vec = bytes - .chunks_exact(2) - .filter_map(|pair| pair.try_into().ok().map(u16::from_le_bytes)) + .as_chunks::<2>() + .0 + .iter() + .map(|pair| u16::from_le_bytes(*pair)) .collect(); String::from_utf16_lossy(&units) } diff --git a/crates/uffs-mft/src/platform/metafile_read.rs b/crates/uffs-mft/src/platform/metafile_read.rs index 5cc53a860..ec43b1928 100644 --- a/crates/uffs-mft/src/platform/metafile_read.rs +++ b/crates/uffs-mft/src/platform/metafile_read.rs @@ -407,8 +407,10 @@ fn scan_index_entries(buf: &[u8], header_start: usize, target: &[u16]) -> Option let name_length = usize::from(*entry.get(16 + 0x40)?); let name_bytes = entry.get(16 + 0x42..16 + 0x42 + name_length * 2)?; let name: Vec = name_bytes - .chunks_exact(2) - .filter_map(|pair| pair.try_into().ok().map(u16::from_le_bytes)) + .as_chunks::<2>() + .0 + .iter() + .map(|pair| u16::from_le_bytes(*pair)) .collect(); if name == target { return Some(crate::ntfs::file_reference_to_frs(file_reference)); diff --git a/crates/uffs-mft/src/usn/windows.rs b/crates/uffs-mft/src/usn/windows.rs index 8db9685f5..25911e27c 100644 --- a/crates/uffs-mft/src/usn/windows.rs +++ b/crates/uffs-mft/src/usn/windows.rs @@ -328,13 +328,10 @@ pub fn read_usn_journal( .filter(|_| name_end <= bytes_returned_usize) .map_or_else(String::new, |name_bytes| { let name_u16: Vec = name_bytes - .chunks_exact(2) - .map(|pair| { - u16::from_le_bytes([ - *pair.first().unwrap_or(&0), - *pair.get(1).unwrap_or(&0), - ]) - }) + .as_chunks::<2>() + .0 + .iter() + .map(|pair| u16::from_le_bytes(*pair)) .collect(); crate::io::parser::unified::decode_name_u16(&name_u16).0 }); From 7a18fbf68aa565384a1c7706a3e726cdbebe9f0f Mon Sep 17 00:00:00 2001 From: Robert M1 <50460704+githubrobbi@users.noreply.github.com> Date: Tue, 7 Jul 2026 09:50:06 -0700 Subject: [PATCH 2/5] build(deps): bump rustc-hash 2.1.3, crossbeam-channel 0.5.16 (delta-audited) Two dependency bumps, each with a proper cargo-vet delta audit anchored on the previously-trusted version (no blanket exemption bump). * rustc-hash 2.1.2 -> 2.1.3: pure-safe Default-impl refactor (macro -> derive(Default)/derive_const(Default), upstream #77); the rand 0.8 -> 0.9 change is confined to the optional random_state feature, which this workspace does not enable, so it never enters the build. Zero unsafe delta. * crossbeam-channel 0.5.15 -> 0.5.16: constify never()/Channel::new() (#1250), a select! matcher tweak for rust-analyzer completion (#1240, semantically equivalent), and an internal std::sync::Mutex -> crate::utils::Mutex swap. Zero unsafe delta, no dependency or build script changes. Both audits recorded in supply-chain/audits.toml as safe-to-deploy; `cargo vet` passes. Vet-Reviewed-Diff: rustc-hash@2.1.2->2.1.3 Vet-Reviewed-Diff: crossbeam-channel@0.5.15->0.5.16 Co-Authored-By: Claude Opus 4.8 --- Cargo.lock | 8 ++++---- Cargo.toml | 4 ++-- supply-chain/audits.toml | 12 ++++++++++++ 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 49825729f..4ece12fb1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -812,9 +812,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.15" +version = "0.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" dependencies = [ "crossbeam-utils", ] @@ -3449,9 +3449,9 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" [[package]] name = "rustix" diff --git a/Cargo.toml b/Cargo.toml index 120252e4c..ebc8cc6a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -295,7 +295,7 @@ which = "8.0.4" strsim = "0.11.1" # ───── Hashing ───── -rustc-hash = "2.1.2" +rustc-hash = "2.1.3" # ───── Formatting / Encoding ───── itoa = "1.0.18" @@ -314,7 +314,7 @@ reqwest = { version = "0.12.28", default-features = false, features = [ # ───── Parallelism ───── rayon = "1.12.0" -crossbeam-channel = "0.5.15" +crossbeam-channel = "0.5.16" # ───── Memory ───── mimalloc = "0.1.52" diff --git a/supply-chain/audits.toml b/supply-chain/audits.toml index e02f71366..e4655bd3e 100644 --- a/supply-chain/audits.toml +++ b/supply-chain/audits.toml @@ -73,6 +73,12 @@ criteria = "safe-to-deploy" delta = "0.16.3 -> 0.16.4" notes = "Delta audit (cargo vet diff 0.16.3 -> 0.16.4, 435-line diff). (1) term.rs: TermInner construction deduplicated into new()/new_buffered()/with_buffer() helpers - pure refactor; new pub fn is_dumb() reads TERM env var (read-only env access, same capability class the crate already had via is_a_color_terminal). (2) utils.rs: Style::from_dotted_str gains is_ascii() guards on '#RRGGBB'/'on_#RRGGBB' parsing - fixes a panic on non-ASCII input that slices mid-UTF-8 char (robustness improvement for untrusted style strings), plus regression tests. (3) windows_term: as_handle() helper removed in favor of out.as_raw_handle() at existing call sites - unchanged unsafe surface (same SetConsoleCursorPosition/SetConsoleCursorInfo calls as before); read_secure backspace arm converted to a match guard, logic identical. No new unsafe blocks, no new fs/net/process capability. Publisher djc (Dirkjan Ochtman, console-rs maintainer, same publisher as 0.16.3)." +[[audits.crossbeam-channel]] +who = "Robert M1 <50460704+githubrobbi@users.noreply.github.com>" +criteria = "safe-to-deploy" +delta = "0.5.15 -> 0.5.16" +notes = "Delta audit (cargo vet diff 0.5.15 -> 0.5.16). Files: CHANGELOG/Cargo.toml(.orig) (version bump only, no dependency changes), src/channel.rs, src/flavors/{never,zero}.rs, src/select_macro.rs, src/utils.rs, src/waker.rs, tests/mpsc.rs. Net unsafe: 0 added, 0 removed. Two upstream changes: (1) #1250 constify never() and the zero/never Channel::new() (pub fn -> pub const fn, Channel {..} -> Self {..}) — no runtime behavior change; (2) #1240 change the select!/select_biased! body matcher from => $body:block to => { $($body:tt)* } so rust-analyzer can auto-complete inside the block — semantically equivalent token capture. channel.rs/waker.rs additionally switch from std::sync::Mutex to the crate internal crate::utils::Mutex wrapper, dropping the .lock().unwrap() poison-handling at each call site; the wrapper is a thin non-poisoning shim over the same std Mutex (no new unsafe). No build script, no new dependencies, no new ambient-capability surface. Publisher taiki-e (crossbeam maintainer, same publisher already trusted for the crossbeam-epoch 0.9.18 -> 0.9.20 delta). Strictly a constification + tooling + internal-mutex refactor; trust extends from the audited 0.5.15 base." + [[audits.crossbeam-epoch]] who = "Robert M1 <50460704+githubrobbi@users.noreply.github.com>" criteria = "safe-to-deploy" @@ -385,6 +391,12 @@ criteria = "safe-to-deploy" delta = "1.8.0 -> 2.1.0" notes = "Delta audit (cargo vet diff 1.8.0 -> 2.1.0, 147-line diff). Proc-macro crate; changes are purely to the token streams it emits: prompt_handler.rs fully qualifies previously-bare type paths (rmcp::model::GetPromptRequestParams, rmcp::service::RequestContext, etc.); task_handler.rs renames model types for the MCP 2025-11-25 spec (GetTaskInfoParams->GetTaskParams, GetTaskResultParams->GetTaskPayloadParams) and swaps struct-literal construction for ::new() constructors. No new unsafe, no fs/net/process/env access, no build script, generated code only references rmcp's own types. Publisher alexhancock (modelcontextprotocol/rust-sdk release chain, same as 1.8.0->2.x series)." +[[audits.rustc-hash]] +who = "Robert M1 <50460704+githubrobbi@users.noreply.github.com>" +criteria = "safe-to-deploy" +delta = "2.1.2 -> 2.1.3" +notes = "Delta audit (cargo vet diff 2.1.2 -> 2.1.3). Files: CHANGELOG/Cargo.toml(.orig) (version + optional `rand` dep 0.8 -> 0.9), src/lib.rs, src/random_state.rs. Zero unsafe added or removed. src/lib.rs replaces the hand-written `default_impl!` macro with `#[cfg_attr(not(nightly), derive(Default))]` / `#[cfg_attr(nightly, derive_const(Default))]` — a pure-safe refactor of the Default impl (fixes the nightly-feature build, upstream PR #77). The only functional change is the rand 0.8 -> 0.9 API migration in src/random_state.rs (rand::thread_rng().gen() -> rand::rng().random_range(..=usize::MAX)), which lives entirely behind the optional rand/random_state feature that this workspace does not enable (no rustc-hash feature activation anywhere in the tree), so neither random_state.rs nor rand 0.9 enters our build. No build script, no new ambient capability, no I/O / FFI / process. Publisher Noratrieb (rustc-hash maintainer, rust-lang). Safe extension from the audited 2.1.2 base." + [[audits.rustls]] who = "Robert M1 <50460704+githubrobbi@users.noreply.github.com>" criteria = "safe-to-deploy" From a540c92e56040d8468e2abba709f31fd2e563dc8 Mon Sep 17 00:00:00 2001 From: Robert M1 <50460704+githubrobbi@users.noreply.github.com> Date: Tue, 7 Jul 2026 10:40:15 -0700 Subject: [PATCH 3/5] docs(crates): point uffs-time/uffs-text README install snippet at 0.6 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The install examples still said `= "0.5"`, a caret range (>=0.5.0,<0.6.0) that resolves to the old 0.5.120 — not the 0.6.x we're about to publish. Copy-pasting off crates.io would pull a stale version. Bump both to "0.6". --- crates/uffs-text/README.md | 2 +- crates/uffs-time/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/uffs-text/README.md b/crates/uffs-text/README.md index 68a9b242b..14e077438 100644 --- a/crates/uffs-text/README.md +++ b/crates/uffs-text/README.md @@ -53,7 +53,7 @@ crates above.** ```toml [dependencies] -uffs-text = "0.5" +uffs-text = "0.6" ``` ## Usage diff --git a/crates/uffs-time/README.md b/crates/uffs-time/README.md index f5373f775..0220e0c1f 100644 --- a/crates/uffs-time/README.md +++ b/crates/uffs-time/README.md @@ -44,7 +44,7 @@ keep using `chrono` / `time` / `jiff` and convert at the boundary via ```toml [dependencies] -uffs-time = "0.5" +uffs-time = "0.6" ``` ## Usage From e4787f09cefddda948aec71c614dda310563c769 Mon Sep 17 00:00:00 2001 From: Robert M1 <50460704+githubrobbi@users.noreply.github.com> Date: Tue, 7 Jul 2026 11:12:35 -0700 Subject: [PATCH 4/5] ci(release): publish crates.io on v* release tags via OIDC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The crates.io OIDC publish job was wired to release-plz's own release detection (`needs: release-plz-release` + `releases_created`), and release-plz.yml only ran on workflow_dispatch. But releases here are cut by the ship pipeline (`just ship` → `just release-tag` pushes a signed `v*` tag), which release-plz never claims — so the publish job was never reached, leaving crates.io stuck at 0.5.120 while the workspace shipped 0.6.x. Re-wire so the publish carries along with every ship release: * add a `push: tags: ['v*']` trigger (the same tags release.yml builds binaries for; `just release-tag` pushes them with a real credential, so the trigger fires — GITHUB_TOKEN anti-loop doesn't apply); * gate the crates-io-publish job on the tag ref + ENABLE_CRATES_IO_PUBLISH, dropping the release-plz `releases_created` dependency (its checkout resolves to the tag, so cargo publishes the released version); * keep release-pr / release jobs workflow_dispatch-only so a tag push runs only the publish job. The job stays in release-plz.yml with the `crates.io-publish` environment, so the existing crates.io trusted-publisher registration is unchanged. Verified: actionlint clean. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/release-plz.yml | 54 ++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 19 deletions(-) diff --git a/.github/workflows/release-plz.yml b/.github/workflows/release-plz.yml index 49335ec8d..62e5a993d 100644 --- a/.github/workflows/release-plz.yml +++ b/.github/workflows/release-plz.yml @@ -270,8 +270,18 @@ on: # `docs/architecture/release-automation-plan.md` §8 (Path B row) and # `just/build.just` `release-pr` / `release-tag`. # - # push: # ← Path B: disabled by design (manual releases) - # branches: [main] + # push.branches:[main] stays disabled by design (Path B — manual + # releases; see the trigger-history comment above). Instead a narrow + # tag trigger drives the OIDC `crates-io-publish` job below: it fires + # exactly when a release tag is cut (the same `v*` tags `release.yml` + # builds binaries for), carrying the crates.io publish along with + # every `just ship` release — no long-lived token, no manual step. + # The release-pr / release jobs stay workflow_dispatch-only (they are + # gated to `github.event_name == 'workflow_dispatch'`), so a tag push + # only runs the publish job. + push: + tags: + - 'v*' workflow_dispatch: # Default to ZERO permissions; each job grants only what it needs. @@ -303,8 +313,10 @@ jobs: # Skip on forks (fork PRs can't access the repo's secrets and would # produce noisy "permission denied" runs). Matches the convention - # release-plz docs recommend. - if: github.repository_owner == 'skyllc-ai' + # release-plz docs recommend. Restricted to workflow_dispatch: the + # `v*` tag trigger is reserved for the crates-io-publish job, so the + # release-pr job must not run on a tag push. + if: github.repository_owner == 'skyllc-ai' && github.event_name == 'workflow_dispatch' # Push branch + open/update PR. Granted at job level so the # release job below can run with a tighter (no pull-requests) @@ -375,7 +387,10 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 - if: github.repository_owner == 'skyllc-ai' + # workflow_dispatch only — the `v*` tag trigger is reserved for the + # crates-io-publish job; this release job must not fire on a tag push + # (release.yml already builds binaries from the tag). + if: github.repository_owner == 'skyllc-ai' && github.event_name == 'workflow_dispatch' # Tag push only — no PR operations from this job. permissions: @@ -436,13 +451,14 @@ jobs: done # ───────────────────────────────────────────────────────────────── - # R7: OIDC Trusted Publishing scaffolding (crates.io) + # OIDC Trusted Publishing to crates.io (ACTIVE) # ───────────────────────────────────────────────────────────────── # - # Phase R7 — OIDC trusted publisher scaffolding. This job is gated - # by the repo variable `ENABLE_CRATES_IO_PUBLISH` (unset → dormant) - # until Phase R8 (first dress rehearsal). It sets up the OIDC token - # exchange with crates.io for passwordless, short-lived credentials. + # Fires on every `v*` release tag (gated by the repo variable + # `ENABLE_CRATES_IO_PUBLISH`), minting a short-lived crates.io token + # via OIDC — no long-lived registry token is ever stored. This is + # what carries the crates.io publish along with each `just ship` + # release tag. # # A repo-variable gate is used instead of a literal `if: false` for # two reasons: (1) actionlint rejects constant `if:` conditions, and @@ -468,20 +484,20 @@ jobs: # See: docs/architecture/release-automation-plan.md §Phase R7/R8 # crates-io-publish: - name: crates.io / OIDC publish (R7 scaffolding) + name: crates.io / OIDC publish runs-on: ubuntu-latest timeout-minutes: 15 - needs: release-plz-release # Two gates, both required: - # 1. Repo variable ENABLE_CRATES_IO_PUBLISH=true — the R9 master + # 1. Repo variable ENABLE_CRATES_IO_PUBLISH=true — the master # switch (unset / any-other-value keeps the job dormant). - # 2. releases_created — `release-plz-release` runs on EVERY push - # to main and no-ops internally when HEAD isn't a release-PR - # merge; without this gate the publish job would fire (and - # page the environment reviewer) on every push, then fail - # republishing the already-live version. - if: ${{ vars.ENABLE_CRATES_IO_PUBLISH == 'true' && needs.release-plz-release.outputs.releases_created == 'true' }} + # 2. A `v*` release-tag push — the ship pipeline (and release.yml) + # cut exactly one such tag per release, so publish fires once + # per released version and never on ordinary pushes. The + # checkout below resolves to the tag ref, so cargo publishes the + # freshly-released version. Re-running against an already-live + # version is a harmless no-op-error from crates.io. + if: ${{ vars.ENABLE_CRATES_IO_PUBLISH == 'true' && startsWith(github.ref, 'refs/tags/v') }} environment: crates.io-publish permissions: From 40218016d3c310887faad292a53b6aab9fb48781 Mon Sep 17 00:00:00 2001 From: Robert M1 <50460704+githubrobbi@users.noreply.github.com> Date: Tue, 7 Jul 2026 11:17:32 -0700 Subject: [PATCH 5/5] =?UTF-8?q?build(toolchain):=20bump=20pinned=20nightly?= =?UTF-8?q?=202026-06-26=20=E2=86=92=202026-07-06?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both blockers tracked in #492 are cleared on this nightly: * ring 0.17.14 const-eval assertion no longer fires — verified: ring compiles clean for BOTH aarch64-apple host and x86_64-pc-windows-msvc (the Windows-msvc ring breakage from the 06-27..30 nightly window is gone); * the new `chunks_exact` clippy lint is handled by the earlier `as_chunks::<2>()` migration in this branch. The new nightly feature-gated `core::io::Error` / `ErrorKind` (rust-lang/rust#103765, #154046), so `std::io::*` is re-exported from the still-incomplete `core::io`. Two consequences, both fixed here: * clippy's `std_instead_of_core` no longer fires on `std::io::*` — removed 10 now-unfulfilled `#[expect(std_instead_of_core)]` (security, client, daemon, mft). No `#[allow]`, no downgrade: the code correctly stays on `std::io::*` (only stable spelling; `core::io::Error` needs the unstable `error_in_core` feature). * rustdoc intra-doc links: qualified 17 `io::{Error,ErrorKind,Result}` links to explicit `std::io::` and degraded the one unresolvable `std::io::Error::other` method link to a plain code span (the std-only `::other` fn isn't resolvable through the core::io re-export yet). All 8 pinned components (rustc, cargo, rustfmt, clippy, rust-src, rust-analyzer, llvm-tools, miri) are available for the new date. Verified on nightly-2026-07-06: host + xwin(windows-msvc) clippy --workspace --all-targets --all-features -D warnings clean, rustdoc -D warnings clean, affected-crate tests 853 pass. Co-Authored-By: Claude Opus 4.8 --- crates/uffs-bench/src/host/mod.rs | 2 +- crates/uffs-client/src/shmem.rs | 6 +++--- crates/uffs-client/src/stdout_kind.rs | 15 --------------- crates/uffs-core/src/compact_cache.rs | 6 +++--- crates/uffs-core/src/compact_mmap.rs | 2 +- crates/uffs-daemon/src/cache/cursor_store.rs | 7 ------- crates/uffs-daemon/src/config.rs | 7 ------- crates/uffs-daemon/src/index/aggregation.rs | 10 ---------- crates/uffs-mft/src/index/storage/file_io.rs | 5 ----- crates/uffs-mft/src/usn/mod.rs | 10 ---------- crates/uffs-security/src/fs.rs | 8 ++++---- crates/uffs-security/src/keystore.rs | 2 +- crates/uffs-security/src/pipe.rs | 8 ++++---- crates/uffs-security/src/runtime_dir.rs | 5 +++-- crates/uffs-security/src/runtime_dir/tests.rs | 6 ------ rust-toolchain.toml | 4 ++-- 16 files changed, 22 insertions(+), 81 deletions(-) diff --git a/crates/uffs-bench/src/host/mod.rs b/crates/uffs-bench/src/host/mod.rs index 0a21ac6c3..b7837d7d6 100644 --- a/crates/uffs-bench/src/host/mod.rs +++ b/crates/uffs-bench/src/host/mod.rs @@ -43,7 +43,7 @@ impl ProcOutput { /// Implemented by [`SystemHost`] (real OS) and [`MockHost`] (in-memory, for /// tests). Methods are intentionally low-level wrappers; higher-level logic in /// `state`, `restore`, and `fingerprint` composes them and maps their -/// [`io::Error`]s into [`crate::error::BenchError`] with path context. +/// [`std::io::Error`]s into [`crate::error::BenchError`] with path context. pub trait Host { /// Read the entire contents of a file. /// diff --git a/crates/uffs-client/src/shmem.rs b/crates/uffs-client/src/shmem.rs index 16bed6b14..1e0ffe837 100644 --- a/crates/uffs-client/src/shmem.rs +++ b/crates/uffs-client/src/shmem.rs @@ -155,7 +155,7 @@ static SHMEM_COUNTER: AtomicU64 = AtomicU64::new(0); /// /// # Errors /// -/// Returns [`io::Error`] if the directory cannot be created. +/// Returns [`std::io::Error`] if the directory cannot be created. fn shmem_dir() -> io::Result { let base = dirs_next::data_local_dir() .unwrap_or_else(|| PathBuf::from(if cfg!(windows) { r"C:\temp" } else { "/tmp" })); @@ -168,7 +168,7 @@ fn shmem_dir() -> io::Result { /// /// # Errors /// -/// Returns [`io::Error`] if the shmem directory cannot be created. +/// Returns [`std::io::Error`] if the shmem directory cannot be created. fn unique_shmem_path() -> io::Result { let dir = shmem_dir()?; let pid = std::process::id(); @@ -567,7 +567,7 @@ pub(crate) const STREAM_CHUNK_BYTES: usize = 4 * 1024 * 1024; /// /// ## Error pinpointing /// -/// Every failure path attaches a step-specific [`io::Error`] kind + +/// Every failure path attaches a step-specific [`std::io::Error`] kind + /// message identifying which stage broke (`open`, `metadata`, /// `mmap`, `write_all`) together with the blob byte size and, for /// write failures, the byte offset reached. This converts opaque diff --git a/crates/uffs-client/src/stdout_kind.rs b/crates/uffs-client/src/stdout_kind.rs index 641b62333..68da28673 100644 --- a/crates/uffs-client/src/stdout_kind.rs +++ b/crates/uffs-client/src/stdout_kind.rs @@ -141,11 +141,6 @@ pub fn write_stdout_buffer(buf: &[u8]) -> std::io::Result<()> { /// well-formed UTF-8. `WriteConsoleW` cannot represent invalid UTF-8 /// anyway, so surfacing the error up-front is strictly better than a /// mangled console write. -#[expect( - clippy::std_instead_of_core, - reason = "core::io::Error is not yet stable — see rust-lang/rust#103765. \ - Remove this expect once `error_in_core` stabilises." -)] pub fn utf8_to_utf16(buf: &[u8]) -> std::io::Result> { let utf8 = core::str::from_utf8(buf) .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))?; @@ -346,11 +341,6 @@ mod platform_windows { /// a pipe/file/NUL stdout would fail at `WriteConsoleW` with /// `ERROR_INVALID_HANDLE`. #[expect(unsafe_code, reason = "FFI to GetStdHandle + WriteConsoleW")] - #[expect( - clippy::std_instead_of_core, - reason = "core::io::Error is not yet stable — see rust-lang/rust#103765. \ - Remove this expect once `error_in_core` stabilises." - )] pub(super) fn write_to_console_w(buf: &[u8]) -> std::io::Result<()> { let utf16 = super::utf8_to_utf16(buf)?; if utf16.is_empty() { @@ -533,11 +523,6 @@ mod shared_tests { /// Invalid UTF-8 must surface as `InvalidData`, not silently /// produce mojibake on the console. #[test] - #[expect( - clippy::std_instead_of_core, - reason = "core::io::ErrorKind is not yet stable — see rust-lang/rust#103765. \ - Remove this expect once `error_in_core` stabilises." - )] fn utf8_to_utf16_invalid_utf8_is_invalid_data_error() { // 0xFF is never valid as a standalone UTF-8 byte. let err = utf8_to_utf16(&[0xFF_u8]).expect_err("must reject invalid UTF-8"); diff --git a/crates/uffs-core/src/compact_cache.rs b/crates/uffs-core/src/compact_cache.rs index 5f510e966..e114c93a5 100644 --- a/crates/uffs-core/src/compact_cache.rs +++ b/crates/uffs-core/src/compact_cache.rs @@ -308,7 +308,7 @@ pub fn compact_runtime_root() -> PathBuf { /// /// # Errors /// -/// Returns an [`io::Error`] if the parent directory cannot be created +/// Returns an [`std::io::Error`] if the parent directory cannot be created /// or if the owner-only permissions cannot be applied (Unix `0o700`, /// Windows owner-only DACL). fn compact_runtime_tempfile_path( @@ -550,9 +550,9 @@ pub fn deserialize_compact( /// /// # Errors /// -/// Returns an [`io::Error`] if: +/// Returns an [`std::io::Error`] if: /// * the cache bytes are truncated, wrong magic, or use a stale/unsupported -/// version (parse error lifted via [`io::Error::other`]); +/// version (parse error lifted via `std::io::Error::other`); /// * `runtime_dir.create_owner_only` fails (path collision, permissions, parent /// missing); /// * writing the layout into the tempfile fails (I/O / disk-full); diff --git a/crates/uffs-core/src/compact_mmap.rs b/crates/uffs-core/src/compact_mmap.rs index 3ce2b7302..09085cf2f 100644 --- a/crates/uffs-core/src/compact_mmap.rs +++ b/crates/uffs-core/src/compact_mmap.rs @@ -146,7 +146,7 @@ impl RuntimeLayout { /// /// # Errors /// -/// Forwards any [`io::Error`] from `set_len`, `seek`, `write_all`, +/// Forwards any [`std::io::Error`] from `set_len`, `seek`, `write_all`, /// `flush`, or `sync_all`. /// /// # Examples diff --git a/crates/uffs-daemon/src/cache/cursor_store.rs b/crates/uffs-daemon/src/cache/cursor_store.rs index 0e122ff8c..a8cb38389 100644 --- a/crates/uffs-daemon/src/cache/cursor_store.rs +++ b/crates/uffs-daemon/src/cache/cursor_store.rs @@ -69,13 +69,6 @@ impl super::journal_loop::CursorStore for DiskCursorStore { /// as "start from journal head" which is the correct fallback /// for a never-saved or corrupted cursor — better to re-replay /// than to silently start from a stale or invalid USN. - #[expect( - clippy::std_instead_of_core, - reason = "`core::io::ErrorKind` is not yet stable — see \ - rust-lang/rust#103765. Mirrors the same pattern \ - used in `crate::config::Config::load_from_path`. \ - Remove this expect once `error_in_core` stabilises." - )] fn load(&self, letter: uffs_mft::platform::DriveLetter) -> u64 { let path = self.cursor_path(letter); match std::fs::read(&path) { diff --git a/crates/uffs-daemon/src/config.rs b/crates/uffs-daemon/src/config.rs index f4bfb67b6..bff427093 100644 --- a/crates/uffs-daemon/src/config.rs +++ b/crates/uffs-daemon/src/config.rs @@ -380,13 +380,6 @@ impl Config { /// All other I/O errors (permission denied, EISDIR on a path /// that exists as a directory, etc.) propagate as /// [`ConfigError::Io`]. - #[expect( - clippy::std_instead_of_core, - reason = "`core::io::ErrorKind` is not yet stable — see \ - rust-lang/rust#103765. Mirrors the same pattern \ - used in `crate::index::aggregation`. Remove this \ - expect once `error_in_core` stabilises." - )] pub(crate) fn load_from_path(path: &Path) -> Result { match std::fs::read_to_string(path) { Ok(body) => Self::from_toml(&body), diff --git a/crates/uffs-daemon/src/index/aggregation.rs b/crates/uffs-daemon/src/index/aggregation.rs index 6cbff194b..2181c19b2 100644 --- a/crates/uffs-daemon/src/index/aggregation.rs +++ b/crates/uffs-daemon/src/index/aggregation.rs @@ -57,11 +57,6 @@ impl DaemonFileReader<'_> { } impl FileReader for DaemonFileReader<'_> { - #[expect( - clippy::std_instead_of_core, - reason = "core::io::Error is not yet stable — see rust-lang/rust#103765. \ - Remove this expect once `error_in_core` stabilises." - )] fn read_first_bytes( &self, record_idx: usize, @@ -81,11 +76,6 @@ impl FileReader for DaemonFileReader<'_> { Ok(buf) } - #[expect( - clippy::std_instead_of_core, - reason = "core::io::Error is not yet stable — see rust-lang/rust#103765. \ - Remove this expect once `error_in_core` stabilises." - )] fn read_all(&self, record_idx: usize, drive_ordinal: u8) -> std::io::Result> { let path = self .resolve_path(record_idx, drive_ordinal) diff --git a/crates/uffs-mft/src/index/storage/file_io.rs b/crates/uffs-mft/src/index/storage/file_io.rs index f7105f564..d0fab798c 100644 --- a/crates/uffs-mft/src/index/storage/file_io.rs +++ b/crates/uffs-mft/src/index/storage/file_io.rs @@ -117,11 +117,6 @@ impl MftIndex { /// # Errors /// /// Returns an error if file reading, decryption, or deserialization fails. - #[expect( - clippy::std_instead_of_core, - reason = "core::io::Error is not yet stable — see rust-lang/rust#103765. \ - Remove this expect once `error_in_core` stabilises." - )] pub fn load_from_file( path: &std::path::Path, ) -> Result<(Self, IndexHeader), Box> { diff --git a/crates/uffs-mft/src/usn/mod.rs b/crates/uffs-mft/src/usn/mod.rs index b6b1b1c34..721f60b07 100644 --- a/crates/uffs-mft/src/usn/mod.rs +++ b/crates/uffs-mft/src/usn/mod.rs @@ -383,11 +383,6 @@ pub use windows::{query_usn_journal, read_targeted_frs_records, read_usn_journal /// /// Always returns an error on non-Windows platforms. #[cfg(not(windows))] -#[expect( - clippy::std_instead_of_core, - reason = "core::io::Error is not yet stable — see rust-lang/rust#103765. \ - Remove this expect once `error_in_core` stabilises." -)] pub fn query_usn_journal( _volume: crate::platform::DriveLetter, ) -> Result { @@ -403,11 +398,6 @@ pub fn query_usn_journal( /// /// Always returns an error on non-Windows platforms. #[cfg(not(windows))] -#[expect( - clippy::std_instead_of_core, - reason = "core::io::Error is not yet stable — see rust-lang/rust#103765. \ - Remove this expect once `error_in_core` stabilises." -)] pub fn read_usn_journal( _volume: crate::platform::DriveLetter, _journal_id: u64, diff --git a/crates/uffs-security/src/fs.rs b/crates/uffs-security/src/fs.rs index 340b77121..50ed59ff7 100644 --- a/crates/uffs-security/src/fs.rs +++ b/crates/uffs-security/src/fs.rs @@ -34,8 +34,8 @@ use std::path::Path; /// /// # Errors /// -/// Returns [`io::ErrorKind::AlreadyExists`] if the path exists, or any other -/// error from the underlying open. +/// Returns [`std::io::ErrorKind::AlreadyExists`] if the path exists, or any +/// other error from the underlying open. pub fn create_new_secure_file(path: &Path) -> io::Result { #[cfg(unix)] { @@ -80,8 +80,8 @@ pub fn create_new_secure_file(path: &Path) -> io::Result { /// /// # Errors /// -/// Returns [`io::ErrorKind::AlreadyExists`] if the path exists, or any other -/// error from the underlying open. +/// Returns [`std::io::ErrorKind::AlreadyExists`] if the path exists, or any +/// other error from the underlying open. pub fn create_new_file_exclusive(path: &Path) -> io::Result { std::fs::OpenOptions::new() .write(true) diff --git a/crates/uffs-security/src/keystore.rs b/crates/uffs-security/src/keystore.rs index 62f424a39..8d0933b07 100644 --- a/crates/uffs-security/src/keystore.rs +++ b/crates/uffs-security/src/keystore.rs @@ -66,7 +66,7 @@ pub fn get_cache_key() -> io::Result<[u8; KEY_SIZE]> { /// /// # Errors /// -/// Returns [`io::Error`] if Keychain access fails (e.g. user denies +/// Returns [`std::io::Error`] if Keychain access fails (e.g. user denies /// permission, Keychain is locked, or the stored key has an invalid size /// after multiple regeneration attempts). #[cfg(target_os = "macos")] diff --git a/crates/uffs-security/src/pipe.rs b/crates/uffs-security/src/pipe.rs index 2bb6e7c9e..4dca141df 100644 --- a/crates/uffs-security/src/pipe.rs +++ b/crates/uffs-security/src/pipe.rs @@ -162,7 +162,7 @@ impl PipeName { /// /// # Errors /// - /// Returns [`io::Error`] if the user SID cannot be resolved. This + /// Returns [`std::io::Error`] if the user SID cannot be resolved. This /// should not happen on a normally functioning Windows session; if /// it does, the caller should log and exit rather than fall back to /// an insecure name. @@ -228,7 +228,7 @@ impl fmt::Display for PipeName { /// /// # Errors /// -/// Returns [`io::Error`] if both resolution paths fail — typically only +/// Returns [`std::io::Error`] if both resolution paths fail — typically only /// in very broken token scenarios. pub(crate) fn current_user_sid_string() -> io::Result { // Try the linked-token path first. @@ -269,7 +269,7 @@ impl OwnerOnlySd { /// /// # Errors /// - /// Returns [`io::Error`] if SID resolution or SDDL conversion fails. + /// Returns [`std::io::Error`] if SID resolution or SDDL conversion fails. pub fn for_current_user() -> io::Result { let sid = current_user_sid_string()?; Self::for_sid_string(&sid) @@ -281,7 +281,7 @@ impl OwnerOnlySd { /// /// # Errors /// - /// Returns [`io::Error`] if the SDDL conversion fails. + /// Returns [`std::io::Error`] if the SDDL conversion fails. pub(crate) fn for_sid_string(sid: &str) -> io::Result { // SDDL: DACL with one ACE — Allow, GenericAll, to . let sddl = format!("D:(A;;GA;;;{sid})"); diff --git a/crates/uffs-security/src/runtime_dir.rs b/crates/uffs-security/src/runtime_dir.rs index d74a81c77..a7949c704 100644 --- a/crates/uffs-security/src/runtime_dir.rs +++ b/crates/uffs-security/src/runtime_dir.rs @@ -131,7 +131,8 @@ pub trait RuntimeDir: Send + Sync { /// name, e.g. via volume GUID). /// * `PermissionDenied` — parent dir is unwriteable, or the process lacks /// permission to set the requested mode/DACL. - /// * Any other [`io::Error`] forwarded from the underlying filesystem call. + /// * Any other [`std::io::Error`] forwarded from the underlying filesystem + /// call. fn create_owner_only(&self, path: &Path) -> io::Result; /// Sweep `//` subdirectories whose pid is no @@ -180,7 +181,7 @@ pub trait RuntimeDir: Send + Sync { /// /// # Errors /// -/// Forwards any [`io::Error`] from the underlying mmap syscall +/// Forwards any [`std::io::Error`] from the underlying mmap syscall /// (`mmap` on Unix, `MapViewOfFile` on Windows). pub fn mmap_read_only(file: &RuntimeFile) -> io::Result { #[expect( diff --git a/crates/uffs-security/src/runtime_dir/tests.rs b/crates/uffs-security/src/runtime_dir/tests.rs index 5225f2a35..31d0943c1 100644 --- a/crates/uffs-security/src/runtime_dir/tests.rs +++ b/crates/uffs-security/src/runtime_dir/tests.rs @@ -43,12 +43,6 @@ fn create_owner_only_writes_and_reads_round_trip() { } #[test] -#[expect( - clippy::std_instead_of_core, - reason = "core::io::ErrorKind is feature-gated (rust-lang/rust#154046); \ - std path is the only stable spelling. Remove this expect \ - once the re-export stabilises." -)] fn create_owner_only_rejects_existing_file() { let tmp = TempDir::new().expect("tempdir"); let path = tmp.path().join("dupe.live"); diff --git a/rust-toolchain.toml b/rust-toolchain.toml index dc4bd63c1..fe4019ff1 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -10,7 +10,7 @@ # CI must NOT override this with `@nightly`; workflows install via # `rustup show` (which reads this file) so the pin is always honored. # -# Current pin: nightly-2026-05-31. Requires ethnum >= 1.5.3 (the +# Current pin: nightly-2026-07-06. Requires ethnum >= 1.5.3 (the # workspace Cargo.lock resolves 1.5.3 transitively via polars-compute + # parquet — verify with `grep -A1 'name = "ethnum"' Cargo.lock`). # @@ -32,7 +32,7 @@ # Run `just toolchain-sync` to re-attempt a channel bump; the CI # pipeline auto-refreshes on `ship --fresh` unless `--skip-toolchain-sync` # is passed. -channel = "nightly-2026-06-26" +channel = "nightly-2026-07-06" # Specify components that should always be available components = [