use crate::{error::to_io_error, utils::Internable}; use base64::{engine::general_purpose, Engine}; use chrono::{NaiveDateTime, ParseError, TimeZone, Utc}; use serde::{de::Error, Deserialize, Deserializer, Serialize, Serializer}; use serde_json::Value; use std::{io, sync::Arc}; /// Formats a JSON number into a stack buffer and parses it, without touching the heap. /// /// `serde_json` renders numbers via `itoa`/`ryu`, so the longest possible output is /// `-1.7976931348623157e-308` at 24 bytes; 32 leaves headroom. Deliberately takes /// `&Number` rather than `impl Display` — `Display for f64` writes `f64::MIN` as 310 /// bytes of decimal expansion, which would silently truncate to `None`. struct StackNumber { buf: [u8; 32], len: usize, } impl std::fmt::Write for StackNumber { fn write_str(&mut self, text: &str) -> std::fmt::Result { let end = self.len.checked_add(text.len()).ok_or(std::fmt::Error)?; self.buf.get_mut(self.len..end).ok_or(std::fmt::Error)?.copy_from_slice(text.as_bytes()); self.len = end; Ok(()) } } fn parse_number(number: &serde_json::Number) -> Option { use std::fmt::Write; let mut out = StackNumber { buf: [0; 32], len: 0 }; write!(out, "{number}").ok()?; std::str::from_utf8(out.buf.get(..out.len)?).ok()?.parse().ok() } fn value_to_string_array(value: &[Value]) -> Vec> { value.iter().filter_map(value_to_arc_str).collect() } fn value_to_arc_str(v: &Value) -> Option> { match v { Value::Bool(value) => Some(value.to_string().intern()), Value::Number(value) => Some(value.to_string().intern()), Value::String(value) => Some(value.intern()), _ => None, } } pub fn string_default_on_null<'de, D>(deserializer: D) -> Result where D: serde::Deserializer<'de>, { let value: Option = Option::deserialize(deserializer)?; Ok(value.unwrap_or_default()) } pub fn deserialize_as_option_string<'de, D>(deserializer: D) -> Result, D::Error> where D: serde::Deserializer<'de>, { let value: Value = serde::Deserialize::deserialize(deserializer)?; match &value { Value::String(s) => Ok(Some(s.to_owned())), Value::Number(s) => Ok(Some(s.to_string())), _ => Ok(None), } } pub fn serialize_option_string_as_null_if_empty(value: &Option, serializer: S) -> Result where T: AsRef, S: serde::Serializer, { match value { None => serializer.serialize_none(), Some(s) if s.as_ref().is_empty() => serializer.serialize_none(), Some(s) => serializer.serialize_str(s.as_ref()), } } pub fn deserialize_as_string<'de, D>(deserializer: D) -> Result where D: serde::Deserializer<'de>, { let value: Value = serde::Deserialize::deserialize(deserializer)?; match &value { Value::String(s) => Ok(s.clone()), Value::Number(s) => Ok(s.to_string()), Value::Null => Ok(String::new()), _ => Ok(value.to_string()), } } pub fn deserialize_as_string_array<'de, D>(deserializer: D) -> Result>>, D::Error> where D: serde::Deserializer<'de>, { Value::deserialize(deserializer).map(|v| match v { Value::String(value) => Some(vec![value.intern()]), Value::Array(value) => Some(value_to_string_array(&value)), _ => None, }) } pub fn deserialize_number_from_string<'de, D, T>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, T: std::str::FromStr, { let raw: Value = Value::deserialize(deserializer)?; match raw { // Null -> None Value::Null => Ok(None), // its a number Value::Number(n) => Ok(parse_number(&n)), // String -> extract first number Value::String(s) => { let s = s.trim(); if s.is_empty() { return Ok(None); } // Find first digit OR a '.' that is immediately followed by a digit; // include optional sign immediately before it (ignoring whitespace). let mut last_non_ws: Option<(usize, char)> = None; let mut num_pos: Option = None; for (i, c) in s.char_indices() { if c.is_ascii_digit() { num_pos = Some(i); break; } if c == '.' && s[i + 1..].chars().next().is_some_and(|n| n.is_ascii_digit()) { num_pos = Some(i); break; } if !c.is_whitespace() { last_non_ws = Some((i, c)); } } let Some(num_i) = num_pos else { return Ok(None); }; let start = match last_non_ws { Some((i, '-' | '+')) => i, _ => num_i, }; let mut it = s[start..].chars().peekable(); // optional sign let mut out = String::new(); if matches!(it.peek(), Some('-' | '+')) { if let Some(val) = it.next() { out.push(val); } while matches!(it.peek(), Some(c) if c.is_whitespace()) { it.next(); } } // digits + optional single dot let mut saw_digit = false; let mut saw_dot = false; while let Some(&c) = it.peek() { if c.is_ascii_digit() { saw_digit = true; out.push(c); it.next(); continue; } if c == '.' && !saw_dot { saw_dot = true; out.push(c); it.next(); continue; } break; } if !saw_digit { return Ok(None); } // Try full parse; if it fails and we included '.', fall back to integer part. if let Ok(v) = out.parse::() { return Ok(Some(v)); } if saw_dot { let int_part = out.split('.').next().unwrap_or(""); if !int_part.is_empty() && int_part != "-" && int_part != "+" { if let Ok(v) = int_part.parse::() { return Ok(Some(v)); } } } Ok(None) } // invalid -> return None _ => Ok(None), } } pub fn deserialize_number_from_string_or_zero<'de, D, T>(deserializer: D) -> Result where D: Deserializer<'de>, T: std::str::FromStr + Default, { match deserialize_number_from_string(deserializer) { Ok(Some(v)) => Ok(v), Ok(None) => Ok(T::default()), Err(e) => Err(e), } } #[inline] pub fn bin_serialize(value: &T) -> io::Result> where T: serde::Serialize, { let mut buf = Vec::new(); ciborium::ser::into_writer(value, &mut buf).map_err(to_io_error)?; Ok(buf) } #[inline] pub fn bin_deserialize(value: &[u8]) -> io::Result where T: for<'a> serde::Deserialize<'a>, { ciborium::de::from_reader(value).map_err(to_io_error) } pub fn u8_16_to_hex(bytes: &[u8; 16]) -> String { bytes.iter().map(|b| format!("{b:02X}")).collect() } pub fn hex_to_u8_16(hex: &str) -> Result<[u8; 16], String> { if hex.len() != 32 { return Err("Hex string must be exactly 32 characters".into()); } let mut out = [0u8; 16]; for (i, chunk) in hex.as_bytes().chunks(2).enumerate() { let s = std::str::from_utf8(chunk).map_err(|_| "Invalid UTF-8")?; out[i] = u8::from_str_radix(s, 16).map_err(|_| "Invalid hex")?; } Ok(out) } pub fn hex_to_secret<'de, D>(deserializer: D) -> Result<[u8; 16], D::Error> where D: serde::Deserializer<'de>, { let s: String = Deserialize::deserialize(deserializer)?; hex_to_u8_16(&s).map_err(serde::de::Error::custom) } pub fn secret_to_hex(bytes: &[u8; 16], serializer: S) -> Result where S: serde::Serializer, { serializer.serialize_str(&u8_16_to_hex(bytes)) } /// Deserializes a timestamp from either a Unix timestamp (seconds) or a UTC datetime string /// in the format "YYYY-MM-DD HH:MM:SS". Note: Datetime strings are interpreted as UTC. pub fn deserialize_timestamp<'de, D>(deserializer: D) -> Result, D::Error> where D: serde::Deserializer<'de>, { // - try to deserialize as seconds // - try to deserialize as date-time string of format like "2028-11-23 14:12:34" let val = Option::::deserialize(deserializer)?; match val { Some(Value::Number(n)) => n.as_i64().ok_or_else(|| serde::de::Error::custom("invalid number")).map(Some), Some(Value::String(s)) => parse_timestamp(&s).map_err(serde::de::Error::custom), Some(Value::Null) => Ok(None), Some(_) => Err(serde::de::Error::custom("expected number or string")), None => Ok(None), } } pub fn parse_timestamp(value: &str) -> Result, ParseError> { let value = value.trim(); if value.is_empty() { return Ok(None); } if let Ok(ts) = value.parse::() { return Ok(Some(ts)); } // "YYYY-MM-DD HH:MM:SS" let dt = NaiveDateTime::parse_from_str(value, "%Y-%m-%d %H:%M:%S")?; let timestamp = Utc.from_utc_datetime(&dt).timestamp(); Ok(Some(timestamp)) } /// Serializes an `Option` as a Base64-encoded, LZ4-compressed string. /// /// - We want to avoid using `serde_json::Value` in the struct to save memory. /// - To avoid JSON escaping issues, we store the JSON content as a string. /// - The string is compressed using LZ4 and encoded in Base64. /// - Works for any JSON content: strings, arrays, and objects. /// - Empty arrays or objects are serialized as `null`. pub fn serialize_json_as_opt_string(value: &Option>, serializer: S) -> Result where S: Serializer, { let s = match value { Some(s) => s, None => return serializer.serialize_none(), }; let bytes = s.as_bytes(); let compressed = lz4_flex::compress_prepend_size(bytes); let encoded = general_purpose::STANDARD_NO_PAD.encode(compressed); serializer.serialize_some(&encoded) } /// Deserializes an `Option` from JSON. /// /// - Accepts both compressed Base64-LZ4 strings and regular JSON strings. /// - Returns `None` for empty arrays, empty objects, null, numbers, or booleans. /// - Decompresses Base64-LZ4 content back into the original string. /// - Handles arrays and objects by converting them to JSON strings if not empty. pub fn deserialize_json_as_opt_string<'de, D>(deserializer: D) -> Result>, D::Error> where D: Deserializer<'de>, { let opt: Option = Option::deserialize(deserializer)?; match opt { None => Ok(None), Some(Value::Array(arr)) if arr.is_empty() => Ok(None), Some(Value::Object(obj)) if obj.is_empty() => Ok(None), Some(Value::String(s)) => { if s.is_empty() { Ok(None) } else { let compressed = match base64::engine::general_purpose::STANDARD_NO_PAD.decode(&s) { Ok(bytes) => bytes, Err(_) => return Ok(Some(s.into())), }; // Plain strings can accidentally be valid Base64; fall back to the raw value instead of dropping it. let decompressed = match lz4_flex::decompress_size_prepended(&compressed) { Ok(bytes) => bytes, Err(_) => return Ok(Some(s.into())), }; match String::from_utf8(decompressed) { Ok(text) => Ok(Some(text.into())), Err(_) => Ok(Some(s.into())), } } } Some(Value::Null | Value::Number(_) | Value::Bool(_)) => Ok(None), Some(v) => Ok(Some(serde_json::to_string(&v).map_err(D::Error::custom)?.into())), } } /// Serialize an Option> in flow-style YAML (- { key: value, ... }) pub fn serialize_option_slice_flow(opt: &Option>, serializer: S) -> Result where T: serde::Serialize, S: serde::Serializer, { match opt { Some(items) if !items.is_empty() => serde_saphyr::FlowSeq(items).serialize(serializer), _ => serializer.serialize_none(), } } /// Serialize an Option> as a block sequence where each item is in flow-style pub fn serialize_option_vec_flow_map_items(opt: &Option>, serializer: S) -> Result where T: serde::Serialize, S: serde::Serializer, { match opt { Some(items) if !items.is_empty() => { let flow_items: Vec<_> = items.iter().map(serde_saphyr::FlowMap).collect(); flow_items.serialize(serializer) } _ => serializer.serialize_none(), } } /// Serialize a Vec as a block sequence where each item is in flow-style (`- { key: value, ... }`). pub fn serialize_vec_flow_map_items(items: &[T], serializer: S) -> Result where T: serde::Serialize, S: serde::Serializer, { let flow_items: Vec<_> = items.iter().map(serde_saphyr::FlowMap).collect(); flow_items.serialize(serializer) } pub fn serialize_number_as_string(value: &N, serializer: S) -> Result where N: std::fmt::Display, S: Serializer, { serializer.serialize_str(&value.to_string()) } /// Serde support for `XtreamCluster` fields. /// Serializes as string (e.g., "live", "video", "series") and deserializes via `FromStr`. pub mod xtream_cluster_serde { use crate::model::XtreamCluster; use serde::{de::Error, Deserialize, Deserializer, Serializer}; use std::str::FromStr; pub fn serialize(value: &XtreamCluster, serializer: S) -> Result where S: Serializer, { serializer.serialize_str(value.as_str()) } pub fn deserialize<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { let raw = String::deserialize(deserializer)?; XtreamCluster::from_str(&raw).map_err(D::Error::custom) } } pub fn serialize_as_base64_padded(value: &String, serializer: S) -> Result where S: Serializer, { // the xtream api uses padding! let encoded = general_purpose::STANDARD.encode(value.as_bytes()); serializer.serialize_str(&encoded) } #[cfg(test)] mod tests { use super::parse_number; use serde_json::Number; /// The stack buffer must hold every number `serde_json` can render. If a future /// `serde_json` bump changes the float formatting, this fails instead of silently /// turning large values into `None`. #[test] fn parse_number_matches_to_string_roundtrip_at_the_extremes() { for value in [0u64, 1, 42, u64::from(u32::MAX), u64::MAX] { let number = Number::from(value); assert_eq!(parse_number::(&number), number.to_string().parse().ok(), "u64 {value}"); } for value in [i64::MIN, -1, 0, i64::MAX] { let number = Number::from(value); assert_eq!(parse_number::(&number), number.to_string().parse().ok(), "i64 {value}"); } for value in [f64::MIN, -f64::MIN_POSITIVE, -0.5, 0.0, 4.7, f64::MAX] { let Some(number) = Number::from_f64(value) else { panic!("{value} is not representable as a JSON number"); }; assert_eq!(parse_number::(&number), number.to_string().parse().ok(), "f64 {value}"); } } #[test] fn parse_number_returns_none_instead_of_panicking_on_type_mismatch() { assert_eq!(parse_number::(&Number::from(-1i64)), None); } }