Skip to main content

view_file/
view_file.rs

1use std::{env, fs};
2
3use txtview::TxtView;
4
5const MAX_BYTES: u64 = 256 * 1024 * 1024;
6
7fn main() -> std::io::Result<()> {
8    let path = env::args()
9        .nth(1)
10        .ok_or_else(|| std::io::Error::other("usage: view_file <path>"))?;
11
12    let len = fs::metadata(&path)
13        .map_err(|e| std::io::Error::other(format!("{}: {}", path, e)))?
14        .len();
15    if len > MAX_BYTES {
16        return Err(std::io::Error::other(format!(
17            "{}: {} bytes is over the {} byte viewing limit",
18            path, len, MAX_BYTES
19        )));
20    }
21    let bytes = fs::read(&path).map_err(|e| std::io::Error::other(format!("{}: {}", path, e)))?;
22    let text = decode(&bytes).map_err(|e| std::io::Error::other(format!("{}: {}", path, e)))?;
23
24    let mut viewer = TxtView::new(text);
25    viewer.run()
26}
27
28/// Decode a file by its byte order mark, otherwise as UTF-8. UTF-16 and
29/// UTF-32 marks are told apart, so no guessing is needed there. Only a
30/// BOM-less file with NUL bytes hints at UTF-16 and the endianness falls
31/// out of where the first NUL sits. Malformed input is an error.
32fn decode(bytes: &[u8]) -> Result<String, String> {
33    match bytes {
34        [0xEF, 0xBB, 0xBF, rest @ ..] => utf8(rest),
35        [0xFF, 0xFE, 0x00, 0x00, rest @ ..] => utf32(rest, true),
36        [0x00, 0x00, 0xFE, 0xFF, rest @ ..] => utf32(rest, false),
37        [0xFF, 0xFE, rest @ ..] => utf16(rest, true),
38        [0xFE, 0xFF, rest @ ..] => utf16(rest, false),
39        _ if bytes.contains(&0) => match bytes {
40            [0, _, ..] => utf16(bytes, false),
41            [_, 0, ..] => utf16(bytes, true),
42            _ => utf8(bytes),
43        },
44        _ => utf8(bytes),
45    }
46}
47
48fn utf8(bytes: &[u8]) -> Result<String, String> {
49    String::from_utf8(bytes.to_vec()).map_err(|_| "not valid UTF-8".to_string())
50}
51
52fn utf16(bytes: &[u8], little: bool) -> Result<String, String> {
53    let mut chunks = bytes.chunks_exact(2);
54    let units = chunks
55        .by_ref()
56        .map(|pair| {
57            let pair = [pair[0], pair[1]];
58            if little {
59                u16::from_le_bytes(pair)
60            } else {
61                u16::from_be_bytes(pair)
62            }
63        })
64        .collect::<Vec<_>>();
65    if !chunks.remainder().is_empty() {
66        return Err("the file ends in the middle of a UTF-16 unit".to_string());
67    }
68    String::from_utf16(&units).map_err(|_| "unpaired UTF-16 surrogate".to_string())
69}
70
71fn utf32(bytes: &[u8], little: bool) -> Result<String, String> {
72    let mut chunks = bytes.chunks_exact(4);
73    if !chunks.remainder().is_empty() {
74        return Err("the file ends in the middle of a UTF-32 unit".to_string());
75    }
76    let mut out = String::new();
77    for quad in chunks.by_ref() {
78        let quad = [quad[0], quad[1], quad[2], quad[3]];
79        let scalar = if little {
80            u32::from_le_bytes(quad)
81        } else {
82            u32::from_be_bytes(quad)
83        };
84        out.push(char::from_u32(scalar).ok_or("a code point past U+10FFFF")?);
85    }
86    Ok(out)
87}