use std::{error::Error as StdError, path::Path, time::Duration};
use bbcat::{Cell, DecodeOptions, Format};
fn stored_zip(name: &str, data: &[u8]) -> Vec<u8> {
fn crc32(data: &[u8]) -> u32 {
let mut crc = 0xffff_ffff_u32;
for &byte in data {
crc ^= u32::from(byte);
for _ in 0..8 {
crc = (crc >> 1) ^ (0xedb8_8320 & 0_u32.wrapping_sub(crc & 1));
}
}
!crc
}
let mut zip = Vec::new();
let crc = crc32(data);
zip.extend_from_slice(b"PK\x03\x04");
zip.extend_from_slice(&20_u16.to_le_bytes());
zip.extend_from_slice(&0_u16.to_le_bytes());
zip.extend_from_slice(&0_u16.to_le_bytes());
zip.extend_from_slice(&[0; 4]);
zip.extend_from_slice(&crc.to_le_bytes());
zip.extend_from_slice(&(data.len() as u32).to_le_bytes());
zip.extend_from_slice(&(data.len() as u32).to_le_bytes());
zip.extend_from_slice(&(name.len() as u16).to_le_bytes());
zip.extend_from_slice(&0_u16.to_le_bytes());
zip.extend_from_slice(name.as_bytes());
zip.extend_from_slice(data);
let central_offset = zip.len();
zip.extend_from_slice(b"PK\x01\x02");
zip.extend_from_slice(&20_u16.to_le_bytes());
zip.extend_from_slice(&20_u16.to_le_bytes());
zip.extend_from_slice(&0_u16.to_le_bytes());
zip.extend_from_slice(&0_u16.to_le_bytes());
zip.extend_from_slice(&[0; 4]);
zip.extend_from_slice(&crc.to_le_bytes());
zip.extend_from_slice(&(data.len() as u32).to_le_bytes());
zip.extend_from_slice(&(data.len() as u32).to_le_bytes());
zip.extend_from_slice(&(name.len() as u16).to_le_bytes());
zip.extend_from_slice(&[0; 8]);
zip.extend_from_slice(&0_u32.to_le_bytes());
zip.extend_from_slice(&0_u32.to_le_bytes());
zip.extend_from_slice(name.as_bytes());
let central_size = zip.len() - central_offset;
zip.extend_from_slice(b"PK\x05\x06");
zip.extend_from_slice(&[0; 4]);
zip.extend_from_slice(&1_u16.to_le_bytes());
zip.extend_from_slice(&1_u16.to_le_bytes());
zip.extend_from_slice(&(central_size as u32).to_le_bytes());
zip.extend_from_slice(&(central_offset as u32).to_le_bytes());
zip.extend_from_slice(&0_u16.to_le_bytes());
zip
}
#[test]
fn decodes_and_encodes_through_the_public_api() -> Result<(), Box<dyn StdError>> {
let document = bbcat::decode_with_options(
b"\x1b[31mHi",
DecodeOptions {
file_name: Some(Path::new("welcome.ans")),
width: Some(4),
},
)?;
assert_eq!(document.format, Format::AnsiText);
assert_eq!((document.screen.width, document.screen.height), (4, 1));
assert_eq!(
document.screen.cell(0, 0).unwrap().character,
u16::from(b'H')
);
assert_eq!(document.screen.glyph_dimensions(), (8, 16));
assert_eq!(document.screen.pixel_dimensions(), Some((32, 16)));
assert_eq!(document.screen.palette(), bbcat::VGA_PALETTE);
assert_eq!(document.screen.color(196), [255, 0, 0]);
assert_eq!(document.screen.palette_256()[235], [38, 38, 38]);
assert_eq!(document.screen.font().unwrap().len(), 256 * 16);
assert!(document.screen.raster().is_none());
assert!(!document.is_animated());
let info = document.info();
assert_eq!(info.format, Format::AnsiText);
assert_eq!((info.columns, info.rows), (4, 1));
assert_eq!(info.pixel_dimensions, Some((32, 16)));
assert_eq!(info.glyph_dimensions, (8, 16));
assert!(info.utf8_supported);
assert!(!info.raster);
assert!(!info.embedded_font);
assert!(!info.animated);
assert_eq!(info.frame_count, 0);
assert!(info.sauce.is_none());
let png = document.encode_png(1)?;
assert!(png.starts_with(b"\x89PNG\r\n\x1a\n"));
Ok(())
}
#[test]
fn exposes_zip_preview_detection_and_errors() {
assert!(bbcat::is_zip(b"PK\x05\x06"));
let error = bbcat::extract_archive_preview(b"PK\x05\x06").unwrap_err();
assert!(error.to_string().contains("end record"));
}
#[test]
fn lists_and_extracts_zip_artwork_through_the_public_api() -> Result<(), Box<dyn StdError>> {
let archive = stored_zip("gallery/demo.ans", b"demo");
assert_eq!(
bbcat::list_archive_artworks(&archive)?,
["gallery/demo.ans"]
);
let entry = bbcat::extract_archive_artwork(&archive, "gallery/demo.ans")?;
assert_eq!(entry.name, "gallery/demo.ans");
assert_eq!(entry.data, b"demo");
Ok(())
}
#[test]
fn preserves_xterm_256_colors_in_screen_and_png() -> Result<(), Box<dyn StdError>> {
let document = bbcat::decode_with_options(
b"\x1b[38;5;196;48;5;235mX",
DecodeOptions {
file_name: Some(Path::new("colors.ans")),
width: Some(1),
},
)?;
let cell = document.screen.cell(0, 0).unwrap();
assert_eq!((cell.foreground, cell.background), (196, 235));
assert_eq!(document.screen.color(cell.foreground), [255, 0, 0]);
assert_eq!(document.screen.color(cell.background), [38, 38, 38]);
let png = document.encode_png(1)?;
assert_eq!(png[24], 8);
Ok(())
}
#[test]
fn fits_png_output_within_a_pixel_bounding_box() -> Result<(), Box<dyn StdError>> {
let document = bbcat::decode_with_options(
b"wide",
DecodeOptions {
file_name: Some(Path::new("wide.ans")),
width: Some(100),
},
)?;
let png = document.encode_png_fit(200, 200)?;
assert_eq!(u32::from_be_bytes(png[16..20].try_into()?), 200);
assert_eq!(u32::from_be_bytes(png[20..24].try_into()?), 4);
let png = document.encode_png_fit(1_000, 8)?;
assert_eq!(u32::from_be_bytes(png[16..20].try_into()?), 400);
assert_eq!(u32::from_be_bytes(png[20..24].try_into()?), 8);
Ok(())
}
#[test]
fn rejects_inconsistent_public_screen_dimensions_without_panicking() {
let mut document = bbcat::decode(b"A").unwrap();
document.screen.width = 2;
let error = document.encode_png(1).unwrap_err();
assert!(error.message().contains("cell buffer"));
document.screen.width = 1;
document.screen.height = 0;
let error = bbcat::write_screen_fit(&mut Vec::new(), &document.screen, 24, 80).unwrap_err();
assert_eq!(error.kind(), std::io::ErrorKind::InvalidData);
assert!(error.to_string().contains("cell buffer"));
}
#[test]
fn reports_the_detected_format() {
let error = bbcat::decode(b"XBIN\x1a").unwrap_err();
assert!(error.message().contains("truncated XBin header"));
let data = concat!(
r#"{"type":"Dimensions","text":"1x1","frame":"SAUCE_record"}"#,
"\n",
r#"{"id":"1","type":"frame","duration_ms":25}"#,
"\n",
r#"{"x":0,"y":0,"text":"X","color":"15","frame":"1"}"#,
);
let document = bbcat::decode(data.as_bytes()).unwrap();
assert_eq!(document.format, Format::DarkDraw);
assert!(document.is_animated());
}
#[test]
fn decodes_tundradraw_true_colors_through_the_public_api() -> Result<(), Box<dyn StdError>> {
let data = [
0x18, b'T', b'U', b'N', b'D', b'R', b'A', b'2', b'4', 6, b'A', 0, 0x12, 0x34, 0x56, 0,
0x65, 0x43, 0x21,
];
let document = bbcat::decode_with_options(
&data,
DecodeOptions {
file_name: Some(Path::new("sample.TND")),
width: Some(1),
},
)?;
assert_eq!(document.format, Format::TundraDraw);
assert_eq!(
document.screen.cell(0, 0).unwrap().character,
u16::from(b'A')
);
assert_eq!(
document.screen.cell_colors(0, 0),
Some(([0x12, 0x34, 0x56], [0x65, 0x43, 0x21]))
);
let png = document.encode_png(1)?;
assert_eq!(&png[16..24], &[0, 0, 0, 8, 0, 0, 0, 16]);
assert_eq!(&png[24..26], &[4, 3]);
let fitted = document.encode_png_fit(4, 8)?;
assert_eq!(&fitted[16..24], &[0, 0, 0, 4, 0, 0, 0, 8]);
assert_eq!(&fitted[24..26], &[4, 3]);
let mut terminal = Vec::new();
bbcat::write_text(&mut terminal, &document.screen)?;
assert!(terminal.starts_with(b"\x1b[38;2;18;52;86;48;2;101;67;33mA"));
Ok(())
}
#[test]
fn cell_colors_handles_inconsistent_public_screen_dimensions() {
let mut document = bbcat::decode_with_options(
b"\x18TUNDRA24A",
DecodeOptions {
file_name: Some(Path::new("sample.tnd")),
width: Some(1),
},
)
.unwrap();
document.screen.height = 2;
document.screen.cells.push(Cell::default());
assert_eq!(document.screen.cell_colors(0, 1), None);
}
#[test]
fn exposes_character_and_raster_formats() -> Result<(), Box<dyn StdError>> {
let xbin = [
b'X', b'B', b'I', b'N', 0x1a, 1, 0, 1, 0, 0, 0x08, b'A', 0x1e,
];
let document = bbcat::decode(&xbin)?;
assert_eq!(document.format, Format::XBin);
assert!(!document.is_animated());
assert_eq!(document.screen.cell(0, 0).unwrap().foreground, 14);
let document = bbcat::decode(b"!|*|c0F|L00000A0A|#")?;
assert_eq!(document.format, Format::Ripscrip);
let raster = document.screen.raster().unwrap();
assert_eq!((raster.width, raster.height), (640, 350));
assert_eq!(raster.pixels[0], 15);
Ok(())
}
#[test]
fn exposes_animation_encoders_and_explicit_asciimation() -> Result<(), Box<dyn StdError>> {
let ansi = b"\x1b[2J\x1b[H\x1b[1;1HA\x1b[1;1HB\x1b[2J";
let document = bbcat::decode_with_options(
ansi,
DecodeOptions {
file_name: Some(Path::new("demo.ans")),
width: Some(1),
},
)?;
assert!(document.is_animated());
let info = document.info();
assert!(info.animated);
assert_eq!(info.frame_count, 2);
assert!(
document
.encode_apng(bbcat::DEFAULT_ANIMATION_BAUD, 1)?
.starts_with(b"\x89PNG\r\n\x1a\n")
);
assert!(
document
.encode_gif(bbcat::DEFAULT_ANIMATION_BAUD, 1)?
.starts_with(b"GIF89a")
);
let mut stream = String::from("2\n");
for _ in 0..13 {
stream.push_str("x\n");
}
let animation = bbcat::decode_asciimation(stream.as_bytes())?;
assert_eq!(animation.frames[0].duration(), Duration::from_millis(200));
assert_eq!(animation.frames[0].rows().len(), 13);
Ok(())
}