use std::io::{Seek, SeekFrom, Write};
use super::{VariableType, MAX_DATA};
#[derive(thiserror::Error, Debug)]
pub enum WriteError {
#[error("Variable data may not exceed {} bytes but would become {0}", MAX_DATA)]
TooLarge(usize),
#[error("Variable name must consist only of uppercase A-Z, \u{03b8}, or after the first character 0-9")]
InvalidName,
}
pub struct Writer<W>
where
W: Write + Seek,
{
w: ChecksumWriter<W>,
data_bytes: u16,
ty: VariableType,
}
impl<W: Write + Seek> Writer<W> {
pub fn new(
mut output: W,
ty: VariableType,
name: &str,
archived: bool,
) -> std::io::Result<Self> {
const THETA: char = '\u{03b8}';
let mut padded_name = [0u8; 8];
for (i, c) in name.chars().enumerate().take(padded_name.len()) {
if !c.is_ascii_uppercase() && c != THETA && (i == 0 && c.is_ascii_digit()) {
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
WriteError::InvalidName,
));
}
padded_name[i] = if c == THETA { 0x5b } else { c as u8 };
}
let header = b"\
**TI83F*\x1a\x0a\0\
TI-8x variable writer by Peter Marheine \
\0\0\
";
debug_assert_eq!(header.len(), 55);
output.write_all(header)?;
let mut output = ChecksumWriter::new(output);
output.enable_checksums(true);
output.write_all(&[0xd, 0, 0, 0, ty as u8])?;
output.write_all(&padded_name)?;
output.write_all(&[0, if archived { 0x80 } else { 0 }, 0, 0])?;
let mut out = Self {
w: output,
data_bytes: 0,
ty,
};
if ty.has_length_prefix() {
out.write_all(&[0, 0])?;
}
Ok(out)
}
pub fn close(self) -> std::io::Result<W> {
let Self {
mut w,
data_bytes,
ty,
} = self;
w.enable_checksums(false);
w.seek(SeekFrom::Current(-(data_bytes as i64) - 17 - 2))?;
w.write_all(&(data_bytes + 17).to_le_bytes())?;
w.enable_checksums(true);
w.seek(SeekFrom::Current(2))?;
w.write_all(&data_bytes.to_le_bytes())?;
w.seek(SeekFrom::Current(11))?;
w.write_all(&data_bytes.to_le_bytes())?;
if ty.has_length_prefix() {
let embedded_len = (data_bytes - 2).to_le_bytes();
w.write_all(&embedded_len)?;
w.seek(SeekFrom::Current(-2))?;
}
w.seek(SeekFrom::Current(data_bytes as i64))?;
let ChecksumWriter {
mut w, checksum, ..
} = w;
w.write_all(&checksum.to_le_bytes())?;
Ok(w)
}
}
impl<W: Write + Seek> Write for Writer<W> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
if (self.data_bytes as usize).saturating_add(buf.len()) > MAX_DATA as usize {
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
WriteError::TooLarge(self.data_bytes as usize + buf.len()),
));
}
let written = self.w.write(buf)?;
self.data_bytes += written as u16;
Ok(written)
}
fn flush(&mut self) -> std::io::Result<()> {
self.w.flush()
}
}
pub struct ChecksumWriter<W> {
w: W,
checksum: u16,
active: bool,
}
impl<W> ChecksumWriter<W> {
fn enable_checksums(&mut self, enable: bool) {
self.active = enable;
}
fn new(w: W) -> Self {
ChecksumWriter {
w,
checksum: 0,
active: false,
}
}
}
impl<W: Write> Write for ChecksumWriter<W> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let written = self.w.write(buf)?;
if self.active {
for &byte in &buf[..written] {
self.checksum = self.checksum.wrapping_add(byte as u16);
}
}
Ok(written)
}
fn flush(&mut self) -> std::io::Result<()> {
self.w.flush()
}
}
impl<W: Seek> Seek for ChecksumWriter<W> {
fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
self.w.seek(pos)
}
}
#[test]
fn checksum_writer_works() {
let mut writer = ChecksumWriter::new(Vec::<u8>::new());
writer.checksum = 0xFF00;
writer.enable_checksums(true);
writer.write_all(&[255, 1, 0, 42]).unwrap();
assert_eq!(writer.checksum, 42);
writer.enable_checksums(false);
writer.write_all(&[1, 2, 3, 4]).unwrap();
assert_eq!(writer.checksum, 42);
assert_eq!(writer.w, &[255, 1, 0, 42, 1, 2, 3, 4]);
}
#[test]
fn empty_program_is_correct() {
use std::io::Cursor;
let mut buf = Vec::<u8>::new();
let writer = Writer::new(
Cursor::new(&mut buf),
VariableType::ProtectedProgram,
"A",
true,
)
.unwrap();
writer.close().unwrap();
assert_eq!(
&buf,
b"**TI83F*\x1a\x0a\0\
TI-8x variable writer by Peter Marheine \
\x13\0\x0d\0\x02\0\x06\
A\0\0\0\0\0\0\0\
\0\x80\
\x02\x00\x00\x00\
\xd8\x00",
);
}