ElyDoc, ElyTable, ElyShow (LibreOfficeKit), ElyPDF (MuPDF), ElyTree (Git), ElyMark (Milkdown and CodeMirror in CEF). ElyMail and ElyAgent follow.
105 lines
3.2 KiB
Rust
105 lines
3.2 KiB
Rust
//! File bytes to editor text and back (§14.3, QA-21).
|
|
|
|
use std::io;
|
|
|
|
const BOM: &[u8] = b"\xEF\xBB\xBF";
|
|
|
|
/// A file as read: its bytes, BOM and line ending.
|
|
#[derive(Clone, Debug)]
|
|
pub struct Source {
|
|
bytes: Vec<u8>,
|
|
bom: bool,
|
|
/// More CRLF than lone LF line ends.
|
|
crlf: bool,
|
|
/// The editor's text: no BOM, CRLF as LF.
|
|
pub text: String,
|
|
}
|
|
|
|
impl Source {
|
|
/// Reads UTF-8, with or without BOM; other bytes fail.
|
|
pub fn decode(bytes: Vec<u8>) -> io::Result<Source> {
|
|
if bytes.starts_with(b"\xFF\xFE") || bytes.starts_with(b"\xFE\xFF") {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::InvalidData,
|
|
"UTF-16 text is not read",
|
|
));
|
|
}
|
|
let bom = bytes.starts_with(BOM);
|
|
let body = &bytes[if bom { BOM.len() } else { 0 }..];
|
|
let text = std::str::from_utf8(body).map_err(|error| {
|
|
let at = error.valid_up_to();
|
|
io::Error::new(
|
|
io::ErrorKind::InvalidData,
|
|
format!("not UTF-8 at byte {at}"),
|
|
)
|
|
})?;
|
|
let crlfs = text.matches("\r\n").count();
|
|
let crlf = crlfs > text.matches('\n').count() - crlfs;
|
|
let text = text.replace("\r\n", "\n");
|
|
Ok(Source {
|
|
bytes,
|
|
bom,
|
|
crlf,
|
|
text,
|
|
})
|
|
}
|
|
|
|
/// The bytes for `text`; unedited text is the file itself.
|
|
pub fn encode(&self, text: &str) -> Vec<u8> {
|
|
if text == self.text {
|
|
return self.bytes.clone();
|
|
}
|
|
let text = match self.crlf {
|
|
true => text.replace('\n', "\r\n"),
|
|
false => text.to_owned(),
|
|
};
|
|
let mut bytes = Vec::with_capacity(text.len() + BOM.len());
|
|
if self.bom {
|
|
bytes.extend_from_slice(BOM);
|
|
}
|
|
bytes.extend_from_slice(text.as_bytes());
|
|
bytes
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::Source;
|
|
|
|
#[test]
|
|
fn unedited_text_writes_the_same_bytes() {
|
|
for bytes in [
|
|
&b"# a\r\n\r\nb\n"[..],
|
|
b"\xEF\xBB\xBF# a\r\n",
|
|
b"a\rb\n",
|
|
b"",
|
|
b"no end",
|
|
] {
|
|
let source = Source::decode(bytes.to_vec()).expect("reads");
|
|
assert_eq!(source.encode(&source.text.clone()), bytes);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn edits_keep_the_bom_and_line_ending() {
|
|
let source = Source::decode(b"\xEF\xBB\xBF# a\r\n\r\nb\r\n".to_vec()).expect("reads");
|
|
assert_eq!(source.text, "# a\n\nb\n");
|
|
assert_eq!(source.encode("# a\n\nc\n"), b"\xEF\xBB\xBF# a\r\n\r\nc\r\n");
|
|
let plain = Source::decode(b"a\nb\n".to_vec()).expect("reads");
|
|
assert_eq!(plain.encode("a\nc\n"), b"a\nc\n");
|
|
}
|
|
|
|
#[test]
|
|
fn mixed_line_ends_follow_the_most() {
|
|
let source = Source::decode(b"a\r\nb\r\nc\n".to_vec()).expect("reads");
|
|
assert_eq!(source.encode("a\nb\nd\n"), b"a\r\nb\r\nd\r\n");
|
|
}
|
|
|
|
#[test]
|
|
fn other_encodings_fail_with_the_place() {
|
|
let error = Source::decode(b"ok \xFF".to_vec()).expect_err("not UTF-8");
|
|
assert!(error.to_string().contains("byte 3"), "{error}");
|
|
assert!(Source::decode(b"\xFF\xFEa\0".to_vec()).is_err());
|
|
}
|
|
}
|