mirror of
https://github.com/TheFunny/TelegramTwitterMediaBot.git
synced 2026-09-30 00:22:08 +00:00
refactor(photo): one plan decides within-limits and decode budget
decode_budget_bytes mirrored the within-limits early return and the MAX_DECODE_BYTES guard that prepare_png and prepare_jpeg each spelled out inline (three copies of the same arithmetic, which the reservation and the branches had to keep in sync by hand). PhotoPlan/plan_photo now hold that decision once and all three call it; the log order and the exact bounds are unchanged. decode_budget_follows_the_processing_decision still pins the reservation against the branches.
This commit is contained in:
@@ -88,30 +88,56 @@ async fn reserve(
|
|||||||
.expect("memory budget semaphore closed")
|
.expect("memory budget semaphore closed")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The decode buffer a downloaded photo will allocate, from its header alone —
|
/// The processing decision for one downloaded photo, taken from its header
|
||||||
/// zero when it is already within Telegram's limits and is uploaded as-is, zero
|
/// alone — the one place the within-limits test and the decode-size guard
|
||||||
/// for a format [`prepare_photo`] does not decode. Mirrors the early return and
|
/// live, so the memory reservation and the branch that acts on it cannot
|
||||||
/// the guard of the two branches below.
|
/// drift.
|
||||||
pub(crate) fn decode_budget_bytes(bytes: &[u8]) -> u64 {
|
enum PhotoPlan {
|
||||||
if let Some((w, h, _depth, color)) = parse_png_header(bytes) {
|
/// Already within Telegram's limits (dimension sum and upload cap): the
|
||||||
if within_limits(w, h, bytes) {
|
/// downloaded file is uploaded untouched, no decode buffer.
|
||||||
return 0;
|
AsIs,
|
||||||
}
|
/// Needs processing: the decode buffer it will allocate, in bytes.
|
||||||
return decode_bytes(w, h, output_channels(color));
|
Decode(u64),
|
||||||
}
|
/// Processing would need a decode buffer over [`MAX_DECODE_BYTES`]: the
|
||||||
if let Some((w, h)) = jpeg_dims(bytes) {
|
/// caller falls back to the item's smaller URL.
|
||||||
if within_limits(w, h, bytes) {
|
TooLarge,
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return decode_bytes(w, h, 3);
|
|
||||||
}
|
|
||||||
0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether a photo is uploaded untouched (Telegram's dimension sum, and the
|
/// [`PhotoPlan`] for a photo whose header said `w`×`h` in `channels` output
|
||||||
/// upload cap its bytes are compared against).
|
/// channels, `len` bytes long.
|
||||||
fn within_limits(w: u32, h: u32, bytes: &[u8]) -> bool {
|
fn plan_photo(w: u32, h: u32, len: usize, channels: usize) -> PhotoPlan {
|
||||||
w + h <= PHOTO_MAX_DIMENSION_SUM && bytes.len() as u64 <= MAX_UPLOAD_BYTES
|
if w + h <= PHOTO_MAX_DIMENSION_SUM && len as u64 <= MAX_UPLOAD_BYTES {
|
||||||
|
return PhotoPlan::AsIs;
|
||||||
|
}
|
||||||
|
let bytes = decode_bytes(w, h, channels);
|
||||||
|
if bytes > MAX_DECODE_BYTES {
|
||||||
|
PhotoPlan::TooLarge
|
||||||
|
} else {
|
||||||
|
PhotoPlan::Decode(bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`PhotoPlan`] from the downloaded bytes: the PNG or JPEG header decides
|
||||||
|
/// (palette counted as RGB, which `EXPAND` produces); anything else is uploaded
|
||||||
|
/// as-is, since [`prepare_photo`] does not decode it.
|
||||||
|
fn photo_plan(bytes: &[u8]) -> PhotoPlan {
|
||||||
|
if let Some((w, h, _depth, color)) = parse_png_header(bytes) {
|
||||||
|
return plan_photo(w, h, bytes.len(), output_channels(color));
|
||||||
|
}
|
||||||
|
if let Some((w, h)) = jpeg_dims(bytes) {
|
||||||
|
return plan_photo(w, h, bytes.len(), 3);
|
||||||
|
}
|
||||||
|
PhotoPlan::AsIs
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The decode buffer a downloaded photo will allocate, from its header alone —
|
||||||
|
/// zero when it is already within Telegram's limits and is uploaded as-is, zero
|
||||||
|
/// for a format [`prepare_photo`] does not decode.
|
||||||
|
pub(crate) fn decode_budget_bytes(bytes: &[u8]) -> u64 {
|
||||||
|
match photo_plan(bytes) {
|
||||||
|
PhotoPlan::Decode(bytes) => bytes,
|
||||||
|
PhotoPlan::AsIs | PhotoPlan::TooLarge => 0,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// JPEG dimensions from the headers, without decoding any pixels.
|
/// JPEG dimensions from the headers, without decoding any pixels.
|
||||||
@@ -314,17 +340,16 @@ fn target_dims(w: u32, h: u32) -> (u32, u32) {
|
|||||||
/// over the upload cap afterwards becomes JPEG.
|
/// over the upload cap afterwards becomes JPEG.
|
||||||
fn prepare_png(file: NamedTempFile, bytes: &[u8]) -> Result<PhotoPrep, String> {
|
fn prepare_png(file: NamedTempFile, bytes: &[u8]) -> Result<PhotoPrep, String> {
|
||||||
let (w, h, _bit_depth, color_type) = parse_png_header(bytes).ok_or("invalid PNG header")?;
|
let (w, h, _bit_depth, color_type) = parse_png_header(bytes).ok_or("invalid PNG header")?;
|
||||||
let size_over = bytes.len() as u64 > MAX_UPLOAD_BYTES;
|
let channels = output_channels(color_type);
|
||||||
if w + h <= PHOTO_MAX_DIMENSION_SUM && !size_over {
|
let plan = plan_photo(w, h, bytes.len(), channels);
|
||||||
|
if let PhotoPlan::AsIs = plan {
|
||||||
return Ok(PhotoPrep::Upload(file));
|
return Ok(PhotoPrep::Upload(file));
|
||||||
}
|
}
|
||||||
log::debug!(
|
log::debug!(
|
||||||
"photo {w}x{h} ({_bit_depth:?} {color_type:?}, {} bytes) needs processing",
|
"photo {w}x{h} ({_bit_depth:?} {color_type:?}, {} bytes) needs processing",
|
||||||
bytes.len()
|
bytes.len()
|
||||||
);
|
);
|
||||||
|
if let PhotoPlan::TooLarge = plan {
|
||||||
let channels = output_channels(color_type);
|
|
||||||
if decode_bytes(w, h, channels) > MAX_DECODE_BYTES {
|
|
||||||
log::warn!("photo decode buffer exceeds the memory budget; falling back to smaller media");
|
log::warn!("photo decode buffer exceeds the memory budget; falling back to smaller media");
|
||||||
return Ok(PhotoPrep::UseFallback);
|
return Ok(PhotoPrep::UseFallback);
|
||||||
}
|
}
|
||||||
@@ -391,11 +416,11 @@ fn prepare_jpeg(file: NamedTempFile, bytes: &[u8]) -> Result<PhotoPrep, String>
|
|||||||
.map_err(|e| format!("jpeg headers: {e}"))?;
|
.map_err(|e| format!("jpeg headers: {e}"))?;
|
||||||
let info = decoder.info().ok_or("jpeg info unavailable")?;
|
let info = decoder.info().ok_or("jpeg info unavailable")?;
|
||||||
let (w, h) = (info.width as u32, info.height as u32);
|
let (w, h) = (info.width as u32, info.height as u32);
|
||||||
let size_over = bytes.len() as u64 > MAX_UPLOAD_BYTES;
|
let plan = plan_photo(w, h, bytes.len(), 3);
|
||||||
if w + h <= PHOTO_MAX_DIMENSION_SUM && !size_over {
|
if let PhotoPlan::AsIs = plan {
|
||||||
return Ok(PhotoPrep::Upload(file));
|
return Ok(PhotoPrep::Upload(file));
|
||||||
}
|
}
|
||||||
if decode_bytes(w, h, 3) > MAX_DECODE_BYTES {
|
if let PhotoPlan::TooLarge = plan {
|
||||||
log::warn!("photo decode buffer exceeds the memory budget; falling back to smaller media");
|
log::warn!("photo decode buffer exceeds the memory budget; falling back to smaller media");
|
||||||
return Ok(PhotoPrep::UseFallback);
|
return Ok(PhotoPrep::UseFallback);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user