1use std::fmt::Write as _;
28
29#[derive(Debug, Clone, Copy)]
32pub struct Medium<'a> {
33 pub ct: &'a str,
34 pub bytes: &'a [u8],
35}
36
37#[derive(Debug, Clone, Copy)]
39pub struct Message<'a> {
40 pub key: &'a str,
43 pub to: &'a [&'a str],
45 pub date: i64,
47 pub words: &'a str,
49 pub media: &'a [Medium<'a>],
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub enum Refused {
56 Number,
58 Empty,
60 Key,
62 Type,
64}
65
66pub fn encode(message: &Message) -> Result<Vec<u8>, Refused> {
68 let mut out = Vec::with_capacity(message.media.iter().map(|m| m.bytes.len()).sum::<usize>() + message.words.len() + 1024);
69 write(message, &mut out)?;
70 Ok(out)
71}
72
73pub fn size(message: &Message) -> Result<usize, Refused> {
77 let mut count = Count(0);
78 write(message, &mut count)?;
79 Ok(count.0)
80}
81
82pub fn size_with_lengths(message: &Message, lengths: &[usize]) -> Result<usize, Refused> {
88 let empty = size(message)?;
89 let grown = lengths.iter().map(|&length| {
90 let mut count = Count(0);
91 uintvar(&mut count, length);
92 length + count.0 - 1
93 });
94 Ok(empty + grown.sum::<usize>())
95}
96
97pub fn room(message: &Message, limit: usize) -> Option<usize> {
106 let empty = size(message).ok()?;
107 let mut longest = Count(0);
108 uintvar(&mut longest, limit);
109 limit.checked_sub(empty)?.checked_sub(longest.0.saturating_sub(1) * message.media.len())
110}
111
112pub fn address(number: &str) -> Option<String> {
117 let number = number.trim();
118 let mut out = String::new();
119 for (i, c) in number.chars().enumerate() {
120 match c {
121 '0'..='9' => out.push(c),
122 '+' if i == 0 => out.push(c),
123 ' ' | '\u{a0}' | '\u{202f}' | '.' | '-' | '(' | ')' | '/' => {}
124 _ => return None,
125 }
126 }
127 (out.chars().filter(char::is_ascii_digit).count() >= 3).then_some(out)
128}
129
130pub const LONGEST_NUMBER: &str = "+000000000000000";
134
135#[derive(Debug, Clone, Default, PartialEq, Eq)]
138pub struct Conf {
139 pub status: u8,
143 pub id: String,
145 pub key: String,
147 pub said: String,
149}
150
151pub const OK: u8 = 0x80;
153
154impl Conf {
155 pub fn ok(&self) -> bool {
157 self.status == OK
158 }
159}
160
161pub fn conf(pdu: &[u8]) -> Option<Conf> {
164 let mut at = Reader { bytes: pdu, at: 0 };
165 if at.byte()? != MESSAGE_TYPE || at.byte()? != SEND_CONF {
166 return None;
167 }
168 let mut conf = Conf::default();
169 let mut status = None;
170 while let Some(field) = at.byte() {
171 match field {
172 TRANSACTION_ID => conf.key = at.text()?,
173 MESSAGE_ID => conf.id = at.text()?,
174 RESPONSE_STATUS => status = Some(at.byte()?),
175 RESPONSE_TEXT => conf.said = at.encoded()?,
176 field if field >= 0x80 => at.skip()?,
177 _ => return None,
178 }
179 }
180 conf.status = status?;
181 Some(conf)
182}
183
184const CONTENT_TYPE: u8 = 0x84;
188const DATE: u8 = 0x85;
189const DELIVERY_REPORT: u8 = 0x86;
190const FROM: u8 = 0x89;
191const MESSAGE_CLASS: u8 = 0x8A;
192const MESSAGE_ID: u8 = 0x8B;
193const MESSAGE_TYPE: u8 = 0x8C;
194const MMS_VERSION: u8 = 0x8D;
195const READ_REPORT: u8 = 0x90;
196const RESPONSE_STATUS: u8 = 0x92;
197const RESPONSE_TEXT: u8 = 0x93;
198const TO: u8 = 0x97;
199const TRANSACTION_ID: u8 = 0x98;
200const SEND_REQ: u8 = 0x80;
201const SEND_CONF: u8 = 0x81;
202const VERSION_1_2: u8 = 0x80 | (1 << 4) | 2;
204const INSERT_ADDRESS: u8 = 0x81;
205const PERSONAL: u8 = 0x80;
206const NO: u8 = 0x81;
207const P_CHARSET: u8 = 0x81;
209const P_NAME: u8 = 0x85;
210const P_TYPE: u8 = 0x89;
211const P_START: u8 = 0x8A;
212const P_CONTENT_LOCATION: u8 = 0x8E;
213const P_CONTENT_ID: u8 = 0xC0;
214const UTF_8: u8 = 106;
216const SHOWN_MS: u32 = 5000;
218
219const NUMBERED: [&str; 0x53] = [
223 "*/*",
224 "text/*",
225 "text/html",
226 "text/plain",
227 "text/x-hdml",
228 "text/x-ttml",
229 "text/x-vCalendar",
230 "text/x-vCard",
231 "text/vnd.wap.wml",
232 "text/vnd.wap.wmlscript",
233 "text/vnd.wap.wta-event",
234 "multipart/*",
235 "multipart/mixed",
236 "multipart/form-data",
237 "multipart/byterantes",
238 "multipart/alternative",
239 "application/*",
240 "application/java-vm",
241 "application/x-www-form-urlencoded",
242 "application/x-hdmlc",
243 "application/vnd.wap.wmlc",
244 "application/vnd.wap.wmlscriptc",
245 "application/vnd.wap.wta-eventc",
246 "application/vnd.wap.uaprof",
247 "application/vnd.wap.wtls-ca-certificate",
248 "application/vnd.wap.wtls-user-certificate",
249 "application/x-x509-ca-cert",
250 "application/x-x509-user-cert",
251 "image/*",
252 "image/gif",
253 "image/jpeg",
254 "image/tiff",
255 "image/png",
256 "image/vnd.wap.wbmp",
257 "application/vnd.wap.multipart.*",
258 "application/vnd.wap.multipart.mixed",
259 "application/vnd.wap.multipart.form-data",
260 "application/vnd.wap.multipart.byteranges",
261 "application/vnd.wap.multipart.alternative",
262 "application/xml",
263 "text/xml",
264 "application/vnd.wap.wbxml",
265 "application/x-x968-cross-cert",
266 "application/x-x968-ca-cert",
267 "application/x-x968-user-cert",
268 "text/vnd.wap.si",
269 "application/vnd.wap.sic",
270 "text/vnd.wap.sl",
271 "application/vnd.wap.slc",
272 "text/vnd.wap.co",
273 "application/vnd.wap.coc",
274 "application/vnd.wap.multipart.related",
275 "application/vnd.wap.sia",
276 "text/vnd.wap.connectivity-xml",
277 "application/vnd.wap.connectivity-wbxml",
278 "application/pkcs7-mime",
279 "application/vnd.wap.hashed-certificate",
280 "application/vnd.wap.signed-certificate",
281 "application/vnd.wap.cert-response",
282 "application/xhtml+xml",
283 "application/wml+xml",
284 "text/css",
285 "application/vnd.wap.mms-message",
286 "application/vnd.wap.rollover-certificate",
287 "application/vnd.wap.locc+wbxml",
288 "application/vnd.wap.loc+xml",
289 "application/vnd.syncml.dm+wbxml",
290 "application/vnd.syncml.dm+xml",
291 "application/vnd.syncml.notification",
292 "application/vnd.wap.xhtml+xml",
293 "application/vnd.wv.csp.cir",
294 "application/vnd.oma.dd+xml",
295 "application/vnd.oma.drm.message",
296 "application/vnd.oma.drm.content",
297 "application/vnd.oma.drm.rights+xml",
298 "application/vnd.oma.drm.rights+wbxml",
299 "application/vnd.wv.csp+xml",
300 "application/vnd.wv.csp+wbxml",
301 "application/vnd.syncml.ds.notification",
302 "audio/*",
303 "video/*",
304 "application/vnd.oma.dd2+xml",
305 "application/mikey",
306];
307
308trait Out {
310 fn put(&mut self, bytes: &[u8]);
311 fn byte(&mut self, byte: u8) {
312 self.put(&[byte]);
313 }
314}
315
316impl Out for Vec<u8> {
317 fn put(&mut self, bytes: &[u8]) {
318 self.extend_from_slice(bytes);
319 }
320}
321
322struct Count(usize);
324
325impl Out for Count {
326 fn put(&mut self, bytes: &[u8]) {
327 self.0 += bytes.len();
328 }
329}
330
331fn uintvar(out: &mut impl Out, value: usize) {
333 let mut bytes = [0u8; 10];
334 let mut at = bytes.len();
335 let mut rest = value;
336 loop {
337 at -= 1;
338 bytes[at] = (rest & 0x7F) as u8 | if at == bytes.len() - 1 { 0 } else { 0x80 };
339 rest >>= 7;
340 if rest == 0 {
341 break;
342 }
343 }
344 out.put(&bytes[at..]);
345}
346
347fn value_length(out: &mut impl Out, length: usize) {
349 if length <= 30 {
350 out.byte(length as u8);
351 } else {
352 out.byte(31);
353 uintvar(out, length);
354 }
355}
356
357fn text(out: &mut impl Out, bytes: &[u8]) {
359 if bytes.first().is_some_and(|b| *b >= 0x80) {
360 out.byte(0x7F);
361 }
362 out.put(bytes);
363 out.byte(0);
364}
365
366fn quoted(out: &mut impl Out, bytes: &[u8]) {
368 out.byte(b'"');
369 out.put(bytes);
370 out.byte(0);
371}
372
373fn long_integer(out: &mut impl Out, value: u64) {
375 let bytes = value.to_be_bytes();
376 let first = bytes.iter().position(|b| *b != 0).unwrap_or(bytes.len());
377 out.byte((bytes.len() - first) as u8);
378 out.put(&bytes[first..]);
379}
380
381fn content_type(out: &mut impl Out, ct: &str) {
383 match NUMBERED.iter().position(|known| *known == ct) {
384 Some(number) => out.byte(0x80 | number as u8),
385 None => text(out, ct.as_bytes()),
386 }
387}
388
389#[derive(Debug, Clone, Copy, PartialEq, Eq)]
391enum Kind {
392 Image,
393 Video,
394 Audio,
395 Card,
396 Other,
397}
398
399fn kind_of(ct: &str) -> Kind {
400 let ct = ct.to_ascii_lowercase();
401 if ct.starts_with("image/") {
402 Kind::Image
403 } else if ct.starts_with("video/") {
404 Kind::Video
405 } else if ct.starts_with("audio/") {
406 Kind::Audio
407 } else if matches!(ct.as_str(), "text/x-vcard" | "text/vcard" | "text/x-vcalendar" | "text/calendar") {
408 Kind::Card
409 } else {
410 Kind::Other
411 }
412}
413
414fn names(media: &[Medium]) -> Vec<(Kind, String)> {
418 let mut seen = [0usize; 5];
419 media
420 .iter()
421 .map(|medium| {
422 let kind = kind_of(medium.ct);
423 let rank = &mut seen[kind as usize];
424 *rank += 1;
425 let ct = medium.ct.to_ascii_lowercase();
426 let (stem, extension) = match kind {
427 Kind::Image => ("image", match ct.as_str() {
428 "image/png" => "png",
429 "image/gif" => "gif",
430 "image/webp" => "webp",
431 "image/bmp" => "bmp",
432 _ => "jpg",
433 }),
434 Kind::Video => ("video", if ct.contains("3gpp") { "3gp" } else if ct.contains("webm") { "webm" } else { "mp4" }),
435 Kind::Audio => ("audio", match ct.as_str() {
436 "audio/amr" => "amr",
437 "audio/mpeg" | "audio/mp3" => "mp3",
438 "audio/ogg" | "audio/opus" => "ogg",
439 "audio/aac" => "aac",
440 "audio/wav" | "audio/x-wav" => "wav",
441 _ => "m4a",
442 }),
443 Kind::Card if ct.contains("cal") => ("calendar", "vcs"),
444 Kind::Card => ("contact", "vcf"),
445 Kind::Other => ("other", "dat"),
446 };
447 (kind, format!("{stem}{rank}.{extension}"))
448 })
449 .collect()
450}
451
452const WORDS_NAME: &str = "text1.txt";
454
455fn smil(parts: &[(Kind, String)], words: bool) -> String {
461 let mut body = String::new();
462 for (kind, name) in parts {
463 let _ = match kind {
464 Kind::Image => write!(body, "<par dur=\"{SHOWN_MS}ms\"><img src=\"{name}\" region=\"Image\" /></par>"),
465 Kind::Video => write!(body, "<par><video src=\"{name}\" region=\"Image\" /></par>"),
466 Kind::Audio => write!(body, "<par><audio src=\"{name}\" /></par>"),
467 Kind::Card | Kind::Other => write!(body, "<par dur=\"{SHOWN_MS}ms\"><ref src=\"{name}\" /></par>"),
468 };
469 }
470 if words {
471 let _ = write!(body, "<par dur=\"{SHOWN_MS}ms\"><text src=\"{WORDS_NAME}\" region=\"Text\" /></par>");
472 }
473 let visual = parts.iter().any(|(kind, _)| matches!(kind, Kind::Image | Kind::Video));
474 let other = parts.iter().any(|(kind, _)| matches!(kind, Kind::Audio | Kind::Card));
475 let layout = match (visual, other, words) {
476 (true, _, true) => r#"<region id="Image" fit="meet" top="0" left="0" height="80%" width="100%"/><region id="Text" top="80%" left="0" height="20%" width="100%"/>"#,
477 (true, _, false) => r#"<region id="Image" fit="meet" top="0" left="0" height="100%" width="100%"/>"#,
478 (false, true, false) => "",
479 _ => r#"<region id="Text" top="0" left="0" height="100%" width="100%"/>"#,
480 };
481 format!("<smil><head><layout><root-layout/>{layout}</layout></head><body>{body}</body></smil>")
482}
483
484fn part(out: &mut impl Out, ct: &str, name: &str, charset: Option<u8>, data: &[u8]) {
488 let mut typed = Vec::new();
489 content_type(&mut typed, ct);
490 typed.push(P_NAME);
491 text(&mut typed, name.as_bytes());
492 if let Some(charset) = charset {
493 typed.push(P_CHARSET);
494 typed.push(0x80 | charset);
495 }
496 let mut headers = Vec::new();
497 value_length(&mut headers, typed.len());
498 headers.extend_from_slice(&typed);
499 let id = name.rsplit_once('.').map_or(name, |(stem, _)| stem);
500 headers.push(P_CONTENT_ID);
501 quoted(&mut headers, format!("<{id}>").as_bytes());
502 headers.push(P_CONTENT_LOCATION);
503 text(&mut headers, name.as_bytes());
504 uintvar(out, headers.len());
505 uintvar(out, data.len());
506 out.put(&headers);
507 out.put(data);
508}
509
510fn write(message: &Message, out: &mut impl Out) -> Result<(), Refused> {
512 let key = message.key.as_bytes();
513 if key.is_empty() || key.len() > 64 || !key.iter().all(|b| b.is_ascii_graphic()) {
514 return Err(Refused::Key);
515 }
516 let numbers: Vec<String> = message.to.iter().map(|n| address(n)).collect::<Option<_>>().ok_or(Refused::Number)?;
517 if numbers.is_empty() {
518 return Err(Refused::Number);
519 }
520 if message.words.is_empty() && message.media.is_empty() {
521 return Err(Refused::Empty);
522 }
523 if message.media.iter().any(|m| m.ct.is_empty() || m.ct.len() > 100 || !m.ct.bytes().all(|b| b.is_ascii_graphic())) {
524 return Err(Refused::Type);
525 }
526 out.put(&[MESSAGE_TYPE, SEND_REQ, TRANSACTION_ID]);
527 text(out, key);
528 out.put(&[MMS_VERSION, VERSION_1_2, DATE]);
529 long_integer(out, u64::try_from(message.date).unwrap_or(0).max(1));
530 out.put(&[FROM, 1, INSERT_ADDRESS]);
531 for number in &numbers {
532 let mut value = vec![0x80 | UTF_8];
534 text(&mut value, format!("{number}/TYPE=PLMN").as_bytes());
535 out.byte(TO);
536 value_length(out, value.len());
537 out.put(&value);
538 }
539 out.put(&[MESSAGE_CLASS, PERSONAL, DELIVERY_REPORT, NO, READ_REPORT, NO, CONTENT_TYPE]);
540 let mut related = Vec::new();
542 content_type(&mut related, "application/vnd.wap.multipart.related");
543 related.push(P_START);
544 text(&mut related, b"<smil>");
545 related.push(P_TYPE);
546 text(&mut related, b"application/smil");
547 value_length(out, related.len());
548 out.put(&related);
549 let named = names(message.media);
550 let words = !message.words.is_empty();
551 uintvar(out, 1 + message.media.len() + usize::from(words));
552 let layout = smil(&named, words);
554 part(out, "application/smil", "smil.xml", None, layout.as_bytes());
555 for (medium, (_, name)) in message.media.iter().zip(&named) {
556 part(out, medium.ct, name, None, medium.bytes);
557 }
558 if words {
559 part(out, "text/plain", WORDS_NAME, Some(UTF_8), message.words.as_bytes());
560 }
561 Ok(())
562}
563
564struct Reader<'a> {
568 bytes: &'a [u8],
569 at: usize,
570}
571
572impl Reader<'_> {
573 fn byte(&mut self) -> Option<u8> {
574 let byte = *self.bytes.get(self.at)?;
575 self.at += 1;
576 Some(byte)
577 }
578
579 fn take(&mut self, count: usize) -> Option<&[u8]> {
580 let end = self.at.checked_add(count)?;
581 let taken = self.bytes.get(self.at..end)?;
582 self.at = end;
583 Some(taken)
584 }
585
586 fn uintvar(&mut self) -> Option<usize> {
587 let mut value: usize = 0;
588 for _ in 0..5 {
589 let byte = self.byte()?;
590 value = (value << 7) | usize::from(byte & 0x7F);
591 if byte & 0x80 == 0 {
592 return Some(value);
593 }
594 }
595 None
596 }
597
598 fn text(&mut self) -> Option<String> {
600 let rest = self.bytes.get(self.at..)?;
601 let end = rest.iter().position(|b| *b == 0)?;
602 let mut bytes = &rest[..end];
603 self.at += end + 1;
604 if bytes.first() == Some(&0x7F) || bytes.first() == Some(&b'"') {
605 bytes = &bytes[1..];
606 }
607 Some(String::from_utf8_lossy(bytes).into_owned())
608 }
609
610 fn encoded(&mut self) -> Option<String> {
612 let first = *self.bytes.get(self.at)?;
613 if first > 31 {
614 return self.text();
615 }
616 let length = self.length()?;
617 let value = self.take(length)?;
618 let mut inner = Reader { bytes: value, at: 0 };
619 match inner.byte()? {
621 byte if byte >= 0x80 => {}
622 byte if byte <= 30 => {
623 inner.take(usize::from(byte))?;
624 }
625 _ => return None,
626 }
627 inner.text()
628 }
629
630 fn length(&mut self) -> Option<usize> {
632 match self.byte()? {
633 31 => self.uintvar(),
634 byte if byte <= 30 => Some(usize::from(byte)),
635 _ => None,
636 }
637 }
638
639 fn skip(&mut self) -> Option<()> {
641 match *self.bytes.get(self.at)? {
642 0..=31 => {
643 let length = self.length()?;
644 self.take(length)?;
645 }
646 32..=127 => {
647 self.text()?;
648 }
649 _ => {
650 self.at += 1;
651 }
652 }
653 Some(())
654 }
655}
656
657#[cfg(test)]
658mod tests {
659 use super::*;
660 use std::path::Path;
661
662 const KEY: &str = "0f3c9a5e7b2d4f6a8c1e3b5d7f9a2c4e";
663 const WRITTEN: i64 = 1_792_324_980;
665
666 fn hex(bytes: &[u8]) -> String {
667 bytes.iter().map(|b| format!("{b:02x}")).collect()
668 }
669
670 fn sha256(bytes: &[u8]) -> String {
671 use sha2::{Digest, Sha256};
672 hex(&Sha256::digest(bytes))
673 }
674
675 fn stand_in(count: usize, seed: u8) -> Vec<u8> {
677 (0..count).map(|i| (i as u8).wrapping_mul(31).wrapping_add(seed)).collect()
678 }
679
680 #[derive(Debug, Default)]
682 struct Read {
683 headers: Vec<(u8, Vec<u8>)>,
684 start: String,
685 parts: Vec<ReadPart>,
686 }
687
688 #[derive(Debug, Default, PartialEq)]
689 struct ReadPart {
690 ct: String,
691 name: String,
692 charset: u8,
693 id: String,
694 location: String,
695 data: Vec<u8>,
696 }
697
698 fn read(pdu: &[u8]) -> Read {
701 let mut r = Reader { bytes: pdu, at: 0 };
702 let mut read = Read::default();
703 loop {
704 let field = r.byte().expect("a header");
705 if field == CONTENT_TYPE {
706 break;
707 }
708 let from = r.at;
709 match field {
710 MESSAGE_TYPE | MMS_VERSION | MESSAGE_CLASS | DELIVERY_REPORT | READ_REPORT => {
711 r.byte().unwrap();
712 }
713 TRANSACTION_ID => {
714 r.text().unwrap();
715 }
716 DATE => {
717 let count = usize::from(r.byte().unwrap());
718 r.take(count).unwrap();
719 }
720 FROM | TO => {
721 let length = r.length().unwrap();
722 r.take(length).unwrap();
723 }
724 _ => panic!("a header Sioul does not write: {field:#x}"),
725 }
726 read.headers.push((field, pdu[from..r.at].to_vec()));
727 }
728 let length = r.length().unwrap();
729 let related = r.take(length).unwrap().to_vec();
730 assert_eq!(related[0], 0x80 | 0x33, "multipart/related");
731 let mut inner = Reader { bytes: &related, at: 1 };
732 assert_eq!(inner.byte(), Some(P_START));
733 read.start = inner.text().unwrap();
734 assert_eq!(inner.byte(), Some(P_TYPE));
735 assert_eq!(inner.text().unwrap(), "application/smil");
736 assert_eq!(inner.at, related.len());
737 let count = r.uintvar().unwrap();
738 for _ in 0..count {
739 let headers_length = r.uintvar().unwrap();
740 let data_length = r.uintvar().unwrap();
741 let headers = r.take(headers_length).unwrap().to_vec();
742 let data = r.take(data_length).unwrap().to_vec();
743 let mut h = Reader { bytes: &headers, at: 0 };
744 let typed_length = h.length().unwrap();
745 let typed = h.take(typed_length).unwrap().to_vec();
746 let mut t = Reader { bytes: &typed, at: 0 };
747 let ct = if typed[0] >= 0x80 {
748 NUMBERED[usize::from(t.byte().unwrap() & 0x7F)].to_string()
749 } else {
750 t.text().unwrap()
751 };
752 let mut part = ReadPart { ct, data, ..ReadPart::default() };
753 while let Some(parameter) = t.byte() {
754 match parameter {
755 P_NAME => part.name = t.text().unwrap(),
756 P_CHARSET => part.charset = t.byte().unwrap() & 0x7F,
757 _ => panic!("a parameter Sioul does not write: {parameter:#x}"),
758 }
759 }
760 while let Some(header) = h.byte() {
761 match header {
762 P_CONTENT_ID => part.id = h.text().unwrap(),
763 P_CONTENT_LOCATION => part.location = h.text().unwrap(),
764 _ => panic!("a part header Sioul does not write: {header:#x}"),
765 }
766 }
767 read.parts.push(part);
768 }
769 assert_eq!(r.at, pdu.len(), "nothing after the last part");
770 read
771 }
772
773 #[test]
774 fn integers_and_lengths_at_their_edges() {
775 let written = |f: &dyn Fn(&mut Vec<u8>)| {
776 let mut out = Vec::new();
777 f(&mut out);
778 out
779 };
780 assert_eq!(written(&|o| uintvar(o, 0)), [0x00]);
781 assert_eq!(written(&|o| uintvar(o, 127)), [0x7F]);
782 assert_eq!(written(&|o| uintvar(o, 128)), [0x81, 0x00]);
783 assert_eq!(written(&|o| uintvar(o, 16_383)), [0xFF, 0x7F]);
784 assert_eq!(written(&|o| uintvar(o, 16_384)), [0x81, 0x80, 0x00]);
785 assert_eq!(written(&|o| uintvar(o, 614_400)), [0xA5, 0xC0, 0x00]);
786 assert_eq!(written(&|o| value_length(o, 30)), [30]);
787 assert_eq!(written(&|o| value_length(o, 31)), [31, 31]);
788 assert_eq!(written(&|o| value_length(o, 128)), [31, 0x81, 0x00]);
789 assert_eq!(written(&|o| long_integer(o, WRITTEN as u64)), [4, 0x6A, 0xD4, 0xB5, 0x74]);
790 assert_eq!(written(&|o| text(o, "é".as_bytes())), [0x7F, 0xC3, 0xA9, 0x00], "a text beginning past 127 is quoted");
791 for value in [0, 1, 127, 128, 300, 16_383, 16_384, 2_097_151, 2_097_152, 614_400] {
792 let mut out = Vec::new();
793 uintvar(&mut out, value);
794 assert_eq!(Reader { bytes: &out, at: 0 }.uintvar(), Some(value));
795 }
796 for length in [0, 30, 31, 127, 128, 20_000] {
797 let mut out = Vec::new();
798 value_length(&mut out, length);
799 assert_eq!(Reader { bytes: &out, at: 0 }.length(), Some(length));
800 }
801 }
802
803 #[test]
807 fn a_picture_with_words_is_written_as_android_writes_it() {
808 let picture = stand_in(200, 7);
809 let media = [Medium { ct: "image/jpeg", bytes: &picture }];
810 let message = Message { key: KEY, to: &["+33 1 99 00 12 34"], date: WRITTEN, words: "Voilà l’entrée.", media: &media };
811 let pdu = encode(&message).unwrap();
812 let mut head = vec![0x8C, 0x80, 0x98];
813 head.extend_from_slice(KEY.as_bytes());
814 head.extend_from_slice(&[0x00, 0x8D, 0x92, 0x85, 0x04, 0x6A, 0xD4, 0xB5, 0x74, 0x89, 0x01, 0x81, 0x97, 24, 0xEA]);
815 head.extend_from_slice(b"+33199001234/TYPE=PLMN\0");
816 head.extend_from_slice(&[0x8A, 0x80, 0x86, 0x81, 0x90, 0x81, 0x84, 27, 0xB3, 0x8A]);
817 head.extend_from_slice(b"<smil>\0\x89application/smil\0\x03");
818 assert_eq!(hex(&pdu[..head.len()]), hex(&head));
819 let read = read(&pdu);
820 assert_eq!(read.headers.len(), 9, "type, key, version, date, from, to, class, two reports");
821 assert_eq!(read.start, "<smil>");
822 let names: Vec<(&str, &str, &str, &str, u8)> = read.parts.iter().map(|p| (p.ct.as_str(), p.name.as_str(), p.id.as_str(), p.location.as_str(), p.charset)).collect();
823 assert_eq!(names, [("application/smil", "smil.xml", "<smil>", "smil.xml", 0), ("image/jpeg", "image1.jpg", "<image1>", "image1.jpg", 0), ("text/plain", "text1.txt", "<text1>", "text1.txt", 106)]);
824 assert_eq!(read.parts[1].data, picture);
825 assert_eq!(read.parts[2].data, "Voilà l’entrée.".as_bytes());
826 let layout = String::from_utf8(read.parts[0].data.clone()).unwrap();
827 assert!(layout.contains(r#"<region id="Image" fit="meet" top="0" left="0" height="80%" width="100%"/><region id="Text""#), "{layout}");
828 assert!(layout.contains(r#"<body><par dur="5000ms"><img src="image1.jpg" region="Image" /></par><par dur="5000ms"><text src="text1.txt" region="Text" /></par></body>"#), "{layout}");
829 assert_eq!(size(&message), Ok(pdu.len()));
830 }
831
832 #[test]
835 fn words_to_two_are_one_message_to_both() {
836 let message = Message { key: KEY, to: &["+33 2 61 91 00 42", "03.53.01.42.42"], date: WRITTEN, words: "Rendez-vous à 18 h.", media: &[] };
837 let read = read(&encode(&message).unwrap());
838 let to: Vec<String> = read.headers.iter().filter(|(field, _)| *field == TO).map(|(_, value)| String::from_utf8_lossy(&value[2..value.len() - 1]).into_owned()).collect();
839 assert_eq!(to, ["+33261910042/TYPE=PLMN", "0353014242/TYPE=PLMN"]);
840 assert_eq!(read.parts.len(), 2);
841 let layout = String::from_utf8(read.parts[0].data.clone()).unwrap();
842 assert_eq!(layout, r#"<smil><head><layout><root-layout/><region id="Text" top="0" left="0" height="100%" width="100%"/></layout></head><body><par dur="5000ms"><text src="text1.txt" region="Text" /></par></body></smil>"#);
843 }
844
845 #[test]
848 fn three_media_are_named_by_their_kind() {
849 let (gif, video, card) = (stand_in(64, 1), stand_in(300, 2), b"BEGIN:VCARD\r\nVERSION:3.0\r\nFN:Alice\r\nEND:VCARD\r\n".to_vec());
850 let media = [Medium { ct: "image/gif", bytes: &gif }, Medium { ct: "video/3gpp", bytes: &video }, Medium { ct: "text/x-vCard", bytes: &card }];
851 let message = Message { key: KEY, to: &["0465714242"], date: WRITTEN, words: "Trois choses.", media: &media };
852 let pdu = encode(&message).unwrap();
853 let read = read(&pdu);
854 let names: Vec<(&str, &str)> = read.parts.iter().map(|p| (p.ct.as_str(), p.name.as_str())).collect();
855 assert_eq!(names, [("application/smil", "smil.xml"), ("image/gif", "image1.gif"), ("video/3gpp", "video1.3gp"), ("text/x-vCard", "contact1.vcf"), ("text/plain", "text1.txt")]);
856 let layout = String::from_utf8(read.parts[0].data.clone()).unwrap();
857 assert!(layout.contains(r#"<par><video src="video1.3gp" region="Image" /></par>"#), "{layout}");
858 assert!(layout.contains(r#"<par dur="5000ms"><ref src="contact1.vcf" /></par>"#), "{layout}");
859 assert!(pdu.windows(b"video/3gpp\0".len()).any(|w| w == b"video/3gpp\0"));
861 assert!(!pdu.windows(b"image/gif".len()).any(|w| w == b"image/gif"));
862 assert_eq!(size(&message), Ok(pdu.len()));
863 }
864
865 #[test]
868 fn the_measure_is_the_message_s_length() {
869 let (large, small) = (stand_in(600_000, 3), stand_in(16_384, 4));
870 let words = "é".repeat(1_000);
871 for media in [vec![Medium { ct: "image/jpeg", bytes: &large }], vec![Medium { ct: "image/png", bytes: &small }; 5], vec![]] {
872 let message = Message { key: KEY, to: &["0536494242"], date: WRITTEN, words: &words, media: &media };
873 assert_eq!(size(&message), Ok(encode(&message).unwrap().len()));
874 }
875 }
876
877 #[test]
879 fn the_size_from_lengths_is_the_message_s_own() {
880 for lengths in [vec![0usize], vec![127, 128], vec![16_383, 16_384, 1], vec![600_000], vec![3, 30_000, 2_097_152]] {
881 let blobs: Vec<Vec<u8>> = lengths.iter().map(|&n| stand_in(n, n as u8)).collect();
882 let full: Vec<Medium> = blobs.iter().map(|bytes| Medium { ct: "image/jpeg", bytes }).collect();
883 let empty: Vec<Medium> = lengths.iter().map(|_| Medium { ct: "image/jpeg", bytes: &[] }).collect();
884 let message = Message { key: KEY, to: &["0199001234"], date: WRITTEN, words: "Le dîner.", media: &full };
885 assert_eq!(size_with_lengths(&Message { media: &empty, ..message }, &lengths), size(&message), "{lengths:?}");
886 }
887 }
888
889 #[test]
893 fn the_media_fill_their_room_and_no_more() {
894 let limit = 614_400;
895 for count in [1usize, 2, 5] {
896 let empty = vec![Medium { ct: "image/jpeg", bytes: &[] }; count];
897 let message = Message { key: KEY, to: &["0199001234"], date: WRITTEN, words: "Les photos de samedi.", media: &empty };
898 let room = room(&message, limit).unwrap();
899 let shares: Vec<Vec<u8>> = (0..count).map(|i| stand_in(room / count + usize::from(i < room % count), i as u8)).collect();
900 let media: Vec<Medium> = shares.iter().map(|bytes| Medium { ct: "image/jpeg", bytes }).collect();
901 let full = size(&Message { media: &media, ..message }).unwrap();
902 assert!(full <= limit && limit - full <= 2 * count, "{count}: {full} of {limit}");
903 }
904 for limit in [2_097_151usize, 2_097_152, 2_100_000, 8 << 20] {
906 let empty = [Medium { ct: "video/mp4", bytes: &[] }];
907 let message = Message { key: KEY, to: &["0199001234"], date: WRITTEN, words: "", media: &empty };
908 let room = room(&message, limit).unwrap();
909 let bytes = stand_in(room, 7);
910 let full = size(&Message { media: &[Medium { ct: "video/mp4", bytes: &bytes }], ..message }).unwrap();
911 assert!(full <= limit && limit - full <= 3, "{limit}: {full}");
912 }
913 let empty = [Medium { ct: "image/jpeg", bytes: &[] }];
914 assert_eq!(room(&Message { key: KEY, to: &["0199001234"], date: WRITTEN, words: &"é".repeat(400_000), media: &empty }, limit), None, "words over the limit leave no room");
915 }
916
917 #[test]
918 fn numbers_are_written_plainly() {
919 assert_eq!(address("05 36 49 12 34").as_deref(), Some("0536491234"));
920 assert_eq!(address(" +33 1-99.00/12 (34) ").as_deref(), Some("+33199001234"));
921 assert_eq!(address("+33\u{202f}1\u{a0}99 00 12 34").as_deref(), Some("+33199001234"));
922 assert_eq!(address("SIOUL"), None, "a sender named in letters");
923 assert_eq!(address("36179").as_deref(), Some("36179"), "a short code is a number; the phone's rules refuse it");
924 assert_eq!(address("12"), None);
925 assert_eq!(address("05+36"), None, "a + inside");
926 }
927
928 #[test]
929 fn what_cannot_be_written_is_refused() {
930 let picture = stand_in(10, 5);
931 let media = [Medium { ct: "image/jpeg", bytes: &picture }];
932 let good = Message { key: KEY, to: &["0199001234"], date: WRITTEN, words: "", media: &media };
933 assert!(encode(&good).is_ok(), "a picture alone");
934 assert_eq!(encode(&Message { to: &[], ..good }), Err(Refused::Number));
935 assert_eq!(encode(&Message { to: &["0199001234", "Alice"], ..good }), Err(Refused::Number));
936 assert_eq!(encode(&Message { media: &[], ..good }), Err(Refused::Empty));
937 assert_eq!(encode(&Message { key: "", ..good }), Err(Refused::Key));
938 assert_eq!(encode(&Message { key: "clé", ..good }), Err(Refused::Key));
939 assert_eq!(size(&Message { media: &[Medium { ct: "image/jpé", bytes: &picture }], ..good }), Err(Refused::Type));
940 assert_eq!(size(&Message { media: &[Medium { ct: "", bytes: &picture }], ..good }), Err(Refused::Type));
941 }
942
943 #[test]
946 fn the_carrier_s_answer_is_read() {
947 let taken = conf_taken();
948 assert_eq!(conf(&taken), Some(Conf { status: OK, id: "msg-20261018-1403-42".into(), key: KEY.into(), said: String::new() }));
949 assert!(conf(&taken).unwrap().ok());
950 let refused = conf_refused();
951 let read = conf(&refused).unwrap();
952 assert_eq!((read.status, read.said.as_str(), read.ok()), (0xE5, "Contenu refusé", false));
953 let mut more = taken.clone();
955 more.extend_from_slice(&[0x83]);
956 more.extend_from_slice(b"http://mms.example.org/x\0");
957 more.extend_from_slice(&[0xA7, 0x80, 0x88, 0x05, 0x81, 0x03, 0x01, 0x51, 0x80]);
958 assert_eq!(conf(&more).map(|c| c.id), Some("msg-20261018-1403-42".into()));
959 assert_eq!(conf(&[0x8C, 0x80, 0x98, b'a', 0]), None, "a send request is no answer");
960 assert_eq!(conf(&taken[..taken.len() - 4]), None, "cut short");
961 assert_eq!(conf(&[0x8C, 0x81, 0x98, b'a', 0, 0x8D, 0x92]), None, "no status");
962 }
963
964 fn conf_taken() -> Vec<u8> {
965 let mut out = vec![0x8C, 0x81, 0x98];
966 text(&mut out, KEY.as_bytes());
967 out.extend_from_slice(&[0x8D, 0x92, 0x92, 0x80, 0x8B]);
968 text(&mut out, b"msg-20261018-1403-42");
969 out
970 }
971
972 fn conf_refused() -> Vec<u8> {
973 let mut out = vec![0x8C, 0x81, 0x98];
974 text(&mut out, KEY.as_bytes());
975 out.extend_from_slice(&[0x8D, 0x92, 0x92, 0xE5, 0x93]);
976 let mut said = vec![0x80 | UTF_8];
977 text(&mut said, "Contenu refusé".as_bytes());
978 value_length(&mut out, said.len());
979 out.extend_from_slice(&said);
980 out
981 }
982
983 fn samples() -> Vec<serde_json::Value> {
986 let picture = stand_in(200, 7);
987 let (gif, video, card) = (stand_in(64, 1), stand_in(300, 2), b"BEGIN:VCARD\r\nVERSION:3.0\r\nFN:Alice\r\nEND:VCARD\r\n".to_vec());
988 let (edge_127, edge_128, large) = (stand_in(127, 8), stand_in(128, 9), stand_in(16_384, 10));
989 let messages: Vec<(&str, Vec<&str>, String, Vec<(&str, &[u8])>)> = vec![
990 ("a picture with words", vec!["+33 1 99 00 12 34"], "Voilà l’entrée.".into(), vec![("image/jpeg", &picture)]),
991 ("words to two", vec!["+33 2 61 91 00 42", "03.53.01.42.42"], "Rendez-vous à 18 h.".into(), vec![]),
992 ("three media", vec!["0465714242"], "Trois choses.".into(), vec![("image/gif", &gif), ("video/3gpp", &video), ("text/x-vCard", &card)]),
993 ("lengths at their edges", vec!["0536494242"], String::new(), vec![("image/png", &edge_127), ("image/png", &edge_128), ("application/x-abc", &edge_127), ("application/x-abcd", &edge_128)]),
995 ("a large picture and long words", vec!["0199001234"], "é".repeat(1_000), vec![("image/jpeg", &large)]),
996 ];
997 let mut out = Vec::new();
998 for (name, to, words, media) in &messages {
999 let media: Vec<Medium> = media.iter().map(|&(ct, bytes)| Medium { ct, bytes }).collect();
1000 let message = Message { key: KEY, to, date: WRITTEN, words, media: &media };
1001 let pdu = encode(&message).unwrap();
1002 let read = read(&pdu);
1003 let parts: Vec<serde_json::Value> = read.parts.iter().map(|p| serde_json::json!({ "ct": p.ct, "name": p.name, "id": p.id, "location": p.location, "charset": p.charset, "size": p.data.len(), "sha256": sha256(&p.data) })).collect();
1004 let numbers: Vec<String> = to.iter().map(|n| address(n).unwrap()).collect();
1005 out.push(serde_json::json!({ "name": name, "kind": "send-req", "key": KEY, "to": numbers, "date": WRITTEN, "parts": parts, "pdu": hex(&pdu) }));
1006 }
1007 out.push(serde_json::json!({ "name": "the carrier took it", "kind": "send-conf", "key": KEY, "status": OK, "id": "msg-20261018-1403-42", "pdu": hex(&conf_taken()) }));
1008 out.push(serde_json::json!({ "name": "the carrier refused its content", "kind": "send-conf", "key": KEY, "status": 0xE5, "id": "", "pdu": hex(&conf_refused()) }));
1009 out
1010 }
1011
1012 #[test]
1018 fn the_samples_android_reads_are_these() {
1019 let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../android/jvm-checks/mms-samples.jsonl");
1020 let now: String = samples().iter().map(|line| format!("{line}\n")).collect();
1021 if std::env::var_os("SIOUL_MMS_SAMPLES_WRITE").is_some() {
1022 std::fs::write(&path, &now).unwrap();
1023 }
1024 let kept = std::fs::read_to_string(&path).unwrap_or_default();
1025 assert!(kept == now, "{} is not what the encoder writes now: SIOUL_MMS_SAMPLES_WRITE=1 cargo test --release -p sioul-core mms, then android/jvm-checks/run.sh", path.display());
1026 }
1027}