1use std::collections::HashMap;
17use std::fmt::Write;
18use std::sync::atomic::{AtomicBool, Ordering};
19use std::sync::OnceLock;
20
21const TABLES_JSON: &str = include_str!("../assets/spec/pycompat-tables.json");
22
23pub const SURROGATE_SENTINEL_BASE: u32 = 0x10FC00;
50
51static STDIN_SURROGATES: AtomicBool = AtomicBool::new(false);
52
53pub fn surrogate_sentinels_active() -> bool {
55 STDIN_SURROGATES.load(Ordering::Relaxed)
56}
57
58pub fn set_surrogate_sentinels_active(on: bool) {
60 STDIN_SURROGATES.store(on, Ordering::Relaxed);
61}
62
63pub fn sentinel_surrogate(c: char) -> Option<u32> {
66 let cp = c as u32;
67 if surrogate_sentinels_active() && (SURROGATE_SENTINEL_BASE + 0x80..=SURROGATE_SENTINEL_BASE + 0xFF).contains(&cp) {
68 Some(0xDC00 + (cp - SURROGATE_SENTINEL_BASE))
69 } else {
70 None
71 }
72}
73
74pub fn decode_stdin_surrogateescape(bytes: &[u8]) -> String {
79 let mut out = String::with_capacity(bytes.len());
80 let mut rest = bytes;
81 loop {
82 match std::str::from_utf8(rest) {
83 Ok(s) => {
84 out.push_str(s);
85 return out;
86 }
87 Err(e) => {
88 let valid = e.valid_up_to();
89 out.push_str(std::str::from_utf8(&rest[..valid]).expect("valid prefix"));
90 let bad = e.error_len().unwrap_or(rest.len() - valid);
91 for &b in &rest[valid..valid + bad] {
92 out.push(
93 char::from_u32(SURROGATE_SENTINEL_BASE + b as u32)
94 .expect("plane-16 PUA sentinel"),
95 );
96 STDIN_SURROGATES.store(true, Ordering::Relaxed);
97 }
98 rest = &rest[valid + bad..];
99 }
100 }
101 }
102}
103
104pub fn encode_stdout_surrogateescape(text: &str) -> std::borrow::Cow<'_, [u8]> {
109 if !surrogate_sentinels_active() || !text.chars().any(|c| sentinel_surrogate(c).is_some()) {
110 return std::borrow::Cow::Borrowed(text.as_bytes());
111 }
112 let mut out = Vec::with_capacity(text.len());
113 for c in text.chars() {
114 if let Some(sur) = sentinel_surrogate(c) {
115 out.push((sur - 0xDC00) as u8);
116 } else {
117 let mut buf = [0u8; 4];
118 out.extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
119 }
120 }
121 std::borrow::Cow::Owned(out)
122}
123
124struct Tables {
125 casefold: HashMap<u32, String>,
126 str_whitespace: Vec<(u32, u32)>,
127 splitlines_boundaries: Vec<u32>,
128 isprintable: Vec<(u32, u32)>,
129 re_digit: Vec<(u32, u32)>,
130 re_word: Vec<(u32, u32)>,
131}
132
133fn parse_ranges(v: &serde_json::Value) -> Vec<(u32, u32)> {
134 v.as_array()
135 .expect("range table must be an array")
136 .iter()
137 .map(|pair| {
138 let p = pair.as_array().expect("range entry must be a pair");
139 (
140 p[0].as_u64().expect("range start") as u32,
141 p[1].as_u64().expect("range end") as u32,
142 )
143 })
144 .collect()
145}
146
147fn tables() -> &'static Tables {
148 static TABLES: OnceLock<Tables> = OnceLock::new();
149 TABLES.get_or_init(|| {
150 let root: serde_json::Value =
151 serde_json::from_str(TABLES_JSON).expect("pycompat-tables.json must parse");
152 let casefold = root["casefold"]
153 .as_object()
154 .expect("casefold table")
155 .iter()
156 .map(|(k, v)| {
157 (
158 k.parse::<u32>().expect("casefold key"),
159 v.as_str().expect("casefold value").to_string(),
160 )
161 })
162 .collect();
163 let splitlines_boundaries = root["splitlines_boundaries"]
164 .as_array()
165 .expect("splitlines_boundaries")
166 .iter()
167 .map(|v| v.as_u64().expect("boundary cp") as u32)
168 .collect();
169 Tables {
170 casefold,
171 str_whitespace: parse_ranges(&root["str_whitespace"]),
172 splitlines_boundaries,
173 isprintable: parse_ranges(&root["isprintable"]),
174 re_digit: parse_ranges(&root["re_digit"]),
175 re_word: parse_ranges(&root["re_word"]),
176 }
177 })
178}
179
180fn in_ranges(ranges: &[(u32, u32)], cp: u32) -> bool {
181 let idx = ranges.partition_point(|&(start, _)| start <= cp);
183 idx > 0 && cp <= ranges[idx - 1].1
184}
185
186pub fn py_casefold(s: &str) -> String {
192 let t = tables();
193 let mut out = String::with_capacity(s.len());
194 for c in s.chars() {
195 match t.casefold.get(&(c as u32)) {
196 Some(folded) => out.push_str(folded),
197 None => out.push(c),
198 }
199 }
200 out
201}
202
203pub fn py_is_space(c: char) -> bool {
206 in_ranges(&tables().str_whitespace, c as u32)
207}
208
209pub fn py_strip(s: &str) -> &str {
211 py_rstrip(py_lstrip(s))
212}
213
214pub fn py_lstrip(s: &str) -> &str {
216 s.trim_start_matches(py_is_space)
217}
218
219pub fn py_rstrip(s: &str) -> &str {
221 s.trim_end_matches(py_is_space)
222}
223
224fn is_line_boundary(c: char) -> bool {
225 let b = &tables().splitlines_boundaries;
226 b.binary_search(&(c as u32)).is_ok()
227}
228
229pub fn py_splitlines(s: &str) -> Vec<&str> {
233 let mut out = Vec::new();
234 let mut start = 0usize;
235 let mut iter = s.char_indices().peekable();
236 while let Some((i, c)) = iter.next() {
237 if is_line_boundary(c) {
238 out.push(&s[start..i]);
239 let mut end = i + c.len_utf8();
240 if c == '\r' {
241 if let Some(&(j, '\n')) = iter.peek() {
242 iter.next();
243 end = j + 1;
244 }
245 }
246 start = end;
247 }
248 }
249 if start < s.len() {
250 out.push(&s[start..]);
251 }
252 out
253}
254
255pub fn first_nonempty_line(s: &str) -> &str {
258 py_splitlines(s)
259 .into_iter()
260 .map(py_strip)
261 .find(|l| !l.is_empty())
262 .unwrap_or("")
263}
264
265pub fn read_text_universal(path: &str) -> Option<String> {
269 let bytes = std::fs::read(path).ok()?;
270 let text = String::from_utf8(bytes).ok()?;
271 Some(text.replace("\r\n", "\n").replace('\r', "\n"))
272}
273
274pub fn py_is_printable(c: char) -> bool {
276 in_ranges(&tables().isprintable, c as u32)
277}
278
279pub fn is_re_digit(c: char) -> bool {
281 in_ranges(&tables().re_digit, c as u32)
282}
283
284pub fn is_re_word(c: char) -> bool {
286 in_ranges(&tables().re_word, c as u32)
287}
288
289pub fn py_repr_str(s: &str) -> String {
298 let quote = if s.contains('\'') && !s.contains('"') {
299 '"'
300 } else {
301 '\''
302 };
303 let mut out = String::with_capacity(s.len() + 2);
304 out.push(quote);
305 for c in s.chars() {
306 if c == quote || c == '\\' {
307 out.push('\\');
308 out.push(c);
309 } else if c == '\t' {
310 out.push_str("\\t");
311 } else if c == '\n' {
312 out.push_str("\\n");
313 } else if c == '\r' {
314 out.push_str("\\r");
315 } else if let Some(sur) = sentinel_surrogate(c) {
316 write!(out, "\\u{sur:04x}").unwrap();
318 } else if py_is_printable(c) {
319 out.push(c);
320 } else {
321 let cp = c as u32;
322 if cp < 0x100 {
323 write!(out, "\\x{cp:02x}").unwrap();
324 } else if cp < 0x10000 {
325 write!(out, "\\u{cp:04x}").unwrap();
326 } else {
327 write!(out, "\\U{cp:08x}").unwrap();
328 }
329 }
330 }
331 out.push(quote);
332 out
333}
334
335pub fn quote_plus(s: &str) -> String {
343 let mut out = String::with_capacity(s.len());
344 for b in s.bytes() {
345 match b {
346 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'_' | b'.' | b'-' | b'~' => {
347 out.push(b as char)
348 }
349 b' ' => out.push('+'),
350 _ => {
351 write!(out, "%{b:02X}").unwrap();
352 }
353 }
354 }
355 out
356}
357
358pub fn quote_plus_urlencode(pairs: &[(&str, &str)]) -> String {
361 pairs
362 .iter()
363 .map(|(k, v)| format!("{}={}", quote_plus(k), quote_plus(v)))
364 .collect::<Vec<_>>()
365 .join("&")
366}
367
368pub fn py_float_repr(x: f64) -> String {
384 if x.is_nan() {
385 return "nan".to_string();
386 }
387 if x.is_infinite() {
388 return if x > 0.0 { "inf" } else { "-inf" }.to_string();
389 }
390 let neg = x.is_sign_negative();
391 let sign = if neg { "-" } else { "" };
392 if x == 0.0 {
393 return format!("{sign}0.0");
394 }
395 let ax = x.abs();
396 let (n, k) = exact_decimal(ax);
397 let exact_decpt = n.len() as i64 - k; let (digits, decpt) = shortest_digits(ax, &n, exact_decpt);
399 if decpt <= -4 || decpt > 16 {
400 let e10 = decpt - 1;
402 let mantissa = if digits.len() > 1 {
403 format!("{}.{}", &digits[..1], &digits[1..])
404 } else {
405 digits.clone()
406 };
407 let (esign, eabs) = if e10 < 0 { ('-', -e10) } else { ('+', e10) };
408 format!("{sign}{mantissa}e{esign}{eabs:02}")
409 } else if decpt <= 0 {
410 format!("{sign}0.{}{}", "0".repeat((-decpt) as usize), digits)
411 } else if (decpt as usize) >= digits.len() {
412 format!(
413 "{sign}{}{}.0",
414 digits,
415 "0".repeat(decpt as usize - digits.len())
416 )
417 } else {
418 let d = decpt as usize;
419 format!("{sign}{}.{}", &digits[..d], &digits[d..])
420 }
421}
422
423fn cmp_rem_half(r: &str) -> std::cmp::Ordering {
426 let first = r.as_bytes()[0];
427 if first > b'5' {
428 std::cmp::Ordering::Greater
429 } else if first < b'5' {
430 std::cmp::Ordering::Less
431 } else if r[1..].bytes().all(|b| b == b'0') {
432 std::cmp::Ordering::Equal
433 } else {
434 std::cmp::Ordering::Greater
435 }
436}
437
438fn strip_trailing_zeros(d: &str) -> &str {
439 let end = d.trim_end_matches('0');
440 if end.is_empty() {
441 &d[..1]
442 } else {
443 end
444 }
445}
446
447fn roundtrips(digits: &str, decpt: i64, x_bits: u64) -> bool {
449 let e = decpt - digits.len() as i64;
450 let text = format!("{digits}e{e}");
451 text.parse::<f64>().map(|v| v.to_bits()) == Ok(x_bits)
452}
453
454fn shortest_digits(x: f64, n: &str, decpt: i64) -> (String, i64) {
461 let x_bits = x.to_bits();
462 for d in 1..=17usize {
463 if d >= n.len() {
464 return (strip_trailing_zeros(n).to_string(), decpt);
466 }
467 let lo = &n[..d];
468 let rem = &n[d..];
469 let hi_full = inc_decimal(lo);
470 let (hi, hi_decpt) = if hi_full.len() > d {
471 (hi_full[..d].to_string(), decpt + 1)
474 } else {
475 (hi_full, decpt)
476 };
477 let lo_ok = roundtrips(lo, decpt, x_bits);
478 let hi_ok = roundtrips(&hi, hi_decpt, x_bits);
479 match (lo_ok, hi_ok) {
480 (true, false) => return (strip_trailing_zeros(lo).to_string(), decpt),
481 (false, true) => return (strip_trailing_zeros(&hi).to_string(), hi_decpt),
482 (true, true) => {
483 let pick_hi = match cmp_rem_half(rem) {
484 std::cmp::Ordering::Greater => true,
485 std::cmp::Ordering::Less => false,
486 std::cmp::Ordering::Equal => {
487 (lo.as_bytes()[d - 1] - b'0') % 2 == 1
488 }
489 };
490 return if pick_hi {
491 (strip_trailing_zeros(&hi).to_string(), hi_decpt)
492 } else {
493 (strip_trailing_zeros(lo).to_string(), decpt)
494 };
495 }
496 (false, false) => continue,
497 }
498 }
499 unreachable!("17 significant digits always round-trip a double")
500}
501
502fn big_mul_small(v: &mut Vec<u64>, m: u64) {
508 let mut carry: u128 = 0;
509 for limb in v.iter_mut() {
510 let p = (*limb as u128) * (m as u128) + carry;
511 *limb = p as u64;
512 carry = p >> 64;
513 }
514 while carry > 0 {
515 v.push(carry as u64);
516 carry >>= 64;
517 }
518}
519
520fn big_shl(v: &mut Vec<u64>, bits: u64) {
521 let words = (bits / 64) as usize;
522 let rem = bits % 64;
523 if rem > 0 {
524 let mut carry: u64 = 0;
525 for limb in v.iter_mut() {
526 let new = (*limb << rem) | carry;
527 carry = *limb >> (64 - rem);
528 *limb = new;
529 }
530 if carry > 0 {
531 v.push(carry);
532 }
533 }
534 if words > 0 {
535 let mut shifted = vec![0u64; words];
536 shifted.append(v);
537 *v = shifted;
538 }
539}
540
541fn big_divmod_small(v: &mut Vec<u64>, d: u64) -> u64 {
542 let mut rem: u128 = 0;
543 for limb in v.iter_mut().rev() {
544 let cur = (rem << 64) | (*limb as u128);
545 *limb = (cur / d as u128) as u64;
546 rem = cur % d as u128;
547 }
548 while v.len() > 1 && *v.last().unwrap() == 0 {
549 v.pop();
550 }
551 rem as u64
552}
553
554fn big_is_zero(v: &[u64]) -> bool {
555 v.iter().all(|&l| l == 0)
556}
557
558fn big_to_decimal(mut v: Vec<u64>) -> String {
559 const CHUNK: u64 = 10_000_000_000_000_000_000; let mut chunks: Vec<u64> = Vec::new();
561 loop {
562 let r = big_divmod_small(&mut v, CHUNK);
563 chunks.push(r);
564 if big_is_zero(&v) {
565 break;
566 }
567 }
568 let mut out = chunks.pop().unwrap().to_string();
569 for c in chunks.iter().rev() {
570 out.push_str(&format!("{c:019}"));
571 }
572 out
573}
574
575fn exact_decimal(x: f64) -> (String, i64) {
578 debug_assert!(x.is_finite() && x >= 0.0);
579 let bits = x.to_bits();
580 let exp_biased = ((bits >> 52) & 0x7ff) as i64;
581 let frac = bits & ((1u64 << 52) - 1);
582 let (m, e) = if exp_biased == 0 {
583 (frac, -1074i64)
584 } else {
585 (frac | (1u64 << 52), exp_biased - 1075)
586 };
587 if m == 0 {
588 return ("0".to_string(), 0);
589 }
590 let mut v = vec![m];
591 if e >= 0 {
592 big_shl(&mut v, e as u64);
593 (big_to_decimal(v), 0)
594 } else {
595 let k = -e;
597 const POW5_27: u64 = 7_450_580_596_923_828_125; let mut rem = k;
599 while rem >= 27 {
600 big_mul_small(&mut v, POW5_27);
601 rem -= 27;
602 }
603 if rem > 0 {
604 big_mul_small(&mut v, 5u64.pow(rem as u32));
605 }
606 (big_to_decimal(v), k)
607 }
608}
609
610fn inc_decimal(q: &str) -> String {
611 let mut digits: Vec<u8> = q.bytes().collect();
612 for d in digits.iter_mut().rev() {
613 if *d == b'9' {
614 *d = b'0';
615 } else {
616 *d += 1;
617 return String::from_utf8(digits).unwrap();
618 }
619 }
620 let mut out = String::with_capacity(digits.len() + 1);
621 out.push('1');
622 out.push_str(std::str::from_utf8(&digits).unwrap());
623 out
624}
625
626fn round_decimal_half_even(n: &str, drop: usize) -> String {
629 if drop > n.len() {
630 return "0".to_string();
632 }
633 let (q, r) = n.split_at(n.len() - drop);
634 let q = if q.is_empty() { "0" } else { q };
635 match cmp_rem_half(r) {
636 std::cmp::Ordering::Less => q.to_string(),
637 std::cmp::Ordering::Greater => inc_decimal(q),
638 std::cmp::Ordering::Equal => {
639 let last = q.as_bytes()[q.len() - 1];
640 if (last - b'0') % 2 == 1 {
641 inc_decimal(q)
642 } else {
643 q.to_string()
644 }
645 }
646 }
647}
648
649pub fn py_round(x: f64, ndigits: i32) -> f64 {
654 if !x.is_finite() || x == 0.0 {
655 return x;
656 }
657 let neg = x < 0.0;
658 let (n, k) = exact_decimal(x.abs());
659 let nd = ndigits as i64;
660 if nd >= k {
661 return x; }
663 let drop = k - nd;
664 if drop > n.len() as i64 {
665 return if neg { -0.0 } else { 0.0 };
666 }
667 let q = round_decimal_half_even(&n, drop as usize);
668 let text = format!("{}{}e{}", if neg { "-" } else { "" }, q, -nd);
669 text.parse::<f64>().expect("decimal string parses")
670}
671
672fn py_fixed(x: f64, nd: usize) -> String {
675 if x.is_nan() {
676 return "nan".to_string();
677 }
678 if x.is_infinite() {
679 return if x > 0.0 { "inf" } else { "-inf" }.to_string();
680 }
681 let sign = if x.is_sign_negative() { "-" } else { "" };
682 let (n, k) = exact_decimal(x.abs());
683 let mut q = if nd as i64 >= k {
684 let mut s = n;
685 s.push_str(&"0".repeat((nd as i64 - k) as usize));
686 s
687 } else {
688 let drop = k - nd as i64;
689 if drop > n.len() as i64 {
690 "0".to_string()
691 } else {
692 round_decimal_half_even(&n, drop as usize)
693 }
694 };
695 if q.len() < nd + 1 {
696 q = format!("{}{}", "0".repeat(nd + 1 - q.len()), q);
697 }
698 if nd == 0 {
699 format!("{sign}{q}")
700 } else {
701 let split = q.len() - nd;
702 format!("{sign}{}.{}", &q[..split], &q[split..])
703 }
704}
705
706pub fn py_format_1f(x: f64) -> String {
709 py_fixed(x, 1)
710}
711
712pub fn py_format_fixed(x: f64, nd: usize) -> String {
715 py_fixed(x, nd)
716}
717
718pub fn py_format_percent0(x: f64) -> String {
722 if x.is_nan() {
723 return "nan%".to_string();
724 }
725 if x.is_infinite() {
726 return if x > 0.0 { "inf%" } else { "-inf%" }.to_string();
727 }
728 let mut s = py_fixed(x * 100.0, 0);
729 s.push('%');
730 s
731}
732
733pub fn py_normpath(path: &str) -> String {
742 if path.is_empty() {
743 return ".".to_string();
744 }
745 let initial_slashes = if path.starts_with('/') {
746 if path.starts_with("//") && !path.starts_with("///") {
747 2
748 } else {
749 1
750 }
751 } else {
752 0
753 };
754 let mut comps: Vec<&str> = Vec::new();
755 for comp in path.split('/') {
756 if comp.is_empty() || comp == "." {
757 continue;
758 }
759 if comp != ".."
760 || (initial_slashes == 0 && comps.is_empty())
761 || comps.last() == Some(&"..")
762 {
763 comps.push(comp);
764 } else if !comps.is_empty() {
765 comps.pop();
766 }
767 }
768 let mut out = "/".repeat(initial_slashes);
769 out.push_str(&comps.join("/"));
770 if out.is_empty() {
771 ".".to_string()
772 } else {
773 out
774 }
775}
776
777pub fn py_abspath(path: &str) -> String {
780 if path.starts_with('/') {
781 return py_normpath(path);
782 }
783 let cwd = std::env::current_dir()
784 .map(|p| p.to_string_lossy().into_owned())
785 .unwrap_or_else(|_| ".".to_string());
786 py_normpath(&format!("{cwd}/{path}"))
787}
788
789pub fn py_relpath(path: &str, start: &str) -> String {
793 let path_abs = py_abspath(path);
794 let start_abs = py_abspath(start);
795 let path_list: Vec<&str> = path_abs.split('/').filter(|c| !c.is_empty()).collect();
796 let start_list: Vec<&str> = start_abs.split('/').filter(|c| !c.is_empty()).collect();
797 let common = path_list
798 .iter()
799 .zip(start_list.iter())
800 .take_while(|(a, b)| a == b)
801 .count();
802 let mut rel: Vec<&str> = Vec::new();
803 rel.resize(start_list.len() - common, "..");
804 rel.extend(&path_list[common..]);
805 if rel.is_empty() {
806 ".".to_string()
807 } else {
808 rel.join("/")
809 }
810}
811
812#[cfg(test)]
813mod tests {
814 use super::*;
815
816 #[test]
817 fn casefold_basics() {
818 assert_eq!(py_casefold("Straße"), "strasse");
819 assert_eq!(py_casefold("ABC"), "abc");
820 }
821
822 #[test]
823 fn strip_python_whitespace() {
824 assert_eq!(py_strip("\u{1c}\u{a0} x \t"), "x");
825 assert_eq!(py_strip("\u{feff}x\u{200b}"), "\u{feff}x\u{200b}");
826 }
827
828 #[test]
829 fn splitlines_crlf() {
830 assert_eq!(py_splitlines("a\r\nb\rc\nd\n"), vec!["a", "b", "c", "d"]);
831 assert_eq!(py_splitlines(""), Vec::<&str>::new());
832 }
833
834 #[test]
835 fn repr_quote_flip() {
836 assert_eq!(py_repr_str("it's"), "\"it's\"");
837 assert_eq!(py_repr_str("both '\""), "'both \\'\"'");
838 assert_eq!(py_repr_str("café"), "'café'");
839 assert_eq!(py_repr_str("\u{7f}"), "'\\x7f'");
840 }
841
842 #[test]
843 fn float_repr_shapes() {
844 assert_eq!(py_float_repr(1e16), "1e+16");
845 assert_eq!(py_float_repr(1e-5), "1e-05");
846 assert_eq!(py_float_repr(0.0001), "0.0001");
847 assert_eq!(py_float_repr(100.0), "100.0");
848 assert_eq!(py_float_repr(-0.0), "-0.0");
849 assert_eq!(py_float_repr(5e-324), "5e-324");
850 }
851
852 #[test]
853 fn round_half_even_exact() {
854 assert_eq!(py_round(2.675, 2), 2.67);
855 assert_eq!(py_round(0.125, 2), 0.12);
856 assert_eq!(py_round(2.5, 0), 2.0);
857 assert!(py_round(-0.4, 0) == 0.0 && py_round(-0.4, 0).is_sign_negative());
858 }
859
860 #[test]
861 fn format_helpers() {
862 assert_eq!(py_format_1f(0.25), "0.2");
863 assert_eq!(py_format_1f(-0.04), "-0.0");
864 assert_eq!(py_format_percent0(0.855), "86%");
865 }
866
867 #[test]
873 fn stdin_surrogateescape_decode_and_reencode() {
874 let cases: &[(&[u8], &[u32])] = &[
875 (b"abc", &[0x61, 0x62, 0x63]),
876 (b"---\n\xcc\n---", &[0x2d, 0x2d, 0x2d, 0x0a, 0x10FCCC, 0x0a, 0x2d, 0x2d, 0x2d]),
877 (b"\xc3\x28", &[0x10FCC3, 0x28]), (b"\xf0\x9f\x98", &[0x10FCF0, 0x10FC9F, 0x10FC98]), (b"\xed\xa0\x80", &[0x10FCED, 0x10FCA0, 0x10FC80]), (b"\xc0\xaf", &[0x10FCC0, 0x10FCAF]), (b"\xc3\xa9", &[0xE9]), ];
883 for (bytes, chars) in cases {
884 let s = decode_stdin_surrogateescape(bytes);
885 let got: Vec<u32> = s.chars().map(|c| c as u32).collect();
886 assert_eq!(&got, chars, "decode of {bytes:?}");
887 assert_eq!(
889 encode_stdout_surrogateescape(&s).as_ref(),
890 *bytes,
891 "re-encode of {bytes:?}"
892 );
893 }
894 assert!(surrogate_sentinels_active());
895 }
896
897 #[test]
898 fn sentinel_repr_is_lone_surrogate_escape() {
899 set_surrogate_sentinels_active(true);
900 let s = decode_stdin_surrogateescape(b"a\xccb");
901 assert_eq!(py_repr_str(&s), "'a\\udcccb'"); }
903
904 #[test]
905 fn normpath_contract_examples() {
906 assert_eq!(py_normpath(""), ".");
908 assert_eq!(py_normpath("a//b/./c/"), "a/b/c");
909 assert_eq!(py_normpath("a/b/../c"), "a/c");
910 assert_eq!(py_normpath("../a"), "../a");
911 assert_eq!(py_normpath("a/../../b"), "../b");
912 assert_eq!(py_normpath("/../a"), "/a");
913 assert_eq!(py_normpath("//a/b"), "//a/b");
914 assert_eq!(py_normpath("///a/b"), "/a/b");
915 assert_eq!(py_normpath("/"), "/");
916 }
917
918 #[test]
919 fn relpath_contract_examples() {
920 assert_eq!(py_relpath("/x/decisions/decisions/a.md", "/x/decisions"), "decisions/a.md");
922 assert_eq!(py_relpath("/x/decisions", "/x/decisions"), ".");
923 assert_eq!(py_relpath("/x/other/a.md", "/x/decisions"), "../other/a.md");
924 assert_eq!(py_relpath("/x/decisions/a.md", "/x/decisions/"), "a.md");
925 }
926}