/** * @file llfs.c * @brief Linked List Filesystem implementation (llfs) * @author Lorenz C. * @version 0.1.1 */ #include #include #include #include #define LOGGER_LEVEL_WARN #include "log.h" #include "llfs.h" /** * @brief The maximum number of files that can be opened concurrently using the POSIX API */ #define POSIX_MAX_FILES 10 extern struct llfs_data_file* llfs_root; static const char* TAG = "llfs"; static size_t file_count = 0; // Cache for the number of files in the filesystem static FILE* file_table[POSIX_MAX_FILES]; #ifndef TESTING static int new_file_table_entry(void); static int free_file_table_entry(int file_id); static FILE* file_id_to_stream(int file_id); #endif static uint8_t file_ext_cmp(const char* filename, const char* ext); int8_t llfs_init(void) { LOG_DEBUG(TAG, "Initializing llfs"); // Initialize the file table for (int i = 0; i < POSIX_MAX_FILES; i++) { file_table[i] = NULL; } // Add stdin, stdout and stderr to the file table file_table[STDIN_FILENO] = stdin; file_table[STDOUT_FILENO] = stdout; file_table[STDERR_FILENO] = stderr; // Calculate the number of files in the filesystem and cache it const struct llfs_data_file* file = llfs_root; file_count = 0; while (file != NULL) { file_count++; file = file->next; } return 0; } size_t llfs_file_list(llfs_file_t* file_list, size_t max_files, char* filter) { size_t count = 0; // Number of files found const struct llfs_data_file* file = llfs_root; // Pointer to the current file if (filter != NULL) { LOG_DEBUG(TAG, "Filtering files with filter: %s", filter); if (filter[0] == '*') { filter++; } } // Iterate over all files in the filesystem while (file != NULL && count < max_files) { // Filter out files with a filename that does not match the filter if (filter != NULL) { if (!file_ext_cmp(file->name, filter)) { file = file->next; continue; } } // Add the file to the file list file_list[count].data = file->data; file_list[count].name = file->name; file_list[count].len = file->len; // Move to the next file count++; file = file->next; } LOG_DEBUG(TAG, "Files found: %d", count); return count; } llfs_file_t* llfs_file_open(const char* name) { const struct llfs_data_file* file = llfs_root; LOG_DEBUG(TAG, "Opening file: %s", name); while (file != NULL) { if (strcmp(file->name, name) == 0) { LOG_DEBUG(TAG, "File found: %s, size: %d", file->name, file->len); return (llfs_file_t*)file; } file = file->next; } LOG_DEBUG(TAG, "File not found: %s", name); return NULL; } llfs_file_t* llfs_next_file(void** mem, char* filter) { struct llfs_data_file* prev_file = (struct llfs_data_file*)*mem; if (prev_file == NULL) { prev_file = llfs_root; } else { prev_file = (struct llfs_data_file*)prev_file->next; } // If a filter is specified, only return files that match the filter if (filter != NULL) { LOG_DEBUG(TAG, "Filtering files with filter: %s", filter); // Remove the '*' from the filter if (filter[0] == '*') { filter++; } while (prev_file != NULL) { if (file_ext_cmp(prev_file->name, filter)) { break; } prev_file = (struct llfs_data_file*)prev_file->next; } } *mem = (void*)prev_file; return (llfs_file_t*)prev_file; } size_t llfs_file_count(void) { return file_count; } char* llfs_get_filename_ext(char* filename) { char* dot = strrchr(filename, '.'); if (dot == NULL || dot == filename) return NULL; return dot + 1; } #ifndef TESTING /** * @brief Newlib open implementation * * @param path * @param flags * @param mode * @return */ int _open(char* path, int flags, int mode) { int file_id; FILE* stream; errno = 0; llfs_file_t* llfs_file; // Add a new entry to the file table file_id = new_file_table_entry(); if (file_id < 0) { LOG_WARN(TAG, "Failed to add new file table entry. %s", strerror(errno)); return -1; } // Get the stream associated with the file id stream = file_id_to_stream(file_id); if (stream == NULL) { LOG_WARN(TAG, "Failed to get file table entry. %s", strerror(errno)); free_file_table_entry(file_id); return -1; } // Get the file from the llfs filesystem llfs_file = llfs_file_open(path); if (llfs_file == NULL) { LOG_DEBUG(TAG, "Failed to open file: %s", path); free_file_table_entry(file_id); return -1; } // Initialize the stream stream->_cookie = (void*)llfs_file; stream->_offset = 0; stream->_flags = __SRD; return file_id; } /** * @brief Newlib close implementation * * @param file_id * @return */ int _close(int file_id) { FILE* stream; // Get the stream associated with the file id stream = file_id_to_stream(file_id); if (stream == NULL) { LOG_WARN(TAG, "Failed to get file table entry. %s", strerror(errno)); return -1; } // Remove the entry from the file table if (free_file_table_entry(file_id) < 0) { LOG_WARN(TAG, "Failed to remove file table entry. %s", strerror(errno)); return -1; } return 0; } /** * @brief Newlib read implementation * * @param file_id * @param ptr * @param len * @return */ size_t _read(int file_id, char* ptr, int len) { FILE* stream; llfs_file_t* llfs_file; size_t bytes_read; // Read from stdin, stdout and stderr is not supported if (file_id == STDIN_FILENO || file_id == STDOUT_FILENO || file_id == STDERR_FILENO) { LOG_DEBUG(TAG, "Trying to read from stdin, stdout or stderr: %d", file_id); return -1; } // Get the stream associated with the file id stream = file_id_to_stream(file_id); if (stream == NULL) { LOG_WARN(TAG, "Failed to get file table entry. %s", strerror(errno)); return -1; } // Get the file from the llfs filesystem associated with the stream llfs_file = (llfs_file_t*)stream->_cookie; if (llfs_file == NULL) { LOG_WARN(TAG, "Failed to get llfs file associated with stream: %d", file_id); return -1; } // Calculate the number of bytes to read (limited by the file size) bytes_read = llfs_file->len - stream->_offset; if (bytes_read > len) { bytes_read = len; } else if (bytes_read == 0) { // End of file stream->_flags |= __SEOF; return 0; } // Copy the data over to the dst buffer memcpy(ptr, llfs_file->data + stream->_offset, bytes_read); stream->_offset += (off_t)bytes_read; return bytes_read; } /** * @brief Newlib isatty implementation * * @param file * @return 1 if the file is stdin, stdout or stderr, 0 otherwise */ int isatty(int file) { if (file == STDIN_FILENO || file == STDOUT_FILENO || file == STDERR_FILENO) { return 1; } return 0; } /** * @brief Newlib lseek implementation * * @param file * @param ptr * @param dir * @return */ off_t _lseek(int file, int ptr, int dir) { FILE* stream; if (file == STDIN_FILENO || file == STDOUT_FILENO || file == STDERR_FILENO) { LOG_DEBUG(TAG, "Trying to seek stdin, stdout or stderr: %d", file); return -1; } stream = file_id_to_stream(file); if (stream == NULL) { LOG_WARN(TAG, "Failed to get file table entry. %s", strerror(errno)); return -1; } switch (dir) { case SEEK_SET: stream->_offset = ptr; break; case SEEK_CUR: stream->_offset += ptr; break; case SEEK_END: stream->_offset = (off_t)((llfs_file_t*)stream->_cookie)->len + ptr; break; default: LOG_WARN(TAG, "Invalid seek direction: %d", dir); return -1; } return stream->_offset; } /** * @brief Newlib fstat implementation * * @param[in] file * @param[out] st * @return */ int _fstat(int file, struct stat* st) { FILE* stream; llfs_file_t *llfs_file; // Check if the file is stdin, stdout or stderr if (file == STDIN_FILENO || file == STDOUT_FILENO || file == STDERR_FILENO) { st->st_mode = S_IFCHR; // Character special file return 0; } // Get the stream associated with the file id stream = file_id_to_stream(file); if (stream == NULL) { LOG_WARN(TAG, "Failed to get file table entry. %s", strerror(errno)); return -1; } // Get the file from the llfs filesystem associated with the stream llfs_file = (llfs_file_t*)stream->_cookie; if (llfs_file == NULL) { LOG_WARN(TAG, "Failed to get llfs file associated with stream: %d", file); return -1; } st->st_mode = S_IFREG; // Regular file st->st_size = (off_t)llfs_file->len; return 0; } /** * @brief Create a new entry in the file table * * @return The file id or -1 if an error occurred. See errno for more information. */ static int new_file_table_entry(void) { FILE* stream; // Try to find an empty entry in the file table for (int i = 0; i < POSIX_MAX_FILES; i++) { if (file_table[i] == NULL) { stream = (FILE*)malloc(sizeof(FILE)); if (stream == NULL) { errno = ENOMEM; // Out of memory return -1; } file_table[i] = stream; return i; } } // No empty entry found errno = ENFILE; // Too many open files return -1; } /** * @brief Remove an entry from the file table * * @param[in] file_id The file id to remove * @return 0 if successful, -1 if an error occurred. See errno for more information. */ static int free_file_table_entry(int file_id) { // Check if the file id is valid if (file_id < 0 || file_id >= POSIX_MAX_FILES) { errno = EBADF; // Bad file number return -1; } // Remove the entry from the file table free(file_table[file_id]); file_table[file_id] = NULL; return 0; } /** * @brief Get the stream associated with a file id * * @param[in] file_id The file id to get the stream for * @return The stream or NULL if an error occurred. See errno for more information. */ static FILE* file_id_to_stream(int file_id) { if (file_id < 0 || file_id >= POSIX_MAX_FILES) { errno = EBADF; // Bad file number return NULL; } return file_table[file_id]; } #endif /** * @brief Check if a filename ends with a given extension * * @param[in] filename The filename to check * @param[in] ext The extension to check for * @return 1 if the filename ends with the extension, 0 otherwise */ static uint8_t file_ext_cmp(const char* const filename, const char* const ext) { uint8_t ext_len = strlen(ext); uint8_t filename_len = strlen(filename); if (filename_len < ext_len) { return 0; } // Compare backwards for (uint8_t i = 0; i < ext_len; i++) { if (filename[filename_len - i] != ext[ext_len - i]) { return 0; } } return 1; }