Skip to content

Commit d62effb

Browse files
committed
Add support for opening existing files using LFNs.
Opening existing directories can be added later. Notably this does not require holding the entire LFN on the stack at once - we match the UTF-8 string slice we're given against the USC-2 words we pull off the disk, in reverse order. The shell example now lets you `cat` and `hexdump` files with long names.
1 parent 7324d9a commit d62effb

File tree

5 files changed

+344
-2
lines changed

5 files changed

+344
-2
lines changed

examples/shell.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,7 @@ impl Context {
340340
/// print a text file
341341
fn cat(&self, filename: &Path) -> Result<(), Error> {
342342
let (dir, filename) = self.resolve_filename(filename)?;
343-
let f = dir.open_file_in_dir(filename, Mode::ReadOnly)?;
343+
let f = dir.open_long_name_file_in_dir(filename, Mode::ReadOnly)?;
344344
let mut data = Vec::new();
345345
while !f.is_eof() {
346346
let mut buffer = vec![0u8; 65536];
@@ -360,7 +360,7 @@ impl Context {
360360
/// print a binary file
361361
fn hexdump(&self, filename: &Path) -> Result<(), Error> {
362362
let (dir, filename) = self.resolve_filename(filename)?;
363-
let f = dir.open_file_in_dir(filename, Mode::ReadOnly)?;
363+
let f = dir.open_long_name_file_in_dir(filename, Mode::ReadOnly)?;
364364
let mut data = Vec::new();
365365
while !f.is_eof() {
366366
let mut buffer = vec![0u8; 65536];

src/fat/volume.rs

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -797,6 +797,146 @@ impl FatVolume {
797797
result
798798
}
799799

800+
/// Get an entry from the given directory
801+
pub(crate) fn find_directory_entry_by_lfn<D>(
802+
&self,
803+
block_cache: &mut BlockCache<D>,
804+
dir_info: &DirectoryInfo,
805+
match_name: &str,
806+
) -> Result<DirEntry, Error<D::Error>>
807+
where
808+
D: BlockDevice,
809+
{
810+
let mut result = Err(Error::NotFound);
811+
enum SeqState<'a> {
812+
/// Looking for the first entry in an LFN sequence
813+
Waiting,
814+
/// Scanning through an LFN sequence
815+
Scanning {
816+
remaining: &'a str,
817+
sequence: u8,
818+
csum: u8,
819+
},
820+
/// Found an entry we like
821+
Found { csum: u8 },
822+
}
823+
824+
let mut state = SeqState::Waiting;
825+
self.iterate_dir_internal(block_cache, dir_info, |de, odde| {
826+
match state {
827+
SeqState::Waiting => {
828+
debug!("Am waiting for LFN start");
829+
let mut remaining = match_name;
830+
if let Some((true, sequence, csum, buffer)) = odde.lfn_contents() {
831+
debug!("{:02x} {:02x} {:04x?}", sequence, csum, buffer);
832+
// trim padding and NUL words off the end of the file name (which is the part that comes first)
833+
for word in buffer
834+
.iter()
835+
.rev()
836+
.skip_while(|b| **b == 0xFFFF)
837+
.skip_while(|b| **b == 0x0000)
838+
{
839+
debug!("Looking at word {:04x}", *word);
840+
// UCS-2 16-bit values are valid Unicode code points - we do not expect surrogate pairs but if we find then, we give up.
841+
let Some(c) = char::from_u32(*word as u32) else {
842+
return Continue::Yes;
843+
};
844+
debug!("Looking at char '{}'", c);
845+
let Some(r) = remaining.strip_suffix(c) else {
846+
debug!("No, didn't want that");
847+
return Continue::Yes;
848+
};
849+
debug!("Liked it! {:?} is left", r);
850+
remaining = r;
851+
}
852+
if sequence == 1 {
853+
// last piece
854+
if remaining.is_empty() {
855+
// found it
856+
state = SeqState::Found { csum }
857+
} else {
858+
// no, we have characters left over
859+
state = SeqState::Waiting
860+
}
861+
} else {
862+
// keep looking
863+
state = SeqState::Scanning {
864+
remaining,
865+
sequence: sequence - 1,
866+
csum,
867+
};
868+
}
869+
}
870+
}
871+
SeqState::Scanning {
872+
remaining,
873+
sequence,
874+
csum,
875+
} => {
876+
debug!(
877+
"Am waiting for more LFN sequence={:02x}, csum={:02x}",
878+
sequence, csum
879+
);
880+
let mut remaining = remaining;
881+
if let Some((false, this_sequence, this_csum, buffer)) = odde.lfn_contents() {
882+
debug!("{:02x} {:02x} {:04x?}", sequence, csum, buffer);
883+
if (this_sequence != sequence) || (this_csum != csum) {
884+
// not what we wanted
885+
debug!(
886+
"No! Got sequence={:02x}, csum={:02x}",
887+
this_sequence, this_csum
888+
);
889+
state = SeqState::Waiting;
890+
return Continue::Yes;
891+
}
892+
for word in buffer.iter().rev() {
893+
// UCS-2 16-bit values are valid Unicode code points - we do not expect surrogate pairs but if we find then, we give up.
894+
debug!("Looking at word {:04x}", *word);
895+
let Some(c) = char::from_u32(*word as u32) else {
896+
return Continue::Yes;
897+
};
898+
debug!("Looking at char '{}'", c);
899+
let Some(r) = remaining.strip_suffix(c) else {
900+
debug!("No, didn't want that");
901+
return Continue::Yes;
902+
};
903+
debug!("Liked it! {:?} is left", r);
904+
remaining = r;
905+
}
906+
if sequence == 1 {
907+
// last piece
908+
if remaining.is_empty() {
909+
// found it
910+
state = SeqState::Found { csum }
911+
} else {
912+
// no, we have characters left over
913+
state = SeqState::Waiting
914+
}
915+
} else {
916+
// keep looking
917+
state = SeqState::Scanning {
918+
remaining,
919+
sequence: sequence - 1,
920+
csum,
921+
};
922+
}
923+
}
924+
}
925+
SeqState::Found { csum } => {
926+
let calc_csum = de.name.csum();
927+
if calc_csum == csum {
928+
result = Ok(de.clone());
929+
return Continue::No;
930+
} else {
931+
debug!("Bad csum {:02x} != {:02x}", calc_csum, csum)
932+
}
933+
}
934+
}
935+
Continue::Yes
936+
})?;
937+
result
938+
}
939+
800940
/// Delete an entry from the given directory
801941
pub(crate) fn delete_directory_entry<D>(
802942
&self,

src/filesystem/directory.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,22 @@ where
197197
Ok(f.to_file(self.volume_mgr))
198198
}
199199

200+
/// Open a file.
201+
///
202+
/// See [`VolumeManager::open_long_name_file_in_dir`] for details, except the
203+
/// directory given is this directory.
204+
pub fn open_long_name_file_in_dir(
205+
&self,
206+
name: &str,
207+
mode: crate::Mode,
208+
) -> Result<crate::File<'_, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES>, crate::Error<D::Error>>
209+
{
210+
let f = self
211+
.volume_mgr
212+
.open_long_name_file_in_dir(self.raw_directory, name, mode)?;
213+
Ok(f.to_file(self.volume_mgr))
214+
}
215+
200216
/// Delete a file/directory.
201217
///
202218
/// See [`VolumeManager::delete_entry_in_dir`] for details, except the

src/volume_mgr.rs

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -731,6 +731,158 @@ where
731731
}
732732
}
733733

734+
/// Open a file with the given Unicode long file name, in the given directory.
735+
///
736+
/// You can only open existing long-file-name files - you cannot create them.
737+
///
738+
/// <div class="warning">
739+
///
740+
/// This function gives you a [`RawFile`] and when you are finished with
741+
/// it, you **must** close the file by calling
742+
/// [`VolumeManager::close_file`] otherwise you will leak internal
743+
/// resources and/or suffer file-system corruption and data loss.
744+
///
745+
/// </div>
746+
///
747+
/// If you want a file handle that closes itself on drop, see
748+
/// [`File`](crate::File).
749+
pub fn open_long_name_file_in_dir(
750+
&self,
751+
directory: RawDirectory,
752+
name: &str,
753+
mode: Mode,
754+
) -> Result<RawFile, Error<D::Error>> {
755+
let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?;
756+
let data = data.deref_mut();
757+
758+
// This check is load-bearing - we do an unchecked push later.
759+
if data.open_files.is_full() {
760+
return Err(Error::TooManyOpenFiles);
761+
}
762+
763+
let directory_idx = data.get_dir_by_id(directory)?;
764+
let volume_id = data.open_dirs[directory_idx].raw_volume;
765+
let volume_idx = data.get_volume_by_id(volume_id)?;
766+
let volume_info = &data.open_volumes[volume_idx];
767+
768+
let dir_entry = match &volume_info.volume_type {
769+
VolumeType::Fat(fat) => fat.find_directory_entry_by_lfn(
770+
&mut data.block_cache,
771+
&data.open_dirs[directory_idx],
772+
name,
773+
),
774+
};
775+
776+
let dir_entry = match dir_entry {
777+
Ok(entry) => {
778+
// we are opening an existing file
779+
entry
780+
}
781+
Err(_)
782+
if (mode == Mode::ReadWriteCreate)
783+
| (mode == Mode::ReadWriteCreateOrTruncate)
784+
| (mode == Mode::ReadWriteCreateOrAppend) =>
785+
{
786+
// We are opening a non-existant file and we cannot do that with LFNs
787+
return Err(Error::NotFound);
788+
}
789+
_ => {
790+
// We are opening a non-existant file, and that's not OK.
791+
return Err(Error::NotFound);
792+
}
793+
};
794+
795+
// Check if it's open already
796+
if data.file_is_open(volume_info.raw_volume, &dir_entry) {
797+
return Err(Error::FileAlreadyOpen);
798+
}
799+
800+
let mode = solve_mode_variant(mode, true);
801+
802+
match mode {
803+
Mode::ReadWriteCreate => {
804+
return Err(Error::FileAlreadyExists);
805+
}
806+
_ => {
807+
if dir_entry.attributes.is_read_only() && mode != Mode::ReadOnly {
808+
return Err(Error::ReadOnly);
809+
}
810+
811+
if dir_entry.attributes.is_directory() {
812+
return Err(Error::OpenedDirAsFile);
813+
}
814+
815+
// Check it's not already open
816+
if data.file_is_open(volume_id, &dir_entry) {
817+
return Err(Error::FileAlreadyOpen);
818+
}
819+
820+
let mode = solve_mode_variant(mode, true);
821+
let raw_file = RawFile(data.id_generator.generate());
822+
823+
let file = match mode {
824+
Mode::ReadOnly => FileInfo {
825+
raw_file,
826+
raw_volume: volume_id,
827+
current_cluster: (0, dir_entry.cluster),
828+
current_offset: 0,
829+
mode,
830+
entry: dir_entry,
831+
dirty: false,
832+
},
833+
Mode::ReadWriteAppend => {
834+
let mut file = FileInfo {
835+
raw_file,
836+
raw_volume: volume_id,
837+
current_cluster: (0, dir_entry.cluster),
838+
current_offset: 0,
839+
mode,
840+
entry: dir_entry,
841+
dirty: false,
842+
};
843+
// seek_from_end with 0 can't fail
844+
file.seek_from_end(0).ok();
845+
file
846+
}
847+
Mode::ReadWriteTruncate => {
848+
let mut file = FileInfo {
849+
raw_file,
850+
raw_volume: volume_id,
851+
current_cluster: (0, dir_entry.cluster),
852+
current_offset: 0,
853+
mode,
854+
entry: dir_entry,
855+
dirty: false,
856+
};
857+
match &mut data.open_volumes[volume_idx].volume_type {
858+
VolumeType::Fat(fat) => fat.truncate_cluster_chain(
859+
&mut data.block_cache,
860+
file.entry.cluster,
861+
)?,
862+
};
863+
file.update_length(0);
864+
match &data.open_volumes[volume_idx].volume_type {
865+
VolumeType::Fat(fat) => {
866+
file.entry.mtime = self.time_source.get_timestamp();
867+
fat.write_entry_to_disk(&mut data.block_cache, &file.entry)?;
868+
}
869+
};
870+
871+
file
872+
}
873+
_ => return Err(Error::Unsupported),
874+
};
875+
876+
// Remember this open file - can't be full as we checked already
877+
unsafe {
878+
data.open_files.push_unchecked(file);
879+
}
880+
881+
Ok(raw_file)
882+
}
883+
}
884+
}
885+
734886
/// Delete a closed file or empty directory with the given filename, if it exists.
735887
pub fn delete_entry_in_dir<N>(
736888
&self,

tests/open_files.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,40 @@ fn open_files() {
9191
volume_mgr.close_volume(volume).expect("close volume");
9292
}
9393

94+
#[test]
95+
fn open_lfn() {
96+
let time_source = utils::make_time_source();
97+
let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap();
98+
let volume_mgr: VolumeManager<utils::RamDisk<Vec<u8>>, utils::TestTimeSource, 4, 2, 1> =
99+
VolumeManager::new_with_limits(disk, time_source, 0xAA00_0000);
100+
let volume = volume_mgr.open_volume(VolumeIdx(1)).expect("open volume");
101+
let root_dir = volume.open_root_dir().expect("open root dir");
102+
let _f = root_dir
103+
.open_long_name_file_in_dir("Copy of Readme.txt", Mode::ReadOnly)
104+
.expect("open file");
105+
106+
assert!(matches!(
107+
root_dir.open_long_name_file_in_dir("Copy of Readme.tx", Mode::ReadOnly),
108+
Err(embedded_sdmmc::Error::NotFound)
109+
));
110+
assert!(matches!(
111+
root_dir.open_long_name_file_in_dir("opy of Readme.txt", Mode::ReadOnly),
112+
Err(embedded_sdmmc::Error::NotFound)
113+
));
114+
assert!(matches!(
115+
root_dir.open_long_name_file_in_dir("Copyof Readme.txt", Mode::ReadOnly),
116+
Err(embedded_sdmmc::Error::NotFound)
117+
));
118+
assert!(matches!(
119+
root_dir.open_long_name_file_in_dir("Copy_of Readme.txt", Mode::ReadOnly),
120+
Err(embedded_sdmmc::Error::NotFound)
121+
));
122+
assert!(matches!(
123+
root_dir.open_long_name_file_in_dir("Nonsense", Mode::ReadOnly),
124+
Err(embedded_sdmmc::Error::NotFound)
125+
));
126+
}
127+
94128
#[test]
95129
fn open_non_raw() {
96130
let time_source = utils::make_time_source();

0 commit comments

Comments
 (0)