1use 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('&', "&")
51 .replace('<', "<")
52 .replace('>', ">")
53 .replace('"', """)
54 .replace('\'', "'")
55}
56fn tag(name: &str, value: impl ToString) -> String {
57 format!("<{name}>{}</{name}>", escape(&value.to_string()))
58}
59pub 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}
215fn 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 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}
514pub const VOICE_UPLOAD_CHUNK_BYTES: usize = 32 * 1024;
520
521pub 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 pub fn request_count(&self) -> usize {
579 self.encoded.len().div_ceil(VOICE_UPLOAD_CHUNK_BYTES)
580 }
581 pub fn is_final(&self) -> bool {
583 self.range().end == self.encoded.len()
584 }
585 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 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 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}
724pub 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}
842pub 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&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 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 <title> & preview</title>"));
1283 assert!(share_xml.contains("<type>5</type>"));
1284 assert!(share_xml.contains("<url>https://example.invalid/a?x=1&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()); 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 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, ¬e).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 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, ¬e).unwrap();
1431 let progress = voice_reply_bytes(0, &[p::uint(7, 5), p::uint(9, 0)]);
1432 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 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 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 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, ¬e).is_ok());
1486 assert!(VoiceUpload::new("filehelper", "synthetic_voice_id", 1, 60_001, ¬e).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 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, ¬e).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&<.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}