spectrusty_formats/tap/
write.rs

1/*
2    Copyright (C) 2020-2022  Rafal Michalski
3
4    This file is part of SPECTRUSTY, a Rust library for building emulators.
5
6    For the full copyright notice, see the lib.rs file.
7*/
8use core::mem;
9use core::slice;
10use core::convert::{TryFrom, TryInto};
11use core::num::NonZeroU32;
12use std::io::{ErrorKind, Error, Write, Seek, SeekFrom, Result};
13use super::pulse::PulseDecodeWriter;
14use super::{Header, checksum};
15
16const LEN_PREFIX_SIZE: u64 = mem::size_of::<u16>() as u64;
17
18/// A tool for writing *TAP* file chunks to byte streams.
19///
20/// Data can be written in one of 3 ways:
21///
22/// * Writing tap chunk data at once with [TapChunkWriter::write_header] or [TapChunkWriter::write_chunk].
23/// * Writing tap chunk data with multiple writes via [TapChunkWriter::begin] and then [TapChunkWriteTran].
24/// * Writing tap chunks from decoded *TAPE* pulse iterators with [TapChunkWriter::write_pulses_as_tap_chunks].
25pub struct TapChunkWriter<W> {
26    chunk_head: u64,
27    mpwr: PulseDecodeWriter<W>
28}
29
30/// A [TapChunkWriter] transaction holder. Created by [TapChunkWriter::begin].
31///
32/// Write data with its [Write] implementation methods.
33/// Then call [TapChunkWriteTran::commit].
34pub struct TapChunkWriteTran<'a, W: Write + Seek> {
35    /// Checksum byte updated with each write.
36    pub checksum: u8,
37    nchunks: usize,
38    uncommitted: u16,
39    writer: &'a mut TapChunkWriter<W>
40}
41
42impl<W> TapChunkWriter<W> {
43    /// Returns the underlying pulse decode writer.
44    pub fn into_inner(self) -> PulseDecodeWriter<W> {
45        self.mpwr
46    }
47    /// Returns a mutable reference to the inner pulse decode writer.
48    pub fn get_mut(&mut self) -> &mut PulseDecodeWriter<W> {
49        &mut self.mpwr
50    }
51    /// Returns a shared reference to the inner pulse decode writer.
52    pub fn get_ref(&self) -> &PulseDecodeWriter<W> {
53        &self.mpwr
54    }
55}
56
57impl<'a, W: Write + Seek> Drop for TapChunkWriteTran<'a, W> {
58    /// Rollbacks if uncommitted. In this instance moves the cursor back to where it was before the
59    /// transaction has started.
60    fn drop(&mut self) {
61        if self.uncommitted != 0 {
62            let chunk_head = self.writer.chunk_head.checked_add(LEN_PREFIX_SIZE).unwrap();
63            let _ = self.writer.mpwr.get_mut().seek(SeekFrom::Start(chunk_head));
64        }
65    }
66}
67
68impl<'a, W: Write + Seek> Write for TapChunkWriteTran<'a, W> {
69    /// Appends data to the current chunk.
70    /// 
71    /// Any number of writes should be followed by [TapChunkWriteTran::commit].
72    fn write(&mut self, buf: &[u8]) -> Result<usize> {
73        let _: u16 = (self.uncommitted as usize).checked_add(buf.len()).unwrap()
74                    .try_into().map_err(|e| Error::new(ErrorKind::WriteZero, e))?;
75        let written = self.writer.mpwr.get_mut().write(buf)?;
76        self.checksum ^= checksum(&buf[..written]);
77        self.uncommitted += written as u16;
78        Ok(written)
79    }
80
81    fn flush(&mut self) -> Result<()> {
82        self.writer.flush()
83    }
84}
85
86impl<'a, W> TapChunkWriteTran<'a, W>
87    where W: Write + Seek
88{
89    /// Commits a *TAP* chunk.
90    ///
91    /// If `with_checksum` is `true` additionally writes a checksum byte of data written so far.
92    ///
93    /// Returns number of *TAP* chunks written including the call to `begin`.
94    pub fn commit(mut self, with_checksum: bool) -> Result<usize> {
95        let mut nchunks = self.nchunks;
96        let mut uncommitted = self.uncommitted;
97        if with_checksum {
98            if let Some(size) = uncommitted.checked_add(1) {
99                uncommitted = size;
100                let checksum = self.checksum;
101                self.write_all(slice::from_ref(&checksum))?;
102            }
103            else {
104                return Err(Error::new(ErrorKind::WriteZero, "chunk is larger than the maximum allowed size"))
105            }
106        }
107        if let Some(size) = NonZeroU32::new(uncommitted.into()) {
108            self.writer.commit_chunk(size)?;
109            nchunks += 1;
110        }
111        Ok(nchunks)
112    }
113}
114
115impl<W> TapChunkWriter<W>
116    where W: Write + Seek
117{
118    /// Returns a new instance of `TapChunkWriter` with the given writer on success.
119    ///
120    /// The stream cursor should be positioned where the next chunk will be written.
121    ///
122    /// This method does not write any data, but moves the stream cursor to make room for
123    /// the next block's length indicator.
124    pub fn try_new(wr: W) -> Result<Self> {
125        let mut mpwr = PulseDecodeWriter::new(wr);
126        let chunk_start = mpwr.get_mut().seek(SeekFrom::Current(LEN_PREFIX_SIZE as i64))?;
127        let chunk_head = chunk_start.checked_sub(LEN_PREFIX_SIZE).unwrap();
128        Ok(TapChunkWriter { chunk_head, mpwr })
129    }
130    /// Flushes the underlying writer, ensuring that all intermediately buffered
131    /// contents reach their destination (invokes [Write::flush]).
132    pub fn flush(&mut self) -> Result<()> {
133        self.mpwr.get_mut().flush()
134    }
135    /// Forces pending pulse decode data transfer to [end][PulseDecodeWriter::end].
136    ///
137    /// Returns the number of *TAP* chunks written.
138    pub fn end_pulse_chunk(&mut self) -> Result<usize> {
139        if let Some(size) = self.mpwr.end()? {
140            self.commit_chunk(size)?;
141            Ok(1)
142        }
143        else {
144            Ok(0)
145        }
146    }
147    /// Writes a provided header as a *TAP* chunk.
148    ///
149    /// Flushes internal [mic pulse writer][PulseDecodeWriter::end] before proceeding with writing the header.
150    ///
151    /// Returns the number of *TAP* chunks written.
152    pub fn write_header(&mut self, header: &Header) -> Result<usize> {
153        self.write_chunk(header.to_tap_chunk())
154    }
155    /// Writes provided data as a *TAP* chunk.
156    ///
157    /// Flushes internal [mic pulse writer][PulseDecodeWriter::end] before proceeding with writing the data.
158    ///
159    /// Returns the number of *TAP* chunks written.
160    pub fn write_chunk<D: AsRef<[u8]>>(&mut self, chunk: D) -> Result<usize> {
161        let data = chunk.as_ref();
162        let size = u16::try_from(data.len()).map_err(|_|
163                    Error::new(ErrorKind::InvalidData, "TAP chunk too large."))?;
164        let nchunks = self.end_pulse_chunk()?;
165        let wr = self.mpwr.get_mut();
166        let chunk_head = wr.seek(SeekFrom::Start(self.chunk_head))?;
167        debug_assert_eq!(chunk_head, self.chunk_head);
168        wr.write_all(&size.to_le_bytes())?;
169        wr.write_all(data)?;
170        let chunk_start = wr.seek(SeekFrom::Current(LEN_PREFIX_SIZE as i64))?;
171        self.chunk_head = chunk_start.checked_sub(LEN_PREFIX_SIZE).unwrap();
172        Ok(nchunks + 1)
173    }
174    /// Creates a transaction allowing for multiple data writes to the same *TAP* chunk.
175    ///
176    /// Flushes internal [mic pulse writer][PulseDecodeWriter::end].
177    ///
178    /// Returns a transaction holder, which can be used to write data to the current chunk.
179    pub fn begin(&mut self) -> Result<TapChunkWriteTran<'_, W>> {
180        let nchunks = self.end_pulse_chunk()?;
181        Ok(TapChunkWriteTran { checksum: 0, nchunks, uncommitted: 0, writer: self })
182    }
183    /// Interprets pulse intervals from the provided iterator as bytes and writes them
184    /// to the underlying writer as *TAP* chunks.
185    ///
186    /// See [PulseDecodeWriter].
187    ///
188    /// Returns the number of *TAP* chunks written.
189    pub fn write_pulses_as_tap_chunks<I>(&mut self, mut iter: I) -> Result<usize>
190        where I: Iterator<Item=NonZeroU32>
191    {
192        let mut chunks = 0;
193        loop {
194            match self.mpwr.write_decoded_pulses(iter.by_ref())? {
195                None => return Ok(chunks),
196                Some(size)  => {
197                    chunks += 1;
198                    self.commit_chunk(size)?;
199                }
200            }
201        }
202    }
203
204    fn commit_chunk(&mut self, size: NonZeroU32) -> Result<()> {
205        let size = u16::try_from(size.get()).map_err(|_|
206                    Error::new(ErrorKind::InvalidData, "TAP chunk too large."))?;
207        // println!("flush chunk: {} at {}", size, self.chunk_head);
208        let wr = self.mpwr.get_mut();
209        let chunk_head = wr.seek(SeekFrom::Start(self.chunk_head))?;
210        debug_assert_eq!(chunk_head, self.chunk_head);
211        wr.write_all(&size.to_le_bytes())?;
212        self.chunk_head = chunk_head.checked_add(LEN_PREFIX_SIZE + size as u64).unwrap();
213        let pos_cur = self.chunk_head.checked_add(LEN_PREFIX_SIZE).unwrap();
214        let pos_next = wr.seek(SeekFrom::Start(pos_cur))?;
215        debug_assert_eq!(pos_next, pos_cur);
216        Ok(())
217    }
218}