1 /*
2  * Copyright (c) 2015 Steve White
3  * Copyright (c) 2022 Travis Geiselbrecht
4  *
5  * Use of this source code is governed by a MIT-style
6  * license that can be found in the LICENSE file or at
7  * https://opensource.org/licenses/MIT
8  */
9 #include "dir.h"
10 
11 #include <lk/cpp.h>
12 #include <lk/err.h>
13 #include <lk/trace.h>
14 #include <endian.h>
15 #include <stdint.h>
16 #include <stdlib.h>
17 #include <stdio.h>
18 #include <string.h>
19 
20 #include "fat_fs.h"
21 #include "fat_priv.h"
22 #include "file_iterator.h"
23 
24 #define LOCAL_TRACE FAT_GLOBAL_TRACE(0)
25 
26 fat_dir::~fat_dir() = default;
27 
28 // structure that represents an open dir handle. holds the offset into the directory
29 // that is being walked.
30 struct fat_dir_cookie {
31     fat_dir *dir;
32 
33     struct list_node node;
34 
35     // next directory index offset in bytes, 0xffffffff for EOD
36     uint32_t index;
37     static const uint32_t index_eod = 0xffffffff;
38 };
39 
40 namespace {
41 
42 // walk one entry into the dir, starting at byte offset into the directory block iterator.
43 // both dbi and offset will be modified during the call.
44 // filles out the entry and returns a pointer into the passed in buffer in out_filename.
45 // NOTE: *must* pass at least a MAX_FILE_NAME_LEN byte char pointer in the filename_buffer slot.
fat_find_next_entry(fat_fs * fat,file_block_iterator & dbi,uint32_t & offset,dir_entry * entry,char filename_buffer[MAX_FILE_NAME_LEN],char ** out_filename)46 status_t fat_find_next_entry(fat_fs *fat, file_block_iterator &dbi, uint32_t &offset, dir_entry *entry,
47         char filename_buffer[MAX_FILE_NAME_LEN], char **out_filename) {
48 
49     DEBUG_ASSERT(entry && filename_buffer && out_filename);
50     DEBUG_ASSERT(offset <= fat->info().bytes_per_sector); // passing offset == bytes_per_sector is okay
51 
52     // lfn parsing state
53     struct lfn_parse_state {
54         size_t  pos = 0;
55         uint8_t last_sequence = 0xff; // 0xff means we haven't seen anything
56                                       // since last reset
57         uint8_t checksum = 0;
58 
59         void reset() {
60             last_sequence = 0xff;
61         }
62     } lfn_state;
63 
64     for (;;) {
65         if (LOCAL_TRACE >= 2) {
66             LTRACEF("dir sector:\n");
67             hexdump8_ex(dbi.get_bcache_ptr(0), fat->info().bytes_per_sector, 0);
68         }
69 
70         // walk within a sector
71         while (offset < fat->info().bytes_per_sector) {
72             LTRACEF_LEVEL(2, "looking at offset %u\n", offset);
73             const uint8_t *ent = dbi.get_bcache_ptr(offset);
74             if (ent[0] == 0) { // no more entries
75                 // we're completely done
76                 LTRACEF("completely done\n");
77                 return ERR_NOT_FOUND;
78             } else if (ent[0] == 0xE5) { // deleted entry
79                 LTRACEF("deleted entry\n");
80                 lfn_state.reset();
81                 offset += DIR_ENTRY_LENGTH;
82                 continue;
83             } else if (ent[0x0B] == (uint8_t)fat_attribute::volume_id) {
84                 // skip volume ids
85                 LTRACEF("skipping volume id\n");
86                 lfn_state.reset();
87                 offset += DIR_ENTRY_LENGTH;
88                 continue;
89             } else if (ent[0x0B] == (uint8_t)fat_attribute::lfn) {
90                 // part of a LFN sequence
91                 uint8_t sequence = ent[0] & ~0x40;
92                 if (ent[0] & 0x40) {
93                     // end sequence, start a new backwards fill of the lfn name
94                     lfn_state.pos = MAX_FILE_NAME_LEN;
95                     filename_buffer[--lfn_state.pos] = 0;
96                     lfn_state.last_sequence = sequence;
97                     lfn_state.checksum = ent[0x0d];
98                     LTRACEF_LEVEL(2, "start of new LFN entry, sequence %u\n", sequence);
99                 } else {
100                     if (lfn_state.last_sequence != sequence + 1) {
101                         // our entry is out of sequence? drop it and start over
102                         LTRACEF("ent out of sequence %u (last sequence %u)\n", sequence, lfn_state.last_sequence);
103                         lfn_state.reset();
104                         offset += DIR_ENTRY_LENGTH;
105                         continue;
106                     }
107                     if (lfn_state.checksum != ent[0x0d]) {
108                         // all of the long sequences need to match the checksum
109                         LTRACEF("ent mismatches previous checksum\n");
110                         lfn_state.reset();
111                         offset += DIR_ENTRY_LENGTH;
112                         continue;
113                     }
114                     lfn_state.last_sequence = sequence;
115                 }
116 
117                 // walk backwards through the entry, picking out unicode characters
118                 // table of unicode character offsets:
119                 const size_t table[] = { 30, 28, 24, 22, 20, 18, 16, 14, 9, 7, 5, 3, 1 };
120                 for (auto off : table) {
121                     uint16_t c = fat_read16(ent, off);
122                     if (c != 0xffff && c != 0x0) {
123                         // TODO: properly deal with unicode -> utf8
124                         filename_buffer[--lfn_state.pos] = c & 0xff;
125                     }
126                 }
127 
128                 LTRACEF_LEVEL(2, "lfn filename thus far: '%s' sequence %hhu\n", filename_buffer + lfn_state.pos, sequence);
129 
130                 // iterate one more entry, since we need to at least need to find the corresponding SFN
131                 offset += DIR_ENTRY_LENGTH;
132                 continue;
133             } else {
134                 // regular entry, extract the short file name
135                 char short_filename[8 + 1 + 3 + 1]; // max short name (8 . 3 NULL)
136                 size_t fname_pos = 0;
137 
138                 // Ignore trailing spaces in filename and/or extension
139                 int fn_len = 8, ext_len = 3;
140                 for (int i = 7; i >= 0; i--) {
141                     if (ent[i] == 0x20) {
142                         fn_len--;
143                     } else {
144                         break;
145                     }
146                 }
147                 for (size_t i = 10; i >= 8; i--) {
148                     if (ent[i] == 0x20) {
149                         ext_len--;
150                     } else {
151                         break;
152                     }
153                 }
154 
155                 for (int i = 0; i < fn_len; i++) {
156                     short_filename[fname_pos++] = ent[i];
157                 }
158                 if (ext_len > 0) {
159                     short_filename[fname_pos++] = '.';
160                     for (int i=0; i < ext_len; i++) {
161                         short_filename[fname_pos++] = ent[8 + i];
162                     }
163                 }
164                 short_filename[fname_pos++] = 0;
165                 DEBUG_ASSERT(fname_pos <= sizeof(short_filename));
166 
167                 // now we have the SFN, see if we just got finished parsing a corresponding LFN
168                 // in the previous entries
169                 if (lfn_state.last_sequence == 1) {
170                     // TODO: compute checksum and make sure it matches
171                     *out_filename = filename_buffer + lfn_state.pos;
172                 } else {
173                     // copy the parsed short file name into the out buffer
174                     strlcpy(filename_buffer, short_filename, sizeof(short_filename));
175                     *out_filename = filename_buffer;
176                 }
177 
178                 lfn_state.reset();
179                 offset += DIR_ENTRY_LENGTH;
180 
181                 // fall through, we've found a file entry
182             }
183 
184             LTRACEF("found filename '%s'\n", *out_filename);
185 
186             // fill out the passed in dir entry and exit
187             uint16_t target_cluster = fat_read16(ent, 0x1a);
188             entry->length = fat_read32(ent, 0x1c);
189             entry->attributes = (fat_attribute)ent[0x0B];
190             entry->start_cluster = target_cluster;
191             return NO_ERROR;
192         }
193 
194         DEBUG_ASSERT(offset <= fat->info().bytes_per_sector);
195 
196         // move to the next sector
197         status_t err = dbi.next_sector();
198         if (err < 0) {
199             break;
200         }
201         // starting over at offset 0 in the new sector
202         offset = 0;
203     }
204 
205     // we're out of entries
206     return ERR_NOT_FOUND;
207 }
208 
fat_find_file_in_dir(fat_fs * fat,uint32_t starting_cluster,const char * name,dir_entry * entry,uint32_t * found_offset)209 status_t fat_find_file_in_dir(fat_fs *fat, uint32_t starting_cluster, const char *name, dir_entry *entry, uint32_t *found_offset) {
210     LTRACEF("start_cluster %u, name '%s', out entry %p\n", starting_cluster, name, entry);
211 
212     DEBUG_ASSERT(fat->lock.is_held());
213 
214     // cache the length of the string we're matching against
215     const size_t namelen = strlen(name);
216 
217     // kick start our directory sector iterator
218     file_block_iterator dbi(fat, starting_cluster);
219     status_t err = dbi.next_sectors(0);
220     if (err < 0) {
221         return err;
222     }
223 
224     uint32_t offset = 0;
225     for (;;) {
226         char filename_buffer[MAX_FILE_NAME_LEN]; // max fat file name length
227         char *filename;
228 
229         // step forward one entry and see if we got something
230         err = fat_find_next_entry(fat, dbi, offset, entry, filename_buffer, &filename);
231         if (err < 0) {
232             return err;
233         }
234 
235         const size_t filenamelen = strlen(filename);
236 
237         // see if we've matched an entry
238         if (filenamelen == namelen && !strnicmp(name, filename, filenamelen)) {
239             // we have, return with a good status
240             *found_offset = offset;
241             return NO_ERROR;
242         }
243     }
244 }
245 
246 } // namespace
247 
fat_walk(fat_fs * fat,const char * path,dir_entry * out_entry,dir_entry_location * loc)248 status_t fat_walk(fat_fs *fat, const char *path, dir_entry *out_entry, dir_entry_location *loc) {
249     LTRACEF("path %s\n", path);
250 
251     DEBUG_ASSERT(fat->lock.is_held());
252 
253     // routine to push the path element ahead one bump
254     // will leave path pointing at the next element, and path_element_size
255     // in characters for the next element (or 0 if finished).
256     size_t path_element_size = 0;
257     auto path_increment = [&path, &path_element_size]() {
258         path += path_element_size;
259         path_element_size = 0;
260 
261         // we're at the end of the string
262         if (*path == 0) {
263             return;
264         }
265 
266         // push path past the next /
267         while (*path == '/' && path != 0) {
268             path++;
269         }
270 
271         // count the size of the next element
272         const char *ptr = path;
273         while (*ptr != '/' && *ptr != 0) {
274             ptr++;
275             path_element_size++;
276         }
277     };
278 
279     // increment it once past leading / and establish an initial path_element_size
280     path_increment();
281     LTRACEF("first path '%s' path_element_size %zu\n", path, path_element_size);
282 
283     // set up the starting cluster to search
284     uint32_t dir_start_cluster;
285     if (fat->info().root_cluster) {
286         dir_start_cluster = fat->info().root_cluster;
287     } else {
288         // fat 12/16 has a linear root dir, cluster 0 is a special case to fat_find_file_in_dir below
289         dir_start_cluster = 0;
290     }
291 
292     // output entry
293     dir_entry entry {};
294 
295     // walk the directory structure
296     for (;;) {
297         char name_element[MAX_FILE_NAME_LEN];
298         strlcpy(name_element, path, MIN(sizeof(name_element), path_element_size + 1));
299 
300         LTRACEF("searching for element %s\n", name_element);
301 
302         uint32_t found_offset = 0;
303         auto status = fat_find_file_in_dir(fat, dir_start_cluster, name_element, &entry, &found_offset);
304         if (status < 0) {
305             return ERR_NOT_FOUND;
306         }
307 
308         // we found something
309         LTRACEF("found dir entry attributes %#hhx length %u start_cluster %u\n",
310                 (uint8_t)entry.attributes, entry.length, entry.start_cluster);
311 
312         // push the name element forward one notch so we can see if we're at the end (or iterate once again)
313         path_increment();
314         if (path_element_size > 0) {
315             // did we find a directory on an inner node of the path? we can continue iterating
316             if (entry.attributes == fat_attribute::directory) {
317                 dir_start_cluster = entry.start_cluster;
318             } else {
319                 LTRACEF("found a non dir at a non terminal path entry, exit\n");
320                 return ERR_NOT_FOUND;
321             }
322         } else {
323             // we got a hit at the terminal entry of the path, pass it out to the caller as a success
324             *out_entry = entry;
325             loc->starting_dir_cluster = dir_start_cluster;
326             loc->dir_offset = found_offset;
327             return NO_ERROR;
328         }
329     }
330 }
331 
opendir_priv(const dir_entry & entry,const dir_entry_location & loc,fat_dir_cookie ** out_cookie)332 status_t fat_dir::opendir_priv(const dir_entry &entry, const dir_entry_location &loc, fat_dir_cookie **out_cookie) {
333     // fill in our file info based on the entry
334     start_cluster_ = entry.start_cluster;
335     length_ = 0; // dirs all have 0 length entry
336     dir_loc_ = loc;
337     inc_ref();
338 
339     // create a dir cookie
340     auto dir_cookie = new fat_dir_cookie;
341     dir_cookie->dir = this;
342     dir_cookie->index = 0;
343 
344     // add it to the dir object
345     list_add_tail(&cookies_, &dir_cookie->node);
346 
347     *out_cookie = dir_cookie;
348 
349     return NO_ERROR;
350 }
351 
opendir(fscookie * cookie,const char * name,dircookie ** dcookie)352 status_t fat_dir::opendir(fscookie *cookie, const char *name, dircookie **dcookie) {
353     auto fat = (fat_fs *)cookie;
354 
355     LTRACEF("cookie %p name '%s' dircookie %p\n", cookie, name, dcookie);
356 
357     AutoLock guard(fat->lock);
358 
359     dir_entry entry;
360     dir_entry_location loc;
361 
362     // special case for /
363     if (name[0] == 0 || !strcmp(name, "/")) {
364         entry.attributes = fat_attribute::directory;
365         entry.length = 0;
366         if (fat->info().fat_bits == 32) {
367             entry.start_cluster = fat->info().root_cluster;
368         } else {
369             entry.start_cluster = 0;
370         }
371 
372         // special case for the root dir
373         // 0:0 is not sufficient, since we could actually find a file in the root dir
374         // on a fat 12/16 volume (magic cluster 0) at offset 0. cluster 1 is never used
375         // so mark root dir as 1:0
376         loc.starting_dir_cluster = 1;
377         loc.dir_offset = 0;
378     } else {
379         status_t err = fat_walk(fat, name, &entry, &loc);
380         if (err != NO_ERROR) {
381             return err;
382         }
383     }
384 
385     // if we walked and found a proper directory, it's a hit
386     if (entry.attributes == fat_attribute::directory) {
387         fat_dir *dir;
388 
389         // see if this dir is already present in the fs list
390         fat_file *file = fat->lookup_file(loc);
391         if (file) {
392             // XXX replace with hand rolled RTTI
393             dir = reinterpret_cast<fat_dir *>(file);
394         } else {
395             dir = new fat_dir(fat);
396         }
397         DEBUG_ASSERT(dir);
398 
399         fat_dir_cookie *dir_cookie;
400 
401         status_t err = dir->opendir_priv(entry, loc, &dir_cookie);
402         if (err < 0) {
403             // weird state, should we dec the ref?
404             PANIC_UNIMPLEMENTED;
405             return err;
406         }
407         DEBUG_ASSERT(dir_cookie);
408 
409         *dcookie = (dircookie *)dir_cookie;
410         return NO_ERROR;
411     } else {
412         return ERR_NOT_FILE;
413     }
414 
415     return ERR_NOT_IMPLEMENTED;
416 };
417 
readdir_priv(fat_dir_cookie * cookie,struct dirent * ent)418 status_t fat_dir::readdir_priv(fat_dir_cookie *cookie, struct dirent *ent) {
419     LTRACEF("dircookie %p ent %p, current index %u\n", cookie, ent, cookie->index);
420 
421     if (!ent)
422         return ERR_INVALID_ARGS;
423 
424     // make sure the cookie makes sense
425     DEBUG_ASSERT((cookie->index % DIR_ENTRY_LENGTH) == 0);
426 
427     char filename_buffer[MAX_FILE_NAME_LEN];
428     char *filename;
429     dir_entry entry;
430 
431     {
432         AutoLock guard(fs_->lock);
433 
434         // kick start our directory sector iterator
435         LTRACEF("start cluster %u\n", start_cluster_);
436         file_block_iterator dbi(fs_, start_cluster_);
437 
438         // move it forward to our index point
439         // also loads the buffer
440         status_t err = dbi.next_sectors(cookie->index / fs_->info().bytes_per_sector);
441         if (err < 0) {
442             return err;
443         }
444 
445         // reset how many sectors the dbi has pushed forward so we can account properly for index shifts later
446         dbi.reset_sector_inc_count();
447 
448         // pass the index in units of sector offset
449         uint32_t offset = cookie->index % fs_->info().bytes_per_sector;
450         err = fat_find_next_entry(fs_, dbi, offset, &entry, filename_buffer, &filename);
451         if (err < 0) {
452             return err;
453         }
454 
455         // bump the index forward by extracting how much the sector iterator pushed things forward
456         uint32_t index_inc = offset - (cookie->index % fs_->info().bytes_per_sector);
457         index_inc += dbi.get_sector_inc_count() * fs_->info().bytes_per_sector;
458         LTRACEF("calculated index increment %u (old index %u, offset %u, sector_inc_count %u)\n",
459                 index_inc, cookie->index, offset, dbi.get_sector_inc_count());
460         cookie->index += index_inc;
461     }
462 
463     // copy the info into the fs layer's entry
464     strlcpy(ent->name, filename, MIN(sizeof(ent->name), MAX_FILE_NAME_LEN));
465 
466     return NO_ERROR;
467 }
468 
readdir(dircookie * dcookie,struct dirent * ent)469 status_t fat_dir::readdir(dircookie *dcookie, struct dirent *ent) {
470     auto cookie = (fat_dir_cookie *)dcookie;
471     auto dir = cookie->dir;
472 
473     return dir->readdir_priv(cookie, ent);
474 }
475 
closedir_priv(fat_dir_cookie * cookie,bool * last_ref)476 status_t fat_dir::closedir_priv(fat_dir_cookie *cookie, bool *last_ref) {
477     LTRACEF("dircookie %p\n", cookie);
478 
479     AutoLock guard(fs_->lock);
480 
481     // remove the dircookie from the list
482     DEBUG_ASSERT(list_in_list(&cookie->node));
483     list_delete(&cookie->node);
484 
485     // delete it
486     delete cookie;
487 
488     // drop a ref to the dir
489     *last_ref = dec_ref();
490     if (*last_ref) {
491         DEBUG_ASSERT(list_is_empty(&cookies_));
492     }
493 
494     return NO_ERROR;
495 }
496 
closedir(dircookie * dcookie)497 status_t fat_dir::closedir(dircookie *dcookie) {
498     auto cookie = (fat_dir_cookie *)dcookie;
499     auto dir = cookie->dir;
500 
501     bool last_ref;
502     status_t err = dir->closedir_priv(cookie, &last_ref);
503     if (err < 0) {
504         return err;
505     }
506 
507     if (last_ref) {
508         LTRACEF("last ref, deleting %p (%u:%u)\n", dir, dir->dir_loc().starting_dir_cluster, dir->dir_loc().dir_offset);
509         delete dir;
510     }
511 
512     return NO_ERROR;
513 }
514 
515