Skip to main content

ferogram_connect/
util.rs

1/*
2 * Copyright (c) 2026 Ankit Chaubey <ankitchaubey.dev@gmail.com>
3 * https://github.com/ankit-chaubey
4 *
5 * Project: ferogram
6 * Website: https://ferogram.dev
7 *
8 * Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
9 * https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
10 * <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your option.
11 * This file may not be copied, modified, or distributed except according
12 * to those terms.
13 */
14
15use std::time::Duration;
16
17use crate::error::ConnectError;
18
19/// CRC-32 using the standard IEEE 802.3 polynomial (for Full transport framing).
20pub fn crc32_ieee(data: &[u8]) -> u32 {
21    const POLY: u32 = 0xedb88320;
22    let mut crc: u32 = 0xffffffff;
23    for &byte in data {
24        let mut b = byte as u32;
25        for _ in 0..8 {
26            let mix = (crc ^ b) & 1;
27            crc >>= 1;
28            if mix != 0 {
29                crc ^= POLY;
30            }
31            b >>= 1;
32        }
33    }
34    crc ^ 0xffffffff
35}
36
37/// Minimum body size above which we attempt zlib compression.
38pub const COMPRESSION_THRESHOLD: usize = 512;
39
40const ID_GZIP_PACKED: u32 = 0x3072cfa1;
41const ID_MSGS_ACK: u32 = 0x62d6b459;
42const ID_MSG_CONTAINER: u32 = 0x73f1f8dc;
43
44/// Generate a random `i64`. Used for `random_id` fields in RPC requests,
45/// where Telegram only needs uniqueness, not cryptographic strength.
46pub fn random_i64() -> i64 {
47    let mut b = [0u8; 8];
48    ferogram_crypto::fill_random(&mut b);
49    i64::from_le_bytes(b)
50}
51
52/// Apply ±20 % random jitter to a backoff delay.
53/// Prevents thundering-herd when many clients reconnect simultaneously
54/// (e.g. after a server restart or a shared network outage).
55pub fn jitter_delay(base_ms: u64) -> Duration {
56    // Use two random bytes for the jitter factor (0..=65535 -> 0.80 … 1.20).
57    let mut b = [0u8; 2];
58    ferogram_crypto::fill_random(&mut b);
59    let rand_frac = u16::from_le_bytes(b) as f64 / 65535.0; // 0.0 … 1.0
60    let factor = 0.80 + rand_frac * 0.40; // 0.80 … 1.20
61    Duration::from_millis((base_ms as f64 * factor) as u64)
62}
63
64/// Decode a TL `bytes` value: the read-side counterpart to [`tl_write_bytes`].
65/// Returns `None` on a truncated or malformed length prefix. Doesn't skip the
66/// 4-byte alignment padding TL normally adds after the payload, since this is
67/// only used to unwrap `gzip_packed`'s single trailing field.
68pub fn tl_read_bytes(data: &[u8]) -> Option<Vec<u8>> {
69    if data.is_empty() {
70        return Some(vec![]);
71    }
72    let (len, start) = if data[0] < 254 {
73        (data[0] as usize, 1)
74    } else if data.len() >= 4 {
75        (
76            data[1] as usize | (data[2] as usize) << 8 | (data[3] as usize) << 16,
77            4,
78        )
79    } else {
80        return None;
81    };
82    if data.len() < start + len {
83        return None;
84    }
85    Some(data[start..start + len].to_vec())
86}
87
88/// Like [`tl_read_bytes`] but decodes the payload as UTF-8 (lossily, so
89/// invalid sequences become replacement characters instead of failing).
90pub fn tl_read_string(data: &[u8]) -> Option<String> {
91    tl_read_bytes(data).map(|b| String::from_utf8_lossy(&b).into_owned())
92}
93
94/// Decompress a `gzip_packed` payload. Tries gzip first (the standard
95/// format); if that fails or yields nothing, retries as raw zlib (no gzip
96/// header) before giving up.
97pub fn gz_inflate(data: &[u8]) -> Result<Vec<u8>, ConnectError> {
98    use std::io::Read;
99    let mut out = Vec::new();
100    if flate2::read::GzDecoder::new(data)
101        .read_to_end(&mut out)
102        .is_ok()
103        && !out.is_empty()
104    {
105        return Ok(out);
106    }
107    out.clear();
108    flate2::read::ZlibDecoder::new(data)
109        .read_to_end(&mut out)
110        .map_err(|_| ConnectError::other("decompression failed"))?;
111    Ok(out)
112}
113
114/// Unwrap a response body if it's wrapped in `gzip_packed`, identified by
115/// its constructor ID; otherwise returns `body` unchanged.
116pub fn maybe_gz_decompress(body: Vec<u8>) -> Result<Vec<u8>, ConnectError> {
117    const ID_GZIP_PACKED_LOCAL: u32 = 0x3072cfa1;
118    if body.len() >= 4 && u32::from_le_bytes(body[0..4].try_into().unwrap()) == ID_GZIP_PACKED_LOCAL
119    {
120        let bytes = tl_read_bytes(&body[4..]).unwrap_or_default();
121        gz_inflate(&bytes)
122    } else {
123        Ok(body)
124    }
125}
126
127/// TL `bytes` wire encoding (used inside gzip_packed).
128pub fn tl_write_bytes(data: &[u8]) -> Vec<u8> {
129    let len = data.len();
130    let mut out = Vec::with_capacity(4 + len);
131    if len < 254 {
132        out.push(len as u8);
133        out.extend_from_slice(data);
134        let pad = (4 - (1 + len) % 4) % 4;
135        out.extend(std::iter::repeat_n(0u8, pad));
136    } else {
137        out.push(0xfe);
138        out.extend_from_slice(&(len as u32).to_le_bytes()[..3]);
139        out.extend_from_slice(data);
140        let pad = (4 - (4 + len) % 4) % 4;
141        out.extend(std::iter::repeat_n(0u8, pad));
142    }
143    out
144}
145
146/// Wrap `data` in a `gzip_packed#3072cfa1 packed_data:bytes` TL frame.
147pub fn gz_pack_body(data: &[u8]) -> Vec<u8> {
148    use std::io::Write;
149    let mut enc = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());
150    let _ = enc.write_all(data);
151    let compressed = enc.finish().unwrap_or_default();
152    let mut out = Vec::with_capacity(4 + 4 + compressed.len());
153    out.extend_from_slice(&ID_GZIP_PACKED.to_le_bytes());
154    out.extend(tl_write_bytes(&compressed));
155    out
156}
157
158/// Optionally compress `data`.  Returns the compressed `gzip_packed` wrapper
159/// if it is shorter than the original; otherwise returns `data` unchanged.
160pub fn maybe_gz_pack(data: &[u8]) -> Vec<u8> {
161    if data.len() <= COMPRESSION_THRESHOLD {
162        return data.to_vec();
163    }
164    let packed = gz_pack_body(data);
165    if packed.len() < data.len() {
166        packed
167    } else {
168        data.to_vec()
169    }
170}
171
172// +: MsgsAck body builder
173
174/// Build the TL body for `msgs_ack#62d6b459 msg_ids:Vector<long>`.
175pub fn build_msgs_ack_body(msg_ids: &[i64]) -> Vec<u8> {
176    // msgs_ack#62d6b459 msg_ids:Vector<long>
177    // Vector<long>: 0x1cb5c415 + count:int + [i64...]
178    let mut out = Vec::with_capacity(4 + 4 + 4 + msg_ids.len() * 8);
179    out.extend_from_slice(&ID_MSGS_ACK.to_le_bytes());
180    out.extend_from_slice(&0x1cb5c415_u32.to_le_bytes()); // Vector constructor
181    out.extend_from_slice(&(msg_ids.len() as u32).to_le_bytes());
182    for &id in msg_ids {
183        out.extend_from_slice(&id.to_le_bytes());
184    }
185    out
186}
187
188/// Build the body of a `msg_container#73f1f8dc` from a list of
189/// `(msg_id, seqno, body)` inner messages.
190///
191/// The caller is responsible for allocating msg_id and seqno for each entry
192/// via `EncryptedSession::alloc_msg_seqno`.
193pub fn build_container_body(messages: &[(i64, i32, &[u8])]) -> Vec<u8> {
194    let total_body: usize = messages.iter().map(|(_, _, b)| 16 + b.len()).sum();
195    let mut out = Vec::with_capacity(8 + total_body);
196    out.extend_from_slice(&ID_MSG_CONTAINER.to_le_bytes());
197    out.extend_from_slice(&(messages.len() as u32).to_le_bytes());
198    for &(msg_id, seqno, body) in messages {
199        out.extend_from_slice(&msg_id.to_le_bytes());
200        out.extend_from_slice(&seqno.to_le_bytes());
201        out.extend_from_slice(&(body.len() as u32).to_le_bytes());
202        out.extend_from_slice(body);
203    }
204    out
205}