mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 13:05:32 +09:00
231 lines
7.2 KiB
Rust
231 lines
7.2 KiB
Rust
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
pub const MAX_MEDIA_PROXY_BYTES: usize = 500 * 1024 * 1024;
|
|
pub const DESKTOP_APP_ORIGIN: &str = "fluxer-app://app";
|
|
|
|
pub const OUTBOUND_USER_AGENT: &str =
|
|
"Mozilla/5.0 (compatible; Fluxerbot/1.0; +https://fluxer.app)";
|
|
pub const MAX_MEDIA_IMAGE_DIMENSION_DEFAULT: u32 = 16_384;
|
|
pub const MAX_MEDIA_IMAGE_PIXELS_DEFAULT: usize =
|
|
MAX_MEDIA_IMAGE_DIMENSION_DEFAULT as usize * MAX_MEDIA_IMAGE_DIMENSION_DEFAULT as usize;
|
|
pub const MAX_INTERNAL_REQUEST_BODY_BYTES: usize =
|
|
MAX_MEDIA_PROXY_BYTES.div_ceil(3) * 4 + 1024 * 1024;
|
|
pub const MAX_VIDEO_PACKETS_FOR_THUMBNAIL: usize = 512;
|
|
pub const DEFAULT_IMAGE_SIZE: u32 = 128;
|
|
pub const MAX_ANIMATED_FRAMES_DEFAULT: u32 = 20_000;
|
|
pub const MAX_ANIMATED_TOTAL_PIXELS_DEFAULT: usize = 4 * MAX_MEDIA_IMAGE_PIXELS_DEFAULT;
|
|
pub const MAX_JXL_DECODE_PIXELS: usize = 4096 * 4096;
|
|
pub const MAX_STATIC_OUTPUT_PIXELS: usize = 6144 * 4096;
|
|
pub const MAX_ANIMATED_OUTPUT_PIXELS: usize = 16 << 20;
|
|
pub const SVG_RASTER_MAX_DIMENSION: u32 = 4096;
|
|
pub const LARGE_SOURCE_LOG_PIXELS: usize = 4096 * 4096;
|
|
const _: () = assert!(MAX_ANIMATED_FRAMES_DEFAULT >= 20_000);
|
|
|
|
pub const IMAGE_SIZES: &[u32] = &[
|
|
16, 20, 22, 24, 28, 32, 40, 44, 48, 56, 60, 64, 80, 96, 100, 128, 160, 240, 256, 300, 320, 480,
|
|
512, 600, 640, 1024, 1280, 1536, 2048, 3072, 4096, 8192, 16384,
|
|
];
|
|
|
|
pub fn snap_to_image_ladder(value: u32) -> u32 {
|
|
let largest = IMAGE_SIZES[IMAGE_SIZES.len() - 1];
|
|
IMAGE_SIZES
|
|
.iter()
|
|
.copied()
|
|
.find(|rung| value <= *rung)
|
|
.unwrap_or(largest)
|
|
}
|
|
|
|
pub fn parse_image_size(raw: Option<&str>) -> u32 {
|
|
let Some(text) = raw else {
|
|
return DEFAULT_IMAGE_SIZE;
|
|
};
|
|
let Ok(value) = text.parse::<u32>() else {
|
|
return DEFAULT_IMAGE_SIZE;
|
|
};
|
|
snap_to_image_ladder(value)
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
|
|
pub enum AssetKind {
|
|
Avatar,
|
|
GuildIcon,
|
|
Banner,
|
|
Splash,
|
|
EmbedSplash,
|
|
Emoji,
|
|
Sticker,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
|
|
pub enum AssetExtension {
|
|
Png,
|
|
Jpeg,
|
|
Webp,
|
|
Gif,
|
|
Apng,
|
|
Avif,
|
|
Heic,
|
|
Heif,
|
|
Jxl,
|
|
Svg,
|
|
}
|
|
|
|
impl AssetExtension {
|
|
pub fn parse(raw: &str) -> Option<Self> {
|
|
match raw.to_ascii_lowercase().as_str() {
|
|
"png" => Some(Self::Png),
|
|
"jpg" | "jpeg" => Some(Self::Jpeg),
|
|
"webp" => Some(Self::Webp),
|
|
"gif" => Some(Self::Gif),
|
|
"apng" => Some(Self::Apng),
|
|
"avif" => Some(Self::Avif),
|
|
"heic" => Some(Self::Heic),
|
|
"heif" => Some(Self::Heif),
|
|
"jxl" => Some(Self::Jxl),
|
|
"svg" => Some(Self::Svg),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub fn name(self) -> &'static str {
|
|
match self {
|
|
Self::Png => "png",
|
|
Self::Jpeg => "jpeg",
|
|
Self::Webp => "webp",
|
|
Self::Gif => "gif",
|
|
Self::Apng => "apng",
|
|
Self::Avif => "avif",
|
|
Self::Heic => "heic",
|
|
Self::Heif => "heif",
|
|
Self::Jxl => "jxl",
|
|
Self::Svg => "svg",
|
|
}
|
|
}
|
|
|
|
pub fn mime(self) -> &'static str {
|
|
match self {
|
|
Self::Png => "image/png",
|
|
Self::Jpeg => "image/jpeg",
|
|
Self::Webp => "image/webp",
|
|
Self::Gif => "image/gif",
|
|
Self::Apng => "image/apng",
|
|
Self::Avif => "image/avif",
|
|
Self::Heic => "image/heic",
|
|
Self::Heif => "image/heif",
|
|
Self::Jxl => "image/jxl",
|
|
Self::Svg => "image/svg+xml",
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub struct Dims {
|
|
pub min: u32,
|
|
pub max: u32,
|
|
}
|
|
|
|
pub fn dims_for(kind: AssetKind) -> Dims {
|
|
match kind {
|
|
AssetKind::Avatar | AssetKind::GuildIcon => Dims {
|
|
min: 128,
|
|
max: 1024,
|
|
},
|
|
AssetKind::Banner | AssetKind::Splash | AssetKind::EmbedSplash => Dims {
|
|
min: 480,
|
|
max: 2400,
|
|
},
|
|
AssetKind::Emoji => Dims { min: 32, max: 512 },
|
|
AssetKind::Sticker => Dims { min: 128, max: 512 },
|
|
}
|
|
}
|
|
|
|
pub fn clamp_size(raw_target: u32, kind: AssetKind) -> u32 {
|
|
let value = raw_target.max(1);
|
|
let dims = dims_for(kind);
|
|
value.clamp(dims.min, dims.max)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn asset_cache_key(raw: Option<&str>, kind: AssetKind) -> u32 {
|
|
clamp_size(parse_image_size(raw), kind)
|
|
}
|
|
|
|
#[test]
|
|
fn image_size_snaps_up_to_the_next_rung() {
|
|
assert_eq!(128, parse_image_size(None));
|
|
assert_eq!(640, parse_image_size(Some("640")));
|
|
assert_eq!(1024, parse_image_size(Some("641")));
|
|
assert_eq!(1024, parse_image_size(Some("1000")));
|
|
assert_eq!(16, parse_image_size(Some("0")));
|
|
assert_eq!(16384, parse_image_size(Some("99999")));
|
|
assert_eq!(128, parse_image_size(Some("not-a-number")));
|
|
assert_eq!(128, parse_image_size(Some("")));
|
|
assert_eq!(128, parse_image_size(Some("-1")));
|
|
assert_eq!(128, parse_image_size(Some("99999999999")));
|
|
}
|
|
|
|
#[test]
|
|
fn off_ladder_size_never_serves_fewer_pixels_than_requested() {
|
|
for requested in 1..=4096u32 {
|
|
let served = parse_image_size(Some(&requested.to_string()));
|
|
assert!(
|
|
served >= requested,
|
|
"size={requested} served {served}, a silent downscale"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn every_rung_snaps_to_itself() {
|
|
for rung in IMAGE_SIZES {
|
|
assert_eq!(*rung, parse_image_size(Some(&rung.to_string())));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn snapping_is_idempotent() {
|
|
for raw in [0u32, 1, 17, 641, 1000, 4097, 99999] {
|
|
let once = parse_image_size(Some(&raw.to_string()));
|
|
let twice = parse_image_size(Some(&once.to_string()));
|
|
assert_eq!(once, twice, "size={raw} did not settle");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn sub_minimum_sizes_collapse_to_one_avatar_cache_key() {
|
|
let canonical = asset_cache_key(Some("128"), AssetKind::Avatar);
|
|
assert_eq!(128, canonical);
|
|
for below in IMAGE_SIZES.iter().take_while(|rung| **rung < 128) {
|
|
assert_eq!(
|
|
canonical,
|
|
asset_cache_key(Some(&below.to_string()), AssetKind::Avatar),
|
|
"size={below} minted a second avatar cache key"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn sub_minimum_sizes_collapse_to_one_banner_cache_key() {
|
|
let canonical = asset_cache_key(Some("480"), AssetKind::Banner);
|
|
assert_eq!(480, canonical);
|
|
for below in IMAGE_SIZES.iter().take_while(|rung| **rung < 480) {
|
|
assert_eq!(
|
|
canonical,
|
|
asset_cache_key(Some(&below.to_string()), AssetKind::Banner),
|
|
"size={below} minted a second banner cache key"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn oversize_requests_collapse_onto_the_kind_maximum() {
|
|
assert_eq!(1024, asset_cache_key(Some("99999"), AssetKind::Avatar));
|
|
assert_eq!(1024, asset_cache_key(Some("1024"), AssetKind::Avatar));
|
|
assert_eq!(512, asset_cache_key(Some("99999"), AssetKind::Emoji));
|
|
assert_eq!(512, asset_cache_key(Some("99999"), AssetKind::Sticker));
|
|
}
|
|
}
|