Skip to main content

ochrepad_core/
text_tools.rs

1//! Text tools (PLAN_v0.3.0 P4): JSON format, minify and validate with our own
2//! tokenizer, Base64 and URL (percent) encoding, and MD5, SHA-1 and SHA-256
3//! hashes.
4//!
5//! The editing tools work on every non-empty selection, or on the whole
6//! document when nothing is selected; each run is one undo step. Without a
7//! selection, blank space at the start and end of the document is left out
8//! (a final line break stays where it is). Text is encoded and hashed as
9//! UTF-8 with the document's line endings, so a whole UTF-8 document hashes
10//! like its file; decoded line breaks become the editor's.
11
12use std::ops::Range;
13
14use md5::Md5;
15use serde::{Deserialize, Serialize};
16use sha1::Sha1;
17use sha2::{Digest, Sha256};
18
19use crate::action::{Action, ActionError};
20use crate::buffer::{Indent, LineEnding};
21use crate::changeset::{Bias, ChangeSet, Edit};
22use crate::history::EditKind;
23use crate::position::{ByteOffset, byte_to_visual_col};
24use crate::resolve::Resolved;
25use crate::selection::{Selection, SelectionSet};
26use crate::text::Document;
27
28/// A text encoding for [`Action::Encode`] and [`Action::Decode`].
29#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
30pub enum Codec {
31    /// Base64 (RFC 4648) with the standard alphabet and `=` padding.
32    /// Decoding also takes the URL-safe alphabet (`-`, `_`), missing
33    /// padding, and spaces or line breaks anywhere.
34    Base64,
35    /// Percent-encoding (RFC 3986): every byte but `A`–`Z`, `a`–`z`,
36    /// `0`–`9`, `-`, `.`, `_` and `~` becomes `%XX`. Decoding leaves `+`
37    /// alone.
38    Url,
39    /// Two hex digits per byte (Notepad++'s Converter, ASCII -> HEX).
40    /// Decoding takes either case, and spaces or line breaks anywhere.
41    Hex,
42}
43
44impl Codec {
45    pub fn name(self) -> &'static str {
46        match self {
47            Codec::Base64 => "Base64",
48            Codec::Url => "URL encoding",
49            Codec::Hex => "hex",
50        }
51    }
52
53    /// Encodes the UTF-8 bytes of `text`.
54    pub fn encode(self, text: &str) -> String {
55        match self {
56            Codec::Base64 => base64_encode(text.as_bytes()),
57            Codec::Url => url_encode(text.as_bytes()),
58            Codec::Hex => hex_encode(text.as_bytes()),
59        }
60    }
61
62    /// Decodes `text`; the bytes must be UTF-8 text.
63    pub fn decode(self, text: &str) -> Result<String, String> {
64        let bytes = match self {
65            Codec::Base64 => base64_decode(text)?,
66            Codec::Url => url_decode(text)?,
67            Codec::Hex => hex_decode(text)?,
68        };
69        String::from_utf8(bytes).map_err(|_| "the decoded bytes are not UTF-8 text".to_owned())
70    }
71}
72
73// ---------------------------------------------------------------------------
74// JSON
75
76/// Where a JSON text stops being valid, and why.
77#[derive(Clone, Debug, PartialEq, Eq)]
78pub struct JsonError {
79    /// Byte offset into the text that was read.
80    pub offset: usize,
81    pub message: String,
82}
83
84/// Where the JSON in a document stops being valid.
85#[derive(Clone, Debug, PartialEq, Eq)]
86pub struct InvalidJson {
87    pub at: ByteOffset,
88    /// Counted from 1, as in the status bar.
89    pub line: usize,
90    /// The visual column, counted from 1, as in the status bar.
91    pub column: usize,
92    pub message: String,
93}
94
95/// Whitespace between JSON tokens (RFC 8259).
96const JSON_SPACE: [char; 4] = [' ', '\t', '\n', '\r'];
97
98/// Format JSON refuses deeper nesting (minifying and validating do not).
99pub const MAX_FORMAT_DEPTH: usize = 1000;
100
101/// One member or element per line, each level indented by `indent` (a tab,
102/// or `indent.width` spaces). Key order, number text and strings are kept
103/// exactly; only the whitespace between tokens changes.
104pub fn format_json(text: &str, indent: Indent) -> Result<String, JsonError> {
105    let unit = indent_unit(indent);
106    run_json(
107        text,
108        Style::Pretty {
109            unit: &unit,
110            base: "",
111        },
112        &mut [],
113    )
114}
115
116/// The JSON with no whitespace between its tokens.
117pub fn minify_json(text: &str) -> Result<String, JsonError> {
118    run_json(text, Style::Minify, &mut [])
119}
120
121/// Checks that `text` is one JSON value (with blank space around it).
122pub fn validate_json(text: &str) -> Result<(), JsonError> {
123    run_json(text, Style::Check, &mut []).map(drop)
124}
125
126fn indent_unit(indent: Indent) -> String {
127    if indent.use_spaces {
128        " ".repeat(indent.width.max(1))
129    } else {
130        "\t".to_owned()
131    }
132}
133
134/// How [`run_json`] writes the JSON back out.
135#[derive(Clone, Copy)]
136enum Style<'a> {
137    /// Check only; write nothing.
138    Check,
139    /// No whitespace between tokens.
140    Minify,
141    /// One member or element per line, indented by `unit` per level after
142    /// `base` (the indentation of the line the JSON starts on).
143    Pretty { unit: &'a str, base: &'a str },
144}
145
146#[derive(Clone, Copy, PartialEq, Eq)]
147enum Expect {
148    /// A value: at the top, after `:`, or after `,` in an array.
149    Value,
150    /// Right after `[`: a value or `]`.
151    FirstElement,
152    /// Right after `{`: a key or `}`.
153    FirstKey,
154    /// After `,` in an object.
155    Key,
156    Colon,
157    /// After a value inside a container.
158    CommaOrClose,
159    /// After the top-level value: only whitespace.
160    End,
161}
162
163/// Writes the output and moves positions from the input to the output.
164struct Writer<'a, 'p> {
165    out: String,
166    style: Style<'a>,
167    /// Sorted input offsets, replaced by output offsets as tokens pass.
168    positions: &'p mut [usize],
169    next: usize,
170    /// Output offset right after the last token.
171    after_last: usize,
172    /// A line break is due before the next member or element.
173    break_due: bool,
174}
175
176impl Writer<'_, '_> {
177    /// Copies the token `text[start..end]`. Positions in the whitespace
178    /// before it stay after the previous token; positions inside it keep
179    /// their place in it.
180    fn token(&mut self, token: &str, start: usize) {
181        if matches!(self.style, Style::Check) {
182            return;
183        }
184        while let Some(p) = self.positions.get_mut(self.next).filter(|p| **p < start) {
185            *p = self.after_last;
186            self.next += 1;
187        }
188        let out_start = self.out.len();
189        self.out.push_str(token);
190        let end = start + token.len();
191        while let Some(p) = self.positions.get_mut(self.next).filter(|p| **p < end) {
192            *p = out_start + (*p - start);
193            self.next += 1;
194        }
195        self.after_last = self.out.len();
196    }
197
198    /// A line break and the indentation for `depth`, when pretty-printing.
199    fn line_break(&mut self, depth: usize) {
200        if let Style::Pretty { unit, base } = self.style {
201            self.out.push('\n');
202            self.out.push_str(base);
203            for _ in 0..depth {
204                self.out.push_str(unit);
205            }
206        }
207    }
208
209    /// Starts a member or element: on a new line if one is due.
210    fn element(&mut self, depth: usize) {
211        if std::mem::take(&mut self.break_due) {
212            self.line_break(depth);
213        }
214    }
215
216    fn space(&mut self) {
217        if let Style::Pretty { .. } = self.style {
218            self.out.push(' ');
219        }
220    }
221
222    /// Positions after the last token go to the end of the output.
223    fn finish(self) -> String {
224        if !matches!(self.style, Style::Check) {
225            for p in &mut self.positions[self.next..] {
226                *p = self.out.len();
227            }
228        }
229        self.out
230    }
231}
232
233/// What is at `i`, for messages.
234fn found(text: &str, i: usize) -> String {
235    text.get(i..)
236        .and_then(|s| s.chars().next())
237        .map_or_else(|| "the end".to_owned(), shown)
238}
239
240/// A character for messages: quoted, or its code point if invisible.
241fn shown(c: char) -> String {
242    if c.is_control() {
243        format!("U+{:04X}", c as u32)
244    } else {
245        format!("'{c}'")
246    }
247}
248
249fn json_error(offset: usize, message: impl Into<String>) -> JsonError {
250    JsonError {
251        offset,
252        message: message.into(),
253    }
254}
255
256/// Reads one JSON value and writes it back in `style`. `positions` (sorted
257/// offsets into `text`) are replaced by where they end up in the output.
258/// Iterative, so deep nesting cannot overflow the stack.
259fn run_json(text: &str, style: Style, positions: &mut [usize]) -> Result<String, JsonError> {
260    let b = text.as_bytes();
261    let capacity = match style {
262        Style::Check => 0,
263        Style::Minify => text.len(),
264        Style::Pretty { .. } => text.len() + text.len() / 2,
265    };
266    let mut w = Writer {
267        out: String::with_capacity(capacity),
268        style,
269        positions,
270        next: 0,
271        after_last: 0,
272        break_due: false,
273    };
274    // Open containers: true for an object.
275    let mut stack: Vec<bool> = Vec::new();
276    let mut expect = Expect::Value;
277    let skip = |mut i: usize| {
278        while i < b.len() && matches!(b[i], b' ' | b'\t' | b'\n' | b'\r') {
279            i += 1;
280        }
281        i
282    };
283    let after_value = |stack: &[bool]| {
284        if stack.is_empty() {
285            Expect::End
286        } else {
287            Expect::CommaOrClose
288        }
289    };
290    let mut i = skip(0);
291    while i < b.len() {
292        let c = b[i];
293        match expect {
294            Expect::End => {
295                return Err(json_error(
296                    i,
297                    format!("unexpected {} after the end of the JSON", found(text, i)),
298                ));
299            }
300            Expect::Colon => {
301                if c != b':' {
302                    return Err(json_error(
303                        i,
304                        format!("expected ':' after the key but found {}", found(text, i)),
305                    ));
306                }
307                w.token(":", i);
308                w.space();
309                expect = Expect::Value;
310                i += 1;
311            }
312            Expect::CommaOrClose => {
313                let object = stack.last() == Some(&true);
314                match c {
315                    b',' => {
316                        w.token(",", i);
317                        w.break_due = true;
318                        expect = if object { Expect::Key } else { Expect::Value };
319                    }
320                    b'}' if object => {
321                        stack.pop();
322                        w.line_break(stack.len());
323                        w.token("}", i);
324                        expect = after_value(&stack);
325                    }
326                    b']' if !object => {
327                        stack.pop();
328                        w.line_break(stack.len());
329                        w.token("]", i);
330                        expect = after_value(&stack);
331                    }
332                    _ => {
333                        return Err(json_error(
334                            i,
335                            format!(
336                                "expected ',' or '{}' but found {}",
337                                if object { '}' } else { ']' },
338                                found(text, i)
339                            ),
340                        ));
341                    }
342                }
343                i += 1;
344            }
345            Expect::FirstKey if c == b'}' => {
346                stack.pop();
347                w.break_due = false;
348                w.token("}", i);
349                expect = after_value(&stack);
350                i += 1;
351            }
352            Expect::FirstElement if c == b']' => {
353                stack.pop();
354                w.break_due = false;
355                w.token("]", i);
356                expect = after_value(&stack);
357                i += 1;
358            }
359            Expect::FirstKey | Expect::Key => {
360                if c == b'"' {
361                    let end = scan_string(text, i)?;
362                    w.element(stack.len());
363                    w.token(&text[i..end], i);
364                    expect = Expect::Colon;
365                    i = end;
366                } else {
367                    let wanted = if expect == Expect::FirstKey {
368                        "a key in quotes or '}'"
369                    } else {
370                        "a key in quotes"
371                    };
372                    return Err(json_error(
373                        i,
374                        format!("expected {wanted} but found {}", found(text, i)),
375                    ));
376                }
377            }
378            Expect::Value | Expect::FirstElement => {
379                let end = match c {
380                    b'{' | b'[' => i + 1,
381                    b'"' => scan_string(text, i)?,
382                    b'-' | b'0'..=b'9' => scan_number(text, i)?,
383                    _ => ["true", "false", "null"]
384                        .into_iter()
385                        .find(|lit| text[i..].starts_with(lit))
386                        .map(|lit| i + lit.len())
387                        .ok_or_else(|| {
388                            json_error(i, format!("expected a value but found {}", found(text, i)))
389                        })?,
390                };
391                w.element(stack.len());
392                w.token(&text[i..end], i);
393                // Formatted, each level indents every line inside it, so the
394                // text grows with the square of the depth: a few KB of
395                // brackets would become gigabytes.
396                if matches!(c, b'{' | b'[')
397                    && matches!(w.style, Style::Pretty { .. })
398                    && stack.len() >= MAX_FORMAT_DEPTH
399                {
400                    return Err(json_error(
401                        i,
402                        format!(
403                            "nested more than {MAX_FORMAT_DEPTH} levels deep, too deep to format"
404                        ),
405                    ));
406                }
407                match c {
408                    b'{' => {
409                        stack.push(true);
410                        w.break_due = true;
411                        expect = Expect::FirstKey;
412                    }
413                    b'[' => {
414                        stack.push(false);
415                        w.break_due = true;
416                        expect = Expect::FirstElement;
417                    }
418                    _ => expect = after_value(&stack),
419                }
420                i = end;
421            }
422        }
423        i = skip(i);
424    }
425    if expect != Expect::End {
426        let message = match expect {
427            Expect::Value | Expect::FirstElement if stack.is_empty() => {
428                "expected a value but found the end".to_owned()
429            }
430            _ => format!(
431                "the JSON ends before its {} is closed",
432                if stack.last() == Some(&true) {
433                    "object"
434                } else {
435                    "array"
436                }
437            ),
438        };
439        return Err(json_error(b.len(), message));
440    }
441    Ok(w.finish())
442}
443
444/// The end of the string starting with the quote at `start`.
445fn scan_string(text: &str, start: usize) -> Result<usize, JsonError> {
446    let b = text.as_bytes();
447    let mut j = start + 1;
448    while j < b.len() {
449        match b[j] {
450            b'"' => return Ok(j + 1),
451            b'\\' => match b.get(j + 1) {
452                Some(b'"' | b'\\' | b'/' | b'b' | b'f' | b'n' | b'r' | b't') => j += 2,
453                Some(b'u') => {
454                    if b.len() >= j + 6 && b[j + 2..j + 6].iter().all(u8::is_ascii_hexdigit) {
455                        j += 6;
456                    } else {
457                        return Err(json_error(j, "\\u must be followed by four hex digits"));
458                    }
459                }
460                _ => return Err(json_error(j, "invalid escape sequence in a string")),
461            },
462            b'\n' | b'\r' => {
463                return Err(json_error(
464                    j,
465                    "a string cannot contain a line break (write \\n)",
466                ));
467            }
468            c if c < 0x20 => {
469                return Err(json_error(
470                    j,
471                    format!("a string cannot contain the control character U+{c:04X}"),
472                ));
473            }
474            _ => j += 1,
475        }
476    }
477    Err(json_error(start, "this string has no closing quote"))
478}
479
480/// The end of the number starting at `start` (a `-` or a digit).
481fn scan_number(text: &str, start: usize) -> Result<usize, JsonError> {
482    let b = text.as_bytes();
483    let digits = |mut j: usize| {
484        while j < b.len() && b[j].is_ascii_digit() {
485            j += 1;
486        }
487        j
488    };
489    let mut j = start;
490    if b[j] == b'-' {
491        j += 1;
492    }
493    match b.get(j) {
494        Some(b'0') => {
495            j += 1;
496            if b.get(j).is_some_and(u8::is_ascii_digit) {
497                return Err(json_error(j, "a number cannot have a leading zero"));
498            }
499        }
500        Some(c) if c.is_ascii_digit() => j = digits(j),
501        _ => return Err(json_error(j, "expected digits after '-'")),
502    }
503    if b.get(j) == Some(&b'.') {
504        let k = digits(j + 1);
505        if k == j + 1 {
506            return Err(json_error(k, "expected digits after the decimal point"));
507        }
508        j = k;
509    }
510    if matches!(b.get(j), Some(b'e' | b'E')) {
511        j += 1;
512        if matches!(b.get(j), Some(b'+' | b'-')) {
513            j += 1;
514        }
515        let k = digits(j);
516        if k == j {
517            return Err(json_error(k, "expected digits in the exponent"));
518        }
519        j = k;
520    }
521    Ok(j)
522}
523
524// ---------------------------------------------------------------------------
525// Base64 and percent-encoding
526
527const BASE64: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
528
529/// Base64 with the standard alphabet and `=` padding.
530pub fn base64_encode(bytes: &[u8]) -> String {
531    let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
532    for chunk in bytes.chunks(3) {
533        let at = |i: usize| chunk.get(i).copied().unwrap_or(0) as u32;
534        let n = (at(0) << 16) | (at(1) << 8) | at(2);
535        let sextet = |shift: u32| BASE64[(n >> shift) as usize & 63] as char;
536        out.push(sextet(18));
537        out.push(sextet(12));
538        out.push(if chunk.len() > 1 { sextet(6) } else { '=' });
539        out.push(if chunk.len() > 2 { sextet(0) } else { '=' });
540    }
541    out
542}
543
544/// Decodes Base64 in the standard or URL-safe alphabet. Spaces and line
545/// breaks are skipped and the `=` padding may be left out.
546pub fn base64_decode(text: &str) -> Result<Vec<u8>, String> {
547    let mut out = Vec::with_capacity(text.len() / 4 * 3 + 2);
548    let (mut acc, mut n, mut padding) = (0u32, 0usize, 0usize);
549    for c in text.chars() {
550        let v = match c {
551            'A'..='Z' => c as u32 - 'A' as u32,
552            'a'..='z' => c as u32 - 'a' as u32 + 26,
553            '0'..='9' => c as u32 - '0' as u32 + 52,
554            '+' | '-' => 62,
555            '/' | '_' => 63,
556            '=' => {
557                padding += 1;
558                continue;
559            }
560            ' ' | '\t' | '\n' | '\r' => continue,
561            _ => return Err(format!("{} is not a Base64 character", shown(c))),
562        };
563        if padding > 0 {
564            return Err("there is text after the '=' padding".to_owned());
565        }
566        acc = (acc << 6) | v;
567        n += 1;
568        if n == 4 {
569            out.extend([(acc >> 16) as u8, (acc >> 8) as u8, acc as u8]);
570            (acc, n) = (0, 0);
571        }
572    }
573    match n {
574        0 if padding == 0 => {}
575        2 if padding <= 2 => out.push((acc >> 4) as u8),
576        3 if padding <= 1 => out.extend([(acc >> 10) as u8, (acc >> 2) as u8]),
577        1 => return Err("the text is cut short: one character too many or too few".to_owned()),
578        _ => return Err("the '=' padding does not fit the text".to_owned()),
579    }
580    Ok(out)
581}
582
583/// Percent-encodes every byte except the RFC 3986 unreserved characters.
584pub fn url_encode(bytes: &[u8]) -> String {
585    const HEX: &[u8; 16] = b"0123456789ABCDEF";
586    let mut out = String::with_capacity(bytes.len());
587    for &b in bytes {
588        if b.is_ascii_alphanumeric() || matches!(b, b'-' | b'.' | b'_' | b'~') {
589            out.push(b as char);
590        } else {
591            out.push('%');
592            out.push(HEX[(b >> 4) as usize] as char);
593            out.push(HEX[(b & 15) as usize] as char);
594        }
595    }
596    out
597}
598
599/// Decodes `%XX` escapes; everything else, `+` included, is kept.
600pub fn url_decode(text: &str) -> Result<Vec<u8>, String> {
601    let b = text.as_bytes();
602    let mut out = Vec::with_capacity(b.len());
603    let mut i = 0;
604    while i < b.len() {
605        if b[i] != b'%' {
606            out.push(b[i]);
607            i += 1;
608            continue;
609        }
610        let hex = |j: usize| b.get(j).and_then(|&x| (x as char).to_digit(16));
611        match (hex(i + 1), hex(i + 2)) {
612            (Some(hi), Some(lo)) => {
613                out.push((hi * 16 + lo) as u8);
614                i += 3;
615            }
616            _ => {
617                let bad: String = text[i..].chars().take(3).collect();
618                return Err(format!(
619                    "\"{bad}\" is not an escape: '%' must be followed by two hex digits"
620                ));
621            }
622        }
623    }
624    Ok(out)
625}
626
627/// Two upper-case hex digits per byte.
628pub fn hex_encode(bytes: &[u8]) -> String {
629    const HEX: &[u8; 16] = b"0123456789ABCDEF";
630    let mut out = String::with_capacity(bytes.len() * 2);
631    for &b in bytes {
632        out.push(HEX[(b >> 4) as usize] as char);
633        out.push(HEX[(b & 15) as usize] as char);
634    }
635    out
636}
637
638/// Reads pairs of hex digits, skipping blanks and line breaks.
639pub fn hex_decode(text: &str) -> Result<Vec<u8>, String> {
640    let mut out = Vec::with_capacity(text.len() / 2);
641    let mut high: Option<u8> = None;
642    for c in text.chars().filter(|c| !c.is_whitespace()) {
643        let Some(digit) = c.to_digit(16) else {
644            return Err(format!("'{c}' is not a hex digit"));
645        };
646        let digit = digit as u8;
647        match high.take() {
648            Some(h) => out.push(h << 4 | digit),
649            None => high = Some(digit),
650        }
651    }
652    if high.is_some() {
653        return Err("the hex digits do not pair up: one is left over".to_owned());
654    }
655    Ok(out)
656}
657
658// ---------------------------------------------------------------------------
659// Hashes
660
661/// Hashes of some text, as lowercase hex.
662#[derive(Clone, Debug, PartialEq, Eq)]
663pub struct Hashes {
664    /// Bytes hashed.
665    pub bytes: usize,
666    /// Line breaks in the text (written with the document's line ending).
667    pub line_breaks: usize,
668    pub md5: String,
669    pub sha1: String,
670    pub sha256: String,
671}
672
673/// Hashes the UTF-8 bytes of `text`, with `ending` for every line break.
674pub fn hashes(text: &str, ending: LineEnding) -> Hashes {
675    let newline = ending_str(ending).as_bytes();
676    let (mut md5, mut sha1, mut sha256) = (Md5::new(), Sha1::new(), Sha256::new());
677    let (mut bytes, mut line_breaks) = (0, 0);
678    let mut feed = |data: &[u8]| {
679        md5.update(data);
680        sha1.update(data);
681        sha256.update(data);
682        bytes += data.len();
683    };
684    for (i, line) in text.split('\n').enumerate() {
685        if i > 0 {
686            feed(newline);
687            line_breaks += 1;
688        }
689        feed(line.as_bytes());
690    }
691    let hex = |digest: &[u8]| digest.iter().map(|b| format!("{b:02x}")).collect();
692    Hashes {
693        bytes,
694        line_breaks,
695        md5: hex(&md5.finalize()),
696        sha1: hex(&sha1.finalize()),
697        sha256: hex(&sha256.finalize()),
698    }
699}
700
701/// The hash tool's text: the non-empty selections joined by line breaks
702/// (as a copy of them would be), or the whole document when nothing is
703/// selected. The flag says it was the whole document.
704pub fn hash_selections(doc: &Document, sels: &SelectionSet, ending: LineEnding) -> (Hashes, bool) {
705    let whole = nothing_selected(sels);
706    let text = if whole {
707        doc.to_string()
708    } else {
709        sels.ranges()
710            .iter()
711            .filter(|s| !s.is_empty())
712            .map(|s| doc.slice(s.range()))
713            .collect::<Vec<_>>()
714            .join("\n")
715    };
716    (hashes(&text, ending), whole)
717}
718
719fn ending_str(ending: LineEnding) -> &'static str {
720    match ending {
721        LineEnding::Lf => "\n",
722        LineEnding::CrLf => "\r\n",
723        LineEnding::Cr => "\r",
724    }
725}
726
727// ---------------------------------------------------------------------------
728// The actions
729
730fn nothing_selected(sels: &SelectionSet) -> bool {
731    sels.ranges().iter().all(Selection::is_empty)
732}
733
734/// `r` without JSON whitespace at either end.
735fn trimmed(doc: &Document, r: Range<ByteOffset>) -> (Range<ByteOffset>, String) {
736    let text = doc.slice(r.clone());
737    let start = r.start.0 + (text.len() - text.trim_start_matches(JSON_SPACE).len());
738    let body = text.trim_matches(JSON_SPACE).to_owned();
739    (ByteOffset(start)..ByteOffset(start + body.len()), body)
740}
741
742/// What the tools work on: every non-empty selection, or the document
743/// without the blank space at its ends. With `trim`, selections lose
744/// theirs too (JSON ignores it).
745fn targets(doc: &Document, sels: &SelectionSet, trim: bool) -> Vec<(Range<ByteOffset>, String)> {
746    if nothing_selected(sels) {
747        return vec![trimmed(doc, ByteOffset(0)..doc.len())];
748    }
749    sels.ranges()
750        .iter()
751        .filter(|s| !s.is_empty())
752        .map(|s| {
753            if trim {
754                trimmed(doc, s.range())
755            } else {
756                (s.range(), doc.slice(s.range()))
757            }
758        })
759        .collect()
760}
761
762/// The leading spaces and tabs of the line holding `pos`.
763fn line_indent(doc: &Document, pos: ByteOffset) -> String {
764    doc.line_text(doc.line_of(pos))
765        .chars()
766        .take_while(|c| *c == ' ' || *c == '\t')
767        .collect()
768}
769
770fn invalid_json(doc: &Document, start: ByteOffset, e: JsonError, tab_width: usize) -> ActionError {
771    let at = ByteOffset(start.0 + e.offset);
772    ActionError::InvalidJson(Box::new(InvalidJson {
773        at,
774        line: doc.line_of(at) + 1,
775        column: byte_to_visual_col(doc, at, tab_width).0 + 1,
776        message: e.message,
777    }))
778}
779
780/// Checks the JSON in each selection, or the document when nothing is
781/// selected (Text Tools > Validate JSON). Not an edit.
782pub fn validate_json_in(
783    doc: &Document,
784    sels: &SelectionSet,
785    tab_width: usize,
786) -> Result<(), ActionError> {
787    for (r, text) in targets(doc, sels, true) {
788        run_json(&text, Style::Check, &mut [])
789            .map_err(|e| invalid_json(doc, r.start, e, tab_width))?;
790    }
791    Ok(())
792}
793
794/// Resolves the text-tool actions, which can fail (`Editor` routes them
795/// here). `indent` is the document's.
796pub(crate) fn resolve(
797    action: &Action,
798    doc: &Document,
799    sels: &SelectionSet,
800    indent: Indent,
801    ending: LineEnding,
802) -> Result<Resolved, ActionError> {
803    match action {
804        Action::FormatJson { indent: chosen } => {
805            let unit = indent_unit(chosen.unwrap_or(indent));
806            json(doc, sels, Some(&unit), indent.width)
807        }
808        Action::MinifyJson => json(doc, sels, None, indent.width),
809        Action::Encode(codec) => recode(doc, sels, |text| {
810            let text = match ending {
811                LineEnding::Lf => text.to_owned(),
812                other => text.replace('\n', ending_str(other)),
813            };
814            Ok(codec.encode(&text))
815        }),
816        Action::Decode(codec) => {
817            let what = if nothing_selected(sels) {
818                "the document"
819            } else {
820                "the selection"
821            };
822            recode(doc, sels, |text| {
823                let decoded = codec.decode(text).map_err(|e| {
824                    ActionError::Decode(format!("{what} is not valid {}: {e}", codec.name()))
825                })?;
826                // Line breaks become the editor's (LF).
827                Ok(if decoded.contains('\r') {
828                    decoded.replace("\r\n", "\n").replace('\r', "\n")
829                } else {
830                    decoded
831                })
832            })
833        }
834        _ => Ok(Resolved::Selections(sels.clone())),
835    }
836}
837
838/// Formats (`unit` is the indent per level) or minifies the JSON of every
839/// target. Without a selection, carets stay on the token they were on.
840fn json(
841    doc: &Document,
842    sels: &SelectionSet,
843    unit: Option<&str>,
844    tab_width: usize,
845) -> Result<Resolved, ActionError> {
846    let whole = nothing_selected(sels);
847    let mut edits = Vec::new();
848    // (selection index, position after the edit) for carets inside the JSON.
849    let mut moved: Vec<(usize, ByteOffset)> = Vec::new();
850    for (r, text) in targets(doc, sels, true) {
851        let base = unit.map(|_| line_indent(doc, r.start)).unwrap_or_default();
852        let style = match unit {
853            Some(unit) => Style::Pretty { unit, base: &base },
854            None => Style::Minify,
855        };
856        let mut inside: Vec<(usize, usize)> = if whole {
857            sels.ranges()
858                .iter()
859                .enumerate()
860                .filter(|(_, s)| r.start <= s.head && s.head <= r.end)
861                .map(|(i, s)| (s.head.0 - r.start.0, i))
862                .collect()
863        } else {
864            Vec::new()
865        };
866        inside.sort_unstable();
867        let mut positions: Vec<usize> = inside.iter().map(|(p, _)| *p).collect();
868        let out = run_json(&text, style, &mut positions)
869            .map_err(|e| invalid_json(doc, r.start, e, tab_width))?;
870        // Nothing before this target changes (there is one without a
871        // selection), so its output starts where it did.
872        moved.extend(
873            inside
874                .iter()
875                .zip(&positions)
876                .map(|((_, i), p)| (*i, ByteOffset(r.start.0 + p))),
877        );
878        if out != text {
879            edits.push(Edit::new(r.start, r.end, out));
880        }
881    }
882    let changes = ChangeSet::new(doc.len(), edits);
883    if changes.is_empty() {
884        return Ok(Resolved::Selections(sels.clone()));
885    }
886    let after = if whole {
887        let ranges = sels
888            .ranges()
889            .iter()
890            .enumerate()
891            .map(|(i, s)| {
892                let pos = moved
893                    .iter()
894                    .find(|(j, _)| *j == i)
895                    .map_or_else(|| changes.map_position(s.head, Bias::Before), |m| m.1);
896                Selection::caret(pos)
897            })
898            .collect();
899        SelectionSet::new(ranges, sels.primary_index())
900    } else {
901        cover_new_text(sels, &changes)
902    };
903    Ok(Resolved::Edit {
904        changes,
905        after,
906        kind: EditKind::Other,
907    })
908}
909
910/// Replaces every target with `f` of its text.
911fn recode(
912    doc: &Document,
913    sels: &SelectionSet,
914    f: impl Fn(&str) -> Result<String, ActionError>,
915) -> Result<Resolved, ActionError> {
916    let mut edits = Vec::new();
917    for (r, text) in targets(doc, sels, false) {
918        let new = f(&text)?;
919        if new != text {
920            edits.push(Edit::new(r.start, r.end, new));
921        }
922    }
923    let changes = ChangeSet::new(doc.len(), edits);
924    if changes.is_empty() {
925        return Ok(Resolved::Selections(sels.clone()));
926    }
927    Ok(Resolved::Edit {
928        after: cover_new_text(sels, &changes),
929        changes,
930        kind: EditKind::Other,
931    })
932}
933
934/// Each selection covers what its text became; carets map through.
935fn cover_new_text(sels: &SelectionSet, changes: &ChangeSet) -> SelectionSet {
936    sels.map(|s| {
937        if s.is_empty() {
938            return Selection::caret(changes.map_position(s.head, Bias::Before));
939        }
940        let start = changes.map_position(s.start(), Bias::Before);
941        let end = changes.map_position(s.end(), Bias::After);
942        if s.head < s.anchor {
943            Selection::new(end, start)
944        } else {
945            Selection::new(start, end)
946        }
947    })
948}
949
950#[cfg(test)]
951mod tests {
952    use super::*;
953
954    const SPACES4: Indent = Indent {
955        use_spaces: true,
956        width: 4,
957    };
958
959    #[test]
960    fn formats_nested_json_keeping_order_numbers_and_strings() {
961        let input = r#"{"z":1,"a":[true,null,{"c":"d"}],"e":{},"f":[],"n":-0.0,"big":1E+10,"s":"caf\u00e9 \"q\" é 漢\n"}"#;
962        let expected = r#"{
963    "z": 1,
964    "a": [
965        true,
966        null,
967        {
968            "c": "d"
969        }
970    ],
971    "e": {},
972    "f": [],
973    "n": -0.0,
974    "big": 1E+10,
975    "s": "caf\u00e9 \"q\" é 漢\n"
976}"#;
977        assert_eq!(format_json(input, SPACES4).unwrap(), expected);
978        assert_eq!(minify_json(expected).unwrap(), input);
979        let tabs = format_json("[1,[2]]", Indent::default()).unwrap();
980        assert_eq!(tabs, "[\n\t1,\n\t[\n\t\t2\n\t]\n]");
981    }
982
983    #[test]
984    fn scalars_and_empty_containers_are_json_too() {
985        for s in [
986            "0", "-1.5e-3", "\"\"", "true", "false", "null", "{}", "[]", " [ ] ",
987        ] {
988            assert!(validate_json(s).is_ok(), "{s}");
989        }
990        assert_eq!(format_json(" {} ", SPACES4).unwrap(), "{}");
991        assert_eq!(minify_json("[ 1 , { } ]").unwrap(), "[1,{}]");
992    }
993
994    #[test]
995    fn invalid_json_says_where_and_why() {
996        let cases: &[(&str, usize, &str)] = &[
997            ("", 0, "expected a value but found the end"),
998            ("{\"a\":1,}", 7, "expected a key in quotes but found '}'"),
999            ("[1,]", 3, "expected a value but found ']'"),
1000            ("[1 2]", 3, "expected ',' or ']' but found '2'"),
1001            ("{\"a\" 1}", 5, "expected ':' after the key but found '1'"),
1002            ("{1:2}", 1, "expected a key in quotes or '}' but found '1'"),
1003            ("{} x", 3, "unexpected 'x' after the end of the JSON"),
1004            ("[1", 2, "the JSON ends before its array is closed"),
1005            ("{\"a\":[", 6, "the JSON ends before its array is closed"),
1006            ("\"abc", 0, "this string has no closing quote"),
1007            (
1008                "\"a\nb\"",
1009                2,
1010                "a string cannot contain a line break (write \\n)",
1011            ),
1012            ("\"\\x\"", 1, "invalid escape sequence in a string"),
1013            ("\"\\u12\"", 1, "\\u must be followed by four hex digits"),
1014            ("01", 1, "a number cannot have a leading zero"),
1015            ("-", 1, "expected digits after '-'"),
1016            ("1.", 2, "expected digits after the decimal point"),
1017            ("1e+", 3, "expected digits in the exponent"),
1018            ("tru", 0, "expected a value but found 't'"),
1019            ("[é]", 1, "expected a value but found 'é'"),
1020            ("// x\n{}", 0, "expected a value but found '/'"),
1021        ];
1022        for (input, offset, message) in cases {
1023            let e = validate_json(input).unwrap_err();
1024            assert_eq!(
1025                (e.offset, e.message.as_str()),
1026                (*offset, *message),
1027                "{input:?}"
1028            );
1029        }
1030    }
1031
1032    #[test]
1033    fn deep_nesting_does_not_overflow_the_stack() {
1034        let deep = format!("{}{}", "[".repeat(100_000), "]".repeat(100_000));
1035        assert!(validate_json(&deep).is_ok());
1036        assert_eq!(minify_json(&deep).unwrap(), deep);
1037        // Formatted, it would take about 40 GB: refused, with where.
1038        let indent = Indent {
1039            use_spaces: true,
1040            width: 4,
1041        };
1042        let err = format_json(&deep, indent).unwrap_err();
1043        assert_eq!(err.offset, MAX_FORMAT_DEPTH);
1044        assert!(
1045            err.message.contains("too deep to format"),
1046            "{}",
1047            err.message
1048        );
1049        // Up to the limit, it formats.
1050        let ok = format!(
1051            "{}{}",
1052            "[".repeat(MAX_FORMAT_DEPTH),
1053            "]".repeat(MAX_FORMAT_DEPTH)
1054        );
1055        assert!(format_json(&ok, indent).is_ok());
1056    }
1057
1058    #[test]
1059    fn positions_follow_their_tokens() {
1060        let text = r#"{"a": 1,  "b": "xyz"}"#;
1061        // Inside "xyz", after the comma, at the start of "b", and at the end.
1062        let mut positions = vec![7, 8, 10, 17, text.len()];
1063        let unit = "  ";
1064        let out = run_json(text, Style::Pretty { unit, base: "" }, &mut positions).unwrap();
1065        assert_eq!(out, "{\n  \"a\": 1,\n  \"b\": \"xyz\"\n}");
1066        let at = |p: usize| &out[p..];
1067        assert!(at(positions[0]).starts_with(",\n"), "after the 1");
1068        assert!(at(positions[1]).starts_with("\n  \"b\""), "after the comma");
1069        assert!(at(positions[2]).starts_with("\"b\""));
1070        assert!(at(positions[3]).starts_with("yz\""));
1071        assert_eq!(positions[4], out.len());
1072    }
1073
1074    #[test]
1075    fn base64_round_trips_every_padding_length() {
1076        let cases = [
1077            ("", ""),
1078            ("f", "Zg=="),
1079            ("fo", "Zm8="),
1080            ("foo", "Zm9v"),
1081            ("foob", "Zm9vYg=="),
1082            ("fooba", "Zm9vYmE="),
1083            ("foobar", "Zm9vYmFy"),
1084            ("é漢👍", "w6nmvKLwn5GN"),
1085        ];
1086        for (plain, encoded) in cases {
1087            assert_eq!(base64_encode(plain.as_bytes()), encoded);
1088            assert_eq!(Codec::Base64.decode(encoded).unwrap(), plain);
1089        }
1090        // No padding, line breaks, the URL-safe alphabet.
1091        assert_eq!(base64_decode("Zm9vYg").unwrap(), b"foob");
1092        assert_eq!(base64_decode("Zm9v\nYmFy\n").unwrap(), b"foobar");
1093        assert_eq!(
1094            base64_decode("-_-_").unwrap(),
1095            base64_decode("+/+/").unwrap()
1096        );
1097    }
1098
1099    #[test]
1100    fn hex_round_trips_and_takes_blanks() {
1101        assert_eq!(Codec::Hex.encode("Hi é"), "486920C3A9");
1102        assert_eq!(Codec::Hex.decode("486920C3A9").unwrap(), "Hi é");
1103        assert_eq!(Codec::Hex.decode("48 69\n20 c3 a9").unwrap(), "Hi é");
1104        assert_eq!(Codec::Hex.encode(""), "");
1105        assert_eq!(Codec::Hex.decode("").unwrap(), "");
1106        assert_eq!(
1107            Codec::Hex.decode("4G").unwrap_err(),
1108            "'G' is not a hex digit"
1109        );
1110        assert!(Codec::Hex.decode("486").is_err(), "a digit left over");
1111        // 0xFF is not UTF-8.
1112        assert!(Codec::Hex.decode("FF").is_err());
1113    }
1114
1115    #[test]
1116    fn bad_base64_is_refused() {
1117        assert_eq!(
1118            base64_decode("Zm9v!").unwrap_err(),
1119            "'!' is not a Base64 character"
1120        );
1121        assert!(base64_decode("Zm9vY").is_err(), "one character too many");
1122        assert!(base64_decode("Zg==Zg==").is_err(), "text after padding");
1123        assert!(
1124            base64_decode("Zm9v=").is_err(),
1125            "padding without a partial group"
1126        );
1127        // 0xFF is not UTF-8.
1128        assert_eq!(
1129            Codec::Base64.decode("/w==").unwrap_err(),
1130            "the decoded bytes are not UTF-8 text"
1131        );
1132    }
1133
1134    #[test]
1135    fn url_encoding_keeps_only_unreserved_characters() {
1136        let plain = "a b&c=d/é?~._-+%";
1137        let encoded = "a%20b%26c%3Dd%2F%C3%A9%3F~._-%2B%25";
1138        assert_eq!(url_encode(plain.as_bytes()), encoded);
1139        assert_eq!(Codec::Url.decode(encoded).unwrap(), plain);
1140        assert_eq!(Codec::Url.decode("a+b%2b%2B").unwrap(), "a+b++");
1141        assert!(url_decode("100%").is_err());
1142        assert!(url_decode("%G0").is_err());
1143        assert_eq!(
1144            Codec::Url.decode("%FF").unwrap_err(),
1145            "the decoded bytes are not UTF-8 text"
1146        );
1147    }
1148
1149    #[test]
1150    fn hashes_use_the_line_ending() {
1151        let h = hashes("abc", LineEnding::CrLf);
1152        assert_eq!(h.md5, "900150983cd24fb0d6963f7d28e17f72");
1153        assert_eq!(h.sha1, "a9993e364706816aba3e25717850c26c9cd0d89d");
1154        assert_eq!(
1155            h.sha256,
1156            "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
1157        );
1158        assert_eq!((h.bytes, h.line_breaks), (3, 0));
1159        let empty = hashes("", LineEnding::Lf);
1160        assert_eq!(
1161            empty.sha256,
1162            "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
1163        );
1164        // A line break is hashed as the document's ending.
1165        let crlf = hashes("a\nb", LineEnding::CrLf);
1166        assert_eq!((crlf.bytes, crlf.line_breaks), (4, 1));
1167        assert_eq!(crlf, hashes("a\r\nb", LineEnding::Lf));
1168    }
1169}