use std::sync::Arc;
use crate::diff::sat_u32;
const BINARY_SNIFF: usize = 8000;
const TOO_LARGE: &str = "(file too large to show)";
pub fn is_binary(bytes: &[u8]) -> bool {
bytes.iter().take(BINARY_SNIFF).any(|&b| b == 0)
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Text {
pub source: Arc<str>,
pub lines: Vec<(u32, u32)>,
pub crlf: Vec<bool>,
pub missing_final_newline: bool,
}
impl Text {
pub fn new(bytes: &[u8]) -> Self {
let source: Arc<str> = match String::from_utf8_lossy(bytes) {
s if u32::try_from(s.len()).is_ok() => s.into(),
_ => TOO_LARGE.into(),
};
let mut lines = Vec::new();
let mut crlf = Vec::new();
let mut start = 0;
for chunk in source.split_inclusive('\n') {
let (line, is_crlf) = match chunk.strip_suffix('\n') {
Some(l) => l.strip_suffix('\r').map_or((l, false), |l| (l, true)),
None => (chunk, false),
};
lines.push((sat_u32(start), sat_u32(start + line.len())));
crlf.push(is_crlf);
start += chunk.len();
}
let missing_final_newline = !source.is_empty() && !source.ends_with('\n');
Self {
source,
lines,
crlf,
missing_final_newline,
}
}
pub fn len(&self) -> usize {
self.lines.len()
}
pub fn is_empty(&self) -> bool {
self.lines.is_empty()
}
pub fn line(&self, i: usize) -> &str {
self.lines
.get(i)
.and_then(|&(a, b)| self.source.get(a as usize..b as usize))
.unwrap_or_default()
}
pub fn line_no(&self, n: u32) -> &str {
n.checked_sub(1).map_or("", |i| self.line(i as usize))
}
pub(crate) fn token(&self, i: usize) -> String {
let mut t = self.line(i).to_owned();
if self.crlf.get(i) == Some(&true) {
t.push('\r');
}
if i + 1 < self.len() || !self.missing_final_newline {
t.push('\n');
}
t
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn splits_lines() {
let t = Text::new(b"a\nb\n");
assert_eq!(t.len(), 2);
assert_eq!(t.line(1), "b");
assert_eq!(t.line_no(2), "b");
assert_eq!((t.line(2), t.line_no(0)), ("", ""));
assert!(!t.missing_final_newline);
}
#[test]
fn tracks_missing_final_newline() {
let t = Text::new(b"a\nb");
assert_eq!(t.len(), 2);
assert!(t.missing_final_newline);
assert_eq!(t.token(1), "b");
assert_eq!(t.token(0), "a\n");
}
#[test]
fn strips_and_records_crlf() {
let t = Text::new(b"a\r\nb\n");
assert_eq!(t.line(0), "a");
assert_eq!(t.crlf, [true, false]);
assert_eq!(t.token(0), "a\r\n");
}
#[test]
fn empty_and_invalid_utf8() {
assert!(Text::new(b"").is_empty());
let t = Text::new(b"caf\xe9\n");
assert_eq!(t.line(0), "caf\u{fffd}");
}
#[test]
fn detects_binary() {
assert!(is_binary(b"PNG\0\x01"));
assert!(!is_binary(b"plain text"));
let mut late_nul = vec![b'a'; 9000];
late_nul.push(0);
assert!(!is_binary(&late_nul));
}
}