chore(repo): remove dead code and useless tests (#3375)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-10 21:43:29 +02:00
1 parent 7e177a1c90
commit 1cd62eb661
1391 files changed
+783 -120415

No files matched your search

+2 -21
View File
@@ -14,8 +14,6 @@ mode="${1:-}"
linux_workspaces=(
"app-store/Cargo.toml"
"audio-apm/Cargo.toml"
"audio-mix/Cargo.toml"
"audio-timing/Cargo.toml"
"encoder-ring/Cargo.toml"
"gateway-socket/Cargo.toml"
"gpu-rebuild/Cargo.toml"
@@ -25,7 +23,6 @@ linux_workspaces=(
"linux-input-hook/Cargo.toml"
"linux-notifications/Cargo.toml"
"linux-portals/Cargo.toml"
"linux-screen-capture/Cargo.toml"
"platform-info/Cargo.toml"
"rt-thread/Cargo.toml"
"rust/Cargo.toml"
@@ -34,10 +31,6 @@ linux_workspaces=(
"webauthn/Cargo.toml"
)
fuzz_workspaces=(
"rust/fuzz/Cargo.toml"
)
macos_workspaces=(
"mac-app-audio/Cargo.toml"
"mac-clipboard/Cargo.toml"
@@ -90,13 +83,6 @@ workspace_category() {
fi
done
for classified_manifest in "${fuzz_workspaces[@]}"; do
if [ "$relative_manifest" = "$classified_manifest" ]; then
printf 'fuzz\n'
return
fi
done
for classified_manifest in "${macos_workspaces[@]}"; do
if [ "$relative_manifest" = "$classified_manifest" ]; then
printf 'macos\n'
@@ -166,7 +152,6 @@ if [ ! -s "$manifest_list" ]; then
fi
linux_count=0
fuzz_count=0
macos_count=0
windows_count=0
@@ -184,7 +169,6 @@ while IFS= read -r manifest; do
category="$(workspace_category "$relative_manifest")"
case "$category" in
linux) linux_count=$((linux_count + 1)) ;;
fuzz) fuzz_count=$((fuzz_count + 1)) ;;
macos) macos_count=$((macos_count + 1)) ;;
windows) windows_count=$((windows_count + 1)) ;;
*) fail "Unclassified desktop native workspace: $relative_manifest" ;;
@@ -194,7 +178,6 @@ done <"$manifest_list"
printf '%s\n' \
"${linux_workspaces[@]}" \
"${fuzz_workspaces[@]}" \
"${macos_workspaces[@]}" \
"${windows_workspaces[@]}" |
LC_ALL=C sort >"$expected_inventory"
@@ -230,7 +213,6 @@ while IFS= read -r manifest; do
clippy)
case "$category" in
linux) printf 'Clippy full Linux/shared coverage: %s\n' "$relative_manifest" ;;
fuzz) printf 'Clippy fuzz-target coverage: %s\n' "$relative_manifest" ;;
macos) printf 'Clippy Linux stub/common coverage; macOS backend not validated: %s\n' "$relative_manifest" ;;
windows) printf 'Clippy Linux stub/common coverage; Windows backend not validated: %s\n' "$relative_manifest" ;;
esac
@@ -239,7 +221,6 @@ while IFS= read -r manifest; do
test)
case "$category" in
linux) printf 'Test full Linux/shared coverage: %s\n' "$relative_manifest" ;;
fuzz) printf 'Test command is a no-op for test=false fuzz binaries: %s\n' "$relative_manifest" ;;
macos) printf 'Test Linux stub/common coverage; macOS backend not validated: %s\n' "$relative_manifest" ;;
windows) printf 'Test Linux stub/common coverage; Windows backend not validated: %s\n' "$relative_manifest" ;;
esac
@@ -249,6 +230,6 @@ while IFS= read -r manifest; do
checked_count=$((checked_count + 1))
done <"$manifest_list"
discovered_count=$((linux_count + fuzz_count + macos_count + windows_count))
printf 'Desktop native workspace categories: discovered=%s linux-full=%s fuzz=%s macos-stub-common=%s windows-stub-common=%s\n' "$discovered_count" "$linux_count" "$fuzz_count" "$macos_count" "$windows_count"
discovered_count=$((linux_count + macos_count + windows_count))
printf 'Desktop native workspace categories: discovered=%s linux-full=%s macos-stub-common=%s windows-stub-common=%s\n' "$discovered_count" "$linux_count" "$macos_count" "$windows_count"
printf 'Desktop native %s summary: checked=%s\n' "$mode" "$checked_count"
-40
View File
@@ -416,8 +416,6 @@ fn gather_wasm_inputs(project_root: &Path) -> Result<StepInputs> {
PathBuf::from("../tools/ci/src/app_wasm.rs"),
PathBuf::from("../tools/ci/src/common.rs"),
PathBuf::from("../tools/ci/src/lib.rs"),
PathBuf::from("../tools/ci/templates/libfluxcore_wrapper.js"),
PathBuf::from("../tools/ci/templates/libfluxcore_wrapper.d.ts"),
markdown_parser_rust_dir.join("Cargo.toml"),
],
)?;
@@ -575,41 +573,3 @@ fn quote_arg(arg: &str) -> String {
format!("{arg:?}")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rel_path_keys_are_posix_like() {
assert_eq!(
rel_path_key(Path::new("scripts/GenerateColorSystem.ts")),
"scripts/GenerateColorSystem.ts"
);
}
#[test]
fn wasm_inputs_cover_the_libfluxwebp_sources_and_headers() {
let app_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fluxer_app");
let inputs = gather_wasm_inputs(&app_dir).expect("gather wasm inputs");
for key in [
"rust/libfluxwebp/Cargo.toml",
"rust/libfluxwebp/Cargo.lock",
"rust/libfluxwebp/src/lib.rs",
"rust/libfluxwebp/shim/stdlib.h",
"rust/libfluxwebp/simd/emmintrin.h",
] {
assert!(inputs.contains_key(key), "missing {key}");
}
assert!(inputs.keys().all(|key| !key.contains("/target/")));
}
#[test]
fn display_command_quotes_globs() {
assert_eq!(
display_command("tcm", &["src", "--pattern", "**/*.module.css"]),
"tcm src --pattern \"**/*.module.css\""
);
}
}
-104
View File
@@ -532,7 +532,6 @@ fn tree_differences(
#[cfg(test)]
mod tests {
use super::*;
use crate::common::parse_version_instant;
use chrono::{DateTime, TimeZone, Utc};
use std::ffi::OsString;
@@ -571,16 +570,6 @@ mod tests {
);
}
#[test]
fn rejects_invalid_calver_time() {
assert_eq!(
parse_version_instant("2026.520.246000")
.unwrap_err()
.to_string(),
"Invalid build version date/time: 2026.520.246000"
);
}
#[test]
fn ghcr_auth_config_merges_existing_auths() {
let temp = tempfile::tempdir().unwrap();
@@ -624,35 +613,6 @@ mod tests {
);
}
#[test]
fn build_command_sets_bake_environment() {
let command = CommandSpec::new("docker")
.args(["buildx", "bake", "-f", "fluxer_app_proxy/docker-bake.hcl"])
.env("IMAGE_REPO", "ghcr.io/example/fluxer-app-proxy")
.env("BUILD_VERSION", "2026.520.1")
.env("PUBLIC_ASSET_BASE_URL", DEFAULT_PUBLIC_ASSET_BASE_URL);
assert_eq!(command.program, OsString::from("docker"));
assert_eq!(
command.args,
vec![
OsString::from("buildx"),
OsString::from("bake"),
OsString::from("-f"),
OsString::from("fluxer_app_proxy/docker-bake.hcl"),
]
);
assert!(command.env.contains(&(
OsString::from("BUILD_VERSION"),
OsString::from("2026.520.1")
)));
}
#[test]
fn hosted_bake_trims_the_local_asset_tree_by_default() {
assert_eq!(DEFAULT_APP_PROXY_BUNDLE_LOCAL_ASSETS, "false");
}
#[test]
fn dockerfile_guards_the_trim_on_an_absolute_asset_base_url() {
let dockerfile = include_str!("../../../fluxer_app_proxy/Dockerfile");
@@ -967,70 +927,6 @@ mod tests {
);
}
#[test]
fn dockerfile_injects_one_canonical_asset_tree_into_every_architecture() {
let dockerfile = include_str!("../../../fluxer_app_proxy/Dockerfile");
for entry in [
"ARG APP_ASSETS_REF=app-assets",
"ARG APP_ASSETS_PLATFORM=$BUILDPLATFORM",
"FROM --platform=${APP_ASSETS_PLATFORM} ${APP_ASSETS_REF} AS app-static",
"COPY --from=app-static /assets ./static/",
] {
assert!(
dockerfile.contains(entry),
"every architecture must serve the injected canonical tree, so the Dockerfile must contain {entry}"
);
}
}
#[test]
fn dockerfile_prepares_the_asset_tree_once_before_the_architecture_stages() {
let dockerfile = include_str!("../../../fluxer_app_proxy/Dockerfile");
let canonical = dockerfile
.split("FROM alpine:3.24 AS app-assets")
.nth(1)
.expect("app-assets stage");
let (canonical, per_architecture) = canonical
.split_once("AS app-static")
.expect("app-static stage");
for entry in ["apk add --no-cache brotli", "precompress_assets.sh"] {
assert!(
canonical.contains(entry),
"the canonical asset tree is prepared once, so {entry} must run in the app-assets stage"
);
assert!(
!per_architecture.contains(entry),
"{entry} must not run again per architecture or the trees stop being byte-identical"
);
}
}
#[test]
fn bake_publishes_the_canonical_asset_image() {
let bake = include_str!("../../../fluxer_app_proxy/docker-bake.hcl");
for entry in [
"target \"app-assets-image\"",
"${BUILD_VERSION}-assets",
"type=registry",
"APP_ASSETS_REF = APP_ASSETS_REF",
"APP_ASSETS_PLATFORM = APP_ASSETS_PLATFORM",
] {
assert!(bake.contains(entry), "docker-bake.hcl must contain {entry}");
}
}
#[test]
fn canonical_assets_platform_falls_back_to_the_bake_default() {
let bake = include_str!("../../../fluxer_app_proxy/docker-bake.hcl");
let declared =
format!("variable \"APP_ASSETS_PLATFORM\" {{ default = \"{AMD64_PLATFORM}\" }}");
assert!(
bake.contains(&declared),
"the parity gate reads APP_ASSETS_PLATFORM and falls back to {AMD64_PLATFORM}, so docker-bake.hcl must declare the same default"
);
}
#[test]
fn self_hosted_workflow_keeps_assets_same_origin() {
let workflow = include_str!("../../../.github/workflows/build-app-proxy-self-hosted.yaml");
+6 -61
View File
@@ -13,8 +13,9 @@ use tempfile::TempDir;
const LIBFLUXCORE_WASM_BINDGEN_VERSION: &str = "0.2.128";
const LIBFLUXCORE_WASM_SIZE_BUDGET_BYTES: u64 = 300 * 1024;
const LIBFLUXCORE_WRAPPER_JS: &str = include_str!("../templates/libfluxcore_wrapper.js");
const LIBFLUXCORE_WRAPPER_DTS: &str = include_str!("../templates/libfluxcore_wrapper.d.ts");
const LIBFLUXCORE_INDEX: &str = "// SPDX-License-Identifier: AGPL-3.0-or-later\n\n\
export * from './libfluxcore_bindgen.js';\n\
export {default} from './libfluxcore_bindgen.js';\n";
const LIBFLUXWEBP_SIMD_WASM_SIZE_BUDGET_BYTES: u64 = 672 * 1024;
const LIBFLUXWEBP_SCALAR_WASM_SIZE_BUDGET_BYTES: u64 = 288 * 1024;
const LIBFLUXWEBP_WASM_IMPORT_MODULE: &str = "./libfluxwebp_bg.js";
@@ -27,12 +28,6 @@ pub struct BuildAppWasmArgs {
app_dir: Option<PathBuf>,
}
#[derive(Debug, Args, Clone)]
pub struct BuildMarkdownParserWasmArgs {
#[arg(long)]
app_dir: Option<PathBuf>,
}
pub fn run_build_app_wasm(args: BuildAppWasmArgs) -> Result<()> {
let app_dir = resolve_app_dir(args.app_dir)?;
build_markdown_parser_wasm(&app_dir)?;
@@ -40,11 +35,6 @@ pub fn run_build_app_wasm(args: BuildAppWasmArgs) -> Result<()> {
build_libfluxwebp_wasm(&app_dir)
}
pub fn run_build_markdown_parser_wasm(args: BuildMarkdownParserWasmArgs) -> Result<()> {
let app_dir = resolve_app_dir(args.app_dir)?;
build_markdown_parser_wasm(&app_dir)
}
const WASM_CC_ENV: &str = "CC_wasm32_unknown_unknown";
const WASM_AR_ENV: &str = "AR_wasm32_unknown_unknown";
@@ -255,21 +245,13 @@ fn build_libfluxcore_wasm(app_dir: &Path) -> Result<()> {
)
.with_context(|| format!("Failed to copy {}", bindgen_wasm_dts_path.display()))?;
fs::write(
out_dir.join("libfluxcore.js"),
libfluxcore_index_js_content(),
)
.with_context(|| {
fs::write(out_dir.join("libfluxcore.js"), LIBFLUXCORE_INDEX).with_context(|| {
format!(
"Failed to write {}",
out_dir.join("libfluxcore.js").display()
)
})?;
fs::write(
out_dir.join("libfluxcore.d.ts"),
libfluxcore_index_dts_content(),
)
.with_context(|| {
fs::write(out_dir.join("libfluxcore.d.ts"), LIBFLUXCORE_INDEX).with_context(|| {
format!(
"Failed to write {}",
out_dir.join("libfluxcore.d.ts").display()
@@ -680,25 +662,6 @@ fn write_with_spdx(path: &Path, content: &str) -> Result<()> {
.with_context(|| format!("Failed to write {}", path.display()))
}
fn libfluxcore_index_js_content() -> String {
format!(
"// SPDX-License-Identifier: AGPL-3.0-or-later\n\n\
import {{crop_rotate_rgba_raw}} from './libfluxcore_bindgen.js';\n\
{LIBFLUXCORE_WRAPPER_JS}\n\
export * from './libfluxcore_bindgen.js';\n\
export {{default}} from './libfluxcore_bindgen.js';\n"
)
}
fn libfluxcore_index_dts_content() -> String {
format!(
"// SPDX-License-Identifier: AGPL-3.0-or-later\n\n\
export * from './libfluxcore_bindgen.js';\n\
export {{default}} from './libfluxcore_bindgen.js';\n\n\
{LIBFLUXCORE_WRAPPER_DTS}"
)
}
fn libfluxcore_package_json_content() -> String {
let manifest = serde_json::json!({
"name": "libfluxcore",
@@ -733,7 +696,7 @@ fn libfluxcore_package_json_content() -> String {
fn libfluxcore_readme_content() -> &'static str {
"<!-- SPDX-License-Identifier: AGPL-3.0-or-later -->\n\
# libfluxcore\n\n\
Rust WebAssembly helpers and JavaScript codec wrappers for Fluxer media processing.\n"
Rust WebAssembly helpers for Fluxer media processing.\n"
}
fn markdown_wasm_bytes_content(wasm: &[u8]) -> String {
@@ -779,16 +742,6 @@ mod tests {
);
}
#[test]
fn markdown_wasm_bytes_content_matches_legacy_node_output() {
assert_eq!(
markdown_wasm_bytes_content(b"hello"),
"// SPDX-License-Identifier: AGPL-3.0-or-later\n\n\
export const MARKDOWN_PARSER_WASM_BASE64 =\n\
\t'aGVsbG8=';\n"
);
}
#[test]
fn libfluxcore_build_keeps_its_output_in_the_crate_target_dir() {
let package_dir = Path::new("/repo/fluxer_app/rust/libfluxcore");
@@ -817,14 +770,6 @@ export const MARKDOWN_PARSER_WASM_BASE64 =\n\
);
}
#[test]
fn libfluxcore_index_reexports_bindgen_module() {
let content = libfluxcore_index_js_content();
assert!(content.contains("import {crop_rotate_rgba_raw} from './libfluxcore_bindgen.js';"));
assert!(content.contains("export * from './libfluxcore_bindgen.js';"));
assert!(content.contains("export function crop_rotate_rgba("));
}
#[test]
fn libfluxcore_bindgen_js_reset_hook_is_inserted() {
let content =
+2 -17
View File
@@ -1,8 +1,8 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use crate::common::{
CALVER_SCHEME, CalverEnv, append_github_env, append_github_output, micro_segment,
month_day_segment, parse_version_instant, resolve_calver, trim_option,
CALVER_SCHEME, CalverEnv, append_github_output, micro_segment, month_day_segment,
parse_version_instant, resolve_calver, trim_option,
};
use anyhow::Result;
use chrono::{Datelike, Timelike, Utc};
@@ -13,10 +13,6 @@ use std::env;
pub struct ResolveCalverArgs {
#[arg(long)]
github_output: bool,
#[arg(long)]
github_env: bool,
#[arg(long, default_value = "BUILD_VERSION")]
env_name: String,
}
pub fn run(args: ResolveCalverArgs) -> Result<()> {
@@ -37,9 +33,6 @@ pub fn run(args: ResolveCalverArgs) -> Result<()> {
("calver_scheme", CALVER_SCHEME),
])?;
}
if args.github_env {
append_github_env(&[(args.env_name.as_str(), resolved.as_str())])?;
}
println!("{resolved}");
Ok(())
}
@@ -128,12 +121,4 @@ mod tests {
assert_eq!(version, "2026.109.0");
}
#[test]
fn calver_rejects_invalid_time() {
assert_eq!(
calver_outputs("2026.520.246000").unwrap_err().to_string(),
"Invalid build version date/time: 2026.520.246000"
);
}
}
-111
View File
@@ -333,115 +333,4 @@ mod tests {
assert_eq!(missing_app_wasm_artifact(root), None);
}
#[test]
fn image_dockerfiles_include_the_release_label_block() {
const REQUIRED: [&str; 8] = [
"LABEL org.opencontainers.image.vendor=\"Fluxer\"",
"LABEL org.opencontainers.image.url=\"https://fluxer.app\"",
"LABEL org.opencontainers.image.documentation=\"https://docs.fluxer.app\"",
"LABEL org.opencontainers.image.source=\"https://github.com/fluxerapp/fluxer\"",
"LABEL org.opencontainers.image.version=\"${BUILD_VERSION}\"",
"LABEL org.opencontainers.image.revision=\"${SOURCE_SHA}\"",
"LABEL org.opencontainers.image.created=\"${SOURCE_DATE}\"",
"LABEL app.fluxer.build-version=\"${BUILD_VERSION}\"",
];
for (name, title, dockerfile) in [
(
"fluxer_admin",
"fluxer-admin",
include_str!("../../../fluxer_admin/Dockerfile"),
),
(
"fluxer_api",
"fluxer-api",
include_str!("../../../fluxer_api/Dockerfile"),
),
(
"fluxer_app_proxy",
"fluxer-app-proxy",
include_str!("../../../fluxer_app_proxy/Dockerfile"),
),
(
"fluxer_docs",
"fluxer-docs",
include_str!("../../../fluxer_docs/Dockerfile"),
),
(
"fluxer_gateway",
"fluxer-gateway",
include_str!("../../../fluxer_gateway/Dockerfile"),
),
(
"fluxer_gifs",
"fluxer-gifs",
include_str!("../../../fluxer_gifs/Dockerfile"),
),
(
"fluxer_media_proxy",
"fluxer-media-proxy",
include_str!("../../../fluxer_media_proxy/Dockerfile"),
),
(
"fluxer_messages",
"fluxer-messages",
include_str!("../../../fluxer_messages/Dockerfile"),
),
(
"fluxer_push",
"fluxer-push",
include_str!("../../../fluxer_push/Dockerfile"),
),
(
"fluxer_snowflakes",
"fluxer-snowflakes",
include_str!("../../../fluxer_snowflakes/Dockerfile"),
),
(
"fluxer_static",
"fluxer-static",
include_str!("../../../fluxer_static/Dockerfile"),
),
(
"fluxer_unfurl",
"fluxer-unfurl",
include_str!("../../../fluxer_unfurl/Dockerfile"),
),
(
"fluxer_users",
"fluxer-users",
include_str!("../../../fluxer_users/Dockerfile"),
),
] {
assert!(
dockerfile.contains("ARG SOURCE_SHA"),
"{name}/Dockerfile must declare ARG SOURCE_SHA"
);
assert!(
dockerfile.contains("ARG SOURCE_DATE"),
"{name}/Dockerfile must declare ARG SOURCE_DATE"
);
assert!(
dockerfile.contains(&format!("LABEL org.opencontainers.image.title=\"{title}\"")),
"{name}/Dockerfile must declare the title {title}"
);
let licenses = match name {
"fluxer_static" => "AGPL-3.0-or-later AND CC-BY-SA-4.0 AND CC-BY-4.0",
_ => "AGPL-3.0-or-later",
};
assert!(
dockerfile.contains(&format!(
"LABEL org.opencontainers.image.licenses=\"{licenses}\""
)),
"{name}/Dockerfile must declare the licenses {licenses}"
);
for label in REQUIRED {
assert!(
dockerfile.contains(label),
"{name}/Dockerfile must declare {label}"
);
}
}
}
}
-93
View File
@@ -6077,14 +6077,6 @@ fn print_tree(root: &Path, max_depth: usize) -> Result<()> {
#[cfg(test)]
mod tests {
use super::*;
use crate::common::parse_version_instant;
use chrono::{DateTime, TimeZone, Utc};
fn dt(year: i32, month: u32, day: u32, hour: u32, minute: u32, second: u32) -> DateTime<Utc> {
Utc.with_ymd_and_hms(year, month, day, hour, minute, second)
.single()
.unwrap()
}
fn live_manifest(platform: &str, arch: &str, shell: &str) -> Value {
json!({
@@ -6249,40 +6241,6 @@ mod tests {
fs::write(path, contents).unwrap();
}
#[test]
fn resolves_explicit_calver_with_precedence() {
let calver_env = CalverEnv {
build_version: Some("2026.520.1".to_string()),
fluxer_build_version: Some("2026.521.2".to_string()),
fluxer_build_date: Some("2026-05-22T03:04:05Z".to_string()),
};
assert_eq!(
resolve_calver(&calver_env, dt(2026, 5, 1, 0, 0, 0)).unwrap(),
"2026.520.1"
);
}
#[test]
fn resolves_generated_calver_from_date_override() {
let calver_env = CalverEnv {
fluxer_build_date: Some("2026-05-20T01:02:03Z".to_string()),
..CalverEnv::default()
};
assert_eq!(
resolve_calver(&calver_env, dt(2026, 1, 1, 0, 0, 0)).unwrap(),
"2026.520.10203"
);
}
#[test]
fn rejects_invalid_explicit_time() {
let error = parse_version_instant("2026.520.246000").unwrap_err();
assert_eq!(
error.to_string(),
"Invalid build version date/time: 2026.520.246000"
);
}
#[test]
fn matrix_skip_flags_filter_individual_arches() {
let mut args = matrix_args();
@@ -6389,57 +6347,6 @@ mod tests {
.collect()
}
#[test]
fn every_build_desktop_workflow_step_dispatches_to_a_desktop_step() {
let steps = BUILD_DESKTOP_WORKFLOW
.lines()
.filter_map(|line| line.trim().strip_prefix("--step "))
.collect::<Vec<_>>();
assert!(steps.contains(&"stage_handoff"));
for step in steps {
assert!(
<DesktopStep as ValueEnum>::from_str(step, false).is_ok(),
"build-desktop.yaml dispatches unknown desktop step {step}"
);
}
assert!(matches!(
<DesktopStep as ValueEnum>::from_str("stage_handoff", false),
Ok(DesktopStep::StageHandoff)
));
}
#[test]
fn the_github_release_is_the_only_destination_for_built_artifacts() {
for job in ["build", "upload", "publish_release"] {
let body = workflow_job(job);
for forbidden in [
"S3_BUCKET",
"S3_ENDPOINT",
"AWS_ACCESS_KEY_ID",
"AWS_SECRET_ACCESS_KEY",
"DOWNLOADS_S3",
"_handoff/",
] {
assert!(
!body.contains(forbidden),
"{job} must not reference {forbidden} now that the downloads bucket is gone"
);
}
}
assert_eq!(
workflow_step_names(workflow_job("publish_release")),
vec![
"Checkout source",
"Set up Rust toolchain (CI helpers)",
"Download GitHub release assets",
"Create token",
"Publish GitHub desktop release",
]
);
}
#[test]
fn the_job_handoff_travels_as_github_actions_artifacts() {
let build = workflow_job("build");
+1 -57
View File
@@ -138,16 +138,6 @@ const DESKTOP_NATIVE_ADDONS: &[DesktopNativeAddon] = &[
pkg_config: &[],
special: DesktopNativeSpecialBuild::None,
},
DesktopNativeAddon {
package_dir: "linux-screen-capture",
package_name: "@fluxer/linux-screen-capture",
crate_name: "fluxer_linux_screen_capture",
node_file_stem: "linux-screen-capture",
required_platform: Some("linux"),
features: &[],
pkg_config: &[],
special: DesktopNativeSpecialBuild::None,
},
DesktopNativeAddon {
package_dir: "mac-app-audio",
package_name: "@fluxer/mac-app-audio",
@@ -1006,7 +996,7 @@ mod tests {
assert!(names.contains(&"win-game-capture"));
assert!(names.contains(&"app-store"));
assert!(names.contains(&"gateway-socket"));
assert_eq!(DESKTOP_NATIVE_ADDONS.len(), 23);
assert_eq!(DESKTOP_NATIVE_ADDONS.len(), 22);
}
#[test]
@@ -1095,45 +1085,6 @@ mod tests {
);
}
#[test]
fn platform_tags_match_legacy_node_helper() {
assert_eq!(platform_tag("linux", "x64").unwrap(), "linux-x64-gnu");
assert_eq!(platform_tag("darwin", "arm64").unwrap(), "darwin-arm64");
assert_eq!(platform_tag("win32", "x64").unwrap(), "win32-x64-msvc");
}
#[test]
fn rust_targets_match_legacy_node_helper() {
assert_eq!(
rust_target_for_platform("linux", "arm64").unwrap(),
"aarch64-unknown-linux-gnu"
);
assert_eq!(
rust_target_for_platform("darwin", "x64").unwrap(),
"x86_64-apple-darwin"
);
assert_eq!(
rust_target_for_platform("win32", "arm64").unwrap(),
"aarch64-pc-windows-msvc"
);
}
#[test]
fn dynamic_library_names_match_legacy_node_helper() {
assert_eq!(
cargo_dynamic_library_file_name("fluxer_webauthn", "linux").unwrap(),
"libfluxer_webauthn.so"
);
assert_eq!(
cargo_dynamic_library_file_name("fluxer-webauthn", "darwin").unwrap(),
"libfluxer_webauthn.dylib"
);
assert_eq!(
cargo_dynamic_library_file_name("fluxer_webauthn", "win32").unwrap(),
"fluxer_webauthn.dll"
);
}
#[test]
fn redistributable_runtime_import_detection_matches_legacy_patterns() {
assert!(is_redistributable_runtime_import("vcruntime140.dll"));
@@ -1210,11 +1161,4 @@ mod tests {
assert!(assert_system32_dependent_load_flag(&unset, "linux").is_ok());
}
}
#[test]
fn linux_library_bundling_denylist_matches_legacy_prefixes() {
assert!(!should_bundle_linux_library("libc.so.6"));
assert!(!should_bundle_linux_library("ld-linux-x86-64.so.2"));
assert!(should_bundle_linux_library("libfido2.so.1"));
}
}
-44
View File
@@ -51,47 +51,3 @@ pub(crate) fn sha256_file(path: &Path) -> Result<String> {
fs::File::open(path).with_context(|| format!("Failed to open {}", path.display()))?;
sha256_reader(file).with_context(|| format!("Failed to read {}", path.display()))
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn remove_file_if_exists_removes_files_and_ignores_missing_paths() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join("artifact.txt");
fs::write(&path, "artifact").unwrap();
remove_file_if_exists(&path).unwrap();
remove_file_if_exists(&path).unwrap();
assert!(!path.exists());
}
#[test]
fn remove_dir_if_exists_removes_directories_and_ignores_missing_paths() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join("dist");
fs::create_dir_all(path.join("nested")).unwrap();
fs::write(path.join("nested").join("artifact.txt"), "artifact").unwrap();
remove_dir_if_exists(&path).unwrap();
remove_dir_if_exists(&path).unwrap();
assert!(!path.exists());
}
#[test]
fn write_json_pretty_creates_parent_directories_and_writes_pretty_json() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join("nested").join("manifest.json");
write_json_pretty(&path, &json!({ "name": "fluxer", "version": 1 })).unwrap();
assert_eq!(
fs::read_to_string(path).unwrap(),
"{\n \"name\": \"fluxer\",\n \"version\": 1\n}\n"
);
}
}
-26
View File
@@ -37,7 +37,6 @@ pub(crate) enum GatewayStep {
Eqwalizer,
Typecheck,
Eunit,
Bench,
AllChecks,
Clean,
}
@@ -112,7 +111,6 @@ pub(crate) fn run_gateway_step(
GatewayStep::Eqwalizer => run_eqwalizer(gateway_dir, eqwalizer_profile),
GatewayStep::Typecheck => run_eqwalizer(gateway_dir, eqwalizer_profile),
GatewayStep::Eunit => run_rebar(gateway_dir, ["as", "test", "eunit"]),
GatewayStep::Bench => run_rebar(gateway_dir, ["eunit", "--module=guild_member_list_bench"]),
GatewayStep::AllChecks => {
run_gateway_check_step("Step 1/5: Format check (erlfmt)", || {
run_gateway_step(gateway_dir, GatewayStep::FmtCheck, eqwalizer_profile)
@@ -436,30 +434,6 @@ mod tests {
);
}
#[test]
fn gateway_nif_build_plan_matches_legacy_artifact_layout() {
let gateway_dir = Path::new("/repo/fluxer_gateway");
let builds = gateway_nif_builds(gateway_dir, NifBuildProfile::Release, "lib", "so");
assert_eq!(builds.len(), 2);
assert_eq!(
builds[0],
GatewayNifBuild {
crate_name: "push_markdown_plaintext_nif".to_string(),
native_dir: PathBuf::from(
"/repo/fluxer_gateway/native/push_markdown_plaintext_nif"
),
cargo_args: vec![OsString::from("build"), OsString::from("--release")],
artifact_path: PathBuf::from(
"/repo/fluxer_gateway/native/push_markdown_plaintext_nif/target/release/libpush_markdown_plaintext_nif.so"
),
output_path: PathBuf::from(
"/repo/fluxer_gateway/priv/push_markdown_plaintext_nif.so"
),
}
);
}
#[test]
fn debug_build_plan_omits_release_arg_and_uses_debug_target_dir() {
let build = gateway_nif_build(
-43
View File
@@ -919,29 +919,6 @@ mod tests {
assert_eq!(services.len(), 18);
}
#[test]
fn every_component_has_a_build_workflow() {
let directory = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../.github/workflows");
let mut workflows = String::new();
for entry in fs::read_dir(&directory).expect("the workflow directory should be readable") {
let path = entry.expect("the workflow entry should be readable").path();
if path
.extension()
.is_some_and(|extension| extension == "yaml")
{
workflows
.push_str(&fs::read_to_string(&path).expect("the workflow should be readable"));
}
}
for component in COMPONENTS {
assert!(
workflows.contains(component.image),
"{} has no build workflow",
component.image
);
}
}
#[test]
fn manifest_requires_every_component() {
let mut resolved = resolved_set();
@@ -1302,26 +1279,6 @@ mod tests {
assert!(error.contains("no bundle commit can be proven"), "{error}");
}
#[test]
fn release_workflow_tags_the_bundle_commit_and_marks_it_latest() {
let workflow = include_str!("../../../.github/workflows/release-image-set.yaml");
for entry in [
"--github-output",
"BUNDLE_COMMIT: ${{ steps.resolve.outputs.bundle_commit }}",
"--target \"${BUNDLE_COMMIT}\"",
"--latest=true",
] {
assert!(
workflow.contains(entry),
"release-image-set.yaml must contain {entry}"
);
}
assert!(
!workflow.contains("${GITHUB_SHA}"),
"the release tag must be cut at the bundle commit, not at the workflow ref"
);
}
#[test]
fn release_filenames_are_stable() {
assert_eq!(
-2
View File
@@ -31,7 +31,6 @@ enum Command {
AppDevServer(app_dev_server::AppDevServerArgs),
BuildAppWasm(app_wasm::BuildAppWasmArgs),
BuildAppProxy(app_proxy::BuildAppProxyArgs),
BuildMarkdownParserWasm(app_wasm::BuildMarkdownParserWasmArgs),
BuildDesktop(desktop::BuildDesktopArgs),
BuildDesktopNativeAddon(desktop_native::BuildDesktopNativeAddonArgs),
BuildGatewayNifs(gateway::BuildGatewayNifsArgs),
@@ -48,7 +47,6 @@ pub async fn run() -> Result<()> {
Command::AppDevServer(args) => app_dev_server::run(args).await,
Command::BuildAppWasm(args) => app_wasm::run_build_app_wasm(args),
Command::BuildAppProxy(args) => app_proxy::run(args).await,
Command::BuildMarkdownParserWasm(args) => app_wasm::run_build_markdown_parser_wasm(args),
Command::BuildDesktop(args) => desktop::run(args).await,
Command::BuildDesktopNativeAddon(args) => {
desktop_native::run_build_desktop_native_addon(args)
-45
View File
@@ -65,48 +65,3 @@ fn collapse_extra_blank_lines(source: &str) -> String {
}
output
}
#[cfg(test)]
mod tests {
use super::*;
fn write_file(path: &Path, contents: &str) {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).unwrap();
}
fs::write(path, contents).unwrap();
}
#[test]
fn find_import_start_only_matches_line_starts() {
assert_eq!(
find_import_start("// import nope\nimport {x} from 'x';\n"),
Some(15)
);
assert_eq!(find_import_start("const value = 'import nope';\n"), None);
}
#[test]
fn collapse_extra_blank_lines_keeps_at_most_one_blank_line() {
assert_eq!(collapse_extra_blank_lines("a\n\n\n\nb\n\nc"), "a\n\nb\n\nc");
}
#[test]
fn clean_generated_files_removes_prelude_and_compacts_blank_lines() {
let temp = tempfile::tempdir().unwrap();
let root = temp.path();
let file = root.join("generated.ts");
write_file(
&file,
"// header\n\n\nimport {x} from 'x';\n\n\n\nexport const y = x;\n\n",
);
write_file(&root.join("keep.txt"), "// header\n");
clean_generated_files(root).unwrap();
assert_eq!(
fs::read_to_string(file).unwrap(),
"import {x} from 'x';\n\nexport const y = x;\n"
);
}
}
-79
View File
@@ -1,79 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
export interface RgbaTransformResult {
rgba: Uint8Array;
width: number;
height: number;
}
export interface DecodedFrame {
rgba: Uint8Array;
width: number;
height: number;
delayMs: number;
}
export interface EncodedApngFrame {
compressed: Uint8Array;
width: number;
height: number;
delayMs: number;
}
export interface EncodedGifChunk {
data: Uint8Array;
width: number;
height: number;
}
export function assemble_apng_frames(frames: Array<EncodedApngFrame>): Uint8Array;
export function assemble_gif_frame_chunks(chunks: Array<EncodedGifChunk>): Uint8Array;
export function crop_and_rotate_apng(
input: Uint8Array,
x: number,
y: number,
width: number,
height: number,
rotation_deg: number,
resize_width?: number | null,
resize_height?: number | null,
): Uint8Array;
export function crop_and_rotate_gif(
input: Uint8Array,
x: number,
y: number,
width: number,
height: number,
rotation_deg: number,
resize_width?: number | null,
resize_height?: number | null,
): Uint8Array;
export function crop_and_rotate_image(
input: Uint8Array,
format_hint: string,
x: number,
y: number,
width: number,
height: number,
rotation_deg: number,
resize_width?: number | null,
resize_height?: number | null,
): Uint8Array;
export function crop_rotate_rgba(
input: Uint8Array,
src_width: number,
src_height: number,
x: number,
y: number,
width: number,
height: number,
rotation_deg: number,
resize_width?: number | null,
resize_height?: number | null,
): RgbaTransformResult;
export function decode_apng_frames(input: Uint8Array): Array<DecodedFrame>;
export function decode_gif_frames(input: Uint8Array): Array<DecodedFrame>;
export function encode_apng_frame_payload(frame: DecodedFrame): EncodedApngFrame;
export function encode_apng_frames(frames: Array<DecodedFrame>): Uint8Array;
export function encode_gif_frame_chunk(frame: DecodedFrame, first?: boolean): EncodedGifChunk;
export function encode_gif_frames(frames: Array<DecodedFrame>): Uint8Array;
-757
View File
@@ -1,757 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {encode as encodePng} from 'fast-png';
import {zlibSync} from 'fflate';
import {applyPalette, GIFEncoder, quantize} from 'gifenc';
import {decompressFrames, parseGIF} from 'gifuct-js';
import {decode as decodeJpeg, encode as encodeJpeg} from 'jpeg-js';
import UPNG from 'upng-js';
const textEncoder = new TextEncoder();
const NULL_U32 = 0xffffffff;
const RGBA_RESULT_HEADER_BYTES = 8;
const MAX_ANIMATION_PIXELS = 200_000_000;
const MAX_STATIC_DECODE_MEMORY_MB = 1024;
function inputBytes(input) {
if (input == null) return new Uint8Array();
return input instanceof Uint8Array ? input : new Uint8Array(input);
}
function arrayBufferFor(bytes) {
if (bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength) return bytes.buffer;
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
}
function nonNegativeU32(value) {
const number = Number(value);
if (!Number.isFinite(number) || number <= 0) return 0;
return Math.min(NULL_U32, Math.floor(number));
}
function cropCoordU32(value) {
const number = Number(value);
if (!Number.isFinite(number) || number <= 0) return 0;
return Math.min(NULL_U32, Math.floor(number));
}
function optionalDimension(value) {
const dimension = nonNegativeU32(value);
return dimension > 0 ? dimension : null;
}
function effectiveRotation(rotationDeg) {
const rotation = ((Math.floor(Number(rotationDeg) || 0) % 360) + 360) % 360;
return rotation === 90 || rotation === 180 || rotation === 270 ? rotation : 0;
}
function normalizedFormat(value) {
const format = String(value ?? '')
.trim()
.toLowerCase();
if (format === 'jpg') return 'jpeg';
if (format === 'apng') return 'png';
if (format === 'animated_webp') return 'webp';
return format;
}
function sniffImageFormat(input) {
const bytes = inputBytes(input);
if (
bytes.length >= 8 &&
bytes[0] === 0x89 &&
bytes[1] === 0x50 &&
bytes[2] === 0x4e &&
bytes[3] === 0x47 &&
bytes[4] === 0x0d &&
bytes[5] === 0x0a &&
bytes[6] === 0x1a &&
bytes[7] === 0x0a
) {
return 'png';
}
if (bytes.length >= 3 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) return 'jpeg';
if (
bytes.length >= 6 &&
bytes[0] === 0x47 &&
bytes[1] === 0x49 &&
bytes[2] === 0x46 &&
bytes[3] === 0x38 &&
(bytes[4] === 0x37 || bytes[4] === 0x39) &&
bytes[5] === 0x61
) {
return 'gif';
}
if (
bytes.length >= 12 &&
bytes[0] === 0x52 &&
bytes[1] === 0x49 &&
bytes[2] === 0x46 &&
bytes[3] === 0x46 &&
bytes[8] === 0x57 &&
bytes[9] === 0x45 &&
bytes[10] === 0x42 &&
bytes[11] === 0x50
) {
return 'webp';
}
if (
bytes.length >= 12 &&
bytes[4] === 0x66 &&
bytes[5] === 0x74 &&
bytes[6] === 0x79 &&
bytes[7] === 0x70 &&
bytes[8] === 0x61 &&
bytes[9] === 0x76 &&
bytes[10] === 0x69 &&
(bytes[11] === 0x66 || bytes[11] === 0x73)
) {
return 'avif';
}
return 'unknown';
}
function readU32FromBytes(bytes, offset) {
return new DataView(bytes.buffer, bytes.byteOffset + offset, 4).getUint32(0, true);
}
function readU16LE(bytes, offset) {
return bytes[offset] | (bytes[offset + 1] << 8);
}
function readU16BE(bytes, offset) {
return (bytes[offset] << 8) | bytes[offset + 1];
}
function readU32BE(bytes, offset) {
return (bytes[offset] * 0x1000000 + ((bytes[offset + 1] << 16) | (bytes[offset + 2] << 8) | bytes[offset + 3])) >>> 0;
}
function parseRgbaTransformResult(bytes) {
if (bytes.byteLength < RGBA_RESULT_HEADER_BYTES) throw new Error('libfluxcore returned a truncated RGBA result');
const width = readU32FromBytes(bytes, 0);
const height = readU32FromBytes(bytes, 4);
const expected = RGBA_RESULT_HEADER_BYTES + width * height * 4;
if (bytes.byteLength !== expected) throw new Error('libfluxcore returned an invalid RGBA result length');
return {rgba: bytes.subarray(RGBA_RESULT_HEADER_BYTES), width, height};
}
function pngDimensions(bytes) {
if (bytes.length < 24 || sniffImageFormat(bytes) !== 'png') return null;
if (bytes[12] !== 0x49 || bytes[13] !== 0x48 || bytes[14] !== 0x44 || bytes[15] !== 0x52) return null;
return {width: readU32BE(bytes, 16), height: readU32BE(bytes, 20)};
}
function gifDimensions(bytes) {
if (bytes.length < 10 || sniffImageFormat(bytes) !== 'gif') return null;
return {width: readU16LE(bytes, 6), height: readU16LE(bytes, 8)};
}
function jpegDimensions(bytes) {
if (bytes.length < 4 || bytes[0] !== 0xff || bytes[1] !== 0xd8) return null;
let offset = 2;
while (offset + 4 <= bytes.length) {
while (offset < bytes.length && bytes[offset] === 0xff) offset += 1;
if (offset >= bytes.length) return null;
const marker = bytes[offset];
offset += 1;
if (marker === 0xd9 || marker === 0xda) return null;
if (offset + 2 > bytes.length) return null;
const length = readU16BE(bytes, offset);
if (length < 2 || offset + length > bytes.length) return null;
if (
marker === 0xc0 ||
marker === 0xc1 ||
marker === 0xc2 ||
marker === 0xc3 ||
marker === 0xc5 ||
marker === 0xc6 ||
marker === 0xc7 ||
marker === 0xc9 ||
marker === 0xca ||
marker === 0xcb ||
marker === 0xcd ||
marker === 0xce ||
marker === 0xcf
) {
if (length < 7) return null;
return {height: readU16BE(bytes, offset + 3), width: readU16BE(bytes, offset + 5)};
}
offset += length;
}
return null;
}
function imageDimensions(bytes, format) {
switch (format) {
case 'png':
return pngDimensions(bytes);
case 'gif':
return gifDimensions(bytes);
case 'jpeg':
return jpegDimensions(bytes);
default:
return null;
}
}
function isNoopTransform(imageWidth, imageHeight, x, y, width, height, rotationDeg, resizeWidth, resizeHeight) {
const cropX = Math.min(cropCoordU32(x), imageWidth);
const cropY = Math.min(cropCoordU32(y), imageHeight);
const cropW = Math.min(nonNegativeU32(width), imageWidth - cropX);
const cropH = Math.min(nonNegativeU32(height), imageHeight - cropY);
const targetW = optionalDimension(resizeWidth) ?? imageWidth;
const targetH = optionalDimension(resizeHeight) ?? imageHeight;
return (
cropX === 0 &&
cropY === 0 &&
cropW === imageWidth &&
cropH === imageHeight &&
effectiveRotation(rotationDeg) === 0 &&
targetW === imageWidth &&
targetH === imageHeight
);
}
export function crop_rotate_rgba(
input,
src_width,
src_height,
x,
y,
width,
height,
rotation_deg,
resize_width,
resize_height,
) {
const sourceWidth = nonNegativeU32(src_width);
const sourceHeight = nonNegativeU32(src_height);
return parseRgbaTransformResult(
// biome-ignore lint/correctness/noUndeclaredVariables: Injected by the generated wasm wrapper at build time.
crop_rotate_rgba_raw(
inputBytes(input),
sourceWidth,
sourceHeight,
cropCoordU32(x),
cropCoordU32(y),
nonNegativeU32(width),
nonNegativeU32(height),
effectiveRotation(rotation_deg),
optionalDimension(resize_width),
optionalDimension(resize_height),
),
);
}
function transformFrame(frame, x, y, width, height, rotationDeg, resizeWidth, resizeHeight) {
const transformed = crop_rotate_rgba(
frame.rgba,
frame.width,
frame.height,
x,
y,
width,
height,
rotationDeg,
resizeWidth,
resizeHeight,
);
return {rgba: transformed.rgba, width: transformed.width, height: transformed.height, delayMs: frame.delayMs};
}
function transformFrames(frames, x, y, width, height, rotationDeg, resizeWidth, resizeHeight) {
let totalPixels = 0;
const transformed = frames.map((frame) => {
const out = transformFrame(frame, x, y, width, height, rotationDeg, resizeWidth, resizeHeight);
totalPixels += out.width * out.height;
if (totalPixels > MAX_ANIMATION_PIXELS) {
throw new Error('Animated image is too large to crop. Try reducing its dimensions or number of frames.');
}
return out;
});
return transformed;
}
function decodePngFrames(input) {
const bytes = inputBytes(input);
const decoded = UPNG.decode(arrayBufferFor(bytes));
const rgbaFrames = UPNG.toRGBA8(decoded);
if (!rgbaFrames.length) throw new Error('PNG has no frames');
return rgbaFrames.map((frame, index) => {
const delayMs = decoded.frames?.[index]?.delay ?? 0;
return {rgba: new Uint8Array(frame), width: decoded.width, height: decoded.height, delayMs};
});
}
function decodeJpegFrame(input) {
const decoded = decodeJpeg(inputBytes(input), {
useTArray: true,
formatAsRGBA: true,
maxMemoryUsageInMB: MAX_STATIC_DECODE_MEMORY_MB,
});
return {rgba: new Uint8Array(decoded.data), width: decoded.width, height: decoded.height, delayMs: 0};
}
function drawGifPatch(canvas, canvasWidth, frame) {
const dims = frame.dims;
const patch = frame.patch;
for (let row = 0; row < dims.height; row += 1) {
const canvasY = dims.top + row;
if (canvasY < 0) continue;
const canvasOffset = (canvasY * canvasWidth + dims.left) * 4;
const patchOffset = row * dims.width * 4;
if (canvasOffset < 0 || canvasOffset >= canvas.length) continue;
for (let col = 0; col < dims.width; col += 1) {
const source = patchOffset + col * 4;
const target = canvasOffset + col * 4;
if (target < 0 || target + 4 > canvas.length || source + 4 > patch.length) continue;
if (patch[source + 3] === 0) continue;
canvas[target] = patch[source];
canvas[target + 1] = patch[source + 1];
canvas[target + 2] = patch[source + 2];
canvas[target + 3] = patch[source + 3];
}
}
}
function clearRectRgba(canvas, canvasWidth, x, y, width, height) {
for (let row = 0; row < height; row += 1) {
const start = ((y + row) * canvasWidth + x) * 4;
const end = start + width * 4;
if (start >= 0 && end <= canvas.length) canvas.fill(0, start, end);
}
}
function collectGifFrames(input, transformOptions) {
const bytes = inputBytes(input);
const parsed = parseGIF(arrayBufferFor(bytes));
const screenWidth = parsed.lsd.width;
const screenHeight = parsed.lsd.height;
const decodedFrames = decompressFrames(parsed, true);
if (!decodedFrames.length) throw new Error('GIF has no frames');
const canvas = new Uint8Array(screenWidth * screenHeight * 4);
let previousCanvas = null;
const frames = [];
for (const frame of decodedFrames) {
if (frame.disposalType === 3) previousCanvas = canvas.slice();
drawGifPatch(canvas, screenWidth, frame);
const sourceFrame = {
rgba: transformOptions ? canvas : canvas.slice(),
width: screenWidth,
height: screenHeight,
delayMs: frame.delay || 100,
};
frames.push(
transformOptions
? transformFrame(
sourceFrame,
transformOptions.x,
transformOptions.y,
transformOptions.width,
transformOptions.height,
transformOptions.rotationDeg,
transformOptions.resizeWidth,
transformOptions.resizeHeight,
)
: sourceFrame,
);
if (frame.disposalType === 2) {
clearRectRgba(canvas, screenWidth, frame.dims.left, frame.dims.top, frame.dims.width, frame.dims.height);
} else if (frame.disposalType === 3 && previousCanvas) {
canvas.set(previousCanvas);
previousCanvas = null;
}
}
return frames;
}
function decodeGifFrames(input) {
return collectGifFrames(input, null);
}
function transformGifFrames(input, x, y, width, height, rotationDeg, resizeWidth, resizeHeight) {
return collectGifFrames(input, {x, y, width, height, rotationDeg, resizeWidth, resizeHeight});
}
function exactGifFrameData(rgba) {
const palette = [];
const colorToIndex = new Map();
const index = new Uint8Array(rgba.length / 4);
let transparentIndex = -1;
for (let offset = 0, pixel = 0; offset < rgba.length; offset += 4, pixel += 1) {
if (rgba[offset + 3] === 0) {
if (transparentIndex === -1) {
if (palette.length >= 256) return null;
transparentIndex = palette.length;
palette.push([0, 0, 0]);
}
index[pixel] = transparentIndex;
continue;
}
const key = `${rgba[offset]},${rgba[offset + 1]},${rgba[offset + 2]}`;
let paletteIndex = colorToIndex.get(key);
if (paletteIndex == null) {
if (palette.length >= 256) return null;
paletteIndex = palette.length;
colorToIndex.set(key, paletteIndex);
palette.push([rgba[offset], rgba[offset + 1], rgba[offset + 2]]);
}
index[pixel] = paletteIndex;
}
if (palette.length === 0) palette.push([0, 0, 0]);
return {index, palette, transparentIndex};
}
function quantizedGifFrameData(rgba) {
const input = rgba.byteOffset === 0 && rgba.byteLength === rgba.buffer.byteLength ? rgba : new Uint8Array(rgba);
const palette = quantize(input, 256, {format: 'rgba4444', oneBitAlpha: true});
const index = applyPalette(input, palette, 'rgba4444');
const transparentIndex = palette.findIndex((color) => color.length >= 4 && color[3] === 0);
const rgbPalette = palette.map((color) => [color[0], color[1], color[2]]);
return {index, palette: rgbPalette, transparentIndex};
}
function writeGifFrame(gif, frame, width, height, first) {
if (frame.width !== width || frame.height !== height) throw new Error('GIF frame dimensions must match');
const frameData = exactGifFrameData(frame.rgba) ?? quantizedGifFrameData(frame.rgba);
const transparent = frameData.transparentIndex >= 0;
gif.writeFrame(frameData.index, width, height, {
palette: frameData.palette,
delay: Math.max(0, Math.round(frame.delayMs || 0)),
repeat: 0,
transparent,
transparentIndex: transparent ? frameData.transparentIndex : 0,
first,
});
}
function encodeGifFrames(frames) {
if (!frames.length) throw new Error('GIF encode requires at least one frame');
const width = frames[0].width;
const height = frames[0].height;
const gif = GIFEncoder();
for (const frame of frames) writeGifFrame(gif, frame, width, height, false);
gif.finish();
return gif.bytes();
}
function encodeGifFrameChunk(frame, first) {
const gif = GIFEncoder({auto: false});
if (first) gif.writeHeader();
writeGifFrame(gif, frame, frame.width, frame.height, first);
return {data: gif.bytes(), width: frame.width, height: frame.height};
}
function assembleGifFrameChunks(chunks) {
if (!chunks.length) throw new Error('GIF chunk assembly requires at least one frame');
const width = chunks[0].width;
const height = chunks[0].height;
const parts = [];
for (const chunk of chunks) {
if (chunk.width !== width || chunk.height !== height) throw new Error('GIF frame dimensions must match');
parts.push(inputBytes(chunk.data));
}
parts.push(new Uint8Array([0x3b]));
return concatBytes(parts);
}
function writeU32BE(bytes, offset, value) {
bytes[offset] = (value >>> 24) & 0xff;
bytes[offset + 1] = (value >>> 16) & 0xff;
bytes[offset + 2] = (value >>> 8) & 0xff;
bytes[offset + 3] = value & 0xff;
}
function writeU16BE(bytes, offset, value) {
bytes[offset] = (value >>> 8) & 0xff;
bytes[offset + 1] = value & 0xff;
}
let pngCrcTable = null;
function crc32(bytes, start, end) {
if (!pngCrcTable) {
pngCrcTable = new Uint32Array(256);
for (let n = 0; n < 256; n += 1) {
let c = n;
for (let k = 0; k < 8; k += 1) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
pngCrcTable[n] = c >>> 0;
}
}
let c = 0xffffffff;
for (let index = start; index < end; index += 1) c = pngCrcTable[(c ^ bytes[index]) & 0xff] ^ (c >>> 8);
return (c ^ 0xffffffff) >>> 0;
}
function pngChunk(type, payload) {
const typeBytes = textEncoder.encode(type);
const chunk = new Uint8Array(12 + payload.length);
writeU32BE(chunk, 0, payload.length);
chunk.set(typeBytes, 4);
chunk.set(payload, 8);
writeU32BE(chunk, 8 + payload.length, crc32(chunk, 4, 8 + payload.length));
return chunk;
}
function concatBytes(parts) {
let total = 0;
for (const part of parts) total += part.length;
const output = new Uint8Array(total);
let offset = 0;
for (const part of parts) {
output.set(part, offset);
offset += part.length;
}
return output;
}
function pngScanlines(rgba, width, height) {
const rowBytes = width * 4;
const output = new Uint8Array((rowBytes + 1) * height);
for (let row = 0; row < height; row += 1) {
const target = row * (rowBytes + 1);
output[target] = 0;
output.set(rgba.subarray(row * rowBytes, row * rowBytes + rowBytes), target + 1);
}
return output;
}
function delayFraction(delayMs) {
const ms = Math.max(0, Math.round(Number(delayMs) || 0));
if (ms === 0) return [0, 100];
if (ms <= 655350) return [Math.min(65535, Math.max(1, Math.round(ms / 10))), 100];
return [Math.min(65535, Math.max(1, Math.round(ms / 1000))), 1];
}
function encodeApngFramePayload(frame) {
return {
compressed: zlibSync(pngScanlines(frame.rgba, frame.width, frame.height), {level: 6}),
width: frame.width,
height: frame.height,
delayMs: frame.delayMs,
};
}
function assembleApngFramePayloads(frames) {
if (!frames.length) throw new Error('APNG encode requires at least one frame');
const width = frames[0].width;
const height = frames[0].height;
const chunks = [new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])];
const ihdr = new Uint8Array(13);
writeU32BE(ihdr, 0, width);
writeU32BE(ihdr, 4, height);
ihdr[8] = 8;
ihdr[9] = 6;
chunks.push(pngChunk('IHDR', ihdr));
const actl = new Uint8Array(8);
writeU32BE(actl, 0, frames.length);
writeU32BE(actl, 4, 0);
chunks.push(pngChunk('acTL', actl));
let sequence = 0;
for (let index = 0; index < frames.length; index += 1) {
const frame = frames[index];
if (frame.width !== width || frame.height !== height) throw new Error('APNG frame dimensions must match');
const fctl = new Uint8Array(26);
writeU32BE(fctl, 0, sequence);
sequence += 1;
writeU32BE(fctl, 4, width);
writeU32BE(fctl, 8, height);
writeU32BE(fctl, 12, 0);
writeU32BE(fctl, 16, 0);
const delay = delayFraction(frame.delayMs);
writeU16BE(fctl, 20, delay[0]);
writeU16BE(fctl, 22, delay[1]);
fctl[24] = 0;
fctl[25] = 0;
chunks.push(pngChunk('fcTL', fctl));
if (index === 0) {
chunks.push(pngChunk('IDAT', inputBytes(frame.compressed)));
} else {
const compressed = inputBytes(frame.compressed);
const payload = new Uint8Array(4 + compressed.length);
writeU32BE(payload, 0, sequence);
sequence += 1;
payload.set(compressed, 4);
chunks.push(pngChunk('fdAT', payload));
}
}
chunks.push(pngChunk('IEND', new Uint8Array()));
return concatBytes(chunks);
}
function encodeApngFrames(frames) {
if (!frames.length) throw new Error('APNG encode requires at least one frame');
const width = frames[0].width;
const height = frames[0].height;
for (const frame of frames) {
if (frame.width !== width || frame.height !== height) throw new Error('APNG frame dimensions must match');
}
return assembleApngFramePayloads(frames.map(encodeApngFramePayload));
}
function encodeStaticFrame(frame, outputFormat) {
switch (outputFormat) {
case 'png':
return encodePng({width: frame.width, height: frame.height, data: frame.rgba, depth: 8, channels: 4});
case 'jpeg':
return encodeJpeg({width: frame.width, height: frame.height, data: frame.rgba}, 92).data;
case 'gif':
return encodeGifFrames([frame]);
default:
throw new Error(`Unsupported static output format: ${outputFormat}`);
}
}
function decodeStaticImage(input, inputFormat) {
switch (inputFormat) {
case 'png':
return decodePngFrames(input)[0];
case 'jpeg':
return decodeJpegFrame(input);
case 'gif':
return decodeGifFrames(input)[0];
default:
throw new Error(`Unsupported static input format: ${inputFormat}`);
}
}
function normalizeFrameInput(frame) {
if (!frame || frame.rgba == null) throw new Error('Frame is missing RGBA data');
return {
rgba: inputBytes(frame.rgba),
width: nonNegativeU32(frame.width),
height: nonNegativeU32(frame.height),
delayMs: Math.max(0, Math.round(Number(frame.delayMs) || 0)),
};
}
function normalizeApngPayloadInput(frame) {
if (!frame || frame.compressed == null) throw new Error('APNG frame is missing compressed data');
return {
compressed: inputBytes(frame.compressed),
width: nonNegativeU32(frame.width),
height: nonNegativeU32(frame.height),
delayMs: Math.max(0, Math.round(Number(frame.delayMs) || 0)),
};
}
function normalizeGifChunkInput(chunk) {
if (!chunk || chunk.data == null) throw new Error('GIF frame chunk is missing data');
return {
data: inputBytes(chunk.data),
width: nonNegativeU32(chunk.width),
height: nonNegativeU32(chunk.height),
};
}
export function decode_gif_frames(input) {
return decodeGifFrames(input);
}
export function decode_apng_frames(input) {
return decodePngFrames(input);
}
export function encode_gif_frames(frames) {
return encodeGifFrames(frames.map(normalizeFrameInput));
}
export function encode_apng_frames(frames) {
return encodeApngFrames(frames.map(normalizeFrameInput));
}
export function encode_gif_frame_chunk(frame, first) {
return encodeGifFrameChunk(normalizeFrameInput(frame), Boolean(first));
}
export function assemble_gif_frame_chunks(chunks) {
return assembleGifFrameChunks(chunks.map(normalizeGifChunkInput));
}
export function encode_apng_frame_payload(frame) {
return encodeApngFramePayload(normalizeFrameInput(frame));
}
export function assemble_apng_frames(frames) {
return assembleApngFramePayloads(frames.map(normalizeApngPayloadInput));
}
export function crop_and_rotate_apng(input, x, y, width, height, rotation_deg, resize_width, resize_height) {
const bytes = inputBytes(input);
const dimensions = pngDimensions(bytes);
if (
dimensions &&
isNoopTransform(dimensions.width, dimensions.height, x, y, width, height, rotation_deg, resize_width, resize_height)
) {
return bytes.slice();
}
const frames = decodePngFrames(bytes);
if (
isNoopTransform(frames[0].width, frames[0].height, x, y, width, height, rotation_deg, resize_width, resize_height)
) {
return bytes.slice();
}
return encodeApngFrames(transformFrames(frames, x, y, width, height, rotation_deg, resize_width, resize_height));
}
export function crop_and_rotate_gif(input, x, y, width, height, rotation_deg, resize_width, resize_height) {
const bytes = inputBytes(input);
const dimensions = gifDimensions(bytes);
if (
dimensions &&
isNoopTransform(dimensions.width, dimensions.height, x, y, width, height, rotation_deg, resize_width, resize_height)
) {
return bytes.slice();
}
return encodeGifFrames(transformGifFrames(bytes, x, y, width, height, rotation_deg, resize_width, resize_height));
}
export function crop_and_rotate_image(
input,
format_hint,
x,
y,
width,
height,
rotation_deg,
resize_width,
resize_height,
) {
const bytes = inputBytes(input);
const inputFormat = sniffImageFormat(bytes);
const requestedFormat = normalizedFormat(format_hint);
const outputFormat = requestedFormat && requestedFormat !== 'unknown' ? requestedFormat : inputFormat;
if (inputFormat === 'webp' || inputFormat === 'avif' || outputFormat === 'webp' || outputFormat === 'avif') {
throw new Error('WebP and AVIF crop/encode use the browser or native media bridge');
}
if (inputFormat === outputFormat) {
const dimensions = imageDimensions(bytes, inputFormat);
if (
dimensions &&
isNoopTransform(
dimensions.width,
dimensions.height,
x,
y,
width,
height,
rotation_deg,
resize_width,
resize_height,
)
) {
return bytes.slice();
}
}
const frame = decodeStaticImage(bytes, inputFormat);
if (
inputFormat === outputFormat &&
isNoopTransform(frame.width, frame.height, x, y, width, height, rotation_deg, resize_width, resize_height)
) {
return bytes.slice();
}
return encodeStaticFrame(
transformFrame(frame, x, y, width, height, rotation_deg, resize_width, resize_height),
outputFormat,
);
}