use std::borrow::Cow;
use std::io::{self, IsTerminal, Write};
use crate::data::writer;
const SEPARATOR: u8 = b'\t';
pub const MAX_BYTES: usize = 64 * 1024;
pub fn tsv(block: &[Vec<String>]) -> String {
let mut out = String::new();
for row in block {
let line: Vec<Cow<'_, str>> = row
.iter()
.map(|value| writer::encode(value, SEPARATOR))
.collect();
out.push_str(&line.join("\t"));
out.push('\n');
}
out
}
pub fn copy(text: &str) -> io::Result<bool> {
if text.len() > MAX_BYTES || !io::stdout().is_terminal() {
return Ok(false);
}
let mut out = io::stdout().lock();
write!(out, "{}", sequence(text))?;
out.flush()?;
Ok(true)
}
fn sequence(text: &str) -> String {
format!("\x1b]52;c;{}\x07", base64(text.as_bytes()))
}
pub fn parse(text: &str) -> Vec<Vec<String>> {
let mut rows = Vec::new();
let mut row = Vec::new();
let mut field = String::new();
let mut quoted = false;
let mut started = false;
let mut chars = text.chars().peekable();
while let Some(c) = chars.next() {
if quoted {
match c {
'"' if chars.peek() == Some(&'"') => {
chars.next();
field.push('"');
}
'"' => quoted = false,
_ => field.push(c),
}
continue;
}
match c {
'"' if !started => quoted = true,
'\t' => {
row.push(std::mem::take(&mut field));
started = false;
continue;
}
'\r' if chars.peek() == Some(&'\n') => {}
'\n' => {
row.push(std::mem::take(&mut field));
rows.push(std::mem::take(&mut row));
started = false;
continue;
}
_ => field.push(c),
}
started = true;
}
if started || !field.is_empty() || !row.is_empty() {
row.push(field);
rows.push(row);
}
rows
}
fn base64(bytes: &[u8]) -> String {
const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
for chunk in bytes.chunks(3) {
let b = [
chunk[0],
*chunk.get(1).unwrap_or(&0),
*chunk.get(2).unwrap_or(&0),
];
let n = u32::from(b[0]) << 16 | u32::from(b[1]) << 8 | u32::from(b[2]);
for i in 0..4 {
if i <= chunk.len() {
out.push(ALPHABET[(n >> (18 - i * 6)) as usize & 0x3f] as char);
} else {
out.push('=');
}
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn block(rows: &[&[&str]]) -> Vec<Vec<String>> {
rows.iter()
.map(|row| row.iter().map(|c| c.to_string()).collect())
.collect()
}
#[test]
fn a_block_goes_out_as_tsv() {
assert_eq!(tsv(&block(&[&["a", "b"], &["c", "d"]])), "a\tb\nc\td\n");
}
#[test]
fn a_value_that_needs_quotes_gets_them_and_survives_the_trip() {
let awkward = block(&[&["one\ttwo", "a \"quoted\" thing"], &["line\nbreak", ""]]);
let wire = tsv(&awkward);
assert_eq!(
wire,
"\"one\ttwo\"\t\"a \"\"quoted\"\" thing\"\n\"line\nbreak\"\t\n"
);
assert_eq!(parse(&wire), awkward, "and comes back what it was");
}
#[test]
fn a_plain_block_round_trips() {
let plain = block(&[&["1", "2", "3"], &["4", "5", "6"]]);
assert_eq!(parse(&tsv(&plain)), plain);
}
#[test]
fn a_paste_from_anywhere_else_is_read_as_it_comes() {
assert_eq!(parse("hello"), block(&[&["hello"]]), "one value, one cell");
assert_eq!(parse("a\tb"), block(&[&["a", "b"]]));
assert_eq!(
parse("a\tb\r\nc\td\r\n"),
block(&[&["a", "b"], &["c", "d"]]),
"and a windows line ending is still a line ending"
);
assert_eq!(
parse("a\nb"),
block(&[&["a"], &["b"]]),
"a last line with no terminator is still a row"
);
assert!(parse("").is_empty(), "and nothing is nothing");
}
#[test]
fn base64_matches_the_standard_alphabet_and_padding() {
assert_eq!(base64(b""), "");
assert_eq!(base64(b"f"), "Zg==");
assert_eq!(base64(b"fo"), "Zm8=");
assert_eq!(base64(b"foo"), "Zm9v");
assert_eq!(base64(b"foobar"), "Zm9vYmFy");
assert_eq!(base64("é".as_bytes()), "w6k=");
}
#[test]
fn the_sequence_is_the_one_terminals_answer() {
assert_eq!(sequence("hi"), "\x1b]52;c;aGk=\x07");
}
#[test]
fn too_much_text_is_not_sent_at_all() {
assert!(!copy(&"x".repeat(MAX_BYTES + 1)).unwrap());
}
}