git_policy_cli/js_text.rs
1//! What:
2//! Text operations with the exact semantics of the JavaScript built-ins the incumbent
3//! applies to strings it shares with the native wrapper.
4//! Why:
5//! Durable strings such as process birth identities are produced by `.trim()` in the
6//! incumbent;
7//! the native wrapper must produce byte-identical strings,
8//! and Rust's
9//! `str::trim` uses a different whitespace set (it trims U+0085 and keeps U+FEFF).
10//!
11//! In TS you'd write (pseudocode):
12//! ```ts
13//! text.trim()
14//! ```
15
16/// What:
17/// Whether a character is whitespace or a line terminator to ECMAScript `trim`.
18/// `char` is one Unicode scalar value.
19/// Why:
20/// ECMA-262 `TrimString` removes `WhiteSpace` (tab,
21/// vertical tab,
22/// form feed,
23/// space,
24/// no-break space,
25/// U+FEFF and every `Zs` character) and `LineTerminator` (LF,
26/// CR,
27/// U+2028,
28/// U+2029);
29/// nothing else.
30///
31/// In TS you'd write (pseudocode):
32/// ```ts
33/// /^\s$/u.test(character) // JavaScript's \s is exactly this set
34/// ```
35pub fn is_javascript_whitespace(character: char) -> bool {
36 // The `Zs` block U+2000 to U+200A is a range; every other member is listed.
37 if ('\u{2000}'..='\u{200a}').contains(&character) {
38 return true;
39 }
40 // `matches!` tests a value against a pattern of alternatives.
41 return matches!(
42 character,
43 '\u{0009}'
44 | '\u{000a}'
45 | '\u{000b}'
46 | '\u{000c}'
47 | '\u{000d}'
48 | '\u{0020}'
49 | '\u{00a0}'
50 | '\u{1680}'
51 | '\u{2028}'
52 | '\u{2029}'
53 | '\u{202f}'
54 | '\u{205f}'
55 | '\u{3000}'
56 | '\u{feff}'
57 );
58}
59
60/// What:
61/// Remove leading and trailing JavaScript whitespace,
62/// as `String.prototype.trim`.
63/// `&str` borrows text;
64/// the result borrows a slice of it.
65/// Why:
66/// Shared strings must be trimmed exactly as the incumbent trims them.
67///
68/// In TS you'd write (pseudocode):
69/// ```ts
70/// function trimJavascript(text: string): string { return text.trim(); }
71/// ```
72pub fn trim_javascript(text: &str) -> &str {
73 return text.trim_matches(is_javascript_whitespace);
74}
75
76/// What:
77/// Remove trailing JavaScript whitespace,
78/// as `String.prototype.trimEnd`.
79/// `&str` borrows text;
80/// the result borrows a slice of it.
81/// Why:
82/// Git's PID file text is trimmed at the end only before its `pid ` prefix is read.
83///
84/// In TS you'd write (pseudocode):
85/// ```ts
86/// function trimJavascriptEnd(text: string): string { return text.trimEnd(); }
87/// ```
88pub fn trim_javascript_end(text: &str) -> &str {
89 return text.trim_end_matches(is_javascript_whitespace);
90}
91
92/// What:
93/// The positive safe integer an all-digit text spells,
94/// or nothing.
95/// Why:
96/// Leading zeros read as `Number` reads them;
97/// an empty text,
98/// a zero,
99/// a non-digit or a
100/// value beyond 2^53 - 1 is no number.
101///
102/// In TS you'd write (pseudocode):
103/// ```ts
104/// /^[0-9]+$/.test(text) && Number.isSafeInteger(Number(text)) && Number(text) > 0 ? Number(text) : undefined
105/// ```
106pub fn decimal_attempt(text: &str) -> Option<i64> {
107 if text.is_empty() || !text.bytes().all(|byte| { return byte.is_ascii_digit(); }) {
108 return None;
109 }
110 let value: i64 = text.parse::<i64>().ok()?;
111 if !(1..=super::json_record::MAX_SAFE_INTEGER).contains(&value) {
112 return None;
113 }
114 return Some(value);
115}
116
117/// The whitespace table stays out of the release executable.
118#[cfg(test)]
119#[path = "js_text_tests.rs"]
120mod tests;