Skip to main content

wechat_protocol/
media_send.rs

1//! Final media-message metadata. CDN upload and message submission are separate
2//! steps; persist source bytes, generated keys and intent before either step.
3use crate::{
4    file_upload::{FileUploadReservation, MAX_RESERVED_FILE_BYTES},
5    media::{MediaQuality, UploadedImage, UploadedMedia, UploadedVideo},
6    protobuf as p, silk,
7    wire::base_request,
8    AccountState,
9};
10use anyhow::{ensure, Result};
11use serde::{Deserialize, Serialize};
12fn clean(s: &str, max: usize) -> Result<&[u8]> {
13    ensure!(
14        !s.is_empty() && s.len() <= max && !s.chars().any(|c| c.is_control()),
15        "Invalid media text"
16    );
17    Ok(s.as_bytes())
18}
19fn hex32(value: &str) -> Result<String> {
20    ensure!(
21        value.len() == 32 && value.bytes().all(|b| b.is_ascii_hexdigit()),
22        "Invalid media MD5"
23    );
24    Ok(value.to_ascii_lowercase())
25}
26fn descriptor(v: &UploadedMedia, kind: MediaQuality) -> Result<()> {
27    descriptor_bound(v, kind, 25 * 1024 * 1024)
28}
29fn descriptor_bound(v: &UploadedMedia, kind: MediaQuality, max_bytes: usize) -> Result<()> {
30    ensure!(
31        v.descriptor.quality == kind
32            && v.descriptor.aes_key.len() == 16
33            && v.plaintext_bytes > 0
34            && v.plaintext_bytes as usize <= max_bytes,
35        "Invalid uploaded media descriptor"
36    );
37    clean(&v.descriptor.file_id, 8192)?;
38    hex32(&v.md5)?;
39    ensure!(
40        v.descriptor.expected_bytes == Some(v.plaintext_bytes as u64)
41            && v.descriptor
42                .expected_md5
43                .as_deref()
44                .is_some_and(|m| m.eq_ignore_ascii_case(&v.md5)),
45        "Uploaded media metadata differs"
46    );
47    Ok(())
48}
49fn escape(s: &str) -> String {
50    s.replace('&', "&amp;")
51        .replace('<', "&lt;")
52        .replace('>', "&gt;")
53        .replace('"', "&quot;")
54        .replace('\'', "&apos;")
55}
56fn tag(name: &str, value: impl ToString) -> String {
57    format!("<{name}>{}</{name}>", escape(&value.to_string()))
58}
59/// Generic URL shares use app subtype 5. Metadata is supplied by the caller;
60/// composing or sending a card never fetches the target page or thumbnail.
61pub fn share_xml(
62    sender: &str,
63    title: &str,
64    url: &str,
65    description: Option<&str>,
66    thumbnail_url: Option<&str>,
67) -> Result<String> {
68    clean(sender, 256)?;
69    ensure!(
70        !title.trim().is_empty()
71            && title.len() <= 16384
72            && !title
73                .chars()
74                .any(|c| c.is_control() && !matches!(c, '\n' | '\t')),
75        "invalid_share_request"
76    );
77    crate::media::validate_shared_link_url(url)?;
78    let description = description.unwrap_or("");
79    ensure!(
80        description.len() <= 16384
81            && !description
82                .chars()
83                .any(|c| c.is_control() && !matches!(c, '\n' | '\t')),
84        "invalid_share_request"
85    );
86    if let Some(url) = thumbnail_url {
87        crate::media::validate_link_thumbnail_url(url)?;
88    }
89    let xml = format!(
90        "<appmsg appid=\"\" sdkver=\"0\">{}{}{}{}{}{}{}{}{}{}</appmsg>{}",
91        tag("title", title),
92        tag("des", description),
93        tag("action", "view"),
94        tag("type", 5),
95        tag("showtype", 0),
96        tag("url", url),
97        tag("lowurl", ""),
98        tag("thumburl", thumbnail_url.unwrap_or("")),
99        tag("sourceusername", ""),
100        tag("sourcedisplayname", ""),
101        tag("fromusername", sender)
102    );
103    ensure!(xml.len() <= 256 * 1024, "invalid_share_request");
104    Ok(xml)
105}
106pub fn share_send_request(
107    state: &AccountState,
108    destination: &str,
109    client_id: &str,
110    created_at: u32,
111    content: &str,
112) -> Result<Vec<u8>> {
113    clean(destination, 256)?;
114    clean(client_id, 1024)?;
115    ensure!(
116        created_at > 0 && !content.is_empty() && content.len() <= 256 * 1024,
117        "invalid_share_request"
118    );
119    let app = p::message(&[
120        p::bytes(1, state.own_username.as_bytes()),
121        p::bytes(2, &[]),
122        p::uint(3, 0),
123        p::bytes(4, destination.as_bytes()),
124        p::uint(5, 5),
125        p::bytes(6, content.as_bytes()),
126        p::uint(7, created_at as u64),
127        p::bytes(8, client_id.as_bytes()),
128        p::int32(10, 1),
129        p::bytes(13, &[]),
130        p::bytes(14, &[]),
131        p::bytes(15, &[]),
132    ])?;
133    p::message(&[
134        p::bytes(1, &base_request(&state.device, state.uin, 0)?),
135        p::bytes(2, &app),
136        p::uint(9, 0),
137        p::uint(10, 0),
138        p::uint(11, 2),
139    ])
140}
141pub fn file_xml(sender: &str, upload: &UploadedMedia) -> Result<String> {
142    file_xml_with_reservation(sender, upload, None)
143}
144pub fn file_xml_with_reservation(
145    sender: &str,
146    upload: &UploadedMedia,
147    reservation: Option<&FileUploadReservation>,
148) -> Result<String> {
149    clean(sender, 1024)?;
150    if let Some(reservation) = reservation {
151        reservation.validate()?;
152        ensure!(
153            matches!(
154                upload.descriptor.quality,
155                MediaQuality::File | MediaQuality::LargeFile
156            ),
157            "Expected an uploaded file descriptor"
158        );
159        descriptor_bound(upload, upload.descriptor.quality, MAX_RESERVED_FILE_BYTES)?;
160    } else {
161        descriptor(upload, MediaQuality::File)?;
162    }
163    let name = &upload.file_name;
164    clean(name, 255)?;
165    ensure!(
166        name != "." && name != ".." && !name.contains(['/', '\\']),
167        "Expected a single filename component"
168    );
169    let extension = name
170        .rsplit_once('.')
171        .filter(|(base, _)| !base.is_empty())
172        .map(|(_, extension)| extension)
173        .unwrap_or("");
174    let mut app = tag("title", name);
175    for name in ["des", "action"] {
176        app.push_str(&tag(name, ""));
177    }
178    app.push_str(&tag("type", 6));
179    app.push_str(&tag("showtype", 0));
180    app.push_str(&tag("soundtype", 0));
181    for name in ["mediatagname", "messageext", "messageaction", "content"] {
182        app.push_str(&tag(name, ""));
183    }
184    app.push_str(&tag("contentattr", 0));
185    for name in ["url", "lowurl", "dataurl", "lowdataurl"] {
186        app.push_str(&tag(name, ""));
187    }
188    app.push_str("<appattach>");
189    app.push_str(&tag("totallen", upload.plaintext_bytes));
190    let aes_key = hex::encode(&upload.descriptor.aes_key);
191    let attach_id = reservation
192        .map(|_| format!("@cdn_{}_{aes_key}_1", upload.descriptor.file_id))
193        .unwrap_or_default();
194    app.push_str(&tag("attachid", attach_id));
195    app.push_str(&tag("emoticonmd5", ""));
196    app.push_str(&tag("fileext", extension));
197    app.push_str(&tag("cdnattachurl", &upload.descriptor.file_id));
198    app.push_str(&tag("aeskey", aes_key));
199    app.push_str(&tag("encryver", if reservation.is_some() { 0 } else { 1 }));
200    if let Some(reservation) = reservation {
201        app.push_str(&tag("overwrite_newmsgid", reservation.overwrite_newmsgid));
202        app.push_str(&tag("fileuploadtoken", &reservation.file_upload_token));
203    }
204    app.push_str("</appattach>");
205    for name in ["extinfo", "sourceusername", "sourcedisplayname", "thumburl"] {
206        app.push_str(&tag(name, ""));
207    }
208    app.push_str(&tag("md5", hex32(&upload.md5)?));
209    app.push_str(&tag("statextstr", ""));
210    Ok(format!(
211        "<appmsg appid=\"\" sdkver=\"0\">{app}</appmsg>{}",
212        tag("fromusername", sender)
213    ))
214}
215/// Reconstruct local metadata from accepted uploaded tuples. These are local
216/// cache records, not a claim that a server returned this exact XML spelling.
217fn local_element(name: &str, attributes: Vec<(&str, String)>) -> String {
218    let attributes = attributes
219        .into_iter()
220        .map(|(key, value)| format!(" {key}=\"{}\"", escape(&value)))
221        .collect::<String>();
222    format!("<msg><{name}{attributes}/></msg>")
223}
224pub fn local_image_xml(image: &UploadedImage) -> Result<String> {
225    descriptor(&image.original, MediaQuality::Original)?;
226    descriptor(&image.standard, MediaQuality::Standard)?;
227    descriptor(&image.thumbnail, MediaQuality::Thumbnail)?;
228    ensure!(
229        image.original.descriptor.aes_key == image.standard.descriptor.aes_key,
230        "Image variants require one main key"
231    );
232    Ok(local_element(
233        "img",
234        vec![
235            ("aeskey", hex::encode(&image.original.descriptor.aes_key)),
236            ("encryver", "1".into()),
237            ("cdnbigimgurl", image.original.descriptor.file_id.clone()),
238            ("cdnmidimgurl", image.standard.descriptor.file_id.clone()),
239            ("cdnthumburl", image.thumbnail.descriptor.file_id.clone()),
240            (
241                "cdnthumbaeskey",
242                hex::encode(&image.thumbnail.descriptor.aes_key),
243            ),
244            ("md5", hex32(&image.original.md5)?),
245            ("hdlength", image.original.plaintext_bytes.to_string()),
246            ("length", image.standard.plaintext_bytes.to_string()),
247            (
248                "cdnthumblength",
249                image.thumbnail.plaintext_bytes.to_string(),
250            ),
251            (
252                "cdnthumbwidth",
253                image.thumbnail.width.unwrap_or(0).to_string(),
254            ),
255            (
256                "cdnthumbheight",
257                image.thumbnail.height.unwrap_or(0).to_string(),
258            ),
259        ],
260    ))
261}
262pub fn local_video_xml(video: &UploadedVideo, seconds: u32) -> Result<String> {
263    descriptor(&video.video, MediaQuality::Video)?;
264    descriptor(&video.thumbnail, MediaQuality::Thumbnail)?;
265    ensure!(seconds > 0, "Invalid video duration");
266    let mut fields = vec![
267        ("aeskey", hex::encode(&video.video.descriptor.aes_key)),
268        ("cdnvideourl", video.video.descriptor.file_id.clone()),
269        ("length", video.video.plaintext_bytes.to_string()),
270        ("md5", hex32(&video.video.md5)?),
271        ("playlength", seconds.to_string()),
272        ("cdnthumburl", video.thumbnail.descriptor.file_id.clone()),
273        (
274            "cdnthumbaeskey",
275            hex::encode(&video.thumbnail.descriptor.aes_key),
276        ),
277        (
278            "cdnthumblength",
279            video.thumbnail.plaintext_bytes.to_string(),
280        ),
281        (
282            "cdnthumbwidth",
283            video.thumbnail.width.unwrap_or(0).to_string(),
284        ),
285        (
286            "cdnthumbheight",
287            video.thumbnail.height.unwrap_or(0).to_string(),
288        ),
289    ];
290    if let Some(original) = &video.original {
291        descriptor_bound(
292            original,
293            MediaQuality::Video,
294            wechat_media::MAX_VIDEO_SOURCE_BYTES,
295        )?;
296        fields.extend([
297            ("cdnrawvideourl", original.descriptor.file_id.clone()),
298            (
299                "cdnrawvideoaeskey",
300                hex::encode(&original.descriptor.aes_key),
301            ),
302            ("rawlength", original.plaintext_bytes.to_string()),
303            ("rawmd5", hex32(&original.md5)?),
304        ]);
305    }
306    Ok(local_element("videomsg", fields))
307}
308pub fn local_voice_xml(encoded_bytes: usize, duration_ms: u32) -> Result<String> {
309    ensure!(
310        encoded_bytes > 0
311            && encoded_bytes <= silk::MAX_NOTE_BYTES
312            && (1..=silk::MAX_NOTE_MS).contains(&duration_ms),
313        "Invalid voice note metadata"
314    );
315    Ok(local_element(
316        "voicemsg",
317        vec![
318            ("voiceformat", "4".into()),
319            ("length", encoded_bytes.to_string()),
320            ("voicelength", duration_ms.to_string()),
321            ("endflag", "1".into()),
322        ],
323    ))
324}
325pub fn local_file_xml(sender: &str, upload: &UploadedMedia) -> Result<String> {
326    local_file_xml_with_reservation(sender, upload, None)
327}
328pub fn local_file_xml_with_reservation(
329    sender: &str,
330    upload: &UploadedMedia,
331    reservation: Option<&FileUploadReservation>,
332) -> Result<String> {
333    Ok(format!(
334        "<msg>{}</msg>",
335        file_xml_with_reservation(sender, upload, reservation)?
336    ))
337}
338pub fn file_send_request(
339    state: &AccountState,
340    destination: &str,
341    client_id: &str,
342    created_at: u32,
343    upload: &UploadedMedia,
344) -> Result<Vec<u8>> {
345    file_send_request_with_reservation(state, destination, client_id, created_at, upload, None)
346}
347pub fn file_send_request_with_reservation(
348    state: &AccountState,
349    destination: &str,
350    client_id: &str,
351    created_at: u32,
352    upload: &UploadedMedia,
353    reservation: Option<&FileUploadReservation>,
354) -> Result<Vec<u8>> {
355    clean(destination, 512)?;
356    clean(client_id, 1024)?;
357    ensure!(created_at > 0, "Invalid media timestamp");
358    let content = file_xml_with_reservation(&state.own_username, upload, reservation)?;
359    let app = p::message(&[
360        p::bytes(1, state.own_username.as_bytes()),
361        p::bytes(2, &[]),
362        p::uint(3, 0),
363        p::bytes(4, destination.as_bytes()),
364        p::uint(5, 6),
365        p::bytes(6, content.as_bytes()),
366        p::uint(7, created_at as u64),
367        p::bytes(8, client_id.as_bytes()),
368        p::int32(10, 1),
369        p::bytes(13, &[]),
370        p::bytes(14, &[]),
371        p::bytes(15, &[]),
372    ])?;
373    let mut fields = vec![
374        p::bytes(1, &base_request(&state.device, state.uin, 0)?),
375        p::bytes(2, &app),
376        p::bytes(5, hex32(&upload.md5)?.as_bytes()),
377    ];
378    if upload.plaintext_bytes > 25 * 1024 * 1024 {
379        fields.push(p::uint(6, 7));
380    }
381    fields.extend([p::uint(9, 0), p::uint(10, 0), p::uint(11, 2)]);
382    p::message(&fields)
383}
384pub fn image_send_request(
385    state: &AccountState,
386    destination: &str,
387    client_id: &str,
388    image: &UploadedImage,
389) -> Result<Vec<u8>> {
390    clean(destination, 512)?;
391    clean(client_id, 4096)?;
392    descriptor(&image.original, MediaQuality::Original)?;
393    descriptor(&image.standard, MediaQuality::Standard)?;
394    descriptor(&image.thumbnail, MediaQuality::Thumbnail)?;
395    ensure!(
396        image.original.descriptor.aes_key == image.standard.descriptor.aes_key,
397        "Image main variants require one shared key"
398    );
399    let thumbnail = &image.thumbnail;
400    let width = thumbnail
401        .width
402        .ok_or_else(|| anyhow::anyhow!("Missing thumbnail width"))?;
403    let height = thumbnail
404        .height
405        .ok_or_else(|| anyhow::anyhow!("Missing thumbnail height"))?;
406    ensure!(
407        width > 0 && width <= 8192 && height > 0 && height <= 8192,
408        "Invalid thumbnail dimensions"
409    );
410    let sk = |s: &[u8]| p::bytes(1, s);
411    p::message(&[
412        p::bytes(1, &base_request(&state.device, state.uin, 0)?),
413        p::bytes(2, &sk(client_id.as_bytes())),
414        p::bytes(3, &sk(state.own_username.as_bytes())),
415        p::bytes(4, &sk(destination.as_bytes())),
416        p::uint(5, thumbnail.plaintext_bytes as u64),
417        p::uint(6, 0),
418        p::uint(7, thumbnail.plaintext_bytes as u64),
419        p::bytes(8, &p::buffer(&[])),
420        p::uint(9, 3),
421        p::uint(11, 1),
422        p::int32(12, 2),
423        p::int32(13, 0),
424        p::bytes(15, image.original.descriptor.file_id.as_bytes()),
425        p::bytes(16, image.standard.descriptor.file_id.as_bytes()),
426        p::bytes(
427            17,
428            hex::encode(&image.original.descriptor.aes_key).as_bytes(),
429        ),
430        p::int32(18, 1),
431        p::int32(19, image.original.plaintext_bytes as i32),
432        p::int32(20, image.standard.plaintext_bytes as i32),
433        p::bytes(21, thumbnail.descriptor.file_id.as_bytes()),
434        p::int32(22, thumbnail.plaintext_bytes as i32),
435        p::int32(23, height as i32),
436        p::int32(24, width as i32),
437        p::bytes(25, hex::encode(&thumbnail.descriptor.aes_key).as_bytes()),
438        p::bytes(27, hex32(&image.original.md5)?.as_bytes()),
439        p::uint(28, 0),
440        p::uint(30, 0),
441        p::uint(36, 0),
442        p::uint(41, 0),
443    ])
444}
445pub fn video_send_request(
446    state: &AccountState,
447    destination: &str,
448    client_id: &str,
449    video: &UploadedVideo,
450    play_seconds: u32,
451) -> Result<Vec<u8>> {
452    clean(destination, 512)?;
453    clean(client_id, 1024)?;
454    ensure!(play_seconds > 0, "Invalid video play duration");
455    descriptor(&video.video, MediaQuality::Video)?;
456    descriptor(&video.thumbnail, MediaQuality::Thumbnail)?;
457    let v = &video.video;
458    let t = &video.thumbnail;
459    let width = t
460        .width
461        .ok_or_else(|| anyhow::anyhow!("Missing video thumbnail width"))?;
462    let height = t
463        .height
464        .ok_or_else(|| anyhow::anyhow!("Missing video thumbnail height"))?;
465    ensure!(
466        width > 0 && height > 0 && width <= 8192 && height <= 8192,
467        "Invalid video thumbnail dimensions"
468    );
469    let mut fields = vec![
470        p::bytes(1, &base_request(&state.device, state.uin, 0)?),
471        p::bytes(2, client_id.as_bytes()),
472        p::bytes(3, state.own_username.as_bytes()),
473        p::bytes(4, destination.as_bytes()),
474        p::uint(5, t.plaintext_bytes as u64),
475        p::uint(6, 0),
476        p::bytes(7, &p::buffer(&[])),
477        p::uint(8, v.plaintext_bytes as u64),
478        p::uint(9, 0),
479        p::bytes(10, &p::buffer(&[])),
480        p::uint(11, play_seconds as u64),
481        p::uint(12, 1),
482        p::uint(13, 0),
483        p::uint(14, 0),
484        p::bytes(15, &[]),
485        p::bytes(16, v.descriptor.file_id.as_bytes()),
486        p::bytes(17, hex::encode(&v.descriptor.aes_key).as_bytes()),
487        p::int32(18, 1),
488        p::bytes(19, t.descriptor.file_id.as_bytes()),
489        p::int32(20, t.plaintext_bytes as i32),
490        p::int32(21, height as i32),
491        p::int32(22, width as i32),
492        p::bytes(23, hex::encode(&t.descriptor.aes_key).as_bytes()),
493        p::bytes(26, hex32(&v.md5)?.as_bytes()),
494        p::bytes(37, &[]),
495        p::uint(38, 0),
496    ];
497    if let Some(original) = &video.original {
498        descriptor_bound(
499            original,
500            MediaQuality::Video,
501            wechat_media::MAX_VIDEO_SOURCE_BYTES,
502        )?;
503        // Linux 4.1.13.26 CGI149's generated M7 serializer emits these
504        // separately from the normal rendition (0x7d094d8..0x7d0954a).
505        fields.extend([
506            p::bytes(48, hex32(&original.md5)?.as_bytes()),
507            p::bytes(49, original.descriptor.file_id.as_bytes()),
508            p::bytes(50, hex::encode(&original.descriptor.aes_key).as_bytes()),
509            p::uint(51, u64::from(original.plaintext_bytes)),
510        ]);
511    }
512    p::message(&fields)
513}
514/// Encoded SILK bytes carried by one `uploadvoice` request. A short-link
515/// request travels in one MMTLS record of at most 65,519 plaintext bytes,
516/// including the route, packet header and envelope, so a 60-second note (about
517/// 126-131 KB from the bundled 16 kbit/s profile) cannot use one request. 32 KiB
518/// matches the `downloadvoice` range size and leaves room for that overhead.
519pub const VOICE_UPLOAD_CHUNK_BYTES: usize = 32 * 1024;
520
521/// One voice note submitted through `uploadvoice`. A note of at most
522/// [`VOICE_UPLOAD_CHUNK_BYTES`] uses a single request with the end flag set.
523/// A larger note is sent as consecutive byte ranges (offset/length) under the
524/// same client message ID; only the last range sets the end flag, and later
525/// ranges carry a server message ID returned for an earlier range. Each range
526/// is submitted once: an error after any submission is an uncertain outcome.
527pub struct VoiceUpload<'a> {
528    destination: &'a str,
529    client_id: &'a str,
530    created_at: u32,
531    duration_ms: u32,
532    encoded: &'a [u8],
533    offset: usize,
534    server_msg_id: Option<u32>,
535    new_msg_id: Option<u64>,
536}
537impl<'a> VoiceUpload<'a> {
538    pub fn new(
539        destination: &'a str,
540        client_id: &'a str,
541        created_at: u32,
542        duration_ms: u32,
543        encoded: &'a [u8],
544    ) -> Result<Self> {
545        clean(destination, 512)?;
546        let client = clean(client_id, 128)?;
547        ensure!(
548            client.len() >= 16
549                && client
550                    .iter()
551                    .all(|b| b.is_ascii_alphanumeric() || *b == b'_' || *b == b'-'),
552            "Invalid voice client ID"
553        );
554        ensure!(
555            created_at > 0
556                && (1..=silk::MAX_NOTE_MS).contains(&duration_ms)
557                && encoded.len() >= 12
558                && encoded.len() <= silk::MAX_NOTE_BYTES
559                && encoded[0] == 2
560                && encoded[1..10] == *b"#!SILK_V3",
561            "Unsupported bounded SILK voice profile"
562        );
563        Ok(Self {
564            destination,
565            client_id,
566            created_at,
567            duration_ms,
568            encoded,
569            offset: 0,
570            server_msg_id: None,
571            new_msg_id: None,
572        })
573    }
574    fn range(&self) -> std::ops::Range<usize> {
575        self.offset..(self.offset + VOICE_UPLOAD_CHUNK_BYTES).min(self.encoded.len())
576    }
577    /// Number of requests needed for the whole note.
578    pub fn request_count(&self) -> usize {
579        self.encoded.len().div_ceil(VOICE_UPLOAD_CHUNK_BYTES)
580    }
581    /// Whether the next request carries the last range and the end flag.
582    pub fn is_final(&self) -> bool {
583        self.range().end == self.encoded.len()
584    }
585    /// Build the request for the next unsent range.
586    pub fn request(&self, state: &AccountState) -> Result<Vec<u8>> {
587        let range = self.range();
588        let chunk = &self.encoded[range.clone()];
589        let client = self.client_id.as_bytes();
590        let mut parts = vec![
591            p::bytes(1, state.own_username.as_bytes()),
592            p::bytes(2, self.destination.as_bytes()),
593            p::uint(3, range.start as u64),
594            p::uint(4, chunk.len() as u64),
595            p::bytes(5, client),
596        ];
597        if let Some(id) = self.server_msg_id {
598            parts.push(p::uint(6, id.into()));
599        }
600        parts.extend([
601            p::uint(7, self.duration_ms.into()),
602            p::bytes(8, &p::buffer(chunk)),
603            p::uint(9, self.is_final().into()),
604            p::bytes(10, &base_request(&state.device, state.uin, 0)?),
605            p::uint(11, 0),
606            p::uint(13, 4),
607            p::uint(15, 0),
608            p::uint(16, 0),
609            p::uint(17, self.created_at.into()),
610        ]);
611        // Only the first range carries this buffer; field 5 identifies every range.
612        if range.start == 0 {
613            parts.push(p::bytes(18, &p::buffer(client)));
614        }
615        parts.push(p::bytes(21, &[]));
616        p::message(&parts)
617    }
618    /// Apply the reply for the request built last. Returns the note's result
619    /// after the final range, or `None` when the next range must be sent. A
620    /// rejection after an earlier range was accepted needs review, because the
621    /// server already holds part of the note under this client message ID.
622    pub fn accept_reply(&mut self, input: &[u8]) -> Result<Option<MediaSendResult>> {
623        let continued = self.offset > 0;
624        if self.is_final() {
625            let mut result = decode_voice_send(input, self.client_id)?;
626            let changed = self.new_msg_id.is_some_and(|known| {
627                result.server_message_id.as_deref() != Some(known.to_string().as_str())
628            });
629            if (continued && result.status == SendStatus::Rejected) || changed {
630                result.status = SendStatus::NeedsReview;
631            }
632            return Ok(Some(result));
633        }
634        let (f, code) = voice_reply(input)?;
635        if code != 0 {
636            let mut result = rejected(code);
637            if continued {
638                result.status = SendStatus::NeedsReview;
639            }
640            return Ok(Some(result));
641        }
642        let scalar = |n| -> Result<Option<u64>> {
643            Ok(match p::optional(&f, n)? {
644                Some(p::Value::Varint(v)) => Some(*v),
645                _ => None,
646            })
647        };
648        for n in [3, 4, 5, 7, 8, 9] {
649            if let Some(v) = scalar(n)? {
650                u32::try_from(v)?;
651            }
652        }
653        ensure!(
654            scalar(9)?.unwrap_or(0) == 0,
655            "Voice upload ended before its last range"
656        );
657        if let Some(id) = scalar(7)?.filter(|id| *id != 0) {
658            let id = u32::try_from(id)?;
659            ensure!(
660                self.server_msg_id.is_none_or(|known| known == id),
661                "Voice upload message ID changed"
662            );
663            self.server_msg_id = Some(id);
664        }
665        if let Some(id) = scalar(12)?.filter(|id| *id != 0) {
666            ensure!(
667                self.new_msg_id.is_none_or(|known| known == id),
668                "Voice upload message ID changed"
669            );
670            self.new_msg_id = Some(id);
671        }
672        self.offset = self.range().end;
673        Ok(None)
674    }
675}
676#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
677#[serde(rename_all = "snake_case")]
678pub enum SendStatus {
679    ServerAccepted,
680    Rejected,
681    NeedsReview,
682}
683#[derive(Clone, Serialize, Deserialize, Debug)]
684#[serde(rename_all = "camelCase")]
685pub struct MediaSendResult {
686    pub status: SendStatus,
687    pub service_code: i32,
688    pub server_message_id: Option<String>,
689    pub client_id_matches: Option<bool>,
690}
691fn rejected(code: i32) -> MediaSendResult {
692    MediaSendResult {
693        status: SendStatus::Rejected,
694        service_code: code,
695        server_message_id: None,
696        client_id_matches: None,
697    }
698}
699pub fn decode_file_send(input: &[u8], client_id: &str) -> Result<MediaSendResult> {
700    let f = p::decode(input)?;
701    let code = p::base_code(&f)?;
702    if code != 0 {
703        return Ok(rejected(code));
704    }
705    for n in [5, 7] {
706        u32::try_from(p::scalar(&f, n)?)?;
707    }
708    let id = p::scalar(&f, 9)?;
709    ensure!(id != 0, "Accepted file response has no server ID");
710    let mut matches = false;
711    for n in [2, 3, 4, 6, 10, 11, 13] {
712        if let Some(v) = p::optional_data(&f, n)? {
713            std::str::from_utf8(v)?;
714            matches |= v == client_id.as_bytes();
715        }
716    }
717    Ok(MediaSendResult {
718        status: SendStatus::ServerAccepted,
719        service_code: code,
720        server_message_id: Some(id.to_string()),
721        client_id_matches: matches.then_some(true),
722    })
723}
724/// Correlate the final reply for a reserved file with its submitted intent.
725pub fn decode_file_send_correlated(
726    input: &[u8],
727    client_id: &str,
728    sender: &str,
729    destination: &str,
730    aes_key: &[u8],
731) -> Result<MediaSendResult> {
732    ensure!(aes_key.len() == 16, "Invalid file AES key");
733    let mut result = decode_file_send(input, client_id)?;
734    if result.status == SendStatus::Rejected {
735        return Ok(result);
736    }
737    let fields = p::decode(input)?;
738    let base = p::decode(p::data(&fields, 1)?)?;
739    ensure!(p::scalar(&base, 1)? == 0, "Invalid file response status");
740    let key = hex::encode(aes_key);
741    let mut correlated = true;
742    for (number, expected) in [
743        (3, sender.as_bytes()),
744        (4, destination.as_bytes()),
745        (6, client_id.as_bytes()),
746        (10, key.as_bytes()),
747    ] {
748        correlated &= p::optional_data(&fields, number)? == Some(expected);
749    }
750    result.status = if correlated {
751        SendStatus::ServerAccepted
752    } else {
753        SendStatus::NeedsReview
754    };
755    result.client_id_matches = Some(correlated);
756    Ok(result)
757}
758pub fn decode_image_send(
759    input: &[u8],
760    client_id: &str,
761    sender: &str,
762    destination: &str,
763) -> Result<MediaSendResult> {
764    let f = p::decode(input)?;
765    for n in 1..=13 {
766        p::optional(&f, n)?;
767    }
768    let code = p::base_code(&f)?;
769    if code != 0 {
770        return Ok(rejected(code));
771    }
772    for n in [2, 6, 7, 8, 9] {
773        u32::try_from(p::scalar(&f, n)?)?;
774    }
775    let nested = |n| -> Result<Vec<u8>> {
776        let item = p::decode(p::data(&f, n)?)?;
777        Ok(p::data(&item, 1)?.to_vec())
778    };
779    let correlated = nested(3)? == client_id.as_bytes()
780        && nested(4)? == sender.as_bytes()
781        && nested(5)? == destination.as_bytes();
782    let id = p::scalar(&f, 10)?;
783    ensure!(id != 0, "Accepted image response has no server ID");
784    for n in [11, 12, 13] {
785        p::optional_data(&f, n)?;
786    }
787    Ok(MediaSendResult {
788        status: if correlated {
789            SendStatus::ServerAccepted
790        } else {
791            SendStatus::NeedsReview
792        },
793        service_code: code,
794        server_message_id: Some(id.to_string()),
795        client_id_matches: Some(correlated),
796    })
797}
798pub fn decode_video_send(input: &[u8], client_id: &str) -> Result<MediaSendResult> {
799    let f = p::decode(input)?;
800    let code = p::base_code(&f)?;
801    if code != 0 {
802        return Ok(rejected(code));
803    }
804    for n in [3, 4, 5] {
805        u32::try_from(p::scalar(&f, n)?)?;
806    }
807    let id = p::scalar(&f, 6)?;
808    ensure!(id != 0, "Accepted video response has no server ID");
809    let mut matches = false;
810    for n in [2, 7, 8] {
811        if let Some(v) = p::optional_data(&f, n)? {
812            std::str::from_utf8(v)?;
813            matches |= v == client_id.as_bytes();
814        }
815    }
816    Ok(MediaSendResult {
817        status: SendStatus::ServerAccepted,
818        service_code: code,
819        server_message_id: Some(id.to_string()),
820        client_id_matches: matches.then_some(true),
821    })
822}
823fn voice_reply(input: &[u8]) -> Result<(Vec<p::Field>, i32)> {
824    let f = p::decode(input)?;
825    for n in 1..=13 {
826        let v = p::optional(&f, n)?;
827        if let Some(v) = v {
828            ensure!(
829                matches!(
830                    (n, v),
831                    (1 | 2 | 6 | 10, p::Value::Bytes(_))
832                        | (3 | 4 | 5 | 7 | 8 | 9 | 11 | 12 | 13, p::Value::Varint(_))
833                ),
834                "Invalid voice response field"
835            );
836        }
837    }
838    let base = p::decode(p::data(&f, 10)?)?;
839    let code = p::scalar(&base, 1)? as u32 as i32;
840    Ok((f, code))
841}
842/// Decode the reply to the request carrying a note's end flag.
843pub fn decode_voice_send(input: &[u8], client_id: &str) -> Result<MediaSendResult> {
844    let (f, code) = voice_reply(input)?;
845    if code != 0 {
846        return Ok(rejected(code));
847    }
848    for n in [3, 4, 5, 7, 8, 9] {
849        u32::try_from(p::scalar(&f, n)?)?;
850    }
851    let id = p::scalar(&f, 12)?;
852    ensure!(id != 0, "Accepted voice response has no server ID");
853    let mut matches = 0;
854    for n in [1, 2, 6] {
855        if let Some(v) = p::optional_data(&f, n)? {
856            std::str::from_utf8(v)?;
857            if v == client_id.as_bytes() {
858                matches += 1;
859            }
860        }
861    }
862    Ok(MediaSendResult {
863        status: if matches == 1 {
864            SendStatus::ServerAccepted
865        } else {
866            SendStatus::NeedsReview
867        },
868        service_code: code,
869        server_message_id: Some(id.to_string()),
870        client_id_matches: Some(matches == 1),
871    })
872}
873
874#[cfg(test)]
875mod tests {
876    use super::*;
877    fn synthetic_state() -> AccountState {
878        AccountState {
879            schema: 1,
880            device: crate::DeviceProfile {
881                device_id: vec![1; 16],
882                device_type: "UnifiedPCLinux synthetic x86_64".into(),
883                client_version: -227271398,
884            },
885            uin: 123,
886            own_username: "synthetic-user".into(),
887            cookie: vec![],
888            session_key: vec![1; 16],
889            request_key: vec![],
890            response_key: vec![],
891            auto_auth_key: vec![],
892            key_info: vec![],
893            database_password: vec![],
894            auth_snapshot: Default::default(),
895            long_host: "long.weixin.qq.com".into(),
896            short_host: "short.weixin.qq.com".into(),
897        }
898    }
899    fn synthetic_file(size: usize, quality: MediaQuality) -> UploadedMedia {
900        UploadedMedia {
901            descriptor: crate::media::MediaDescriptor {
902                file_id: "synthetic-file-id".into(),
903                aes_key: vec![1; 16],
904                quality,
905                expected_bytes: Some(size as u64),
906                expected_md5: Some("a".repeat(32)),
907                strict_size: true,
908                expires_at: None,
909            },
910            plaintext_bytes: size.try_into().unwrap(),
911            md5: "a".repeat(32),
912            sha256: "b".repeat(64),
913            file_name: "synthetic&file.txt".into(),
914            width: None,
915            height: None,
916        }
917    }
918    fn synthetic_reservation() -> FileUploadReservation {
919        FileUploadReservation {
920            file_upload_token: "v1_c3ludGhldGlj".into(),
921            overwrite_newmsgid: u64::MAX,
922        }
923    }
924    #[test]
925    fn file_upload_reservation_ordinary_request_schema() {
926        let state = synthetic_state();
927        let raw = crate::file_upload::file_upload_request(
928            &state,
929            "filehelper",
930            "filehelper_1234567_42",
931            "synthetic&file.TXT",
932            32 * 1024 * 1024,
933            &"A".repeat(32),
934        )
935        .unwrap();
936        let fields = p::decode(&raw).unwrap();
937        assert_eq!(
938            fields.iter().map(|f| f.number).collect::<Vec<_>>(),
939            [1, 2, 3, 4, 5, 7, 12]
940        );
941        assert_eq!(p::data(&fields, 2).unwrap(), b"filehelper");
942        assert_eq!(p::data(&fields, 3).unwrap(), b"filehelper_1234567_42");
943        assert_ne!(p::data(&fields, 2).unwrap(), state.own_username.as_bytes());
944        assert_eq!(p::scalar(&fields, 4).unwrap(), 1);
945        assert_eq!(p::scalar(&fields, 7).unwrap(), 0);
946        assert_eq!(p::scalar(&fields, 12).unwrap(), 0);
947        let base = p::decode(p::data(&fields, 1).unwrap()).unwrap();
948        assert_eq!(p::scalar(&base, 2).unwrap(), 123);
949        assert_eq!(p::scalar(&base, 6).unwrap(), 0);
950        let info = p::decode(p::data(&fields, 5).unwrap()).unwrap();
951        assert_eq!(
952            info.iter().map(|f| f.number).collect::<Vec<_>>(),
953            [1, 2, 3, 4]
954        );
955        assert_eq!(p::data(&info, 1).unwrap(), b"synthetic&file.TXT");
956        assert_eq!(p::data(&info, 2).unwrap(), b"TXT");
957        assert_eq!(
958            p::data(&info, 3).unwrap(),
959            b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
960        );
961        assert_eq!(p::scalar(&info, 4).unwrap(), 33_554_432);
962        let final_client_id = "filehelper_1234567_42_xwechat_1";
963        let final_request = file_send_request_with_reservation(
964            &state,
965            "filehelper",
966            final_client_id,
967            1_234_567,
968            &synthetic_file(MAX_RESERVED_FILE_BYTES, MediaQuality::LargeFile),
969            Some(&synthetic_reservation()),
970        )
971        .unwrap();
972        let final_fields = p::decode(&final_request).unwrap();
973        let final_app = p::decode(p::data(&final_fields, 2).unwrap()).unwrap();
974        assert_eq!(p::data(&final_app, 8).unwrap(), final_client_id.as_bytes());
975        assert_ne!(
976            p::data(&fields, 3).unwrap(),
977            p::data(&final_app, 8).unwrap()
978        );
979        assert!(
980            final_client_id.starts_with(std::str::from_utf8(p::data(&fields, 3).unwrap()).unwrap())
981        );
982        assert!(crate::file_upload::file_upload_request(
983            &state,
984            "bad\npeer",
985            "filehelper_1234567_42",
986            "file.txt",
987            1,
988            &"a".repeat(32)
989        )
990        .is_err());
991        for progress_id in ["", "bad\nidentity", &"a".repeat(1025)] {
992            assert!(crate::file_upload::file_upload_request(
993                &state,
994                "filehelper",
995                progress_id,
996                "file.txt",
997                1,
998                &"a".repeat(32)
999            )
1000            .is_err());
1001        }
1002    }
1003    #[test]
1004    fn reserved_file_metadata_and_plaintext_boundary_flag() {
1005        let state = synthetic_state();
1006        let reservation = synthetic_reservation();
1007        let threshold = 25 * 1024 * 1024;
1008        for size in [
1009            threshold - 17,
1010            threshold - 16,
1011            threshold - 15,
1012            threshold - 1,
1013            threshold,
1014            threshold + 1,
1015            MAX_RESERVED_FILE_BYTES,
1016        ] {
1017            let upload = synthetic_file(size, MediaQuality::LargeFile);
1018            let raw = file_send_request_with_reservation(
1019                &state,
1020                "filehelper",
1021                "synthetic-client",
1022                1,
1023                &upload,
1024                Some(&reservation),
1025            )
1026            .unwrap();
1027            let fields = p::decode(&raw).unwrap();
1028            assert_eq!(p::optional(&fields, 6).unwrap().is_some(), size > threshold);
1029            if size > threshold {
1030                assert_eq!(p::scalar(&fields, 6).unwrap(), 7);
1031            }
1032            let app = p::decode(p::data(&fields, 2).unwrap()).unwrap();
1033            let xml = std::str::from_utf8(p::data(&app, 6).unwrap()).unwrap();
1034            assert!(xml.contains("<encryver>0</encryver>"));
1035            assert!(xml.contains(
1036                "<attachid>@cdn_synthetic-file-id_01010101010101010101010101010101_1</attachid>"
1037            ));
1038            assert!(xml.contains("<fileuploadtoken>v1_c3ludGhldGlj</fileuploadtoken>"));
1039            assert!(xml.contains("<overwrite_newmsgid>18446744073709551615</overwrite_newmsgid>"));
1040            assert!(xml.contains("<title>synthetic&amp;file.txt</title>"));
1041            assert_eq!(
1042                local_file_xml_with_reservation(&state.own_username, &upload, Some(&reservation))
1043                    .unwrap(),
1044                format!("<msg>{xml}</msg>")
1045            );
1046            assert!(
1047                file_send_request(&state, "filehelper", "synthetic-client", 1, &upload).is_err()
1048            );
1049        }
1050        // Upload routing changes at an AES-padding boundary; the app-message
1051        // field6 still follows plaintext size, including at exactly 25 MiB.
1052        assert_eq!(((threshold - 17) / 16 + 1) * 16, threshold - 16);
1053        assert_eq!(((threshold - 16) / 16 + 1) * 16, threshold);
1054        let small = synthetic_file(1, MediaQuality::File);
1055        assert!(file_xml("synthetic-user", &small)
1056            .unwrap()
1057            .contains("<encryver>1</encryver>"));
1058        assert!(!file_xml("synthetic-user", &small)
1059            .unwrap()
1060            .contains("fileuploadtoken"));
1061        let too_large = synthetic_file(MAX_RESERVED_FILE_BYTES + 1, MediaQuality::LargeFile);
1062        assert!(
1063            file_xml_with_reservation("synthetic-user", &too_large, Some(&reservation)).is_err()
1064        );
1065        let mut mismatch = synthetic_file(MAX_RESERVED_FILE_BYTES, MediaQuality::LargeFile);
1066        mismatch.descriptor.expected_md5 = Some("0".repeat(32));
1067        assert!(
1068            file_xml_with_reservation("synthetic-user", &mismatch, Some(&reservation)).is_err()
1069        );
1070    }
1071    #[test]
1072    fn reserved_file_acceptance_requires_all_correlations() {
1073        let response = |sender: &[u8], destination: &[u8], client: &[u8], key: &[u8]| {
1074            p::message(&[
1075                p::bytes(1, &p::int32(1, 0)),
1076                p::bytes(3, sender),
1077                p::bytes(4, destination),
1078                p::uint(5, 1),
1079                p::bytes(6, client),
1080                p::uint(7, 1),
1081                p::uint(9, u64::MAX),
1082                p::bytes(10, key),
1083                p::bytes(13, b"synthetic-client"),
1084            ])
1085            .unwrap()
1086        };
1087        let key = b"01010101010101010101010101010101";
1088        for (sender, destination, client, aes, status) in [
1089            (
1090                &b"synthetic-user"[..],
1091                &b"filehelper"[..],
1092                &b"synthetic-client"[..],
1093                &key[..],
1094                SendStatus::ServerAccepted,
1095            ),
1096            (
1097                &b"wrong"[..],
1098                &b"filehelper"[..],
1099                &b"synthetic-client"[..],
1100                &key[..],
1101                SendStatus::NeedsReview,
1102            ),
1103            (
1104                &b"synthetic-user"[..],
1105                &b"wrong"[..],
1106                &b"synthetic-client"[..],
1107                &key[..],
1108                SendStatus::NeedsReview,
1109            ),
1110            (
1111                &b"synthetic-user"[..],
1112                &b"filehelper"[..],
1113                &b"wrong"[..],
1114                &key[..],
1115                SendStatus::NeedsReview,
1116            ),
1117            (
1118                &b"synthetic-user"[..],
1119                &b"filehelper"[..],
1120                &b"synthetic-client"[..],
1121                &b"00000000000000000000000000000000"[..],
1122                SendStatus::NeedsReview,
1123            ),
1124        ] {
1125            let raw = response(sender, destination, client, aes);
1126            let result = decode_file_send_correlated(
1127                &raw,
1128                "synthetic-client",
1129                "synthetic-user",
1130                "filehelper",
1131                &[1; 16],
1132            )
1133            .unwrap();
1134            assert_eq!(result.status, status);
1135            assert_eq!(result.server_message_id, Some(u64::MAX.to_string()));
1136        }
1137        let rejected = p::bytes(1, &p::int32(1, -13));
1138        assert_eq!(
1139            decode_file_send_correlated(
1140                &rejected,
1141                "synthetic-client",
1142                "synthetic-user",
1143                "filehelper",
1144                &[1; 16]
1145            )
1146            .unwrap()
1147            .status,
1148            SendStatus::Rejected
1149        );
1150    }
1151    #[test]
1152    fn video_original_metadata_is_independent_and_old_plans_still_decode() {
1153        use crate::media::MediaDescriptor;
1154        let uploaded = |quality, id: &str, key: u8, size: u32, digest: &str| UploadedMedia {
1155            descriptor: MediaDescriptor {
1156                file_id: id.into(),
1157                aes_key: vec![key; 16],
1158                quality,
1159                expected_bytes: Some(u64::from(size)),
1160                expected_md5: Some(digest.into()),
1161                strict_size: true,
1162                expires_at: None,
1163            },
1164            plaintext_bytes: size,
1165            md5: digest.into(),
1166            sha256: "c".repeat(64),
1167            file_name: "synthetic.mp4".into(),
1168            width: Some(16),
1169            height: Some(12),
1170        };
1171        let mut video = UploadedVideo {
1172            video: uploaded(MediaQuality::Video, "normal-object", 1, 30, &"1".repeat(32)),
1173            thumbnail: uploaded(
1174                MediaQuality::Thumbnail,
1175                "thumbnail-object",
1176                2,
1177                20,
1178                &"2".repeat(32),
1179            ),
1180            original: None,
1181        };
1182        let mut old_plan = serde_json::to_value(&video).unwrap();
1183        old_plan.as_object_mut().unwrap().remove("original");
1184        let cold: UploadedVideo = serde_json::from_value(old_plan).unwrap();
1185        assert!(cold.original.is_none());
1186        let normal =
1187            video_send_request(&synthetic_state(), "filehelper", "client", &cold, 4).unwrap();
1188        let normal = p::decode(&normal).unwrap();
1189        for number in 48..=51 {
1190            assert!(!normal.iter().any(|field| field.number == number));
1191        }
1192        video.original = Some(uploaded(
1193            MediaQuality::Video,
1194            "raw-object",
1195            3,
1196            wechat_media::MAX_VIDEO_SOURCE_BYTES as u32,
1197            &"A".repeat(32),
1198        ));
1199        let request =
1200            video_send_request(&synthetic_state(), "filehelper", "client", &video, 4).unwrap();
1201        let fields = p::decode(&request).unwrap();
1202        assert_eq!(p::data(&fields, 16).unwrap(), b"normal-object");
1203        assert_eq!(p::data(&fields, 17).unwrap(), "01".repeat(16).as_bytes());
1204        assert_eq!(p::scalar(&fields, 8).unwrap(), 30);
1205        assert_eq!(p::data(&fields, 26).unwrap(), "1".repeat(32).as_bytes());
1206        assert_eq!(p::data(&fields, 48).unwrap(), "a".repeat(32).as_bytes());
1207        assert_eq!(p::data(&fields, 49).unwrap(), b"raw-object");
1208        assert_eq!(p::data(&fields, 50).unwrap(), "03".repeat(16).as_bytes());
1209        assert_eq!(p::scalar(&fields, 51).unwrap(), 52_428_800);
1210        let xml = local_video_xml(&video, 4).unwrap();
1211        for expected in [
1212            "cdnvideourl=\"normal-object\"",
1213            "length=\"30\"",
1214            "cdnrawvideourl=\"raw-object\"",
1215            "rawlength=\"52428800\"",
1216            "rawmd5=\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"",
1217        ] {
1218            assert!(xml.contains(expected));
1219        }
1220        assert!(xml.contains(&format!("cdnrawvideoaeskey=\"{}\"", "03".repeat(16))));
1221        for corruption in 0..4 {
1222            let mut altered = video.clone();
1223            let original = altered.original.as_mut().unwrap();
1224            match corruption {
1225                0 => original.descriptor.quality = MediaQuality::LargeFile,
1226                1 => original.descriptor.expected_md5 = Some("0".repeat(32)),
1227                2 => {
1228                    original.plaintext_bytes += 1;
1229                    original.descriptor.expected_bytes = Some(u64::from(original.plaintext_bytes));
1230                }
1231                _ => original.descriptor.expected_bytes = None,
1232            }
1233            assert!(
1234                video_send_request(&synthetic_state(), "filehelper", "client", &altered, 4)
1235                    .is_err()
1236            );
1237            assert!(local_video_xml(&altered, 4).is_err());
1238        }
1239    }
1240
1241    #[test]
1242    fn metadata_matches_independent_synthetic_wire_vectors() {
1243        use crate::media::MediaDescriptor;
1244        let state = synthetic_state();
1245        let descriptor = |quality, file_name: &str| UploadedMedia {
1246            descriptor: MediaDescriptor {
1247                file_id: "synthetic-file-id".into(),
1248                aes_key: vec![1; 16],
1249                quality,
1250                expected_bytes: Some(30),
1251                expected_md5: Some("0".repeat(32)),
1252                strict_size: true,
1253                expires_at: None,
1254            },
1255            plaintext_bytes: 30,
1256            md5: "0".repeat(32),
1257            sha256: "0".repeat(64),
1258            file_name: file_name.into(),
1259            width: Some(16),
1260            height: Some(12),
1261        };
1262        let expected: serde_json::Value =
1263            serde_json::from_str(include_str!("../fixtures/synthetic-media-wire.json")).unwrap();
1264        let file = file_send_request(
1265            &state,
1266            "filehelper",
1267            "synthetic_client_id_0001",
1268            1234567,
1269            &descriptor(MediaQuality::File, "example&.txt"),
1270        )
1271        .unwrap();
1272        assert_eq!(hex::encode(file), expected["fileHex"]);
1273        let share_xml = share_xml(
1274            "synthetic-user",
1275            "A <title> & preview",
1276            "https://example.invalid/a?x=1&y=2",
1277            Some("Description <as text>"),
1278            Some("https://example.invalid/preview.jpg"),
1279        )
1280        .unwrap();
1281        assert!(share_xml.starts_with("<appmsg "));
1282        assert!(share_xml.contains("<title>A &lt;title&gt; &amp; preview</title>"));
1283        assert!(share_xml.contains("<type>5</type>"));
1284        assert!(share_xml.contains("<url>https://example.invalid/a?x=1&amp;y=2</url>"));
1285        assert!(share_xml.ends_with("<fromusername>synthetic-user</fromusername>"));
1286        assert!(!share_xml.contains("<msg>"));
1287        let share =
1288            share_send_request(&state, "room@chatroom", "share-client", 1234567, &share_xml)
1289                .unwrap();
1290        let outer = p::decode(&share).unwrap();
1291        let app = p::decode(p::data(&outer, 2).unwrap()).unwrap();
1292        assert_eq!(p::scalar(&app, 5).unwrap(), 5);
1293        assert_eq!(p::data(&app, 4).unwrap(), b"room@chatroom");
1294        assert_eq!(p::data(&app, 6).unwrap(), share_xml.as_bytes());
1295        assert_eq!(p::scalar(&app, 7).unwrap(), 1234567);
1296        assert_eq!(p::data(&app, 8).unwrap(), b"share-client");
1297        assert!(p::optional_data(&outer, 5).unwrap().is_none()); // No file MD5 for a URL share.
1298        let image = UploadedImage {
1299            original: descriptor(MediaQuality::Original, "image.jpg"),
1300            standard: descriptor(MediaQuality::Standard, "image.jpg"),
1301            thumbnail: descriptor(MediaQuality::Thumbnail, "image.jpg"),
1302        };
1303        assert_eq!(
1304            hex::encode(
1305                image_send_request(&state, "filehelper", "synthetic_client_id_0001", &image)
1306                    .unwrap()
1307            ),
1308            expected["imageHex"]
1309        );
1310        let video = UploadedVideo {
1311            video: descriptor(MediaQuality::Video, "video.mp4"),
1312            thumbnail: descriptor(MediaQuality::Thumbnail, "thumb.jpg"),
1313            original: None,
1314        };
1315        assert_eq!(
1316            hex::encode(
1317                video_send_request(&state, "filehelper", "synthetic_client_id_0001", &video, 4)
1318                    .unwrap()
1319            ),
1320            expected["videoHex"]
1321        );
1322        // A note that fits one request keeps the single end-flagged request shape.
1323        let encoded = [vec![2], b"#!SILK_V3".to_vec(), vec![0; 6]].concat();
1324        let upload = VoiceUpload::new(
1325            "filehelper",
1326            "synthetic_client_id_0001",
1327            1234567,
1328            4000,
1329            &encoded,
1330        )
1331        .unwrap();
1332        assert_eq!((upload.request_count(), upload.is_final()), (1, true));
1333        assert_eq!(
1334            hex::encode(upload.request(&state).unwrap()),
1335            expected["voiceHex"]
1336        );
1337    }
1338    fn synthetic_note(len: usize) -> Vec<u8> {
1339        let mut note = [vec![2], b"#!SILK_V3".to_vec()].concat();
1340        let mut seed = 0x2545_f491_4f6c_dd1du64;
1341        note.extend((note.len()..len).map(|_| {
1342            seed ^= seed << 13;
1343            seed ^= seed >> 7;
1344            seed ^= seed << 17;
1345            seed as u8
1346        }));
1347        note
1348    }
1349    fn voice_reply_bytes(code: i32, extra: &[Vec<u8>]) -> Vec<u8> {
1350        let mut parts = vec![p::bytes(10, &p::int32(1, code))];
1351        parts.extend_from_slice(extra);
1352        p::message(&parts).unwrap()
1353    }
1354    #[test]
1355    fn long_voice_note_uses_consecutive_ranges_of_one_client_id() {
1356        let state = synthetic_state();
1357        let client = "synthetic_voice_id";
1358        let note = synthetic_note(2 * VOICE_UPLOAD_CHUNK_BYTES + 100);
1359        let mut upload = VoiceUpload::new("filehelper", client, 1234567, 60_000, &note).unwrap();
1360        assert_eq!(upload.request_count(), 3);
1361        let mut sent = vec![];
1362        for (index, (offset, length)) in [
1363            (0, VOICE_UPLOAD_CHUNK_BYTES),
1364            (VOICE_UPLOAD_CHUNK_BYTES, VOICE_UPLOAD_CHUNK_BYTES),
1365            (2 * VOICE_UPLOAD_CHUNK_BYTES, 100),
1366        ]
1367        .into_iter()
1368        .enumerate()
1369        {
1370            let f = p::decode(&upload.request(&state).unwrap()).unwrap();
1371            assert_eq!(p::data(&f, 5).unwrap(), client.as_bytes());
1372            assert_eq!(p::scalar(&f, 3).unwrap(), offset as u64);
1373            assert_eq!(p::scalar(&f, 4).unwrap(), length as u64);
1374            assert_eq!(p::scalar(&f, 7).unwrap(), 60_000);
1375            assert_eq!(p::scalar(&f, 9).unwrap(), u64::from(index == 2));
1376            assert_eq!(p::scalar(&f, 11).unwrap(), 0);
1377            assert_eq!(p::scalar(&f, 13).unwrap(), 4);
1378            if index == 0 {
1379                assert_eq!(
1380                    p::unbuffer(p::data(&f, 18).unwrap()).unwrap(),
1381                    client.as_bytes()
1382                );
1383            } else {
1384                assert!(
1385                    p::optional_data(&f, 18).unwrap().is_none(),
1386                    "continuation range {index} repeats first-range metadata"
1387                );
1388            }
1389            // Later ranges continue the server message ID returned by the first.
1390            assert_eq!(
1391                p::optional(&f, 6).unwrap().is_some(),
1392                index > 0,
1393                "range {index}"
1394            );
1395            if index > 0 {
1396                assert_eq!(p::scalar(&f, 6).unwrap(), 77);
1397            }
1398            let data = p::unbuffer(p::data(&f, 8).unwrap()).unwrap();
1399            assert_eq!(data.len(), length);
1400            sent.extend(data);
1401            let reply = if index < 2 {
1402                voice_reply_bytes(
1403                    0,
1404                    &[
1405                        p::uint(3, offset as u64),
1406                        p::uint(4, length as u64),
1407                        p::bytes(6, client.as_bytes()),
1408                        p::uint(7, 77),
1409                        p::uint(9, 0),
1410                    ],
1411                )
1412            } else {
1413                let mut extra = vec![p::bytes(6, client.as_bytes()), p::uint(12, 9_000)];
1414                extra.extend([3, 4, 5, 7, 8, 9].map(|n| p::uint(n, 0)));
1415                voice_reply_bytes(0, &extra)
1416            };
1417            let result = upload.accept_reply(&reply).unwrap();
1418            assert_eq!(result.is_some(), index == 2);
1419            if let Some(result) = result {
1420                assert_eq!(result.status, SendStatus::ServerAccepted);
1421                assert_eq!(result.server_message_id.as_deref(), Some("9000"));
1422            }
1423        }
1424        assert_eq!(sent, note);
1425    }
1426    #[test]
1427    fn long_voice_note_partial_or_odd_replies_stop_without_acceptance() {
1428        let note = synthetic_note(2 * VOICE_UPLOAD_CHUNK_BYTES + 100);
1429        let start =
1430            || VoiceUpload::new("filehelper", "synthetic_voice_id", 1, 60_000, &note).unwrap();
1431        let progress = voice_reply_bytes(0, &[p::uint(7, 5), p::uint(9, 0)]);
1432        // Rejecting the first range leaves nothing on the server.
1433        let mut upload = start();
1434        let result = upload
1435            .accept_reply(&voice_reply_bytes(-2, &[]))
1436            .unwrap()
1437            .unwrap();
1438        assert_eq!(
1439            (result.status, result.service_code),
1440            (SendStatus::Rejected, -2)
1441        );
1442        // A later rejection follows accepted ranges and needs review.
1443        let mut upload = start();
1444        assert!(upload.accept_reply(&progress).unwrap().is_none());
1445        let result = upload
1446            .accept_reply(&voice_reply_bytes(-2, &[]))
1447            .unwrap()
1448            .unwrap();
1449        assert_eq!(result.status, SendStatus::NeedsReview);
1450        let mut upload = start();
1451        assert!(upload.accept_reply(&progress).unwrap().is_none());
1452        assert!(upload.accept_reply(&progress).unwrap().is_none());
1453        assert!(upload.is_final());
1454        let result = upload
1455            .accept_reply(&voice_reply_bytes(-2, &[]))
1456            .unwrap()
1457            .unwrap();
1458        assert_eq!(result.status, SendStatus::NeedsReview);
1459        // An early end flag or a changed server message ID is an uncertain error.
1460        let mut upload = start();
1461        assert!(upload
1462            .accept_reply(&voice_reply_bytes(0, &[p::uint(9, 1)]))
1463            .is_err());
1464        let mut upload = start();
1465        assert!(upload.accept_reply(&progress).unwrap().is_none());
1466        assert!(upload
1467            .accept_reply(&voice_reply_bytes(0, &[p::uint(7, 6)]))
1468            .is_err());
1469        // A final ID that differs from one announced earlier needs review.
1470        let mut upload = start();
1471        let early_id = voice_reply_bytes(0, &[p::uint(12, 41)]);
1472        assert!(upload.accept_reply(&early_id).unwrap().is_none());
1473        assert!(upload.accept_reply(&early_id).unwrap().is_none());
1474        let mut extra = vec![p::bytes(6, b"synthetic_voice_id"), p::uint(12, 42)];
1475        extra.extend([3, 4, 5, 7, 8, 9].map(|n| p::uint(n, 0)));
1476        let result = upload
1477            .accept_reply(&voice_reply_bytes(0, &extra))
1478            .unwrap()
1479            .unwrap();
1480        assert_eq!(result.status, SendStatus::NeedsReview);
1481    }
1482    #[test]
1483    fn voice_note_bounds_follow_the_sixty_second_profile() {
1484        let note = synthetic_note(silk::MAX_NOTE_BYTES);
1485        assert!(VoiceUpload::new("filehelper", "synthetic_voice_id", 1, 60_000, &note).is_ok());
1486        assert!(VoiceUpload::new("filehelper", "synthetic_voice_id", 1, 60_001, &note).is_err());
1487        let oversized = synthetic_note(silk::MAX_NOTE_BYTES + 1);
1488        assert!(
1489            VoiceUpload::new("filehelper", "synthetic_voice_id", 1, 60_000, &oversized).is_err()
1490        );
1491        assert!(local_voice_xml(silk::MAX_NOTE_BYTES, 60_000).is_ok());
1492        assert!(local_voice_xml(silk::MAX_NOTE_BYTES + 1, 60_000).is_err());
1493        assert!(local_voice_xml(1000, 60_001).is_err());
1494        assert!(local_voice_xml(1000, 0).is_err());
1495    }
1496    #[test]
1497    fn largest_voice_request_fits_one_short_link_record() {
1498        // Worst case: longest accepted identities, a full incompressible range,
1499        // either packet mode and the longest host name the route can carry.
1500        let mut state = synthetic_state();
1501        state.own_username = "u".repeat(256);
1502        let destination = "d".repeat(512);
1503        let client = "c".repeat(128);
1504        let note = synthetic_note(silk::MAX_NOTE_BYTES);
1505        let upload = VoiceUpload::new(&destination, &client, u32::MAX, 60_000, &note).unwrap();
1506        let request = upload.request(&state).unwrap();
1507        let path = b"/cgi-bin/micromsg-bin/uploadvoice";
1508        for keys in [vec![], vec![2; 24]] {
1509            state.request_key = keys.clone();
1510            state.response_key = keys;
1511            let packet = crate::business::AuthenticatedPacket::prepare(&state, 127, &request)
1512                .unwrap()
1513                .packet
1514                .clone();
1515            let route = crate::mmtls::section(
1516                &[
1517                    (path.len() as u16).to_be_bytes().as_slice(),
1518                    path,
1519                    &253u16.to_be_bytes(),
1520                    &[b'h'; 253],
1521                    &crate::mmtls::section(&packet),
1522                ]
1523                .concat(),
1524            );
1525            assert!(route.len() <= 65519, "{}", route.len());
1526            assert!(crate::mmtls::seal(0x17, &route, &[0; 16], &[0; 12], 2).is_ok());
1527        }
1528    }
1529    #[test]
1530    fn voice_response_needs_unique_correlation() {
1531        let mut parts = vec![p::bytes(10, &p::uint(1, 0)), p::uint(12, u64::MAX)];
1532        for n in [3, 4, 5, 7, 8, 9] {
1533            parts.push(p::uint(n, 0));
1534        }
1535        let no_echo = p::message(&parts).unwrap();
1536        assert_eq!(
1537            decode_voice_send(&no_echo, "synthetic_voice_id")
1538                .unwrap()
1539                .status,
1540            SendStatus::NeedsReview
1541        );
1542        parts.push(p::bytes(6, b"synthetic_voice_id"));
1543        let result = decode_voice_send(&p::message(&parts).unwrap(), "synthetic_voice_id").unwrap();
1544        assert_eq!(result.status, SendStatus::ServerAccepted);
1545        assert_eq!(result.server_message_id, Some(u64::MAX.to_string()));
1546        parts.push(p::bytes(2, b"synthetic_voice_id"));
1547        assert_eq!(
1548            decode_voice_send(&p::message(&parts).unwrap(), "synthetic_voice_id")
1549                .unwrap()
1550                .status,
1551            SendStatus::NeedsReview
1552        );
1553    }
1554    #[test]
1555    fn xml_escapes_filename_and_rejects_paths() {
1556        use crate::media::MediaDescriptor;
1557        let mut upload = UploadedMedia {
1558            descriptor: MediaDescriptor {
1559                file_id: "synthetic-file-id".into(),
1560                aes_key: vec![1; 16],
1561                quality: MediaQuality::File,
1562                expected_bytes: Some(3),
1563                expected_md5: Some("0".repeat(32)),
1564                strict_size: true,
1565                expires_at: None,
1566            },
1567            plaintext_bytes: 3,
1568            md5: "0".repeat(32),
1569            sha256: "0".repeat(64),
1570            file_name: "test&<.txt".into(),
1571            width: None,
1572            height: None,
1573        };
1574        let xml = file_xml("synthetic-user", &upload).unwrap();
1575        assert!(xml.contains("test&amp;&lt;.txt"));
1576        upload.file_name = "../secret".into();
1577        assert!(file_xml("synthetic-user", &upload).is_err());
1578    }
1579    #[test]
1580    fn rejected_response_does_not_require_success_fields() {
1581        let reply = p::bytes(1, &p::int32(1, -2));
1582        assert_eq!(
1583            decode_file_send(&reply, "client").unwrap().status,
1584            SendStatus::Rejected
1585        );
1586    }
1587}