Skip to main content

openjph_core/
file.rs

1//! File I/O abstractions — port of `ojph_file.h/cpp`.
2//!
3//! Provides trait-based input/output streams and concrete implementations
4//! backed by files or in-memory buffers.
5
6use std::fs::File;
7use std::io::{self, Read, Write};
8
9use crate::error::{OjphError, Result};
10
11// ---------------------------------------------------------------------------
12// SeekFrom
13// ---------------------------------------------------------------------------
14
15/// Origin for seek operations (mirrors [`std::io::SeekFrom`] but decoupled
16/// from the standard library to match the C++ API).
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum SeekFrom {
19    /// Seek from the beginning of the stream.
20    Start,
21    /// Seek relative to the current position.
22    Current,
23    /// Seek from the end of the stream.
24    End,
25}
26
27impl SeekFrom {
28    /// Converts to the standard library equivalent, given an `offset`.
29    fn to_std(self, offset: i64) -> io::SeekFrom {
30        match self {
31            Self::Start => io::SeekFrom::Start(offset as u64),
32            Self::Current => io::SeekFrom::Current(offset),
33            Self::End => io::SeekFrom::End(offset),
34        }
35    }
36}
37
38// ---------------------------------------------------------------------------
39// Output trait
40// ---------------------------------------------------------------------------
41
42/// Trait for sequential / seekable output streams — port of `outfile_base`.
43pub trait OutfileBase {
44    /// Writes `data` and returns the number of bytes written.
45    fn write(&mut self, data: &[u8]) -> Result<usize>;
46
47    /// Returns the current byte position in the stream.
48    fn tell(&self) -> i64 {
49        0
50    }
51
52    /// Seeks to the given `offset` from `whence`.
53    fn seek(&mut self, _offset: i64, _whence: SeekFrom) -> Result<()> {
54        Err(OjphError::Unsupported("seek not supported".into()))
55    }
56
57    /// Flushes any buffered data.
58    fn flush(&mut self) -> Result<()> {
59        Ok(())
60    }
61}
62
63// ---------------------------------------------------------------------------
64// Input trait
65// ---------------------------------------------------------------------------
66
67/// Trait for sequential / seekable input streams — port of `infile_base`.
68pub trait InfileBase {
69    /// Reads up to `buf.len()` bytes, returning the count actually read.
70    fn read(&mut self, buf: &mut [u8]) -> Result<usize>;
71
72    /// Seeks to the given `offset` from `whence`.
73    fn seek(&mut self, offset: i64, whence: SeekFrom) -> Result<()>;
74
75    /// Returns the current byte position.
76    fn tell(&self) -> i64;
77
78    /// Returns `true` when the end of the stream has been reached.
79    fn eof(&self) -> bool;
80}
81
82// =========================================================================
83// J2cOutfile — wraps `std::fs::File`
84// =========================================================================
85
86/// File-backed output stream — port of `j2c_outfile`.
87pub struct J2cOutfile {
88    file: File,
89    pos: i64,
90}
91
92impl J2cOutfile {
93    /// Opens (creates / truncates) the file at `path`.
94    pub fn open(path: &str) -> Result<Self> {
95        let file = File::create(path)?;
96        Ok(Self { file, pos: 0 })
97    }
98}
99
100impl OutfileBase for J2cOutfile {
101    fn write(&mut self, data: &[u8]) -> Result<usize> {
102        let n = self.file.write(data)?;
103        self.pos += n as i64;
104        Ok(n)
105    }
106
107    fn tell(&self) -> i64 {
108        self.pos
109    }
110
111    fn seek(&mut self, offset: i64, whence: SeekFrom) -> Result<()> {
112        use std::io::Seek;
113        let new_pos = self.file.seek(whence.to_std(offset))?;
114        self.pos = new_pos as i64;
115        Ok(())
116    }
117
118    fn flush(&mut self) -> Result<()> {
119        self.file.flush()?;
120        Ok(())
121    }
122}
123
124// =========================================================================
125// J2cInfile — wraps `std::fs::File`
126// =========================================================================
127
128/// File-backed input stream — port of `j2c_infile`.
129pub struct J2cInfile {
130    file: File,
131    pos: i64,
132    at_eof: bool,
133}
134
135impl J2cInfile {
136    /// Opens an existing file for reading.
137    pub fn open(path: &str) -> Result<Self> {
138        let file = File::open(path)?;
139        Ok(Self {
140            file,
141            pos: 0,
142            at_eof: false,
143        })
144    }
145}
146
147impl InfileBase for J2cInfile {
148    fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
149        let n = self.file.read(buf)?;
150        self.pos += n as i64;
151        if n == 0 && !buf.is_empty() {
152            self.at_eof = true;
153        }
154        Ok(n)
155    }
156
157    fn seek(&mut self, offset: i64, whence: SeekFrom) -> Result<()> {
158        use std::io::Seek;
159        let new_pos = self.file.seek(whence.to_std(offset))?;
160        self.pos = new_pos as i64;
161        self.at_eof = false;
162        Ok(())
163    }
164
165    fn tell(&self) -> i64 {
166        self.pos
167    }
168
169    fn eof(&self) -> bool {
170        self.at_eof
171    }
172}
173
174// =========================================================================
175// MemOutfile — memory-backed output
176// =========================================================================
177
178/// In-memory output stream that grows as data is written — port of
179/// `mem_outfile`.
180pub struct MemOutfile {
181    buf: Vec<u8>,
182    pos: usize,
183}
184
185impl MemOutfile {
186    /// Creates a new, empty memory output stream.
187    pub fn new() -> Self {
188        Self {
189            buf: Vec::new(),
190            pos: 0,
191        }
192    }
193
194    /// Creates a memory output stream with pre-allocated capacity.
195    pub fn with_capacity(cap: usize) -> Self {
196        Self {
197            buf: Vec::with_capacity(cap),
198            pos: 0,
199        }
200    }
201
202    /// Returns a reference to all data written so far.
203    pub fn get_data(&self) -> &[u8] {
204        &self.buf
205    }
206
207    /// Returns the total number of bytes written.
208    pub fn len(&self) -> usize {
209        self.buf.len()
210    }
211
212    /// Returns `true` if no data has been written.
213    pub fn is_empty(&self) -> bool {
214        self.buf.is_empty()
215    }
216}
217
218impl Default for MemOutfile {
219    fn default() -> Self {
220        Self::new()
221    }
222}
223
224impl OutfileBase for MemOutfile {
225    fn write(&mut self, data: &[u8]) -> Result<usize> {
226        if self.pos == self.buf.len() {
227            self.buf.extend_from_slice(data);
228        } else {
229            // Overwrite existing bytes, extending if necessary.
230            let end = self.pos + data.len();
231            if end > self.buf.len() {
232                self.buf.resize(end, 0);
233            }
234            self.buf[self.pos..end].copy_from_slice(data);
235        }
236        self.pos += data.len();
237        Ok(data.len())
238    }
239
240    fn tell(&self) -> i64 {
241        self.pos as i64
242    }
243
244    fn seek(&mut self, offset: i64, whence: SeekFrom) -> Result<()> {
245        let new_pos = match whence {
246            SeekFrom::Start => offset,
247            SeekFrom::Current => self.pos as i64 + offset,
248            SeekFrom::End => self.buf.len() as i64 + offset,
249        };
250        if new_pos < 0 {
251            return Err(OjphError::InvalidParam("seek before start".into()));
252        }
253        self.pos = new_pos as usize;
254        Ok(())
255    }
256
257    fn flush(&mut self) -> Result<()> {
258        Ok(())
259    }
260}
261
262// =========================================================================
263// MemInfile — memory-backed input
264// =========================================================================
265
266/// Read-only input stream over a borrowed byte slice — port of `mem_infile`.
267pub struct MemInfile<'a> {
268    data: &'a [u8],
269    pos: usize,
270}
271
272impl<'a> MemInfile<'a> {
273    /// Creates a new memory input stream over `data`.
274    pub fn new(data: &'a [u8]) -> Self {
275        Self { data, pos: 0 }
276    }
277}
278
279impl InfileBase for MemInfile<'_> {
280    fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
281        let remaining = self.data.len().saturating_sub(self.pos);
282        let n = buf.len().min(remaining);
283        buf[..n].copy_from_slice(&self.data[self.pos..self.pos + n]);
284        self.pos += n;
285        Ok(n)
286    }
287
288    fn seek(&mut self, offset: i64, whence: SeekFrom) -> Result<()> {
289        let new_pos = match whence {
290            SeekFrom::Start => offset,
291            SeekFrom::Current => self.pos as i64 + offset,
292            SeekFrom::End => self.data.len() as i64 + offset,
293        };
294        if new_pos < 0 || new_pos as usize > self.data.len() {
295            return Err(OjphError::InvalidParam("seek out of range".into()));
296        }
297        self.pos = new_pos as usize;
298        Ok(())
299    }
300
301    fn tell(&self) -> i64 {
302        self.pos as i64
303    }
304
305    fn eof(&self) -> bool {
306        self.pos >= self.data.len()
307    }
308}