build: require Rust 1.98.1

This commit is contained in:
bfloat16
2026-09-07 21:43:15 +08:00
parent 6250ca4e98
commit 53ef36c837
9 changed files with 28 additions and 39 deletions
+1 -1
View File
@@ -17,7 +17,7 @@ resolver = "2"
[workspace.package] [workspace.package]
version = "1.2.0" version = "1.2.0"
edition = "2024" edition = "2024"
rust-version = "1.85" rust-version = "1.98.1"
license = "AGPL-3.0-only" license = "AGPL-3.0-only"
[workspace.dependencies] [workspace.dependencies]
+1 -1
View File
@@ -2,7 +2,7 @@
## Building ## Building
The pinned Rust toolchain is defined in `rust-toolchain.toml`. Build the CLI with `cargo build --release`; the binary is written to `target/release/senbei.exe` on Windows. Rust 1.98.1 is required and pinned in `rust-toolchain.toml`. Build the CLI with `cargo build --release`; the binary is written to `target/release/senbei.exe` on Windows.
The workspace crates are portable where their APIs are pure. The browser binding is outside the workspace and is checked with `cargo check --manifest-path senbei-wasm/Cargo.toml` or built with `wasm-pack`. The workspace crates are portable where their APIs are pure. The browser binding is outside the workspace and is checked with `cargo check --manifest-path senbei-wasm/Cargo.toml` or built with `wasm-pack`.
+1 -1
View File
@@ -1,3 +1,3 @@
[toolchain] [toolchain]
channel = "stable" channel = "1.98.1"
targets = ["x86_64-pc-windows-msvc", "wasm32-unknown-unknown"] targets = ["x86_64-pc-windows-msvc", "wasm32-unknown-unknown"]
+5 -11
View File
@@ -552,7 +552,7 @@ pub fn transform_segment(
let mut state = seed; let mut state = seed;
let mut left = 0xe34e_ac63_u32; let mut left = 0xe34e_ac63_u32;
let mut right = 0x07b4_8238_u32; let mut right = 0x07b4_8238_u32;
for (index, chunk) in transformed.chunks_exact_mut(4).enumerate() { for (index, chunk) in transformed.as_chunks_mut::<4>().0.iter_mut().enumerate() {
let index32 = u32::try_from(index) let index32 = u32::try_from(index)
.map_err(|_| Error::Invalid("segment word index exceeds u32".to_owned()))?; .map_err(|_| Error::Invalid("segment word index exceeds u32".to_owned()))?;
left = state left = state
@@ -564,10 +564,7 @@ pub fn transform_segment(
.wrapping_add(right.wrapping_sub(0x1605_a81c).wrapping_mul(right)) .wrapping_add(right.wrapping_sub(0x1605_a81c).wrapping_mul(right))
.wrapping_shl(index32 & 7); .wrapping_shl(index32 & 7);
state = left ^ right; state = left ^ right;
let bytes: [u8; 4] = chunk let mut value = u32::from_le_bytes(*chunk);
.try_into()
.map_err(|_| Error::Invalid("invalid transformed word".to_owned()))?;
let mut value = u32::from_le_bytes(bytes);
value = value.wrapping_add(0xb43b_9baf_u32.wrapping_mul(index32 & 0x0d)); value = value.wrapping_add(0xb43b_9baf_u32.wrapping_mul(index32 & 0x0d));
value ^= 0xaf57_f7fb_u32.wrapping_mul(index32 & 3); value ^= 0xaf57_f7fb_u32.wrapping_mul(index32 & 3);
value = value.wrapping_sub(state) ^ state; value = value.wrapping_sub(state) ^ state;
@@ -577,13 +574,10 @@ pub fn transform_segment(
if decrypt_aes { if decrypt_aes {
let cipher = Aes256::new_from_slice(aes_key) let cipher = Aes256::new_from_slice(aes_key)
.map_err(|_| Error::Invalid("invalid AES-256 key length".to_owned()))?; .map_err(|_| Error::Invalid("invalid AES-256 key length".to_owned()))?;
let aligned_size = transformed.len() & !0x0f;
let mut previous = [0_u8; 16]; let mut previous = [0_u8; 16];
for chunk in transformed[..aligned_size].chunks_exact_mut(16) { for chunk in transformed.as_chunks_mut::<16>().0 {
let mut ciphertext = [0_u8; 16]; let ciphertext = *chunk;
ciphertext.copy_from_slice(chunk); cipher.decrypt_block((&mut *chunk).into());
// chunk is exactly one block (chunks_exact_mut(16)).
cipher.decrypt_block(chunk.try_into().expect("chunk is one block"));
for (byte, prior) in chunk.iter_mut().zip(previous) { for (byte, prior) in chunk.iter_mut().zip(previous) {
*byte ^= prior; *byte ^= prior;
} }
+3 -7
View File
@@ -177,18 +177,14 @@ fn decrypt_header(raw: &[u8], constant: u32) -> Result<Stage1Header> {
} }
fn decrypt_words(ciphertext: &[u8], key: u32, constant: u32) -> Result<Vec<u8>> { fn decrypt_words(ciphertext: &[u8], key: u32, constant: u32) -> Result<Vec<u8>> {
if ciphertext.len() % 4 != 0 { if !ciphertext.len().is_multiple_of(4) {
return invalid("Stage 1 word cipher input is not 4-byte aligned"); return invalid("Stage 1 word cipher input is not 4-byte aligned");
} }
let mut plaintext = ciphertext.to_vec(); let mut plaintext = ciphertext.to_vec();
for (index, chunk) in plaintext.chunks_exact_mut(4).enumerate() { for (index, chunk) in plaintext.as_chunks_mut::<4>().0.iter_mut().enumerate() {
let index = u32::try_from(index) let index = u32::try_from(index)
.map_err(|_| Error::Invalid("Stage 1 word index exceeds u32".to_owned()))?; .map_err(|_| Error::Invalid("Stage 1 word index exceeds u32".to_owned()))?;
let mut word = u32::from_le_bytes( let mut word = u32::from_le_bytes(*chunk);
chunk
.try_into()
.map_err(|_| Error::Invalid("Stage 1 word has an invalid size".to_owned()))?,
);
word = word.wrapping_add(index.wrapping_add(3).wrapping_mul(key)); word = word.wrapping_add(index.wrapping_add(3).wrapping_mul(key));
word ^= constant.wrapping_mul(index.wrapping_add(1)); word ^= constant.wrapping_mul(index.wrapping_add(1));
chunk.copy_from_slice(&word.to_le_bytes()); chunk.copy_from_slice(&word.to_le_bytes());
+2 -6
View File
@@ -130,13 +130,9 @@ fn decrypt_record(raw: &[u8], index: usize, state: u32) -> Result<Record> {
let mut accumulator = 0x7993_4cf6_u32; let mut accumulator = 0x7993_4cf6_u32;
let mut feedback = 0xf02f_7685_u32; let mut feedback = 0xf02f_7685_u32;
let mut words = [0_u32; RECORD_SIZE / 4]; let mut words = [0_u32; RECORD_SIZE / 4];
for (word_index, chunk) in raw.chunks_exact(4).enumerate() { for (word_index, chunk) in raw.as_chunks::<4>().0.iter().enumerate() {
feedback = feedback.wrapping_mul(feedback); feedback = feedback.wrapping_mul(feedback);
let cipher = u32::from_le_bytes( let cipher = u32::from_le_bytes(*chunk);
chunk
.try_into()
.map_err(|_| Error::Invalid("record word has an invalid size".to_owned()))?,
);
let mut value = gf32_mul_fixed(cipher ^ (feedback >> 3)) ^ index_mask; let mut value = gf32_mul_fixed(cipher ^ (feedback >> 3)) ^ index_mask;
value = value.wrapping_add(accumulator).wrapping_add(state); value = value.wrapping_add(accumulator).wrapping_add(state);
value = value.wrapping_sub(mix >> ((word_index * 4 + 3) & 5)); value = value.wrapping_sub(mix >> ((word_index * 4 + 3) & 5));
+13 -11
View File
@@ -477,7 +477,8 @@ fn restore_hidden_symbols(
dynstr: SectionHeader, dynstr: SectionHeader,
patch_data: &[u8], patch_data: &[u8],
) -> Result<(Vec<u8>, Vec<u8>, HiddenSymbolReport)> { ) -> Result<(Vec<u8>, Vec<u8>, HiddenSymbolReport)> {
if dynsym.entry_size != ELF64_SYMBOL_SIZE as u64 || dynsym.size % ELF64_SYMBOL_SIZE as u64 != 0 if dynsym.entry_size != ELF64_SYMBOL_SIZE as u64
|| !dynsym.size.is_multiple_of(ELF64_SYMBOL_SIZE as u64)
{ {
return invalid("unexpected .dynsym entry layout"); return invalid("unexpected .dynsym entry layout");
} }
@@ -584,11 +585,13 @@ fn restore_hidden_symbols(
} }
fn dynamic_symbol_names(symbols: &[u8], strings: &[u8]) -> Result<Vec<Vec<u8>>> { fn dynamic_symbol_names(symbols: &[u8], strings: &[u8]) -> Result<Vec<Vec<u8>>> {
if symbols.len() % ELF64_SYMBOL_SIZE != 0 { if !symbols.len().is_multiple_of(ELF64_SYMBOL_SIZE) {
return invalid("dynamic symbol table is not entry-aligned"); return invalid("dynamic symbol table is not entry-aligned");
} }
symbols symbols
.chunks_exact(ELF64_SYMBOL_SIZE) .as_chunks::<ELF64_SYMBOL_SIZE>()
.0
.iter()
.map(|symbol| { .map(|symbol| {
let name_offset = read_u32(symbol, 0)? as usize; let name_offset = read_u32(symbol, 0)? as usize;
Ok(read_c_string(strings, name_offset, strings.len())?.to_vec()) Ok(read_c_string(strings, name_offset, strings.len())?.to_vec())
@@ -698,7 +701,7 @@ fn patch_dynamic_tags(
dynamic: SectionHeader, dynamic: SectionHeader,
values: &BTreeMap<u64, u64>, values: &BTreeMap<u64, u64>,
) -> Result<()> { ) -> Result<()> {
if dynamic.size % 0x10 != 0 { if !dynamic.size.is_multiple_of(0x10) {
return invalid(".dynamic size is not entry-aligned"); return invalid(".dynamic size is not entry-aligned");
} }
let start = usize_from_u64(dynamic.offset, ".dynamic offset")?; let start = usize_from_u64(dynamic.offset, ".dynamic offset")?;
@@ -732,7 +735,7 @@ fn patch_dynamic_tags(
} }
fn dynamic_contains_tag(output: &[u8], dynamic: SectionHeader, wanted: u64) -> Result<bool> { fn dynamic_contains_tag(output: &[u8], dynamic: SectionHeader, wanted: u64) -> Result<bool> {
if dynamic.size % 0x10 != 0 { if !dynamic.size.is_multiple_of(0x10) {
return invalid(".dynamic size is not entry-aligned"); return invalid(".dynamic size is not entry-aligned");
} }
let start = usize_from_u64(dynamic.offset, ".dynamic offset")?; let start = usize_from_u64(dynamic.offset, ".dynamic offset")?;
@@ -1418,13 +1421,12 @@ fn validate_restored_binary(
rela_plt.size / ELF64_RELA_SIZE as u64, rela_plt.size / ELF64_RELA_SIZE as u64,
"restored PLT relocation count", "restored PLT relocation count",
)?; )?;
if let Some(expected) = materialization { if let Some(expected) = materialization
if dynamic_symbols != expected.new_symbol_count && (dynamic_symbols != expected.new_symbol_count
|| dynamic_relocations != expected.rela_dyn_count || dynamic_relocations != expected.rela_dyn_count
|| pltgot_relocations != expected.rela_plt_count || pltgot_relocations != expected.rela_plt_count)
{ {
return invalid("restored ELF table counts do not match materialization report"); return invalid("restored ELF table counts do not match materialization report");
}
} }
Ok(ValidationReport { Ok(ValidationReport {
format: "ELF64".to_owned(), format: "ELF64".to_owned(),
+1 -1
View File
@@ -396,7 +396,7 @@ impl<'a> Unpacker<'a> {
// non-critical for false-positive rejection. // non-critical for false-positive rejection.
if v8 < info6 { if v8 < info6 {
let delta = info6.wrapping_sub(v8); let delta = info6.wrapping_sub(v8);
if delta <= 0x1000 && delta % 0x200 == 0 { if delta <= 0x1000 && delta.is_multiple_of(0x200) {
anchor = Some(probe); anchor = Some(probe);
break; break;
} }
+1
View File
@@ -2,6 +2,7 @@
name = "senbei-wasm" name = "senbei-wasm"
version = "1.2.0" version = "1.2.0"
edition = "2024" edition = "2024"
rust-version = "1.98.1"
description = "WebAssembly bindings for senbei (browser frontend assets live in web/)" description = "WebAssembly bindings for senbei (browser frontend assets live in web/)"
license = "AGPL-3.0-only" license = "AGPL-3.0-only"