A custom OS for the xteink x4 ebook reader
1// sd card file operations
2//
3// all I/O through embedded-sdmmc AsyncVolumeManager; functions are
4// synchronous, wrapping async ops with poll_once (SPI bus is blocking
5// so every .await resolves immediately)
6//
7// returns the unified Error type (re-exported as StorageError for
8// backward compat); apps receive it through KernelHandle
9
10use core::ops::ControlFlow;
11
12use embedded_sdmmc::{Mode, RawFile};
13
14use crate::drivers::sdcard::{SdStorage, SdStorageInner, poll_once};
15use crate::error::{Error, ErrorKind};
16
17pub const PLUMP_DIR: &str = "_PLUMP";
18pub const TITLES_FILE: &str = "TITLES.BIN";
19pub const TITLE_CAP: usize = 64;
20
21// backward-compatible alias
22pub type StorageError = Error;
23
24#[derive(Clone, Copy)]
25pub struct DirEntry {
26 pub name: [u8; 13],
27 pub name_len: u8,
28 pub is_dir: bool,
29 pub size: u32,
30 pub title: [u8; TITLE_CAP],
31 pub title_len: u8,
32}
33
34impl DirEntry {
35 pub const EMPTY: Self = Self {
36 name: [0u8; 13],
37 name_len: 0,
38 is_dir: false,
39 size: 0,
40 title: [0u8; TITLE_CAP],
41 title_len: 0,
42 };
43
44 pub fn name_str(&self) -> &str {
45 core::str::from_utf8(&self.name[..self.name_len as usize]).unwrap_or("?")
46 }
47
48 pub fn display_name(&self) -> &str {
49 let len = (self.title_len & 0x7F) as usize;
50 if len > 0 {
51 core::str::from_utf8(&self.title[..len]).unwrap_or(self.name_str())
52 } else {
53 self.name_str()
54 }
55 }
56
57 pub fn has_real_title(&self) -> bool {
58 self.title_len > 0 && self.title_len & 0x80 == 0
59 }
60
61 pub fn set_title(&mut self, s: &[u8]) {
62 let n = s.len().min(TITLE_CAP);
63 self.title[..n].copy_from_slice(&s[..n]);
64 self.title_len = n as u8;
65 }
66
67 // write a humanized SFN into the title buffer as a soft fallback;
68 // does not prevent the title scanner from resolving a real title
69 pub fn humanize_sfn(&mut self) {
70 let nlen = self.name_len as usize;
71 if nlen == 0 || self.has_real_title() {
72 return;
73 }
74 let src = &self.name[..nlen];
75 // check if name is all-uppercase (typical 8.3 SFN)
76 let all_upper = src.iter().all(|&b| !b.is_ascii_lowercase());
77 if !all_upper {
78 return; // mixed case: user-supplied LFN, leave as-is
79 }
80 let n = nlen.min(TITLE_CAP);
81 let dot_pos = src.iter().position(|&b| b == b'.').unwrap_or(n);
82 for i in 0..n {
83 if i == 0 {
84 self.title[i] = src[i]; // keep first char uppercase
85 } else if i > dot_pos {
86 self.title[i] = src[i].to_ascii_lowercase(); // lowercase ext
87 } else {
88 self.title[i] = src[i].to_ascii_lowercase();
89 }
90 }
91 self.title_len = 0x80 | n as u8;
92 }
93}
94
95pub struct DirPage {
96 pub total: usize,
97 pub count: usize,
98}
99
100fn ext_eq(name: &[u8], target: &[u8]) -> bool {
101 let dot = match name.iter().rposition(|&b| b == b'.') {
102 Some(p) => p,
103 None => return false,
104 };
105 let ext = &name[dot + 1..];
106 ext.len() == target.len() && ext.eq_ignore_ascii_case(target)
107}
108
109fn has_supported_ext(name: &[u8]) -> bool {
110 ext_eq(name, b"TXT") || ext_eq(name, b"EPUB") || ext_eq(name, b"EPU") || ext_eq(name, b"MD")
111}
112
113// build "NAME.EXT" bytes from a ShortFileName
114
115fn sfn_to_bytes(name: &embedded_sdmmc::ShortFileName, out: &mut [u8; 13]) -> u8 {
116 let base = name.base_name();
117 let ext = name.extension();
118 let mut pos = 0usize;
119 let blen = base.len().min(8);
120 out[..blen].copy_from_slice(&base[..blen]);
121 pos += blen;
122 if !ext.is_empty() {
123 out[pos] = b'.';
124 pos += 1;
125 let elen = ext.len().min(3);
126 out[pos..pos + elen].copy_from_slice(&ext[..elen]);
127 pos += elen;
128 }
129 pos as u8
130}
131
132// file-operation macros; each evaluates to Result<T, Error>
133// none use ? internally so caller cleanup is never bypassed
134
135macro_rules! op_file_size {
136 ($inner:expr, $dir:expr, $name:expr) => {
137 $inner
138 .mgr
139 .find_directory_entry($dir, $name)
140 .await
141 .map(|e| e.size)
142 .map_err(|_| Error::new(ErrorKind::OpenFile, "file_size"))
143 };
144}
145
146macro_rules! op_read_chunk {
147 ($inner:expr, $dir:expr, $name:expr, $offset:expr, $buf:expr) => {
148 match $inner
149 .mgr
150 .open_file_in_dir($dir, $name, Mode::ReadOnly)
151 .await
152 {
153 Err(_) => Err(Error::new(ErrorKind::OpenFile, "read_chunk")),
154 Ok(file) => {
155 let result = match $inner.mgr.file_seek_from_start(file, $offset) {
156 Ok(()) => $inner
157 .mgr
158 .read(file, $buf)
159 .await
160 .map_err(|_| Error::new(ErrorKind::ReadFailed, "read_chunk")),
161 Err(_) => Err(Error::new(ErrorKind::SeekFailed, "read_chunk")),
162 };
163 let _ = $inner.mgr.close_file(file).await;
164 if let Ok(n) = &result {
165 $crate::perf::counters::inc_sd_reads();
166 $crate::perf::counters::add_sd_bytes_read(*n as u32);
167 }
168 result
169 }
170 }
171 };
172}
173
174macro_rules! op_read_start {
175 ($inner:expr, $dir:expr, $name:expr, $buf:expr) => {
176 match $inner
177 .mgr
178 .open_file_in_dir($dir, $name, Mode::ReadOnly)
179 .await
180 {
181 Err(_) => Err(Error::new(ErrorKind::OpenFile, "read_start")),
182 Ok(file) => {
183 let size = $inner.mgr.file_length(file).unwrap_or(0);
184 let result = $inner
185 .mgr
186 .read(file, $buf)
187 .await
188 .map_err(|_| Error::new(ErrorKind::ReadFailed, "read_start"));
189 let _ = $inner.mgr.close_file(file).await;
190 let mapped = result.map(|n| {
191 $crate::perf::counters::inc_sd_reads();
192 $crate::perf::counters::add_sd_bytes_read(n as u32);
193 (size, n)
194 });
195 mapped
196 }
197 }
198 };
199}
200
201macro_rules! op_write {
202 ($inner:expr, $dir:expr, $name:expr, $data:expr) => {
203 match $inner
204 .mgr
205 .open_file_in_dir($dir, $name, Mode::ReadWriteCreateOrTruncate)
206 .await
207 {
208 Err(_) => Err(Error::new(ErrorKind::OpenFile, "write")),
209 Ok(file) => {
210 let data_ref = $data;
211 let result = if data_ref.is_empty() {
212 Ok(())
213 } else {
214 $inner
215 .mgr
216 .write(file, data_ref)
217 .await
218 .map_err(|_| Error::new(ErrorKind::WriteFailed, "write"))
219 };
220 let _ = $inner.mgr.close_file(file).await;
221 if result.is_ok() {
222 $crate::perf::counters::inc_sd_writes();
223 $crate::perf::counters::add_sd_bytes_written(data_ref.len() as u32);
224 }
225 result
226 }
227 }
228 };
229}
230
231macro_rules! op_append {
232 ($inner:expr, $dir:expr, $name:expr, $data:expr) => {
233 match $inner
234 .mgr
235 .open_file_in_dir($dir, $name, Mode::ReadWriteCreateOrAppend)
236 .await
237 {
238 Err(_) => Err(Error::new(ErrorKind::OpenFile, "append")),
239 Ok(file) => {
240 let data_ref = $data;
241 let result = if data_ref.is_empty() {
242 Ok(())
243 } else {
244 $inner
245 .mgr
246 .write(file, data_ref)
247 .await
248 .map_err(|_| Error::new(ErrorKind::WriteFailed, "append"))
249 };
250 let _ = $inner.mgr.close_file(file).await;
251 if result.is_ok() {
252 $crate::perf::counters::inc_sd_writes();
253 $crate::perf::counters::add_sd_bytes_written(data_ref.len() as u32);
254 }
255 result
256 }
257 }
258 };
259}
260
261macro_rules! op_delete {
262 ($inner:expr, $dir:expr, $name:expr) => {{
263 $inner
264 .mgr
265 .delete_entry_in_dir($dir, $name)
266 .await
267 .map_err(|_| Error::new(ErrorKind::DeleteFailed, "delete"))
268 }};
269}
270
271// dir-scoping macros; open subdir, execute body, close handle
272
273macro_rules! in_dir {
274 ($inner:expr, $dirname:expr, |$dir:ident| $body:expr) => {
275 match $inner.mgr.open_dir($inner.root, $dirname).await {
276 Err(_) => Err(Error::new(ErrorKind::OpenDir, "in_dir")),
277 Ok($dir) => {
278 let _r = $body;
279 let _ = $inner.mgr.close_dir($dir);
280 _r
281 }
282 }
283 };
284}
285
286macro_rules! in_subdir {
287 ($inner:expr, $d1:expr, $d2:expr, |$dir:ident| $body:expr) => {
288 match $inner.mgr.open_dir($inner.root, $d1).await {
289 Err(_) => Err(Error::new(ErrorKind::OpenDir, "in_subdir")),
290 Ok(_mid) => match $inner.mgr.open_dir(_mid, $d2).await {
291 Err(_) => {
292 let _ = $inner.mgr.close_dir(_mid);
293 Err(Error::new(ErrorKind::OpenDir, "in_subdir"))
294 }
295 Ok($dir) => {
296 let _r = $body;
297 let _ = $inner.mgr.close_dir($dir);
298 let _ = $inner.mgr.close_dir(_mid);
299 _r
300 }
301 },
302 }
303 };
304}
305
306fn borrow(sd: &SdStorage) -> core::result::Result<core::cell::RefMut<'_, SdStorageInner>, Error> {
307 sd.borrow_inner()
308 .ok_or(Error::new(ErrorKind::NoCard, "storage::borrow"))
309}
310
311// streaming file handle — keeps one file open across multiple writes
312//
313// must be closed via close(); dropping without closing leaks the
314// handle in the volume manager (it will refuse to open the file again).
315// debug builds panic on leak; release builds log an error.
316
317/// Handle to an open file on the SD card.
318///
319/// Created via [`SdStorage::create_file`]. Must be consumed via
320/// [`close()`](OpenFile::close) — dropping without closing leaks the
321/// handle inside the volume manager.
322pub struct OpenFile {
323 raw: Option<RawFile>,
324}
325
326impl OpenFile {
327 /// Write a chunk of data to the open file.
328 pub fn write(&self, sd: &SdStorage, data: &[u8]) -> crate::error::Result<()> {
329 if data.is_empty() {
330 return Ok(());
331 }
332 let raw = self.raw.expect("OpenFile::write after close");
333 poll_once(async {
334 let mut guard = borrow(sd)?;
335 guard
336 .mgr
337 .write(raw, data)
338 .await
339 .map_err(|_| Error::new(ErrorKind::WriteFailed, "OpenFile::write"))?;
340 crate::perf::counters::inc_sd_writes();
341 crate::perf::counters::add_sd_bytes_written(data.len() as u32);
342 Ok(())
343 })
344 }
345
346 /// Close the file, flushing metadata to SD. Consumes self.
347 pub fn close(mut self, sd: &SdStorage) -> crate::error::Result<()> {
348 let raw = self.raw.take().expect("OpenFile::close called twice");
349 poll_once(async {
350 let mut guard = borrow(sd)?;
351 guard
352 .mgr
353 .close_file(raw)
354 .await
355 .map_err(|_| Error::new(ErrorKind::WriteFailed, "OpenFile::close"))
356 })
357 }
358}
359
360impl Drop for OpenFile {
361 fn drop(&mut self) {
362 if self.raw.is_some() {
363 // file handle leaked — volume manager still thinks it is open
364 log::error!("OpenFile dropped without close()! Handle leaked.");
365 debug_assert!(false, "OpenFile dropped without close()");
366 }
367 }
368}
369
370impl SdStorage {
371 /// Create (or truncate) a file in the root directory and return
372 /// an [`OpenFile`] handle for streaming writes.
373 pub fn create_file(&self, name: &str) -> crate::error::Result<OpenFile> {
374 poll_once(async {
375 let mut guard = borrow(self)?;
376 let inner = &mut *guard;
377 let raw = inner
378 .mgr
379 .open_file_in_dir(inner.root, name, Mode::ReadWriteCreateOrTruncate)
380 .await
381 .map_err(|_| Error::new(ErrorKind::OpenFile, "SdStorage::create_file"))?;
382 Ok(OpenFile { raw: Some(raw) })
383 })
384 }
385
386 /// Write an entire file atomically (create/truncate + write + close).
387 pub fn write_file(&self, name: &str, data: &[u8]) -> crate::error::Result<()> {
388 poll_once(async {
389 let mut guard = borrow(self)?;
390 let inner = &mut *guard;
391 op_write!(inner, inner.root, name, data)
392 })
393 }
394
395 /// Append data to an existing file (open + seek-to-end + write + close).
396 pub fn append_root_file(&self, name: &str, data: &[u8]) -> crate::error::Result<()> {
397 poll_once(async {
398 let mut guard = borrow(self)?;
399 let inner = &mut *guard;
400 op_append!(inner, inner.root, name, data)
401 })
402 }
403
404 /// Delete a file from the root directory.
405 pub fn delete_file(&self, name: &str) -> crate::error::Result<()> {
406 poll_once(async {
407 let mut guard = borrow(self)?;
408 let inner = &mut *guard;
409 op_delete!(inner, inner.root, name)
410 })
411 }
412
413 /// List supported files in the root directory.
414 pub fn list_root_files(&self, buf: &mut [DirEntry]) -> crate::error::Result<usize> {
415 poll_once(async {
416 let mut guard = borrow(self)?;
417 let inner = &mut *guard;
418
419 let mut count = 0usize;
420 let mut total = 0usize;
421
422 inner
423 .mgr
424 .iterate_dir(inner.root, |entry| {
425 if entry.attributes.is_volume() || entry.attributes.is_directory() {
426 return ControlFlow::Continue(());
427 }
428
429 let mut name_buf = [0u8; 13];
430 let name_len = sfn_to_bytes(&entry.name, &mut name_buf);
431 let sfn = &name_buf[..name_len as usize];
432
433 if sfn.is_empty() || sfn[0] == b'.' || sfn[0] == b'_' {
434 return ControlFlow::Continue(());
435 }
436 if !has_supported_ext(sfn) {
437 return ControlFlow::Continue(());
438 }
439
440 total += 1;
441
442 if count < buf.len() {
443 buf[count] = DirEntry {
444 name: name_buf,
445 name_len,
446 is_dir: false,
447 size: entry.size,
448 title: [0u8; TITLE_CAP],
449 title_len: 0,
450 };
451 count += 1;
452 }
453 ControlFlow::Continue(())
454 })
455 .await
456 .map_err(|_| Error::new(ErrorKind::ReadFailed, "list_root_files"))?;
457
458 if total > count {
459 log::warn!(
460 "dir: {} supported files on SD, only {} fit in buffer (max {})",
461 total,
462 count,
463 buf.len(),
464 );
465 }
466 Ok(count)
467 })
468 }
469
470 // root file reads
471
472 /// Get the size of a file in the root directory.
473 pub fn file_size(&self, name: &str) -> crate::error::Result<u32> {
474 poll_once(async {
475 let mut guard = borrow(self)?;
476 let inner = &mut *guard;
477 op_file_size!(inner, inner.root, name)
478 })
479 }
480
481 /// Read a chunk from a file in the root directory at the given offset.
482 pub fn read_file_chunk(
483 &self,
484 name: &str,
485 offset: u32,
486 buf: &mut [u8],
487 ) -> crate::error::Result<usize> {
488 poll_once(async {
489 let mut guard = borrow(self)?;
490 let inner = &mut *guard;
491 op_read_chunk!(inner, inner.root, name, offset, buf)
492 })
493 }
494
495 /// Read from the start of a file in the root directory.
496 /// Returns (file_size, bytes_read).
497 pub fn read_file_start(
498 &self,
499 name: &str,
500 buf: &mut [u8],
501 ) -> crate::error::Result<(u32, usize)> {
502 poll_once(async {
503 let mut guard = borrow(self)?;
504 let inner = &mut *guard;
505 op_read_start!(inner, inner.root, name, buf)
506 })
507 }
508
509 // named-directory file operations
510
511 /// Write a file in a named subdirectory of root.
512 pub fn write_file_in_dir(
513 &self,
514 dir: &str,
515 name: &str,
516 data: &[u8],
517 ) -> crate::error::Result<()> {
518 poll_once(async {
519 let mut guard = borrow(self)?;
520 let inner = &mut *guard;
521 in_dir!(inner, dir, |dir_h| op_write!(inner, dir_h, name, data))
522 })
523 }
524
525 /// Read from the start of a file in a named subdirectory of root.
526 /// Returns (file_size, bytes_read).
527 pub fn read_file_start_in_dir(
528 &self,
529 dir: &str,
530 name: &str,
531 buf: &mut [u8],
532 ) -> crate::error::Result<(u32, usize)> {
533 poll_once(async {
534 let mut guard = borrow(self)?;
535 let inner = &mut *guard;
536 in_dir!(inner, dir, |dir_h| op_read_start!(inner, dir_h, name, buf))
537 })
538 }
539
540 // _PLUMP/ directory management
541
542 /// Ensure the _PLUMP directory exists (async, for boot path).
543 pub async fn ensure_plump_dir_async(&self) -> crate::error::Result<()> {
544 let mut guard = borrow(self)?;
545 let inner = &mut *guard;
546
547 if let Ok(dir) = inner.mgr.open_dir(inner.root, PLUMP_DIR).await {
548 let _ = inner.mgr.close_dir(dir);
549 return Ok(());
550 }
551 match inner.mgr.make_dir_in_dir(inner.root, PLUMP_DIR).await {
552 Ok(()) => Ok(()),
553 Err(embedded_sdmmc::Error::DirAlreadyExists) => Ok(()),
554 Err(_) => Err(Error::new(ErrorKind::WriteFailed, "ensure_plump_dir_async")),
555 }
556 }
557
558 /// Ensure a subdirectory exists under _PLUMP/.
559 pub fn ensure_plump_subdir(&self, name: &str) -> crate::error::Result<()> {
560 let exists = poll_once(async {
561 let mut guard = borrow(self)?;
562 let inner = &mut *guard;
563 in_dir!(inner, PLUMP_DIR, |plump_h| {
564 match inner.mgr.open_dir(plump_h, name).await {
565 Ok(sub) => {
566 let _ = inner.mgr.close_dir(sub);
567 Ok::<_, Error>(true)
568 }
569 Err(_) => Ok(false),
570 }
571 })
572 })?;
573
574 if exists {
575 return Ok(());
576 }
577
578 poll_once(async {
579 let mut guard = borrow(self)?;
580 let inner = &mut *guard;
581 in_dir!(inner, PLUMP_DIR, |plump_h| {
582 match inner.mgr.make_dir_in_dir(plump_h, name).await {
583 Ok(()) => Ok::<_, Error>(()),
584 Err(embedded_sdmmc::Error::DirAlreadyExists) => Ok(()),
585 Err(_) => Err(Error::new(ErrorKind::WriteFailed, "ensure_plump_subdir")),
586 }
587 })
588 })
589 }
590
591 // _PLUMP/ direct file operations (cache files live directly in _PLUMP/)
592
593 /// Read a chunk from a file in _PLUMP/.
594 pub fn read_chunk_in_plump(
595 &self,
596 name: &str,
597 offset: u32,
598 buf: &mut [u8],
599 ) -> crate::error::Result<usize> {
600 poll_once(async {
601 let mut guard = borrow(self)?;
602 let inner = &mut *guard;
603 in_dir!(inner, PLUMP_DIR, |dir_h| op_read_chunk!(
604 inner, dir_h, name, offset, buf
605 ))
606 })
607 }
608
609 /// Write (create/truncate) a file in _PLUMP/.
610 pub fn write_in_plump(&self, name: &str, data: &[u8]) -> crate::error::Result<()> {
611 poll_once(async {
612 let mut guard = borrow(self)?;
613 let inner = &mut *guard;
614 in_dir!(inner, PLUMP_DIR, |dir_h| op_write!(inner, dir_h, name, data))
615 })
616 }
617
618 /// Append data to a file in _PLUMP/.
619 pub fn append_in_plump(&self, name: &str, data: &[u8]) -> crate::error::Result<()> {
620 poll_once(async {
621 let mut guard = borrow(self)?;
622 let inner = &mut *guard;
623 in_dir!(inner, PLUMP_DIR, |dir_h| op_append!(
624 inner, dir_h, name, data
625 ))
626 })
627 }
628
629 /// Get the size of a file in _PLUMP/.
630 pub fn file_size_in_plump(&self, name: &str) -> crate::error::Result<u32> {
631 poll_once(async {
632 let mut guard = borrow(self)?;
633 let inner = &mut *guard;
634 in_dir!(inner, PLUMP_DIR, |dir_h| op_file_size!(inner, dir_h, name))
635 })
636 }
637
638 /// Delete a file in _PLUMP/.
639 pub fn delete_in_plump(&self, name: &str) -> crate::error::Result<()> {
640 poll_once(async {
641 let mut guard = borrow(self)?;
642 let inner = &mut *guard;
643 in_dir!(inner, PLUMP_DIR, |dir_h| op_delete!(inner, dir_h, name))
644 })
645 }
646
647 /// Seek to offset and write data in a file in _PLUMP/.
648 /// Used to update the chapter offset table after all chapters are appended.
649 pub fn write_at_in_plump(
650 &self,
651 name: &str,
652 offset: u32,
653 data: &[u8],
654 ) -> crate::error::Result<()> {
655 poll_once(async {
656 let mut guard = borrow(self)?;
657 let inner = &mut *guard;
658 in_dir!(inner, PLUMP_DIR, |dir_h| {
659 match inner
660 .mgr
661 .open_file_in_dir(dir_h, name, Mode::ReadWriteCreateOrAppend)
662 .await
663 {
664 Err(_) => Err(Error::new(ErrorKind::OpenFile, "write_at")),
665 Ok(file) => {
666 let result = match inner.mgr.file_seek_from_start(file, offset) {
667 Ok(()) => inner
668 .mgr
669 .write(file, data)
670 .await
671 .map_err(|_| Error::new(ErrorKind::WriteFailed, "write_at")),
672 Err(_) => Err(Error::new(ErrorKind::SeekFailed, "write_at")),
673 };
674 let _ = inner.mgr.close_file(file).await;
675 if result.is_ok() {
676 crate::perf::counters::inc_sd_writes();
677 crate::perf::counters::add_sd_bytes_written(data.len() as u32);
678 }
679 result
680 }
681 }
682 })
683 })
684 }
685
686 // _PLUMP subdirectory file operations
687
688 /// Write (create/truncate) a file in _PLUMP/<dir>/.
689 pub fn write_in_plump_subdir(
690 &self,
691 dir: &str,
692 name: &str,
693 data: &[u8],
694 ) -> crate::error::Result<()> {
695 poll_once(async {
696 let mut guard = borrow(self)?;
697 let inner = &mut *guard;
698 in_subdir!(inner, PLUMP_DIR, dir, |sub_h| op_write!(
699 inner, sub_h, name, data
700 ))
701 })
702 }
703
704 /// Append data to a file in _PLUMP/<dir>/.
705 pub fn append_in_plump_subdir(
706 &self,
707 dir: &str,
708 name: &str,
709 data: &[u8],
710 ) -> crate::error::Result<()> {
711 poll_once(async {
712 let mut guard = borrow(self)?;
713 let inner = &mut *guard;
714 in_subdir!(inner, PLUMP_DIR, dir, |sub_h| op_append!(
715 inner, sub_h, name, data
716 ))
717 })
718 }
719
720 /// Read a chunk from a file in _PLUMP/<dir>/.
721 pub fn read_chunk_in_plump_subdir(
722 &self,
723 dir: &str,
724 name: &str,
725 offset: u32,
726 buf: &mut [u8],
727 ) -> crate::error::Result<usize> {
728 poll_once(async {
729 let mut guard = borrow(self)?;
730 let inner = &mut *guard;
731 in_subdir!(inner, PLUMP_DIR, dir, |sub_h| op_read_chunk!(
732 inner, sub_h, name, offset, buf
733 ))
734 })
735 }
736
737 /// Get the size of a file in _PLUMP/<dir>/.
738 pub fn file_size_in_plump_subdir(
739 &self,
740 dir: &str,
741 name: &str,
742 ) -> crate::error::Result<u32> {
743 poll_once(async {
744 let mut guard = borrow(self)?;
745 let inner = &mut *guard;
746 in_subdir!(inner, PLUMP_DIR, dir, |sub_h| op_file_size!(
747 inner, sub_h, name
748 ))
749 })
750 }
751
752 /// Delete a file in _PLUMP/<dir>/.
753 pub fn delete_in_plump_subdir(
754 &self,
755 dir: &str,
756 name: &str,
757 ) -> crate::error::Result<()> {
758 poll_once(async {
759 let mut guard = borrow(self)?;
760 let inner = &mut *guard;
761 in_subdir!(inner, PLUMP_DIR, dir, |sub_h| op_delete!(inner, sub_h, name))
762 })
763 }
764
765 // title mapping
766
767 /// Append a title line to _PLUMP/TITLES.BIN.
768 pub fn save_title(&self, filename: &str, title: &str) -> crate::error::Result<()> {
769 let name_bytes = filename.as_bytes();
770 let title_bytes = title.as_bytes();
771 let title_len = title_bytes.len().min(TITLE_CAP);
772 let line_len = name_bytes.len() + 1 + title_len + 1;
773 if line_len > 128 {
774 return Err(Error::new(
775 ErrorKind::WriteFailed,
776 "save_title: line too long",
777 ));
778 }
779 let mut line = [0u8; 128];
780 line[..name_bytes.len()].copy_from_slice(name_bytes);
781 line[name_bytes.len()] = b'\t';
782 line[name_bytes.len() + 1..name_bytes.len() + 1 + title_len]
783 .copy_from_slice(&title_bytes[..title_len]);
784 line[name_bytes.len() + 1 + title_len] = b'\n';
785
786 self.append_in_plump(TITLES_FILE, &line[..line_len])
787 }
788}
789