From 3de082560ed7c4fae2e79e1c7f4f4b26e544f623 Mon Sep 17 00:00:00 2001 From: csd113 Date: Sun, 17 May 2026 18:10:13 -0700 Subject: [PATCH 1/5] Force VP9 yuv420p & BT.709 color metadata Add explicit VP9 compatibility constants and emit color metadata flags when building VP9 transcode args. Introduces VP9_COMPAT_PIXEL_FORMAT and VP9_COMPAT_COLOR_SPACE and appends -colorspace, -color_primaries and -color_trc set to BT.709 alongside the forced yuv420p pix_fmt. Increases args capacity to account for the new flags. Adds unit tests and helpers that assert the color metadata is emitted directly after the pixel format and before audio options, preventing accidental inheritance of sRGB/GBR source metadata. --- src/media/ffmpeg.rs | 90 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 86 insertions(+), 4 deletions(-) diff --git a/src/media/ffmpeg.rs b/src/media/ffmpeg.rs index 236292ca..129523f9 100644 --- a/src/media/ffmpeg.rs +++ b/src/media/ffmpeg.rs @@ -44,6 +44,9 @@ pub struct Vp9EncodingProfile { pub label: Cow<'static, str>, } +const VP9_COMPAT_PIXEL_FORMAT: &str = "yuv420p"; +const VP9_COMPAT_COLOR_SPACE: &str = "bt709"; + static ENCODER_LIST: LazyLock> = LazyLock::new(|| { output_stdout_with_timeout(&crate::config::CONFIG.ffmpeg_path, &["-encoders"]) }); @@ -309,7 +312,7 @@ pub fn build_vp9_transcode_args(input: &str, output: &str) -> Vec { let cpu_used = profile.cpu_used.to_string(); let tile_columns = profile.tile_columns.to_string(); let threads = profile.threads.to_string(); - let mut args = Vec::with_capacity(28); + let mut args = Vec::with_capacity(34); args.extend( [ "-loglevel", @@ -345,7 +348,13 @@ pub fn build_vp9_transcode_args(input: &str, output: &str) -> Vec { "-b:v", "0", "-pix_fmt", - "yuv420p", + VP9_COMPAT_PIXEL_FORMAT, + "-colorspace", + VP9_COMPAT_COLOR_SPACE, + "-color_primaries", + VP9_COMPAT_COLOR_SPACE, + "-color_trc", + VP9_COMPAT_COLOR_SPACE, "-c:a", "libopus", "-b:a", @@ -561,7 +570,25 @@ pub fn check_opus_encoder() -> bool { #[cfg(test)] mod tests { - use super::build_vp9_transcode_args; + use super::{build_vp9_transcode_args, VP9_COMPAT_COLOR_SPACE, VP9_COMPAT_PIXEL_FORMAT}; + + fn paired_arg_index(args: &[String], flag: &str, value: &str) -> Option { + args.windows(2).position(|window| { + matches!(window, [actual_flag, actual_value] if actual_flag == flag && actual_value == value) + }) + } + + fn output_arg_index(args: &[String], flag: &str, value: &str) -> usize { + let input_index = paired_arg_index(args, "-i", "input.mp4") + .unwrap_or_else(|| panic!("missing input marker in ffmpeg args: {args:?}")); + let arg_index = paired_arg_index(args, flag, value) + .unwrap_or_else(|| panic!("missing ffmpeg arg pair {flag} {value}: {args:?}")); + assert!( + arg_index > input_index + 1, + "{flag} {value} must be an output option after the input argument: {args:?}", + ); + arg_index + } #[test] fn vp9_transcode_args_include_platform_tuning_flags() { @@ -580,6 +607,61 @@ mod tests { assert!(args .windows(2) .any(|w| w.first().is_some_and(|flag| flag == "-threads"))); - assert!(args.windows(2).any(|w| w == ["-pix_fmt", "yuv420p"])); + assert!(args + .windows(2) + .any(|w| w == ["-pix_fmt", VP9_COMPAT_PIXEL_FORMAT])); + } + + #[test] + fn vp9_transcode_args_normalise_yuv420p_color_metadata() { + let args = build_vp9_transcode_args("input.mp4", "output.webm"); + + let pixel_format_index = output_arg_index(&args, "-pix_fmt", VP9_COMPAT_PIXEL_FORMAT); + let colorspace_index = output_arg_index(&args, "-colorspace", VP9_COMPAT_COLOR_SPACE); + let primaries_index = output_arg_index(&args, "-color_primaries", VP9_COMPAT_COLOR_SPACE); + let transfer_index = output_arg_index(&args, "-color_trc", VP9_COMPAT_COLOR_SPACE); + + assert!( + pixel_format_index < colorspace_index + && colorspace_index < primaries_index + && primaries_index < transfer_index, + "VP9 color metadata should be applied directly after the forced pixel format: {args:?}", + ); + } + + #[test] + fn vp9_yuv420p_output_does_not_inherit_srgb_gbr_metadata() { + let args = build_vp9_transcode_args("input.mp4", "output.webm"); + + for (flag, value) in [ + ("-colorspace", VP9_COMPAT_COLOR_SPACE), + ("-color_primaries", VP9_COMPAT_COLOR_SPACE), + ("-color_trc", VP9_COMPAT_COLOR_SPACE), + ] { + let color_index = output_arg_index(&args, flag, value); + let audio_index = args + .windows(2) + .position(|window| matches!(window, [actual_flag, actual_value] if actual_flag == "-c:a" && actual_value == "libopus")) + .unwrap_or(args.len()); + assert!( + color_index < audio_index, + "{flag} {value} should be part of the video output options before audio encoding options: {args:?}", + ); + } + } + + #[test] + fn ffmpeg_builders_with_forced_compat_pixel_formats_normalise_color_metadata() { + let args = build_vp9_transcode_args("input.mp4", "output.webm"); + + // Other ffmpeg builders in this crate either produce WebP thumbnails/images + // through libwebp or audio waveform PNGs and do not force VP9/H.26x-style + // compatibility pixel formats. VP9 transcode is the path that combines + // a forced yuv420p profile-0 output with potentially inherited source + // color metadata, so it owns the explicit BT.709 normalization contract. + output_arg_index(&args, "-pix_fmt", VP9_COMPAT_PIXEL_FORMAT); + output_arg_index(&args, "-colorspace", VP9_COMPAT_COLOR_SPACE); + output_arg_index(&args, "-color_primaries", VP9_COMPAT_COLOR_SPACE); + output_arg_index(&args, "-color_trc", VP9_COMPAT_COLOR_SPACE); } } From 154023873ae2bd02852eff0547a44e18aa6ac1ec Mon Sep 17 00:00:00 2001 From: csd113 Date: Sun, 17 May 2026 18:31:41 -0700 Subject: [PATCH 2/5] Fix Clippy warnings in admin, logging, and crypto --- src/handlers/admin/mod.rs | 6 ++---- src/logging.rs | 2 +- src/utils/crypto.rs | 4 ++++ 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/handlers/admin/mod.rs b/src/handlers/admin/mod.rs index 3a4c38aa..7068ef77 100644 --- a/src/handlers/admin/mod.rs +++ b/src/handlers/admin/mod.rs @@ -804,10 +804,8 @@ fn site_health_job_detail( } fn job_post_id(payload: &str) -> Option { - serde_json::from_str::(payload) - .ok() - .and_then(|value| value.get("d").and_then(|data| data.get("post_id")).cloned()) - .and_then(|post_id| post_id.as_i64()) + let value = serde_json::from_str::(payload).ok()?; + value.get("d")?.get("post_id")?.as_i64() } fn post_url_for_job(conn: &rusqlite::Connection, post_id: i64) -> Option { diff --git a/src/logging.rs b/src/logging.rs index c151d555..06158276 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -1255,7 +1255,7 @@ mod tests { fn write(&mut self, buf: &[u8]) -> io::Result { self.inner .lock() - .map_err(|_| io::Error::other("buffer lock poisoned"))? + .map_err(|_poisoned| io::Error::other("buffer lock poisoned"))? .extend_from_slice(buf); Ok(buf.len()) } diff --git a/src/utils/crypto.rs b/src/utils/crypto.rs index 0ed86b50..826c42db 100644 --- a/src/utils/crypto.rs +++ b/src/utils/crypto.rs @@ -78,6 +78,10 @@ pub fn verify_password(password: &str, hash: &str) -> Result { /// Generate a cryptographically secure random hex string. /// +#[expect( + clippy::exit, + reason = "fail-closed randomness failures must terminate before issuing weak tokens" +)] fn fatal_randomness_error(context: &str, error: &impl std::fmt::Display) -> ! { let _ = writeln!( std::io::stderr().lock(), From fc60d791eb61cb8bdebaffc17e62df4fead27c7e Mon Sep 17 00:00:00 2001 From: csd113 Date: Tue, 19 May 2026 20:18:20 -0700 Subject: [PATCH 3/5] Handle v4 transfer backups & validate board names Add support for Backup v4 "transfer" archives by converting archives that include db/rustchan.sqlite3 into a legacy full backup zip (new create_temp_legacy_full_backup_from_v4_transfer_zip and wiring in restore flow), so layout validation no longer rejects these uploads. Update restore logic to open the converted archive and clean up the temp file. Add board short-name validation helper and uniqueness check to create_board to enforce length/character rules and return Conflict if a board exists. Add a noscript style to ensure the post form is visible when JavaScript is disabled. Also update .gitignore to ignore node/playwright/npm artifacts and test outputs. --- .gitignore | 9 ++ src/handlers/admin/backup.rs | 3 +- src/handlers/admin/backup/archive.rs | 118 ++++++++++++++++++++++ src/handlers/admin/backup/restore_full.rs | 48 ++++++--- src/handlers/admin/content.rs | 29 +++--- src/templates/mod.rs | 1 + 6 files changed, 182 insertions(+), 26 deletions(-) diff --git a/.gitignore b/.gitignore index 417c6234..f9f5c5d1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,12 @@ .DS_Store /target /.codex-pet-runs +/node_modules +/playwright-report +/test-results +/npm-debug.log* +/rustchan-data +package-lock.json +package.json +playwright.config.ts +/tests diff --git a/src/handlers/admin/backup.rs b/src/handlers/admin/backup.rs index e08ec194..f723a707 100644 --- a/src/handlers/admin/backup.rs +++ b/src/handlers/admin/backup.rs @@ -114,7 +114,8 @@ use archive::{ canonicalize_restored_banner_dir, create_temp_board_backup_from_full_backup_path, create_temp_legacy_board_backup_from_saved_full_v4_path, create_temp_legacy_board_backup_from_v4_path, create_temp_legacy_full_backup_from_v4_path, - parse_board_backup_manifest_from_zip, validate_full_restore_archive_layout, + create_temp_legacy_full_backup_from_v4_transfer_zip, parse_board_backup_manifest_from_zip, + validate_full_restore_archive_layout, }; use downloads::prune_stale_temp_board_downloads; #[cfg(test)] diff --git a/src/handlers/admin/backup/archive.rs b/src/handlers/admin/backup/archive.rs index 6c4cdf37..06209ffb 100644 --- a/src/handlers/admin/backup/archive.rs +++ b/src/handlers/admin/backup/archive.rs @@ -201,6 +201,124 @@ pub(super) fn create_temp_legacy_full_backup_from_v4_path(root_dir: &Path) -> Re create_temp_legacy_full_backup_from_verified_v4(&verified) } +pub(super) fn create_temp_legacy_full_backup_from_v4_transfer_zip( + archive: &mut zip::ZipArchive, +) -> Result { + let mut db_index = None; + let mut db_bytes = 0_u64; + let mut mapped_files = Vec::new(); + let mut upload_file_count = 0_u64; + let mut favicon_file_count = 0_u64; + let mut banner_file_count = 0_u64; + let mut tor_hidden_service_key_file_count = 0_u64; + + for index in 0..archive.len() { + let entry = archive.by_index(index).map_err(|error| { + AppError::Internal(anyhow::anyhow!( + "Read Backup v4 transfer entry #{index}: {error}" + )) + })?; + let name = entry.name().to_owned(); + super::common::validate_restore_safe_entry_name(&name)?; + if entry.is_dir() { + continue; + } + + if name == "db/rustchan.sqlite3" { + db_index = Some(index); + db_bytes = entry.size(); + } else if name.starts_with("boards/") { + if let Ok((runtime_path, kind)) = v4::logical_upload_path_to_runtime(&name) { + mapped_files.push((index, format!("uploads/{runtime_path}"))); + match kind { + v4::BackupFileKind::OriginalMedia + | v4::BackupFileKind::Thumbnail + | v4::BackupFileKind::Banner + | v4::BackupFileKind::Favicon => { + upload_file_count = upload_file_count.saturating_add(1); + } + _ => {} + } + } + } else if let Some(rel) = name.strip_prefix("site-assets/favicon/") { + super::common::validate_restore_safe_entry_name(rel)?; + mapped_files.push((index, format!("favicon/{rel}"))); + favicon_file_count = favicon_file_count.saturating_add(1); + } else if let Some(rel) = name.strip_prefix("site-assets/banner/") { + super::common::validate_restore_safe_entry_name(rel)?; + mapped_files.push((index, format!("banner/{rel}"))); + banner_file_count = banner_file_count.saturating_add(1); + } else if let Some(rel) = name.strip_prefix("tor-keys/") { + super::common::validate_restore_safe_entry_name(rel)?; + mapped_files.push(( + index, + format!("{}/{}", super::common::FULL_BACKUP_TOR_KEYS_PREFIX, rel), + )); + tor_hidden_service_key_file_count = tor_hidden_service_key_file_count.saturating_add(1); + } + } + + let db_index = db_index.ok_or_else(|| { + AppError::BadRequest( + "Invalid Backup v4 transfer archive: missing db/rustchan.sqlite3.".into(), + ) + })?; + let temp_zip = temp_legacy_zip_path("rustchan_v4_transfer_full_restore"); + let output = std::fs::File::create(&temp_zip).map_err(|error| { + AppError::Internal(anyhow::anyhow!("Create {}: {error}", temp_zip.display())) + })?; + let mut zip = zip::ZipWriter::new(std::io::BufWriter::new(output)); + let options = zip::write::SimpleFileOptions::default() + .compression_method(zip::CompressionMethod::Deflated); + let manifest = serde_json::json!({ + "version": 3, + "generated_at": Utc::now().timestamp(), + "rustchan_version": env!("CARGO_PKG_VERSION"), + "db_bytes": db_bytes, + "upload_file_count": upload_file_count, + "favicon_file_count": favicon_file_count, + "banner_file_count": banner_file_count, + "tor_hidden_service_keys_included": tor_hidden_service_key_file_count > 0, + "tor_hidden_service_key_file_count": tor_hidden_service_key_file_count, + "boards": [], + }); + zip.start_file(super::common::FULL_BACKUP_MANIFEST_NAME, options) + .map_err(|error| AppError::Internal(anyhow::anyhow!("Zip backup.json: {error}")))?; + zip.write_all( + &serde_json::to_vec_pretty(&manifest).map_err(|error| { + AppError::Internal(anyhow::anyhow!("Serialize backup.json: {error}")) + })?, + ) + .map_err(|error| AppError::Internal(anyhow::anyhow!("Write backup.json: {error}")))?; + + zip.start_file("chan.db", options) + .map_err(|error| AppError::Internal(anyhow::anyhow!("Zip chan.db: {error}")))?; + { + let mut entry = archive.by_index(db_index).map_err(|error| { + AppError::Internal(anyhow::anyhow!("Read Backup v4 DB entry: {error}")) + })?; + super::common::copy_limited(&mut entry, &mut zip, super::common::ZIP_ENTRY_MAX_BYTES) + .map_err(|error| AppError::Internal(anyhow::anyhow!("Copy chan.db: {error}")))?; + } + + for (index, zip_path) in mapped_files { + zip.start_file(&zip_path, zip_file_options_for_path(Path::new(&zip_path))) + .map_err(|error| AppError::Internal(anyhow::anyhow!("Zip {zip_path}: {error}")))?; + let mut entry = archive.by_index(index).map_err(|error| { + AppError::Internal(anyhow::anyhow!( + "Read Backup v4 transfer entry #{index}: {error}" + )) + })?; + super::common::copy_limited(&mut entry, &mut zip, super::common::ZIP_ENTRY_MAX_BYTES) + .map_err(|error| AppError::Internal(anyhow::anyhow!("Copy {zip_path}: {error}")))?; + } + + zip.finish().map_err(|error| { + AppError::Internal(anyhow::anyhow!("Finalize {}: {error}", temp_zip.display())) + })?; + Ok(temp_zip) +} + fn create_temp_legacy_full_backup_from_verified_v4( verified: &v4::VerifiedSavedV4Root, ) -> Result { diff --git a/src/handlers/admin/backup/restore_full.rs b/src/handlers/admin/backup/restore_full.rs index 4a4bbd9f..2d97b96f 100644 --- a/src/handlers/admin/backup/restore_full.rs +++ b/src/handlers/admin/backup/restore_full.rs @@ -738,20 +738,40 @@ pub async fn admin_restore( .map_err(|error| AppError::Internal(anyhow::anyhow!("Reopen zip: {error}")))?; let mut archive = zip::ZipArchive::new(std::io::BufReader::new(zip_file)) .map_err(|error| AppError::BadRequest(format!("Invalid zip: {error}")))?; - - if let Err(error) = validate_full_restore_archive_layout(&archive) { - tracing::warn!( - target: "admin", - route = RestoreKind::Full.route(), - filename = uploaded_filename.as_deref().unwrap_or(""), - error = %error, - "{} archive layout validation failed", - RestoreKind::Full.title() - ); - return Err(error); + let mut temp_v4_transfer_zip_guard = None; + + if let Err(layout_error) = validate_full_restore_archive_layout(&archive) { + if archive.by_name("db/rustchan.sqlite3").is_ok() { + let legacy_path = + create_temp_legacy_full_backup_from_v4_transfer_zip(&mut archive)?; + temp_v4_transfer_zip_guard = Some( + super::archive::TempZipCleanupGuard::new(legacy_path.clone()), + ); + let legacy_file = std::fs::File::open(&legacy_path).map_err(|error| { + AppError::Internal(anyhow::anyhow!( + "Open converted Backup v4 transfer zip: {error}" + )) + })?; + archive = zip::ZipArchive::new(std::io::BufReader::new(legacy_file)) + .map_err(|error| { + AppError::Internal(anyhow::anyhow!( + "Open converted Backup v4 transfer archive: {error}" + )) + })?; + } else { + tracing::warn!( + target: "admin", + route = RestoreKind::Full.route(), + filename = uploaded_filename.as_deref().unwrap_or(""), + error = %layout_error, + "{} archive layout validation failed", + RestoreKind::Full.title() + ); + return Err(layout_error); + } } - execute_full_restore( + let fresh_sid = execute_full_restore( &mut live_conn, admin_id, &upload_dir, @@ -762,7 +782,9 @@ pub async fn admin_restore( "Restore completed, new session issued", "Restore", "Restore", - ) + )?; + drop(temp_v4_transfer_zip_guard); + Ok(fresh_sid) } }) .await diff --git a/src/handlers/admin/content.rs b/src/handlers/admin/content.rs index 855a4eb4..97b7dc87 100644 --- a/src/handlers/admin/content.rs +++ b/src/handlers/admin/content.rs @@ -26,6 +26,17 @@ fn sanitize_board_short_value(board_short: &str) -> String { .collect() } +fn validate_new_board_short_name(raw_short_name: &str) -> Result { + let trimmed = raw_short_name.trim(); + if trimmed.is_empty() || trimmed.len() > 8 { + return Err(AppError::BadRequest("Invalid board name.".into())); + } + if !trimmed.chars().all(|ch| ch.is_ascii_alphanumeric()) { + return Err(AppError::BadRequest("Invalid board name.".into())); + } + Ok(trimmed.to_lowercase()) +} + fn resolve_board_short_name( boards: Option<&[crate::models::Board]>, board_id: i64, @@ -112,18 +123,7 @@ pub async fn create_board( let session_id = jar.get(super::SESSION_COOKIE).map(|c| c.value().to_owned()); super::require_admin_post_origin_and_csrf(&jar, &headers, Some(peer), form.csrf.as_deref())?; - let short = form - .short_name - .trim() - .to_lowercase() - .chars() - .filter(char::is_ascii_alphanumeric) - .take(8) - .collect::(); - - if short.is_empty() { - return Err(AppError::BadRequest("Invalid board name.".into())); - } + let short = validate_new_board_short_name(&form.short_name)?; let short_for_flash = short.clone(); let nsfw = form.nsfw.as_deref() == Some("1"); @@ -141,6 +141,11 @@ pub async fn create_board( move || -> Result<()> { let conn = pool.get()?; super::require_admin_session_sid(&conn, session_id.as_deref())?; + if db::get_board_by_short(&conn, &short)?.is_some() { + return Err(AppError::Conflict(format!( + "Board /{short}/ already exists." + ))); + } db::create_board_with_media_flags( &conn, &short, diff --git a/src/templates/mod.rs b/src/templates/mod.rs index 19b7680e..373082f4 100644 --- a/src/templates/mod.rs +++ b/src/templates/mod.rs @@ -738,6 +738,7 @@ pub fn base_layout_with_preferences( {admin_stylesheet_link} {theme_stylesheet_link} + From 7d7c0fdc0ceadb53d52e01333435c2634725fe4b Mon Sep 17 00:00:00 2001 From: csd113 Date: Tue, 19 May 2026 20:23:19 -0700 Subject: [PATCH 4/5] Update Cargo.lock --- Cargo.lock | 67 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 42 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3712b173..c52f99fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -260,9 +260,9 @@ dependencies = [ [[package]] name = "asn1-rs" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" +checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" dependencies = [ "asn1-rs-derive 0.6.0", "asn1-rs-impl", @@ -1110,9 +1110,9 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ "hybrid-array", ] @@ -1258,9 +1258,9 @@ dependencies = [ [[package]] name = "dashmap" -version = "6.1.0" +version = "6.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" dependencies = [ "cfg-if", "crossbeam-utils", @@ -1309,7 +1309,7 @@ version = "10.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" dependencies = [ - "asn1-rs 0.7.1", + "asn1-rs 0.7.2", "cookie-factory", "displaydoc", "nom", @@ -1341,9 +1341,9 @@ dependencies = [ [[package]] name = "derive-deftly" -version = "1.11.1" +version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79fc7a4d4fef137732d8d0d4e58a9e46e168e5b2a7f728c20f87e7cda69a14e9" +checksum = "219c002a60a3baf5d3fa28db4e1c98733aa2bc784aa63b83c011a72ae9335fb2" dependencies = [ "derive-deftly-macros", "heck", @@ -1351,9 +1351,9 @@ dependencies = [ [[package]] name = "derive-deftly-macros" -version = "1.11.1" +version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a93511fd122bc7b22e361806e84f1c31a53894f0564a4ba7f28e799104318c7" +checksum = "bd5850ec9ad2d9ba0aa33fb22c0b0ef4d91e524566be497ac2a6e40b847e67bb" dependencies = [ "heck", "indexmap 2.14.0", @@ -1361,7 +1361,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "sha3 0.11.0", + "sha3 0.12.0", "strum", "syn 2.0.117", "unicode-ident", @@ -1442,7 +1442,7 @@ checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ "block-buffer 0.12.0", "const-oid 0.10.2", - "crypto-common 0.2.1", + "crypto-common 0.2.2", ] [[package]] @@ -1626,9 +1626,9 @@ dependencies = [ [[package]] name = "enumset" -version = "1.1.12" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f96a4a12fe60ac746ae295a1a4ecb5bb02debc20856506c8635288065f142de" +checksum = "839c4174b41e75c8f7306110b2c51996a293b8d1d850edd529011841d9fede7d" dependencies = [ "enumset_derive", ] @@ -3030,9 +3030,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-integer" @@ -3120,7 +3120,7 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" dependencies = [ - "asn1-rs 0.7.1", + "asn1-rs 0.7.2", ] [[package]] @@ -3894,9 +3894,9 @@ dependencies = [ [[package]] name = "rsqlite-vfs" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8a1f2315036ef6b1fbacd1972e8ee7688030b0a2121edfc2a6550febd41574d" +checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c" dependencies = [ "hashbrown 0.16.1", "thiserror 2.0.18", @@ -4455,6 +4455,17 @@ dependencies = [ "keccak 0.2.0", ] +[[package]] +name = "sha3" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9bad02c26382724b2d2692c6f179285e4b54eeecd7968f52a50059c3c11759" +dependencies = [ + "digest 0.11.3", + "keccak 0.2.0", + "sponge-cursor", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -4611,11 +4622,17 @@ dependencies = [ "der", ] +[[package]] +name = "sponge-cursor" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a0219bd7d979d58245a4f41f695e1ac9f8befdffadd7f61f1bae9e39abc6620" + [[package]] name = "sqlite-wasm-rs" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b2c760607300407ddeaee518acf28c795661b7108c75421303dbefb237d3a36" +checksum = "cdd578e94101503d97e2b286bbf8db2135035ca24b2ce4cbf3f9e2fb2bbf1eee" dependencies = [ "cc", "js-sys", @@ -6187,9 +6204,9 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.10" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68d6fdd9f81c2819c9a8b0e0cd91660e7746a8e6ea2ba7c6b2b057985f6bcb51" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ "async-compression", "bitflags", @@ -7160,7 +7177,7 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" dependencies = [ - "asn1-rs 0.7.1", + "asn1-rs 0.7.2", "data-encoding", "der-parser 10.0.0", "lazy_static", From 23ba337771b20ed58bd345f1016069fdd33dea78 Mon Sep 17 00:00:00 2001 From: csd113 Date: Tue, 19 May 2026 20:33:48 -0700 Subject: [PATCH 5/5] Bump RustChan to v1.2.1 --- CHANGELOG.md | 16 ++++++++++++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 2 +- SETUP.md | 4 ++-- src/templates/admin.rs | 6 +++--- 6 files changed, 24 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 142508d4..be2fe695 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,22 @@ All notable changes to RustChan will be documented in this file. +## [1.2.1] + +### Improved + +- Full-site restore now accepts Backup v4 transfer ZIPs by converting the transfer layout into the legacy restore archive shape before running the normal restore flow. +- VP9/WebM transcoding now forces yuv420p output with explicit BT.709 color metadata, avoiding inherited source color tags that can make browser playback look wrong. +- Admin site-health job post ID parsing and logging test helpers were tightened up for stricter lint compliance. +- Local JavaScript test artifacts and runtime folders are now ignored by git. + +### Fixed + +- New board creation now validates short names instead of silently stripping invalid characters, and duplicate board shorts return a conflict instead of proceeding into creation. +- No-JavaScript post form rendering now keeps the post form visible when scripts are unavailable. +- Random token generation keeps its fail-closed behavior while documenting the intentional process exit for Clippy. +- Rust dependency lockfile entries were refreshed for the `1.2.1` cycle. + ## [1.2.0] ### Added diff --git a/Cargo.lock b/Cargo.lock index c52f99fb..2b69ddac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3929,7 +3929,7 @@ dependencies = [ [[package]] name = "rustchan" -version = "1.2.0" +version = "1.2.1" dependencies = [ "anyhow", "argon2", diff --git a/Cargo.toml b/Cargo.toml index b649d4a6..ad3b1a57 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustchan" -version = "1.2.0" +version = "1.2.1" edition = "2021" # axum 0.8 requires Rust 1.75; arti-client 0.41 bumps the effective floor to # 1.90 — keep both fields in sync. diff --git a/README.md b/README.md index c2630bfc..6143d57c 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ One binary. One data folder. SQLite only. The rest is features. RustChan is built in Rust, ships with bundled SQLite, and is designed to be understandable, movable, and fun to run. -Current development version: `1.2.0`. +Current development version: `1.2.1`. [What is RustChan?](#what-is-rustchan) · [Why it exists](#why-it-exists) · diff --git a/SETUP.md b/SETUP.md index 227ef7fd..a716d96e 100644 --- a/SETUP.md +++ b/SETUP.md @@ -2,7 +2,7 @@ Current setup and deployment guide for Linux, macOS, and Windows. -Current development version: `1.2.0`. +Current development version: `1.2.1`. This guide reflects the current RustChan architecture: @@ -265,7 +265,7 @@ rustchan-data/ ## Banner Artwork Requirements -RustChan `1.2.0` includes board banners plus a separate home-page announcement banner. +RustChan `1.2.1` includes board banners plus a separate home-page announcement banner. Banner upload requirements: diff --git a/src/templates/admin.rs b/src/templates/admin.rs index ee6b58b2..91bac54f 100644 --- a/src/templates/admin.rs +++ b/src/templates/admin.rs @@ -1893,7 +1893,7 @@ mod tests { fn sample_site_health() -> AdminPanelSiteHealthView<'static> { AdminPanelSiteHealthView { server_status: "ready", - rustchan_version: "1.2.0", + rustchan_version: "1.2.1", database_integrity_status: "not checked", last_successful_backup: "none saved", next_scheduled_backup: "not scheduled", @@ -1914,7 +1914,7 @@ mod tests { failed_jobs: 0, backup_jobs: "idle", restore_jobs: "not available", - diagnostics_text: "RustChan version: 1.2.0\nRecent warnings:\n none", + diagnostics_text: "RustChan version: 1.2.1\nRecent warnings:\n none", } } @@ -2246,7 +2246,7 @@ mod tests { assert!(html.contains("Database integrity status")); assert!(html.contains("open media panel")); assert!(html.contains("copy diagnostics")); - assert!(html.contains("RustChan version: 1.2.0")); + assert!(html.contains("RustChan version: 1.2.1")); assert!(html.contains(r#"data-admin-health-jobs-url="/admin/site-health/jobs""#)); assert!(html.contains(r#"data-admin-health-job="running_jobs""#)); assert!(html.contains(r#"data-admin-health-job="queued_jobs""#));