mirror of
https://github.com/google/pebble.git
synced 2025-07-04 13:57:04 -04:00
2386 lines
76 KiB
C
2386 lines
76 KiB
C
/*
|
|
* Copyright 2024 Google LLC
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
#include "pfs.h"
|
|
|
|
#include <inttypes.h>
|
|
#include <stddef.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "console/prompt.h"
|
|
#include "drivers/flash.h"
|
|
#include "drivers/task_watchdog.h"
|
|
#include "flash_region/filesystem_regions.h"
|
|
#include "flash_region/flash_region.h"
|
|
#include "kernel/pbl_malloc.h"
|
|
#include "kernel/pebble_tasks.h"
|
|
#include "kernel/util/sleep.h"
|
|
#include "os/mutex.h"
|
|
#include "services/common/analytics/analytics.h"
|
|
#include "services/normal/filesystem/flash_translation.h"
|
|
#include "system/hexdump.h"
|
|
#include "system/logging.h"
|
|
#include "system/passert.h"
|
|
#include "util/attributes.h"
|
|
#include "util/crc8.h"
|
|
#include "util/legacy_checksum.h"
|
|
#include "util/math.h"
|
|
|
|
static PebbleRecursiveMutex *s_pfs_mutex = NULL;
|
|
|
|
#define IS_FILE_TYPE(file_type, type) ((file_type) == (type))
|
|
|
|
#define PFS_PAGE_SIZE FLASH_FILESYSTEM_BLOCK_SIZE
|
|
#define PFS_PAGES_PER_ERASE_SECTOR (SECTOR_SIZE_BYTES / PFS_PAGE_SIZE)
|
|
#define GC_REGION_SIZE SECTOR_SIZE_BYTES
|
|
|
|
// The filesystem is broken into discrete blocks called 'pages'. Each page has
|
|
// a header that describes the contents contained within it. Static fields are
|
|
// CRC protected and are verified each time a file is opened. Convenience
|
|
// defines and the struct are defined below.
|
|
|
|
#define PAGE_FLAG_ERASED_PAGE (1 << 0) // page erase completed
|
|
#define PAGE_FLAG_DELETED_PAGE (1 << 1) // page was deleted
|
|
#define PAGE_FLAG_START_PAGE (1 << 2) // first page of file
|
|
#define PAGE_FLAG_CONT_PAGE (1 << 3) // continuation page of file
|
|
|
|
#define PAGE_FLAGS_BIT_SET(page_flags, type) ((~(page_flags) & (type)) != 0)
|
|
|
|
#define DELETED_START_PAGE_MASK \
|
|
(PAGE_FLAG_ERASED_PAGE | PAGE_FLAG_DELETED_PAGE | PAGE_FLAG_START_PAGE)
|
|
#define DELETED_CONT_PAGE_MASK \
|
|
(PAGE_FLAG_ERASED_PAGE | PAGE_FLAG_DELETED_PAGE | PAGE_FLAG_CONT_PAGE)
|
|
|
|
// Header Layout Overview
|
|
// First Page of a file:
|
|
// | PageHeader | FileHeader | FileMetaData | name | File Data
|
|
// Continuation Pages:
|
|
// | PageHeader | File Data
|
|
|
|
#define IS_PAGE_TYPE(page_flags, type) \
|
|
(PAGE_FLAGS_BIT_SET(page_flags, type) && \
|
|
!PAGE_FLAGS_BIT_SET(page_flags, PAGE_FLAG_DELETED_PAGE))
|
|
|
|
#define SET_PAGE_FLAGS(page_flags, type) ((page_flags) &= ~(type))
|
|
|
|
#define PFS_MAGIC 0x50
|
|
#define PFS_VERS 0x01
|
|
#define PFS_CUR_VERSION ((PFS_MAGIC << 8) | PFS_VERS)
|
|
|
|
#define LAST_WRITTEN_TAG 0xfe
|
|
#define LAST_WRITTEN_UNMARK 0xfc
|
|
|
|
typedef struct PACKED PageHeader {
|
|
uint16_t version;
|
|
uint8_t last_written; //!< used by wear leveling algo
|
|
uint8_t page_flags;
|
|
uint8_t rsvd0[4];
|
|
uint32_t erase_count;
|
|
uint8_t rsvd1[9]; //!< for future extensions
|
|
uint8_t next_page_crc;
|
|
uint16_t next_page;
|
|
uint32_t hdr_crc; //!< a crc for all data that comes before it
|
|
} PageHeader;
|
|
|
|
typedef struct PACKED FileHeader {
|
|
uint32_t file_size;
|
|
uint8_t file_type;
|
|
uint8_t file_namelen;
|
|
uint8_t rsvd[6];
|
|
uint32_t hdr_crc;
|
|
} FileHeader;
|
|
|
|
// File metadata stored immediately after the header in the first page
|
|
typedef struct PACKED FileMetaData {
|
|
uint16_t tmp_state;
|
|
uint16_t create_state;
|
|
uint16_t delete_state;
|
|
uint8_t rsvd[10];
|
|
uint8_t uuid[16]; //!< rsvd for UUIDs in the future
|
|
char name[0];
|
|
} FileMetaData;
|
|
|
|
#define TMP_STATE_DONE 0x0
|
|
#define CREATE_STATE_DONE 0x0
|
|
#define DELETE_STATE_DONE 0x0
|
|
|
|
#define TMP_STATE_OFFSET (offsetof(FileMetaData, tmp_state))
|
|
#define CREATE_STATE_OFFSET (offsetof(FileMetaData, create_state))
|
|
#define DELETE_STATE_OFFSET (offsetof(FileMetaData, delete_state))
|
|
|
|
#define AVAIL_BYTES_OFFSET (sizeof(PageHeader))
|
|
#define FILEHEADER_OFFSET (sizeof(PageHeader))
|
|
#define METADATA_OFFSET (FILEHEADER_OFFSET + sizeof(FileHeader))
|
|
#define FILEDATA_LEN (sizeof(FileHeader) + sizeof(FileMetaData))
|
|
#define FILE_NAME_OFFSET (FILEHEADER_OFFSET + FILEDATA_LEN)
|
|
|
|
// defines & struct for data that needs to be tracked once a file is opened
|
|
#define INVALID_PAGE ((uint16_t)~0)
|
|
|
|
#define GC_FILE_NAME "GC"
|
|
#define GC_DATA_VALID (0x1)
|
|
|
|
typedef struct {
|
|
uint8_t version;
|
|
uint8_t flags;
|
|
uint16_t gc_start_page;
|
|
uint32_t page_mask;
|
|
uint8_t num_entries;
|
|
} GCData;
|
|
|
|
#define GCDATA_VALID(flags) ((~(flags) & GC_DATA_VALID) != 0)
|
|
|
|
typedef struct {
|
|
uint16_t virtual_pg;
|
|
uint16_t physical_pg;
|
|
uint16_t contiguous_pgs;
|
|
} FilePageCache;
|
|
|
|
typedef struct File {
|
|
// file specifics loaded from header
|
|
char *name;
|
|
uint8_t namelen;
|
|
uint32_t file_size;
|
|
uint16_t start_page; //!< the physical page at which the file begins
|
|
uint16_t start_offset; //!< offset at which file data begins
|
|
uint8_t file_type;
|
|
|
|
// items dynamically changing
|
|
uint8_t op_flags;
|
|
bool is_tmp;
|
|
uint32_t offset; // the current offset within the file
|
|
uint16_t curr_page; // the current page the offset is on
|
|
FilePageCache *pg_cache;
|
|
uint8_t pg_cache_len;
|
|
} File;
|
|
|
|
// The backing information tracked using the handle returned to callers
|
|
#define FD_STATUS_IN_USE 0x0 // A caller is using this fd
|
|
#define FD_STATUS_UNREFERENCED 0x1 // Valid data, no one using
|
|
#define FD_STATUS_FREE 0x2 // No data in the fd
|
|
|
|
// max number of files (cache size) that can be opened at any given time
|
|
#define PFS_FD_SET_SIZE 8
|
|
// 1 fd dedicated for GC (we always want an FD available for this operation!)
|
|
#define GC_FD_HANDLE_ID (FD_INDEX_OFFSET + PFS_FD_SET_SIZE)
|
|
#define GC_FD_SET_SIZE 1
|
|
#define MAX_FD_HANDLES (PFS_FD_SET_SIZE + GC_FD_SET_SIZE)
|
|
// Offset for FD numbers so that zero can't be a valid FD. This makes it much
|
|
// less likely for a file descriptor in an uninitialized object to reference a
|
|
// valid open file.
|
|
#define FD_INDEX_OFFSET 1001
|
|
|
|
static uint16_t time_closed_counter = 0;
|
|
|
|
typedef struct FileDesc {
|
|
File file;
|
|
uint16_t time_closed; //!< used for fd caching scheme
|
|
uint8_t fd_status;
|
|
} FileDesc;
|
|
|
|
static FileDesc s_pfs_avail_fd[MAX_FD_HANDLES];
|
|
// All accesses to s_pfs_avail_fd should be handled through the PFS_FD macro.
|
|
#define PFS_FD(fd) s_pfs_avail_fd[(fd)-FD_INDEX_OFFSET]
|
|
|
|
typedef struct GCBlock {
|
|
bool block_valid;
|
|
uint8_t block_writes;
|
|
uint16_t gc_start_page;
|
|
} GCBlock;
|
|
|
|
static GCBlock s_gc_block;
|
|
|
|
// This is used by unit tests to clear out static state and simulate a reboot.
|
|
void pfs_reset_all_state(void) {
|
|
s_gc_block = (GCBlock){};
|
|
memset(s_pfs_avail_fd, 0, sizeof(s_pfs_avail_fd));
|
|
time_closed_counter = 0;
|
|
}
|
|
|
|
#define FD_VALID(fd) ((((fd) >= FD_INDEX_OFFSET) && \
|
|
((fd) < (FD_INDEX_OFFSET+MAX_FD_HANDLES))) && \
|
|
(PFS_FD(fd).fd_status == FD_STATUS_IN_USE))
|
|
|
|
#define VALID_TYPE(type) ((type) == FILE_TYPE_STATIC)
|
|
|
|
typedef struct {
|
|
ListNode list_node;
|
|
//! Name of the file to watch
|
|
const char* name;
|
|
//! Which events will invoke callbacks (see FILE_CHANGED_EVENT_ flags in pfs.h)
|
|
uint8_t event_flags;
|
|
//! Caller provided data pointer
|
|
void *data;
|
|
//! Callback pointer
|
|
PFSFileChangedCallback callback;
|
|
} PFSFileChangedCallbackNode;
|
|
|
|
static uint8_t *s_pfs_page_flags_cache = NULL;
|
|
static uint16_t s_pfs_page_count = 0;
|
|
static uint32_t s_pfs_size = 0;
|
|
static ListNode *s_head_callback_node_list = NULL;
|
|
|
|
// In the interest of being able to leverage sector erases / minimize seek time
|
|
// for large files, deploying a variable length page size may be beneficial.
|
|
// Therefore, isolating the page offset related calculations to one location.
|
|
static uint32_t prv_page_to_flash_offset(uint16_t page) {
|
|
return ((uint32_t)page * PFS_PAGE_SIZE);
|
|
}
|
|
|
|
static void prv_flash_read(void *buffer, uint32_t size, uint32_t offset) {
|
|
if ((offset + size) <= s_pfs_size) {
|
|
ftl_read(buffer, size, offset);
|
|
} else {
|
|
PBL_LOG(LOG_LEVEL_ERROR, "FS read out of bounds 0x%x", (int)offset);
|
|
}
|
|
}
|
|
|
|
// Invalidates s_pfs_page_flags_cache for a given range of bytes. This should be called after the
|
|
// contents of the backing-flash are changed so that we re-read the page flags into our cache.
|
|
static void prv_invalidate_page_flags_cache(uint32_t offset, uint32_t size) {
|
|
if (!s_pfs_page_flags_cache) {
|
|
return;
|
|
}
|
|
|
|
// Prefetch any page flags which fall within the range of bytes which have been updated.
|
|
const uint16_t start_page = offset / PFS_PAGE_SIZE;
|
|
const uint16_t end_page = (offset + size - 1) / PFS_PAGE_SIZE;
|
|
PBL_ASSERTN(end_page < s_pfs_page_count);
|
|
const int page_flags_offset = offsetof(PageHeader, page_flags);
|
|
for (uint16_t pg = start_page; pg <= end_page; pg++) {
|
|
prv_flash_read(&s_pfs_page_flags_cache[pg], sizeof(s_pfs_page_flags_cache[pg]),
|
|
prv_page_to_flash_offset(pg) + page_flags_offset);
|
|
}
|
|
}
|
|
|
|
static void prv_invalidate_page_flags_cache_all(void) {
|
|
prv_invalidate_page_flags_cache(0, s_pfs_page_count * PFS_PAGE_SIZE);
|
|
}
|
|
|
|
static void prv_flash_write(const void *buffer, uint32_t size, uint32_t offset) {
|
|
if ((offset + size) <= s_pfs_size) {
|
|
ftl_write(buffer, size, offset);
|
|
prv_invalidate_page_flags_cache(offset, size);
|
|
} else {
|
|
PBL_LOG(LOG_LEVEL_ERROR, "FS write out of bounds 0x%x", (int)offset);
|
|
}
|
|
}
|
|
|
|
// Erases all pages for the sector which begins at 'start_page'
|
|
static void prv_flash_erase_sector(uint16_t start_page) {
|
|
uint32_t offset = PFS_PAGE_SIZE * start_page;
|
|
if (offset < s_pfs_size) {
|
|
ftl_erase_sector(PFS_PAGE_SIZE * PFS_PAGES_PER_ERASE_SECTOR, offset);
|
|
prv_invalidate_page_flags_cache(offset, PFS_PAGE_SIZE * PFS_PAGES_PER_ERASE_SECTOR);
|
|
} else {
|
|
PBL_LOG(LOG_LEVEL_ERROR, "Erase out of bounds, 0x%x", (int)start_page);
|
|
}
|
|
}
|
|
|
|
static uint32_t free_bytes_in_page(uint16_t page) {
|
|
return (PFS_PAGE_SIZE - AVAIL_BYTES_OFFSET);
|
|
}
|
|
|
|
static bool page_type_bits_set(uint8_t page_flags, uint8_t type_mask) {
|
|
type_mask = ~type_mask;
|
|
return (page_flags == type_mask);
|
|
}
|
|
|
|
static bool page_is_deleted(uint8_t page_flags) {
|
|
return (page_type_bits_set(page_flags, DELETED_START_PAGE_MASK) ||
|
|
page_type_bits_set(page_flags, DELETED_CONT_PAGE_MASK));
|
|
}
|
|
|
|
static bool page_is_erased(uint8_t page_flags) {
|
|
return (page_type_bits_set(page_flags, PAGE_FLAG_ERASED_PAGE));
|
|
}
|
|
|
|
static bool page_is_unallocated(uint8_t page_flags) {
|
|
return (page_is_deleted(page_flags) || page_is_erased(page_flags) ||
|
|
(page_flags == 0xff));
|
|
}
|
|
|
|
static uint8_t prv_get_page_flags(uint16_t pg) {
|
|
#if UNITTEST
|
|
// no caching for unit tests
|
|
uint8_t flash_value = 0;
|
|
prv_flash_read(&flash_value, sizeof(flash_value),
|
|
prv_page_to_flash_offset(pg) + offsetof(PageHeader, page_flags));
|
|
return flash_value;
|
|
#else
|
|
PBL_ASSERTN(s_pfs_page_flags_cache && (pg < s_pfs_page_count));
|
|
return s_pfs_page_flags_cache[pg];
|
|
#endif
|
|
}
|
|
|
|
static void prv_build_page_flags_cache(void) {
|
|
#if UNITTEST
|
|
// no caching for unit tests
|
|
return;
|
|
#endif
|
|
|
|
// if it already exists, free it first
|
|
if (s_pfs_page_flags_cache) {
|
|
kernel_free(s_pfs_page_flags_cache);
|
|
s_pfs_page_flags_cache = NULL;
|
|
}
|
|
|
|
// if there are no pages in PFS, we don't need a cache
|
|
if (s_pfs_page_count == 0) {
|
|
return;
|
|
}
|
|
|
|
// allocate the new cache
|
|
s_pfs_page_flags_cache = kernel_malloc_check(s_pfs_page_count * sizeof(*s_pfs_page_flags_cache));
|
|
|
|
// read and set each of the page flags into the cache
|
|
prv_invalidate_page_flags_cache_all();
|
|
}
|
|
|
|
static void update_curr_state(uint16_t start_page, uint32_t offset,
|
|
uint16_t state) {
|
|
offset += prv_page_to_flash_offset(start_page) + METADATA_OFFSET;
|
|
prv_flash_write((uint8_t *)&state, sizeof(state), offset);
|
|
}
|
|
|
|
static bool get_curr_state(uint16_t start_page, uint32_t offset,
|
|
uint16_t state) {
|
|
uint16_t curr_state;
|
|
offset += prv_page_to_flash_offset(start_page) + METADATA_OFFSET;
|
|
prv_flash_read((uint8_t *)&curr_state, sizeof(state), offset);
|
|
return (curr_state == state);
|
|
}
|
|
|
|
static bool is_create_complete(uint16_t start_page) {
|
|
return (get_curr_state(start_page, CREATE_STATE_OFFSET, CREATE_STATE_DONE));
|
|
}
|
|
|
|
static bool is_delete_complete(uint16_t start_page) {
|
|
return (get_curr_state(start_page, DELETE_STATE_OFFSET, DELETE_STATE_DONE));
|
|
}
|
|
|
|
static bool is_tmp_file(uint16_t start_page) {
|
|
return (!get_curr_state(start_page, TMP_STATE_OFFSET, TMP_STATE_DONE));
|
|
}
|
|
|
|
static uint32_t compute_pg_header_crc(PageHeader *hdr) {
|
|
PageHeader crc_hdr = *hdr;
|
|
// don't factor fields which can change after file write into crc calc
|
|
crc_hdr.last_written = 0xff;
|
|
|
|
return legacy_defective_checksum_memory(&crc_hdr,
|
|
offsetof(PageHeader, hdr_crc));
|
|
}
|
|
|
|
static uint32_t compute_file_header_crc(FileHeader *hdr) {
|
|
return legacy_defective_checksum_memory(hdr, offsetof(FileHeader, hdr_crc));
|
|
}
|
|
|
|
// the start page is written to, the end page is not written to.
|
|
static void prv_write_erased_header_on_page_range(uint16_t start, uint16_t end,
|
|
int erase_count) {
|
|
// create a header representing an erase header
|
|
PageHeader pg_hdr;
|
|
memset(&pg_hdr, 0xff, sizeof(pg_hdr));
|
|
pg_hdr.version = PFS_CUR_VERSION;
|
|
pg_hdr.erase_count = erase_count;
|
|
SET_PAGE_FLAGS(pg_hdr.page_flags, PAGE_FLAG_ERASED_PAGE);
|
|
|
|
// write that header to each region in pfs
|
|
uint8_t erased_header_size = offsetof(PageHeader, erase_count) + sizeof(pg_hdr.erase_count);
|
|
for (uint16_t i = start; i < end; i++) {
|
|
prv_flash_write((uint8_t*)&pg_hdr, erased_header_size, prv_page_to_flash_offset(i));
|
|
}
|
|
}
|
|
|
|
typedef enum {
|
|
PageHdrValid = 0,
|
|
PageAndFileHdrValid = 1,
|
|
HdrCrcCorrupt = -1,
|
|
HdrVersionCheckFail = -2
|
|
} ReadHeaderStatus;
|
|
|
|
static ReadHeaderStatus read_header(uint16_t page, PageHeader *pg_hdr,
|
|
FileHeader *file_hdr) {
|
|
prv_flash_read((uint8_t *)pg_hdr, sizeof(*pg_hdr), prv_page_to_flash_offset(page));
|
|
|
|
if (compute_pg_header_crc(pg_hdr) != pg_hdr->hdr_crc) {
|
|
return (HdrCrcCorrupt);
|
|
}
|
|
|
|
if (pg_hdr->version > PFS_CUR_VERSION) {
|
|
PBL_LOG(LOG_LEVEL_ERROR, "Unexpected Version Header, 0x%x",
|
|
(int)pg_hdr->version);
|
|
return (HdrVersionCheckFail); // let caller handle
|
|
}
|
|
|
|
if (!IS_PAGE_TYPE(pg_hdr->page_flags, PAGE_FLAG_START_PAGE)) {
|
|
return (PageHdrValid);
|
|
}
|
|
|
|
prv_flash_read((uint8_t *)file_hdr, sizeof(*file_hdr), FILEHEADER_OFFSET +
|
|
prv_page_to_flash_offset(page));
|
|
|
|
if (compute_file_header_crc(file_hdr) != file_hdr->hdr_crc) {
|
|
return (HdrCrcCorrupt);
|
|
}
|
|
|
|
return (PageAndFileHdrValid);
|
|
}
|
|
|
|
static status_t write_file_header(FileHeader *hdr, uint16_t pg) {
|
|
hdr->hdr_crc = compute_file_header_crc(hdr);
|
|
prv_flash_write((uint8_t *)hdr, sizeof(*hdr), prv_page_to_flash_offset(pg) +
|
|
FILEHEADER_OFFSET);
|
|
return (S_SUCCESS);
|
|
}
|
|
|
|
static status_t write_pg_header(PageHeader *hdr, uint16_t pg) {
|
|
// recover current erase count which is updated in erase routine
|
|
prv_flash_read((uint8_t *)&hdr->erase_count, sizeof(hdr->erase_count),
|
|
prv_page_to_flash_offset(pg) + offsetof(PageHeader, erase_count));
|
|
prv_flash_read((uint8_t *)&hdr->last_written, sizeof(hdr->last_written),
|
|
prv_page_to_flash_offset(pg) + offsetof(PageHeader, last_written));
|
|
|
|
hdr->hdr_crc = compute_pg_header_crc(hdr);
|
|
prv_flash_write((uint8_t *)hdr, sizeof(*hdr), prv_page_to_flash_offset(pg));
|
|
|
|
return (S_SUCCESS);
|
|
}
|
|
|
|
// note: the goal here is to do as few flash reads as possible
|
|
// while scanning the flash to find a given file.
|
|
static status_t locate_flash_file(const char *name, uint16_t *page) {
|
|
const int file_namelen_offset = FILEHEADER_OFFSET +
|
|
offsetof(FileHeader, file_namelen);
|
|
uint8_t namelen = strlen(name);
|
|
|
|
for (uint16_t pg = 0; pg < s_pfs_page_count; pg++) {
|
|
PageHeader pg_hdr;
|
|
FileHeader file_hdr;
|
|
pg_hdr.page_flags = prv_get_page_flags(pg);
|
|
|
|
if (!IS_PAGE_TYPE(pg_hdr.page_flags, PAGE_FLAG_START_PAGE)) {
|
|
continue; // only start pages contain file name info
|
|
}
|
|
|
|
prv_flash_read((uint8_t *)&file_hdr.file_namelen, sizeof(file_hdr.file_namelen),
|
|
prv_page_to_flash_offset(pg) + file_namelen_offset);
|
|
|
|
if (file_hdr.file_namelen == namelen) {
|
|
char file_name[namelen];
|
|
|
|
prv_flash_read((uint8_t *)file_name, namelen, prv_page_to_flash_offset(pg) +
|
|
FILE_NAME_OFFSET);
|
|
|
|
if ((memcmp(name, file_name, namelen) == 0) && (!is_tmp_file(pg))) {
|
|
|
|
if (read_header(pg, &pg_hdr, &file_hdr) == HdrCrcCorrupt) {
|
|
PBL_LOG(LOG_LEVEL_WARNING, "%d: CRC corrupt", pg);
|
|
continue;
|
|
}
|
|
|
|
*page = pg;
|
|
return (S_SUCCESS);
|
|
}
|
|
}
|
|
}
|
|
|
|
return (E_DOES_NOT_EXIST);
|
|
}
|
|
|
|
// Populates 'hdr' with what the new erase header for the 'page' specified
|
|
// should look like
|
|
static int get_updated_erase_hdr(PageHeader *hdr, uint16_t page) {
|
|
memset(hdr, 0xff, sizeof(*hdr));
|
|
|
|
// before wiping a page, get its erase_count. This is not currently used but
|
|
// enables future wear leveling improvements / analysis
|
|
prv_flash_read((uint8_t *)&hdr->erase_count, sizeof(hdr->erase_count),
|
|
prv_page_to_flash_offset(page) + offsetof(PageHeader, erase_count));
|
|
prv_flash_read((uint8_t *)&hdr->last_written, sizeof(hdr->last_written),
|
|
prv_page_to_flash_offset(page) + offsetof(PageHeader, last_written));
|
|
|
|
// feed watchdog since erases can take a while & give lower priority tasks
|
|
// a little time in case we are calling this from a high priority task and
|
|
// stalling them
|
|
task_watchdog_bit_set(pebble_task_get_current());
|
|
psleep(1);
|
|
|
|
// mark the page as erased. This way we know that the erase completed
|
|
// next time we scan the sector
|
|
SET_PAGE_FLAGS(hdr->page_flags, PAGE_FLAG_ERASED_PAGE);
|
|
if (hdr->erase_count == 0xffffffff) {
|
|
// should only happen after a filesystem format so assume 0 but could
|
|
// also occur if we reboot during an erase cycle
|
|
hdr->erase_count = 0;
|
|
}
|
|
hdr->erase_count++;
|
|
hdr->version = PFS_CUR_VERSION;
|
|
|
|
if (hdr->last_written != LAST_WRITTEN_TAG) {
|
|
hdr->last_written = 0xff; // reset last written tag
|
|
}
|
|
|
|
return (S_SUCCESS);
|
|
}
|
|
|
|
static int s_last_page_written = 0;
|
|
#if UNITTEST
|
|
static int s_test_last_page_written_override = -1;
|
|
#endif
|
|
|
|
static void update_last_written_page(void) {
|
|
for (uint16_t pg = 0; pg < s_pfs_page_count; pg++) {
|
|
PageHeader hdr;
|
|
prv_flash_read((uint8_t *)&hdr.last_written, sizeof(hdr.last_written),
|
|
prv_page_to_flash_offset(pg) + offsetof(PageHeader, last_written));
|
|
if (hdr.last_written == LAST_WRITTEN_TAG) {
|
|
s_last_page_written = pg;
|
|
PBL_LOG(LOG_LEVEL_INFO, "Last written page %d", (int)s_last_page_written);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// should only happen after a filesystem format
|
|
PBL_LOG(LOG_LEVEL_WARNING, "Couldn't resolve last written pg");
|
|
s_last_page_written = s_pfs_page_count - 1;
|
|
#if UNITTEST
|
|
if (s_test_last_page_written_override != -1) {
|
|
s_last_page_written = s_test_last_page_written_override;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
//! @param region - The erase sector we want to scan through
|
|
//! @param first_free_page - Populated with the first erased page found
|
|
//! in the region. If none are found, populated with INVALID_PAGE
|
|
//! @return A bitmask indicating which pages in the sector are occupied.
|
|
//! For example, 0b1001 would indicate page 0 and page 3 within the sector
|
|
//! are in use
|
|
static uint32_t prv_get_sector_page_status(uint16_t region,
|
|
uint16_t *first_free_page) {
|
|
|
|
// our bitmask needs to be large enough to describe all the pages in a sector
|
|
_Static_assert((sizeof(uint32_t) * 8) >= PFS_PAGES_PER_ERASE_SECTOR,
|
|
"Number of PFS pages is larger than bitmask");
|
|
|
|
*first_free_page = INVALID_PAGE;
|
|
|
|
uint16_t start_pg = region * PFS_PAGES_PER_ERASE_SECTOR;
|
|
uint16_t end_pg = start_pg + PFS_PAGES_PER_ERASE_SECTOR;
|
|
uint32_t sectors_active = 0;
|
|
for (uint16_t pg = start_pg; pg < end_pg; pg++) {
|
|
uint8_t page_flags = prv_get_page_flags(pg);
|
|
|
|
if (page_is_erased(page_flags)) {
|
|
if (*first_free_page == INVALID_PAGE) {
|
|
*first_free_page = pg;
|
|
}
|
|
} else if (!page_is_unallocated(page_flags)) {
|
|
sectors_active |= (0x1 << (pg % PFS_PAGES_PER_ERASE_SECTOR));
|
|
}
|
|
}
|
|
|
|
return (sectors_active);
|
|
}
|
|
|
|
//! Scans through the filesystem and finds a sector with no pages that
|
|
//! are active
|
|
//!
|
|
//! @param skip_gc_region - will skip checking the region that is
|
|
//! used for garbage collection
|
|
//! @return the beginning page in the region which is free or -1 on failure
|
|
static int prv_find_free_erase_region(bool skip_gc_region) {
|
|
int num_erase_regions = s_pfs_page_count / PFS_PAGES_PER_ERASE_SECTOR;
|
|
int start_region = s_last_page_written / PFS_PAGES_PER_ERASE_SECTOR;
|
|
int end_region = start_region + num_erase_regions;
|
|
|
|
uint16_t gc_erase_block = s_gc_block.gc_start_page / PFS_PAGES_PER_ERASE_SECTOR;
|
|
|
|
for (int region = start_region; region < end_region; region++) {
|
|
int erase_region = region % num_erase_regions;
|
|
|
|
if (skip_gc_region && (erase_region == gc_erase_block)) {
|
|
continue;
|
|
}
|
|
|
|
uint16_t free_pg;
|
|
uint32_t sectors_active = prv_get_sector_page_status(erase_region, &free_pg);
|
|
if ((__builtin_popcount(sectors_active) == 0)) {
|
|
return (erase_region * PFS_PAGES_PER_ERASE_SECTOR);
|
|
}
|
|
}
|
|
|
|
return (-1);
|
|
}
|
|
|
|
static status_t garbage_collect_sector(uint16_t *free_page,
|
|
uint16_t sector_start_page, uint32_t sectors_active);
|
|
|
|
//! Updates the last written page to point to next_page
|
|
static NOINLINE void prv_update_last_written_page(uint16_t next_page) {
|
|
PageHeader hdr = { 0 };
|
|
|
|
uint16_t prev_written_page = s_last_page_written;
|
|
// unmark the previous page as last written (should only have one pg
|
|
// marked as written at any given time).
|
|
prv_flash_read((uint8_t *)&hdr.last_written, sizeof(hdr.last_written),
|
|
prv_page_to_flash_offset(prev_written_page) +
|
|
offsetof(PageHeader, last_written));
|
|
|
|
if (hdr.last_written == LAST_WRITTEN_TAG) {
|
|
hdr.last_written = LAST_WRITTEN_UNMARK;
|
|
prv_flash_write((uint8_t *)&hdr.last_written, sizeof(hdr.last_written),
|
|
prv_page_to_flash_offset(prev_written_page) +
|
|
offsetof(PageHeader, last_written));
|
|
}
|
|
|
|
hdr.last_written = LAST_WRITTEN_TAG;
|
|
|
|
prv_flash_write((uint8_t *)&hdr.last_written, sizeof(hdr.last_written),
|
|
prv_page_to_flash_offset(next_page) + offsetof(PageHeader, last_written));
|
|
}
|
|
|
|
//! The wear leveling strategy deployed is as follows:
|
|
//! Always track the last page which was written. Every time a new page needs
|
|
//! to be allocated, search for the next page that comes after the
|
|
//! 'last written' page.
|
|
//!
|
|
//! Note:
|
|
//! - This is the only routine that should ever tag a page as last written.
|
|
//! - This routine can be called at any time to force garbage collection at
|
|
//! opportune times (i.e in an idle task). For this scenario, use_page
|
|
//! described below should be 'false'.
|
|
//! @param free_page - Populated with a free page that is erased and available
|
|
//! to be written on. Value should initially be set to INVALID_PAGE if it's
|
|
//! the first page being allocated for a file. Afterward the value should be
|
|
//! the previously allocated page
|
|
//! @param use_gc_allocator - should be true iff the page should be allocated
|
|
//! from the region dedicated for garbage collection handling
|
|
//! @param use_page - should be true iff the page is about to be used in a file.
|
|
static status_t find_free_page(uint16_t *free_page, bool use_gc_allocator,
|
|
bool use_page) {
|
|
|
|
// if we are allocating a file from the garbage collection region,
|
|
// we don't need to search for free pages since we know what ones to use
|
|
if (use_gc_allocator) {
|
|
uint16_t next_page = (*free_page == INVALID_PAGE) ? s_gc_block.gc_start_page :
|
|
(*free_page + 1);
|
|
PBL_ASSERTN(s_gc_block.block_valid && (next_page >= s_gc_block.gc_start_page) &&
|
|
(next_page < (s_gc_block.gc_start_page + PFS_PAGES_PER_ERASE_SECTOR)));
|
|
*free_page = next_page;
|
|
return (S_SUCCESS);
|
|
}
|
|
|
|
uint16_t next_page = INVALID_PAGE;
|
|
uint16_t start_pg = (s_last_page_written + 1) % s_pfs_page_count;
|
|
uint16_t remaining_pgs_in_block = PFS_PAGES_PER_ERASE_SECTOR -
|
|
(start_pg % PFS_PAGES_PER_ERASE_SECTOR);
|
|
|
|
uint16_t gc_erase_region = s_gc_block.gc_start_page / PFS_PAGES_PER_ERASE_SECTOR;
|
|
bool in_gc_region = (gc_erase_region == (start_pg / PFS_PAGES_PER_ERASE_SECTOR));
|
|
|
|
// are we looking for free pages in the sector we last wrote to?
|
|
if (remaining_pgs_in_block < PFS_PAGES_PER_ERASE_SECTOR) {
|
|
// are any of the pages already erased?
|
|
for (uint16_t pg = 0; pg < remaining_pgs_in_block && !in_gc_region; pg++) {
|
|
uint16_t curr_page = start_pg + pg;
|
|
uint8_t page_flags = prv_get_page_flags(curr_page);
|
|
|
|
if (page_is_erased(page_flags)) {
|
|
next_page = curr_page;
|
|
break;
|
|
}
|
|
}
|
|
|
|
start_pg += remaining_pgs_in_block;
|
|
}
|
|
|
|
// we should now be processing on a sector aligned boundary
|
|
PBL_ASSERTN((start_pg % PFS_PAGES_PER_ERASE_SECTOR) == 0);
|
|
|
|
// if we could not find a free page in the sector we were previously using
|
|
// we need to scan through the erase regions and either perform some garbage
|
|
// collection or find an erased page in another erase region
|
|
if (next_page == INVALID_PAGE) {
|
|
int num_erase_regions = s_pfs_page_count / PFS_PAGES_PER_ERASE_SECTOR;
|
|
uint16_t start_region = start_pg / PFS_PAGES_PER_ERASE_SECTOR;
|
|
|
|
for (uint16_t region = 0; region < num_erase_regions; region++) {
|
|
uint16_t curr_region = (region + start_region) % num_erase_regions;
|
|
|
|
if (s_gc_block.block_valid && (gc_erase_region == curr_region)) {
|
|
// don't use pre-allocated garbage collection regions
|
|
continue;
|
|
}
|
|
|
|
uint32_t sectors_active = prv_get_sector_page_status(curr_region, &next_page);
|
|
if (next_page != INVALID_PAGE) {
|
|
// we have found a page which is already erased
|
|
break;
|
|
} else if (__builtin_popcount(sectors_active) < PFS_PAGES_PER_ERASE_SECTOR) {
|
|
// we can erase this region and have at least 1 free page after
|
|
uint16_t sector_start_pg = curr_region * PFS_PAGES_PER_ERASE_SECTOR;
|
|
garbage_collect_sector(&next_page, sector_start_pg, sectors_active);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (next_page != INVALID_PAGE) { // a free page was found
|
|
if (use_page) {
|
|
prv_update_last_written_page(next_page);
|
|
}
|
|
|
|
*free_page = s_last_page_written = next_page;
|
|
return (S_SUCCESS);
|
|
}
|
|
|
|
return (E_OUT_OF_STORAGE);
|
|
}
|
|
|
|
//! Note: expects that the caller does _not_ hold the pfs mutex
|
|
//! Note: If pages are already pre-erased on the FS, this routine will return
|
|
//! very quickly. If we need to do erases, it will take longer because this
|
|
//! operation can take seconds to complete on certain flash parts
|
|
//!
|
|
//! @param file_size - The amount of file space to erase
|
|
//! @param max_elapsed_ticks - The max amount of time to spend attempting to
|
|
//! find / create the free space. If 0, then there is no timeout
|
|
static void pfs_prepare_for_file_creation(uint32_t file_size,
|
|
uint32_t max_elapsed_ticks) {
|
|
uint16_t pages_to_find = (file_size + PFS_PAGE_SIZE) / PFS_PAGE_SIZE;
|
|
uint16_t free_page = 0;
|
|
|
|
uint32_t start_ticks = rtc_get_ticks();
|
|
|
|
uint16_t last_written_page = s_last_page_written;
|
|
while ((pages_to_find > 0) && (free_page != INVALID_PAGE)) {
|
|
mutex_lock_recursive(s_pfs_mutex);
|
|
find_free_page(&free_page, false, false);
|
|
mutex_unlock_recursive(s_pfs_mutex);
|
|
// TODO: might be nice to only sleep here if we had to perform GC as part
|
|
// of finding a free page
|
|
if ((pages_to_find % 4) == 0) {
|
|
psleep(2);
|
|
}
|
|
pages_to_find--;
|
|
|
|
uint32_t elapsed_ticks = rtc_get_ticks() - start_ticks;
|
|
if (max_elapsed_ticks != 0 && (elapsed_ticks > max_elapsed_ticks)) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
mutex_lock_recursive(s_pfs_mutex);
|
|
s_last_page_written = last_written_page; // reset our tracker
|
|
mutex_unlock_recursive(s_pfs_mutex);
|
|
}
|
|
|
|
// In the future, the next_page field may be updated dynamically (i.e to resize
|
|
// a file). Use a CRC to catch corruption issues in this field.
|
|
static uint8_t crc8_next_page(uint16_t next_page) {
|
|
return crc8_calculate_bytes((uint8_t*)&next_page, sizeof(next_page), true /* big_endian */);
|
|
}
|
|
|
|
static status_t get_next_page(uint16_t curr_page, uint16_t *next_page) {
|
|
PageHeader hdr;
|
|
prv_flash_read((uint8_t *)&hdr.next_page_crc, sizeof(hdr.next_page_crc) +
|
|
sizeof(hdr.next_page), prv_page_to_flash_offset(curr_page) +
|
|
offsetof(PageHeader, next_page_crc));
|
|
*next_page = hdr.next_page;
|
|
|
|
if (*next_page == INVALID_PAGE) {
|
|
return (S_NO_MORE_ITEMS);
|
|
}
|
|
|
|
if (crc8_next_page(*next_page) == hdr.next_page_crc) {
|
|
if (*next_page < s_pfs_page_count) {
|
|
return (S_SUCCESS);
|
|
}
|
|
}
|
|
|
|
return (E_INTERNAL); // the next page pointer is corrupt
|
|
}
|
|
|
|
static status_t unlink_flash_file(uint16_t page) {
|
|
uint16_t first_page = page;
|
|
if (page > s_pfs_page_count) { // should never happen
|
|
return (E_INTERNAL);
|
|
}
|
|
|
|
// Mark the files to indicate that they are ready to be erased
|
|
PageHeader hdr;
|
|
hdr.page_flags = 0xff;
|
|
SET_PAGE_FLAGS(hdr.page_flags, PAGE_FLAG_DELETED_PAGE);
|
|
int rv = S_SUCCESS;
|
|
int unlink_count = 0;
|
|
do {
|
|
if ((page > s_pfs_page_count) || (unlink_count > s_pfs_page_count)) {
|
|
rv = E_INTERNAL; // should never happen
|
|
break;
|
|
}
|
|
prv_flash_write((uint8_t *)&hdr.page_flags, sizeof(hdr.page_flags),
|
|
prv_page_to_flash_offset(page) + offsetof(PageHeader, page_flags));
|
|
|
|
unlink_count++;
|
|
} while (get_next_page(page, &page) == S_SUCCESS);
|
|
|
|
// Add a tag to indicate that all pages within a file have been marked for
|
|
// deletion we check for this during reboot to clean up a partial delete
|
|
update_curr_state(first_page, DELETE_STATE_OFFSET, DELETE_STATE_DONE);
|
|
|
|
return (rv);
|
|
}
|
|
|
|
static status_t create_flash_file(File *f) {
|
|
status_t rv;
|
|
uint16_t start_page = INVALID_PAGE;
|
|
|
|
PageHeader pg_hdr;
|
|
memset(&pg_hdr, 0xff, sizeof(pg_hdr));
|
|
|
|
bool use_gc_allocator = (strcmp(f->name, GC_FILE_NAME) == 0);
|
|
|
|
if ((rv = find_free_page(&start_page, use_gc_allocator, true)) != S_SUCCESS) {
|
|
return (rv);
|
|
}
|
|
|
|
pg_hdr.version = PFS_CUR_VERSION;
|
|
SET_PAGE_FLAGS(pg_hdr.page_flags,
|
|
PAGE_FLAG_START_PAGE | PAGE_FLAG_ERASED_PAGE);
|
|
|
|
// Note: We have already allocated 1 pg so just subtract 1 to roundup
|
|
// We assume all pages are the same size
|
|
int pgs_needed = (f->file_size + FILEDATA_LEN + strlen(f->name) - 1) /
|
|
free_bytes_in_page(start_page);
|
|
uint16_t curr_page = start_page;
|
|
uint16_t next_page = start_page;
|
|
|
|
for (; pgs_needed >= 0; pgs_needed--) {
|
|
// flag the page as in use
|
|
prv_flash_write((uint8_t *)&pg_hdr.page_flags, sizeof(pg_hdr.page_flags),
|
|
prv_page_to_flash_offset(curr_page) + offsetof(PageHeader, page_flags));
|
|
|
|
if (pgs_needed > 0) { // do we need to find a free page
|
|
if ((rv = find_free_page(&next_page, use_gc_allocator, true)) != S_SUCCESS) {
|
|
unlink_flash_file(start_page); // on failure, unallocate
|
|
return (rv);
|
|
}
|
|
pg_hdr.next_page_crc = crc8_next_page(next_page);
|
|
pg_hdr.next_page = next_page;
|
|
write_pg_header(&pg_hdr, curr_page);
|
|
curr_page = next_page;
|
|
|
|
// continuation page header settings
|
|
memset(&pg_hdr, 0xff, sizeof(PageHeader));
|
|
pg_hdr.version = PFS_CUR_VERSION;
|
|
SET_PAGE_FLAGS(pg_hdr.page_flags,
|
|
PAGE_FLAG_CONT_PAGE | PAGE_FLAG_ERASED_PAGE);
|
|
} else {
|
|
write_pg_header(&pg_hdr, curr_page);
|
|
break; // we are done
|
|
}
|
|
}
|
|
|
|
// we have succesfully allocated space for the file, so add file specific info
|
|
f->start_page = f->curr_page = start_page;
|
|
|
|
FileHeader file_hdr;
|
|
memset(&file_hdr, 0xff, sizeof(file_hdr));
|
|
file_hdr.file_namelen = strlen(f->name);
|
|
file_hdr.file_size = f->file_size;
|
|
file_hdr.file_type = f->file_type;
|
|
write_file_header(&file_hdr, start_page);
|
|
|
|
prv_flash_write((uint8_t *)f->name, strlen(f->name),
|
|
prv_page_to_flash_offset(start_page) + FILE_NAME_OFFSET);
|
|
|
|
if (!f->is_tmp) {
|
|
update_curr_state(f->start_page, TMP_STATE_OFFSET, TMP_STATE_DONE);
|
|
}
|
|
|
|
// finally, mark the creation as complete
|
|
update_curr_state(start_page, CREATE_STATE_OFFSET, CREATE_STATE_DONE);
|
|
|
|
return (S_SUCCESS);
|
|
}
|
|
|
|
static status_t scan_to_offset(File *f, uint32_t *pg_offset) {
|
|
uint32_t data_offset = f->offset + f->start_offset;
|
|
|
|
// a read or write could have ended at a page boundary so check for that
|
|
if ((f->curr_page == INVALID_PAGE) ||
|
|
((data_offset % free_bytes_in_page(f->curr_page)) == 0)) {
|
|
uint16_t next_page = f->start_page;
|
|
int pages_to_seek = (data_offset / free_bytes_in_page(f->start_page));
|
|
|
|
int closest_match = -1;
|
|
if (((f->op_flags & OP_FLAG_USE_PAGE_CACHE) != 0) && (f->pg_cache != NULL)) {
|
|
|
|
// Flash pages are singly linked together with the next pointer located
|
|
// on the current flash page. This means the optimal page to find in the
|
|
// cache is the one closest to what we are looking for without going past
|
|
// it
|
|
for (int i = 0; i < f->pg_cache_len; i++) {
|
|
FilePageCache *pgc = &f->pg_cache[i];
|
|
if (pgc->virtual_pg > pages_to_seek) {
|
|
continue;
|
|
} else if ((closest_match == -1) ||
|
|
(f->pg_cache[closest_match].virtual_pg < pgc->virtual_pg)) {
|
|
closest_match = i;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (closest_match != -1) {
|
|
FilePageCache *close_pg = &f->pg_cache[closest_match];
|
|
|
|
pages_to_seek -= f->pg_cache[closest_match].virtual_pg;
|
|
next_page = f->pg_cache[closest_match].physical_pg;
|
|
|
|
// if we still are not on the page we are looking for, see how
|
|
// many contiguous pages we can skip ahead.
|
|
if (pages_to_seek > 0) {
|
|
uint16_t contig_pgs = MIN(close_pg->contiguous_pgs, pages_to_seek);
|
|
pages_to_seek -= contig_pgs;
|
|
next_page += contig_pgs;
|
|
}
|
|
}
|
|
|
|
for (uint16_t i = 0; i < pages_to_seek; i++) {
|
|
if (get_next_page(next_page, &next_page) != S_SUCCESS) {
|
|
return (E_RANGE);
|
|
}
|
|
}
|
|
f->curr_page = next_page;
|
|
}
|
|
|
|
*pg_offset = data_offset % free_bytes_in_page(f->curr_page);
|
|
return (S_SUCCESS);
|
|
}
|
|
|
|
static int mark_fd_free(int fd) {
|
|
if (PFS_FD(fd).file.name != NULL) {
|
|
kernel_free(PFS_FD(fd).file.name);
|
|
PFS_FD(fd).file.name = NULL;
|
|
}
|
|
if (PFS_FD(fd).file.pg_cache != NULL) {
|
|
kernel_free(PFS_FD(fd).file.pg_cache);
|
|
PFS_FD(fd).file.pg_cache = NULL;
|
|
PFS_FD(fd).file.pg_cache_len = 0;
|
|
}
|
|
|
|
PFS_FD(fd).fd_status = FD_STATUS_FREE;
|
|
|
|
return (S_SUCCESS);
|
|
}
|
|
|
|
typedef enum {
|
|
FDBusy = 2,
|
|
FDAlreadyLoaded = 1,
|
|
FDAvail = 0,
|
|
NoFDAvail = -1
|
|
} AvailFdStatus;
|
|
|
|
//! @param is_tmp specified to indicate whether or not you are looking for
|
|
//! a tmp file
|
|
static AvailFdStatus get_avail_fd(const char *name, int *fdp, bool is_tmp) {
|
|
// First search to see if the fd has already been located
|
|
for (int fd = FD_INDEX_OFFSET; fd < FD_INDEX_OFFSET+MAX_FD_HANDLES; fd++) {
|
|
File *f = &PFS_FD(fd).file;
|
|
if ((f->is_tmp == is_tmp) && (f->name != NULL)) {
|
|
if (strcmp(f->name, name) == 0) {
|
|
PBL_ASSERTN(PFS_FD(fd).fd_status != FD_STATUS_FREE);
|
|
*fdp = fd;
|
|
return ((PFS_FD(fd).fd_status == FD_STATUS_IN_USE) ? FDBusy : FDAlreadyLoaded);
|
|
}
|
|
}
|
|
}
|
|
|
|
// a simple least-recently-accessed cache scheme
|
|
int unref = -1;
|
|
uint16_t curr_time_closed = 0;
|
|
|
|
for (int fd = FD_INDEX_OFFSET; fd < FD_INDEX_OFFSET+PFS_FD_SET_SIZE; fd++) {
|
|
if (PFS_FD(fd).fd_status == FD_STATUS_FREE) {
|
|
*fdp = fd;
|
|
return (FDAvail);
|
|
}
|
|
if (PFS_FD(fd).fd_status == FD_STATUS_UNREFERENCED) {
|
|
if ((unref == -1) || (PFS_FD(fd).time_closed < curr_time_closed)) {
|
|
unref = fd;
|
|
curr_time_closed = PFS_FD(fd).time_closed;
|
|
}
|
|
}
|
|
}
|
|
|
|
*fdp = unref;
|
|
if (unref != -1) {
|
|
mark_fd_free(unref); // clean up previous file state
|
|
}
|
|
|
|
return ((*fdp != -1) ? FDAvail : NoFDAvail);
|
|
}
|
|
|
|
/*
|
|
* Exported PFS APIs
|
|
*/
|
|
|
|
size_t pfs_get_file_size(int fd) {
|
|
mutex_lock_recursive(s_pfs_mutex);
|
|
|
|
size_t res = 0;
|
|
if (FD_VALID(fd)) {
|
|
res = PFS_FD(fd).file.file_size;
|
|
}
|
|
|
|
mutex_unlock_recursive(s_pfs_mutex);
|
|
return (res);
|
|
}
|
|
|
|
int pfs_read(int fd, void *buf_ptr, size_t size) {
|
|
uint8_t *buf = buf_ptr;
|
|
mutex_lock_recursive(s_pfs_mutex);
|
|
|
|
int res = E_UNKNOWN;
|
|
if (!FD_VALID(fd) || (buf == NULL) || (size == 0)) {
|
|
res = E_INVALID_ARGUMENT;
|
|
goto cleanup;
|
|
}
|
|
|
|
File *file = &PFS_FD(fd).file;
|
|
|
|
if ((file->op_flags & OP_FLAG_READ) == 0) {
|
|
res = E_INVALID_ARGUMENT;
|
|
goto cleanup;
|
|
}
|
|
|
|
if ((file->offset + size) > file->file_size) {
|
|
PBL_LOG(LOG_LEVEL_DEBUG, "Out of bound read at %d",
|
|
(int)(file->offset + size));
|
|
res = E_RANGE;
|
|
goto cleanup;
|
|
}
|
|
|
|
uint32_t pg_offset;
|
|
if (scan_to_offset(file, &pg_offset) != S_SUCCESS) {
|
|
res = E_INTERNAL;
|
|
goto cleanup;
|
|
}
|
|
|
|
// we have found the page from which to start reading data from
|
|
size_t bytes_read = 0;
|
|
while (bytes_read < size) {
|
|
size_t bytes_to_read = MIN(free_bytes_in_page(file->curr_page) - pg_offset,
|
|
size - bytes_read);
|
|
|
|
prv_flash_read(buf + bytes_read, bytes_to_read,
|
|
prv_page_to_flash_offset(file->curr_page) + AVAIL_BYTES_OFFSET + pg_offset);
|
|
|
|
bytes_read += bytes_to_read;
|
|
file->offset += bytes_to_read;
|
|
|
|
if (bytes_read == size) {
|
|
break; // we are done
|
|
}
|
|
|
|
pg_offset = 0; // first usable byte next page
|
|
if (get_next_page(file->curr_page, &file->curr_page) != S_SUCCESS) {
|
|
PBL_LOG(LOG_LEVEL_WARNING, "R:Couldn't find next page for %d",
|
|
file->curr_page);
|
|
res = E_INTERNAL;
|
|
goto cleanup;
|
|
}
|
|
}
|
|
|
|
res = bytes_read;
|
|
cleanup:
|
|
mutex_unlock_recursive(s_pfs_mutex);
|
|
return (res);
|
|
}
|
|
|
|
int pfs_seek(int fd, int offset, FSeekType seek_type) {
|
|
mutex_lock_recursive(s_pfs_mutex);
|
|
int res = E_UNKNOWN;
|
|
if (!FD_VALID(fd)) {
|
|
res = E_INVALID_ARGUMENT;
|
|
goto cleanup;
|
|
}
|
|
|
|
int new_offset = PFS_FD(fd).file.offset;
|
|
if (seek_type == FSeekSet) {
|
|
new_offset = offset;
|
|
} else if (seek_type == FSeekCur) {
|
|
new_offset += offset;
|
|
}
|
|
|
|
// allow one to seek to very EOF
|
|
if ((new_offset >= 0) &&
|
|
(new_offset <= (int)PFS_FD(fd).file.file_size)) {
|
|
|
|
if (PFS_FD(fd).file.offset != (uint32_t)new_offset) {
|
|
PFS_FD(fd).file.offset = (uint32_t)new_offset;
|
|
PFS_FD(fd).file.curr_page = INVALID_PAGE;
|
|
}
|
|
res = new_offset;
|
|
} else {
|
|
res = E_RANGE;
|
|
}
|
|
|
|
cleanup:
|
|
mutex_unlock_recursive(s_pfs_mutex);
|
|
return (res);
|
|
}
|
|
|
|
int pfs_write(int fd, const void *buf_ptr, size_t size) {
|
|
const uint8_t *buf = buf_ptr;
|
|
mutex_lock_recursive(s_pfs_mutex);
|
|
int res = E_UNKNOWN;
|
|
if (!FD_VALID(fd) || (buf == NULL) || (size == 0)) {
|
|
res = E_INVALID_ARGUMENT;
|
|
goto cleanup;
|
|
}
|
|
|
|
File *file = &PFS_FD(fd).file;
|
|
|
|
if ((file->op_flags & (OP_FLAG_WRITE | OP_FLAG_OVERWRITE)) == 0) {
|
|
res = E_INVALID_ARGUMENT;
|
|
goto cleanup;
|
|
}
|
|
|
|
if ((file->offset + size) > file->file_size) {
|
|
res = E_RANGE;
|
|
goto cleanup;
|
|
}
|
|
|
|
uint32_t pg_offset;
|
|
if (scan_to_offset(file, &pg_offset) != S_SUCCESS) {
|
|
res = E_INTERNAL;
|
|
goto cleanup;
|
|
}
|
|
|
|
size_t bytes_written = 0;
|
|
while (bytes_written < size) {
|
|
size_t bytes_to_write = MIN(free_bytes_in_page(file->curr_page) - pg_offset,
|
|
size - bytes_written);
|
|
|
|
prv_flash_write(buf + bytes_written, bytes_to_write,
|
|
prv_page_to_flash_offset(file->curr_page) + AVAIL_BYTES_OFFSET + pg_offset);
|
|
|
|
bytes_written += bytes_to_write;
|
|
file->offset += bytes_to_write;
|
|
|
|
if (bytes_written == size) {
|
|
break;
|
|
}
|
|
|
|
pg_offset = 0; // first usable byte next page
|
|
if (get_next_page(file->curr_page, &file->curr_page) != S_SUCCESS) {
|
|
PBL_LOG(LOG_LEVEL_WARNING, "W:Couldn't find next page for %d",
|
|
file->curr_page);
|
|
res = E_INTERNAL;
|
|
goto cleanup;
|
|
}
|
|
}
|
|
|
|
res = bytes_written;
|
|
cleanup:
|
|
mutex_unlock_recursive(s_pfs_mutex);
|
|
return (res);
|
|
}
|
|
|
|
uint32_t pfs_get_size(void) {
|
|
// one sector is needed for internal book keeping
|
|
return s_pfs_size - GC_REGION_SIZE;
|
|
}
|
|
|
|
void pfs_set_size(uint32_t new_size, bool new_region_erased) {
|
|
uint32_t prev_size = s_pfs_size;
|
|
s_pfs_size = new_size;
|
|
s_pfs_page_count = new_size / PFS_PAGE_SIZE;
|
|
|
|
// re-build the flags cache
|
|
prv_build_page_flags_cache();
|
|
|
|
if (new_region_erased) {
|
|
prv_write_erased_header_on_page_range((prev_size/PFS_PAGE_SIZE),
|
|
(new_size/PFS_PAGE_SIZE), 1);
|
|
}
|
|
|
|
update_last_written_page();
|
|
}
|
|
|
|
bool pfs_active_in_region(uint32_t start_address, uint32_t ending_address) {
|
|
uint16_t starting_page = start_address / PFS_PAGE_SIZE;
|
|
uint16_t ending_page = (ending_address) / PFS_PAGE_SIZE;
|
|
|
|
for (uint16_t pg = starting_page; pg < ending_page; pg++) {
|
|
|
|
PageHeader hdr;
|
|
|
|
// read version first, check magic, then check version and make sure it makes sense
|
|
prv_flash_read((uint8_t *)&hdr.version, sizeof(hdr.version),
|
|
prv_page_to_flash_offset(pg) + offsetof(PageHeader, version));
|
|
|
|
if ((hdr.version >> 8) != PFS_MAGIC) {
|
|
continue;
|
|
}
|
|
|
|
if (hdr.version > PFS_CUR_VERSION) {
|
|
PBL_LOG(LOG_LEVEL_WARNING, "Incompatible version of PFS active, 0x%x",
|
|
hdr.version);
|
|
|
|
// pfs filesystem is a newer version than we support
|
|
return (false);
|
|
}
|
|
|
|
hdr.page_flags = prv_get_page_flags(pg);
|
|
// read the header flags to see if the page is a file start page or an erased page
|
|
if (IS_PAGE_TYPE(hdr.page_flags, PAGE_FLAG_ERASED_PAGE)
|
|
|| IS_PAGE_TYPE(hdr.page_flags, PAGE_FLAG_START_PAGE)
|
|
|| IS_PAGE_TYPE(hdr.page_flags, PAGE_FLAG_CONT_PAGE)
|
|
|| page_is_deleted(hdr.page_flags)) {
|
|
|
|
return (true);
|
|
}
|
|
}
|
|
|
|
// pfs filesystem is not active
|
|
return (false);
|
|
}
|
|
|
|
// migration utility
|
|
// Returns true if valid PFS file found, false otherwise
|
|
bool pfs_active(void) {
|
|
return pfs_active_in_region(0, s_pfs_size);
|
|
}
|
|
|
|
// Scans through the filesystem to see if we rebooted while a file was in the
|
|
// middle of being created and cleans up these partial files.
|
|
void pfs_reboot_cleanup(void) {
|
|
static uint16_t curr_pg = 0;
|
|
|
|
for (; curr_pg < s_pfs_page_count; curr_pg++) {
|
|
uint8_t page_flags = prv_get_page_flags(curr_pg);
|
|
|
|
if (IS_PAGE_TYPE(page_flags, PAGE_FLAG_START_PAGE)) {
|
|
if (!is_create_complete(curr_pg)) { // make sure file creation completed
|
|
PBL_LOG(LOG_LEVEL_WARNING, "File at %d creation did not complete ",
|
|
curr_pg);
|
|
unlink_flash_file(curr_pg);
|
|
} else if (is_tmp_file(curr_pg)) { // make sure this isn't a temp file
|
|
PBL_LOG(LOG_LEVEL_WARNING, "Removing temp file at %d", curr_pg);
|
|
unlink_flash_file(curr_pg);
|
|
}
|
|
} else if (page_type_bits_set(page_flags, DELETED_START_PAGE_MASK) &&
|
|
!is_delete_complete(curr_pg)) {
|
|
PBL_LOG(LOG_LEVEL_WARNING, "Delete of %d did not complete", curr_pg);
|
|
unlink_flash_file(curr_pg);
|
|
}
|
|
}
|
|
|
|
update_last_written_page();
|
|
}
|
|
|
|
static void prv_handle_sector_erase(uint16_t start_page, bool update_erase_count) {
|
|
if (!update_erase_count) {
|
|
prv_flash_erase_sector(start_page);
|
|
return;
|
|
}
|
|
|
|
uint16_t max_erase = 0;
|
|
uint16_t last_written_pg = INVALID_PAGE;
|
|
PageHeader hdr;
|
|
for (int i = 0; i < PFS_PAGES_PER_ERASE_SECTOR; i++) {
|
|
get_updated_erase_hdr(&hdr, i + start_page);
|
|
if (hdr.erase_count > max_erase) {
|
|
max_erase = hdr.erase_count;
|
|
}
|
|
|
|
if (hdr.last_written == LAST_WRITTEN_TAG) {
|
|
last_written_pg = i + start_page;
|
|
}
|
|
}
|
|
|
|
prv_flash_erase_sector(start_page);
|
|
prv_write_erased_header_on_page_range(start_page,
|
|
start_page + PFS_PAGES_PER_ERASE_SECTOR, max_erase);
|
|
|
|
if (last_written_pg != INVALID_PAGE) {
|
|
hdr.last_written = LAST_WRITTEN_TAG;
|
|
prv_flash_write((uint8_t *)&hdr.last_written, sizeof(hdr.last_written),
|
|
prv_page_to_flash_offset(last_written_pg) +
|
|
offsetof(PageHeader, last_written));
|
|
}
|
|
}
|
|
|
|
static bool prv_update_gc_reserved_region(void) {
|
|
if (!s_gc_block.block_valid || (s_gc_block.block_writes > 5)) {
|
|
int free_region_start = prv_find_free_erase_region(s_gc_block.block_valid);
|
|
|
|
if (free_region_start >= 0) {
|
|
s_gc_block = (GCBlock) {
|
|
.block_valid = true,
|
|
.block_writes = 0,
|
|
.gc_start_page = free_region_start
|
|
};
|
|
PBL_LOG(LOG_LEVEL_DEBUG, "New Erase Region: %d", s_gc_block.gc_start_page);
|
|
return (true);
|
|
}
|
|
|
|
return (false);
|
|
}
|
|
|
|
return (true); // gc block must be valid to get here
|
|
}
|
|
|
|
static bool watch_list_find_str(ListNode *node, void *data) {
|
|
PFSFileChangedCallbackNode *filechg_node = (PFSFileChangedCallbackNode *)node;
|
|
return (strcmp(filechg_node->name, (char *)data) == 0);
|
|
}
|
|
|
|
PFSCallbackHandle pfs_watch_file(const char* filename, PFSFileChangedCallback callback,
|
|
uint8_t event_flags, void* data) {
|
|
mutex_lock_recursive(s_pfs_mutex);
|
|
|
|
PFSFileChangedCallbackNode *node = kernel_malloc_check(sizeof(PFSFileChangedCallbackNode));
|
|
*node = (PFSFileChangedCallbackNode) {
|
|
.callback = callback,
|
|
.event_flags = event_flags,
|
|
.data = data
|
|
};
|
|
|
|
// find out if we already have a string for this particular filename
|
|
ListNode *find_str = list_find(s_head_callback_node_list, watch_list_find_str, (char *)filename);
|
|
if (find_str == NULL) {
|
|
node->name = kernel_strdup_check(filename);
|
|
} else {
|
|
node->name = ((PFSFileChangedCallbackNode *)find_str)->name;
|
|
}
|
|
|
|
s_head_callback_node_list = list_prepend(s_head_callback_node_list, &node->list_node);
|
|
mutex_unlock_recursive(s_pfs_mutex);
|
|
|
|
return node;
|
|
}
|
|
|
|
void pfs_unwatch_file(PFSCallbackHandle cb_handle) {
|
|
mutex_lock_recursive(s_pfs_mutex);
|
|
|
|
PFSFileChangedCallbackNode *callback_node = (PFSFileChangedCallbackNode *)cb_handle;
|
|
|
|
PBL_ASSERTN(callback_node->list_node.next != NULL || callback_node->list_node.prev != NULL
|
|
|| s_head_callback_node_list == &callback_node->list_node);
|
|
PBL_ASSERTN(list_contains(s_head_callback_node_list, &callback_node->list_node));
|
|
list_remove(&callback_node->list_node, &(s_head_callback_node_list), NULL);
|
|
|
|
// if no one is watching the file anymore, free the string
|
|
ListNode *find_str = list_find(s_head_callback_node_list, watch_list_find_str,
|
|
(char *)callback_node->name);
|
|
if (find_str == NULL) {
|
|
kernel_free((void *)(callback_node->name));
|
|
}
|
|
|
|
kernel_free(callback_node);
|
|
|
|
mutex_unlock_recursive(s_pfs_mutex);
|
|
}
|
|
|
|
// IMPORTANT: This call assumes that the caller has already grabbed s_pfs_mutex
|
|
static void prv_invoke_watch_file_callbacks(const char* file_name, uint8_t event) {
|
|
PFSFileChangedCallbackNode *callback_node =
|
|
(PFSFileChangedCallbackNode *)s_head_callback_node_list;
|
|
while (callback_node) {
|
|
if (!strcmp(callback_node->name, file_name) && (callback_node->event_flags & event)) {
|
|
callback_node->callback(callback_node->data);
|
|
}
|
|
callback_node = (PFSFileChangedCallbackNode *)list_get_next(&callback_node->list_node);
|
|
}
|
|
}
|
|
|
|
status_t pfs_close(int fd) {
|
|
mutex_lock_recursive(s_pfs_mutex);
|
|
|
|
int res = E_UNKNOWN;
|
|
if (!FD_VALID(fd)) {
|
|
res = E_INVALID_ARGUMENT;
|
|
goto cleanup;
|
|
}
|
|
|
|
File *f = &PFS_FD(fd).file;
|
|
if (f->is_tmp) {
|
|
// TODO: For safety, could disallow this op if user has orig file hdl open
|
|
pfs_remove(f->name);
|
|
// Note: if we reboot before updating the tmp state flag to done, the tmp &
|
|
// original file will be deleted. This is an extremely small window, but
|
|
// could be resolved by checking on reboot to see if both versions exist.
|
|
// If both exist, the orig is valid. Iff tmp exists, the tmp file is valid
|
|
update_curr_state(f->start_page, TMP_STATE_OFFSET, TMP_STATE_DONE);
|
|
f->is_tmp = false;
|
|
}
|
|
|
|
// Note: We don't free f->name here because we keep the file metadata
|
|
// (including the name, so we can detect hits) in the cache until we
|
|
// have to evict it to make room for a new file.
|
|
|
|
PFS_FD(fd).fd_status = FD_STATUS_UNREFERENCED;
|
|
PFS_FD(fd).time_closed = time_closed_counter++;
|
|
|
|
// If this file was modified, invoke the callbacks
|
|
if (f->op_flags & (OP_FLAG_WRITE | OP_FLAG_OVERWRITE)) {
|
|
// IMPORTANT: prv_invoke_watch_file_callbacks assumes that we already have s_pfs_mutex
|
|
prv_invoke_watch_file_callbacks(f->name, FILE_CHANGED_EVENT_CLOSED);
|
|
}
|
|
|
|
res = S_SUCCESS;
|
|
cleanup:
|
|
mutex_unlock_recursive(s_pfs_mutex);
|
|
return (res);
|
|
}
|
|
|
|
status_t pfs_close_and_remove(int fd) {
|
|
mutex_lock_recursive(s_pfs_mutex);
|
|
|
|
status_t res = E_UNKNOWN;
|
|
if (!FD_VALID(fd)) {
|
|
res = E_INVALID_ARGUMENT;
|
|
} else {
|
|
File *f = &PFS_FD(fd).file;
|
|
char file_name[f->namelen + 1];
|
|
file_name[f->namelen] = '\0';
|
|
memcpy(file_name, f->name, f->namelen);
|
|
|
|
if ((res = pfs_close(fd)) >= 0) {
|
|
res = pfs_remove(file_name);
|
|
}
|
|
}
|
|
|
|
mutex_unlock_recursive(s_pfs_mutex);
|
|
return (res);
|
|
}
|
|
|
|
status_t pfs_remove(const char *name) {
|
|
if (name == NULL) {
|
|
return E_INVALID_ARGUMENT;
|
|
}
|
|
size_t namelen = strlen(name);
|
|
if ((namelen < 1) || (namelen > FILE_MAX_NAME_LEN)) {
|
|
return (E_INVALID_ARGUMENT);
|
|
}
|
|
|
|
mutex_lock_recursive(s_pfs_mutex);
|
|
uint16_t page = 0;
|
|
int fd;
|
|
status_t rv = get_avail_fd(name, &fd, false);
|
|
if (rv >= FDAlreadyLoaded) { // the file is in the cache
|
|
if (rv == FDBusy) {
|
|
PBL_CROAK("Cannot delete %s, it is currently in use",
|
|
PFS_FD(fd).file.name);
|
|
}
|
|
page = PFS_FD(fd).file.start_page;
|
|
mark_fd_free(fd);
|
|
} else if ((rv = locate_flash_file(name, &page)) != S_SUCCESS) {
|
|
goto cleanup; // could not find the file on flash
|
|
}
|
|
|
|
rv = unlink_flash_file(page);
|
|
// IMPORTANT: prv_invoke_watch_file_callbacks assumes that we already have s_pfs_mutex
|
|
prv_invoke_watch_file_callbacks(name, FILE_CHANGED_EVENT_REMOVED);
|
|
cleanup:
|
|
mutex_unlock_recursive(s_pfs_mutex);
|
|
return (rv);
|
|
}
|
|
|
|
PFSFileListEntry *pfs_create_file_list(PFSFilenameTestCallback callback) {
|
|
ListNode *head = NULL;
|
|
|
|
mutex_lock_recursive(s_pfs_mutex);
|
|
|
|
const int file_namelen_offset = FILEHEADER_OFFSET + offsetof(FileHeader, file_namelen);
|
|
|
|
for (uint16_t pg = 0; pg < s_pfs_page_count; pg++) {
|
|
PageHeader pg_hdr;
|
|
pg_hdr.page_flags = prv_get_page_flags(pg);
|
|
|
|
if (!IS_PAGE_TYPE(pg_hdr.page_flags, PAGE_FLAG_START_PAGE)) {
|
|
continue; // only start pages contain file name info
|
|
}
|
|
|
|
FileHeader file_hdr;
|
|
prv_flash_read((uint8_t *)&file_hdr.file_namelen, sizeof(file_hdr.file_namelen),
|
|
prv_page_to_flash_offset(pg) + file_namelen_offset);
|
|
|
|
char file_name[file_hdr.file_namelen + 1];
|
|
prv_flash_read((uint8_t *)file_name, file_hdr.file_namelen,
|
|
prv_page_to_flash_offset(pg) + FILE_NAME_OFFSET);
|
|
file_name[file_hdr.file_namelen] = 0;
|
|
|
|
if (callback && !callback(file_name)) {
|
|
// Don't include
|
|
continue;
|
|
}
|
|
|
|
// Make sure the rest of the page header contents are valid. We are doing this after the
|
|
// filename filter call because it requires more flash reads and is likely slower than the
|
|
// filter call.
|
|
if (read_header(pg, &pg_hdr, &file_hdr) != PageAndFileHdrValid) {
|
|
PBL_LOG(LOG_LEVEL_WARNING, "%d: Invalid page/file header", pg);
|
|
continue;
|
|
}
|
|
|
|
// Add a new entry
|
|
PFSFileListEntry *entry = kernel_malloc_check(sizeof(PFSFileListEntry)
|
|
+ file_hdr.file_namelen + 1);
|
|
*entry = (PFSFileListEntry) {};
|
|
strcpy(entry->name, file_name);
|
|
head = list_insert_before(head, &entry->list_node);
|
|
}
|
|
mutex_unlock_recursive(s_pfs_mutex);
|
|
return (PFSFileListEntry *)head;
|
|
}
|
|
|
|
void pfs_delete_file_list(PFSFileListEntry *head) {
|
|
ListNode *node = (ListNode *)head;
|
|
ListNode *next;
|
|
while (node) {
|
|
next = node->next;
|
|
kernel_free(node);
|
|
node = next;
|
|
}
|
|
}
|
|
|
|
// PBL-19098 Refactor this to share code with pfs_create_file_list
|
|
void pfs_remove_files(PFSFilenameTestCallback callback) {
|
|
mutex_lock_recursive(s_pfs_mutex);
|
|
|
|
const int file_namelen_offset = FILEHEADER_OFFSET + offsetof(FileHeader, file_namelen);
|
|
|
|
for (uint16_t pg = 0; pg < s_pfs_page_count; pg++) {
|
|
PageHeader pg_hdr;
|
|
pg_hdr.page_flags = prv_get_page_flags(pg);
|
|
|
|
if (!IS_PAGE_TYPE(pg_hdr.page_flags, PAGE_FLAG_START_PAGE)) {
|
|
continue; // only start pages contain file name info
|
|
}
|
|
|
|
FileHeader file_hdr;
|
|
prv_flash_read((uint8_t *)&file_hdr.file_namelen, sizeof(file_hdr.file_namelen),
|
|
prv_page_to_flash_offset(pg) + file_namelen_offset);
|
|
|
|
char file_name[file_hdr.file_namelen + 1];
|
|
prv_flash_read((uint8_t *)file_name, file_hdr.file_namelen,
|
|
prv_page_to_flash_offset(pg) + FILE_NAME_OFFSET);
|
|
file_name[file_hdr.file_namelen] = 0;
|
|
|
|
if (callback && !callback(file_name)) {
|
|
// Don't include
|
|
continue;
|
|
}
|
|
|
|
// Make sure the rest of the page header contents are valid. We are doing this after the
|
|
// filename filter call because it requires more flash reads and is likely slower than the
|
|
// filter call.
|
|
if (read_header(pg, &pg_hdr, &file_hdr) != PageAndFileHdrValid) {
|
|
PBL_LOG(LOG_LEVEL_WARNING, "%d: Invalid page/file header", pg);
|
|
continue;
|
|
}
|
|
|
|
int fd;
|
|
status_t rv = get_avail_fd(file_name, &fd, false);
|
|
if (rv >= FDAlreadyLoaded) { // the file is in the cache
|
|
if (rv == FDBusy) {
|
|
PBL_CROAK("Cannot delete %s, it is currently in use",
|
|
s_pfs_avail_fd[fd].file.name);
|
|
}
|
|
mark_fd_free(fd);
|
|
}
|
|
|
|
unlink_flash_file(pg);
|
|
// IMPORTANT: prv_invoke_watch_file_callbacks assumes that we already have s_pfs_mutex
|
|
prv_invoke_watch_file_callbacks(file_name, FILE_CHANGED_EVENT_REMOVED);
|
|
}
|
|
mutex_unlock_recursive(s_pfs_mutex);
|
|
}
|
|
|
|
#define MAX_PAGE_CACHE_ENTRIES 10 // 6 bytes per entry
|
|
static void update_page_cache(FilePageCache *fpc, int *cur_idx,
|
|
FilePageCache *toadd) {
|
|
|
|
int optimal_idx = *cur_idx;
|
|
if ((*cur_idx) == MAX_PAGE_CACHE_ENTRIES) {
|
|
// default index to overwrite if nothing better is found
|
|
optimal_idx = MAX_PAGE_CACHE_ENTRIES - 1;
|
|
|
|
uint8_t contiguous_pgs = fpc[0].contiguous_pgs;
|
|
|
|
// find the entry with the smallest number of sequential pages as this
|
|
// will be the best page to remove from the cache
|
|
for (int i = 0; i < MAX_PAGE_CACHE_ENTRIES; i++) {
|
|
if (fpc[i].contiguous_pgs < contiguous_pgs) {
|
|
optimal_idx = i;
|
|
contiguous_pgs = fpc[i].contiguous_pgs;
|
|
}
|
|
}
|
|
|
|
// only kick the current cache entry if it's worse than the one
|
|
// we are adding
|
|
if (fpc[optimal_idx].contiguous_pgs > toadd->contiguous_pgs) {
|
|
return;
|
|
}
|
|
} else {
|
|
(*cur_idx)++; // we are adding a new entry
|
|
}
|
|
|
|
fpc[optimal_idx] = *toadd;
|
|
}
|
|
|
|
static NOINLINE void allocate_page_cache(int fd) {
|
|
File *f = &PFS_FD(fd).file;
|
|
|
|
if (f->pg_cache != NULL) {
|
|
return; // already cached
|
|
}
|
|
|
|
if ((f->file_size / free_bytes_in_page(f->start_page)) < 1) {
|
|
return; // only one page in use so we don't need to cache anything
|
|
}
|
|
|
|
// Note: If there was more space for statics or stack space we could
|
|
// put this temporary buffer there
|
|
FilePageCache *fpc =
|
|
kernel_malloc_check(sizeof(FilePageCache) * MAX_PAGE_CACHE_ENTRIES);
|
|
memset(fpc, 0x00, sizeof(FilePageCache) * MAX_PAGE_CACHE_ENTRIES);
|
|
|
|
uint16_t virtual_pg = 0;
|
|
uint16_t curr_page = f->start_page;
|
|
uint16_t next_page;
|
|
int cur_idx = 0;
|
|
|
|
FilePageCache curr = {
|
|
.virtual_pg = 0,
|
|
.physical_pg = f->start_page,
|
|
.contiguous_pgs = 0
|
|
};
|
|
|
|
while (get_next_page(curr_page, &next_page) == S_SUCCESS) {
|
|
if (next_page == (curr_page + 1)) {
|
|
curr.contiguous_pgs++;
|
|
} else {
|
|
update_page_cache(&fpc[0], &cur_idx, &curr);
|
|
|
|
// reset logic for next entry
|
|
curr.virtual_pg = virtual_pg + 1;
|
|
curr.physical_pg = next_page;
|
|
curr.contiguous_pgs = 0;
|
|
}
|
|
|
|
curr_page = next_page;
|
|
virtual_pg++;
|
|
}
|
|
|
|
// see if the last set should be added to the cache
|
|
update_page_cache(&fpc[0], &cur_idx, &curr);
|
|
|
|
// The cache is likely to be around for a while and there is no reason to
|
|
// burn up more memory than necessary for a long duration
|
|
f->pg_cache = kernel_malloc(sizeof(FilePageCache) * cur_idx);
|
|
if (f->pg_cache != NULL) { // if we are not OOM
|
|
memcpy(f->pg_cache, fpc, sizeof(FilePageCache) * cur_idx);
|
|
f->pg_cache_len = cur_idx;
|
|
}
|
|
|
|
kernel_free(fpc);
|
|
}
|
|
|
|
///
|
|
/// Helper routines for pfs_open()
|
|
///
|
|
|
|
//! Returns true iff the file is found in the cache and the fd is ready to use
|
|
//! fd_used >= 0 if we were able to allocate a fd for the file (regardless of
|
|
//! whether or not its in the cache), else it reflects the error code
|
|
static NOINLINE bool file_found_in_cache(const char *name, uint8_t op_flags, int *fd_used) {
|
|
int fd, res;
|
|
bool is_tmp = ((op_flags & OP_FLAG_OVERWRITE) != 0);
|
|
bool file_found = false;
|
|
|
|
if ((res = get_avail_fd(name, &fd, is_tmp)) == NoFDAvail) {
|
|
res = E_OUT_OF_RESOURCES;
|
|
goto cleanup;
|
|
} else if (res == FDBusy) {
|
|
res = E_BUSY; // the file is already open
|
|
goto cleanup;
|
|
}
|
|
|
|
File *file = &PFS_FD(fd).file;
|
|
|
|
// settings for cached & new fds
|
|
file->op_flags = op_flags;
|
|
file->offset = 0; // (re)set seek position
|
|
file->is_tmp = is_tmp;
|
|
|
|
if (res == FDAlreadyLoaded) { // we found the FD in cache!
|
|
file->curr_page = file->start_page;
|
|
|
|
bool perform_crc_check = (op_flags & OP_FLAG_SKIP_HDR_CRC_CHECK) == 0;
|
|
if (perform_crc_check) {
|
|
// make sure the header is not corrupted
|
|
PageHeader pg_hdr;
|
|
FileHeader file_hdr;
|
|
if ((res = read_header(file->start_page, &pg_hdr, &file_hdr)) !=
|
|
PageAndFileHdrValid) {
|
|
mark_fd_free(fd); // file has been corrupted so clear fd
|
|
goto cleanup;
|
|
}
|
|
}
|
|
|
|
PFS_FD(fd).fd_status = FD_STATUS_IN_USE;
|
|
file_found = true;
|
|
}
|
|
|
|
cleanup:
|
|
*fd_used = (res >= 0) ? fd : res;
|
|
return (file_found);
|
|
}
|
|
|
|
// handles the creation of a file which was not previously on the FS
|
|
static NOINLINE status_t pfs_open_handle_create_request(int fd, uint8_t file_type,
|
|
size_t start_size) {
|
|
|
|
if (!VALID_TYPE(file_type) || (start_size == 0)) {
|
|
return (E_INVALID_ARGUMENT);
|
|
}
|
|
|
|
File *file = &PFS_FD(fd).file;
|
|
file->file_size = start_size;
|
|
file->file_type = file_type;
|
|
|
|
// temporarily mark the file as in use so no one tries to use the fd once we
|
|
// release the lock
|
|
if (fd != GC_FD_HANDLE_ID) {
|
|
FileDesc *file_desc = &PFS_FD(fd);
|
|
uint8_t curr_status = file_desc->fd_status;
|
|
file_desc->fd_status = FD_STATUS_IN_USE;
|
|
mutex_unlock_recursive(s_pfs_mutex);
|
|
pfs_prepare_for_file_creation(start_size, 0 /* no timeout */);
|
|
mutex_lock_recursive(s_pfs_mutex);
|
|
file_desc->fd_status = curr_status;
|
|
}
|
|
|
|
int res = create_flash_file(file);
|
|
return (res);
|
|
}
|
|
|
|
// given the fd and start page of a file, loads file description with relevent
|
|
// info about file so it can be read from
|
|
static NOINLINE status_t pfs_open_handle_read_request(int fd, uint16_t page) {
|
|
PageHeader pg_hdr;
|
|
FileHeader file_hdr;
|
|
int hdr_rv;
|
|
|
|
if ((hdr_rv = read_header(page, &pg_hdr, &file_hdr)) == PageAndFileHdrValid) {
|
|
File *file = &PFS_FD(fd).file;
|
|
file->file_size = file_hdr.file_size;
|
|
file->file_type = file_hdr.file_type;
|
|
file->start_page = file->curr_page = page;
|
|
return (S_SUCCESS);
|
|
}
|
|
|
|
PBL_LOG(LOG_LEVEL_WARNING, "Could not read header %d", hdr_rv);
|
|
return (E_INTERNAL);
|
|
}
|
|
|
|
static int file_found_or_added_to_pfs(int fd, const char *name,
|
|
uint8_t op_flags, uint8_t file_type, size_t start_size) {
|
|
|
|
uint16_t page = 0;
|
|
int res = locate_flash_file(name, &page);
|
|
|
|
if ((res != S_SUCCESS) && (res != E_DOES_NOT_EXIST)) { // unexpected error
|
|
goto cleanup;
|
|
}
|
|
|
|
// check to see if we are trying to read the file and it doesn't exist
|
|
bool is_read_only = (op_flags & (OP_FLAG_READ | OP_FLAG_WRITE |
|
|
OP_FLAG_OVERWRITE)) == OP_FLAG_READ;
|
|
bool is_tmp = ((op_flags & OP_FLAG_OVERWRITE) != 0);
|
|
if ((is_read_only || is_tmp) && (res == E_DOES_NOT_EXIST)) {
|
|
goto cleanup;
|
|
}
|
|
|
|
// Prepare the new FD
|
|
FileDesc *file_desc = &PFS_FD(fd);
|
|
File *file = &PFS_FD(fd).file;
|
|
|
|
file_desc->fd_status = FD_STATUS_UNREFERENCED; // set to IN_USE on success
|
|
if ((file->name = kernel_strdup(name)) == NULL) {
|
|
res = E_OUT_OF_MEMORY;
|
|
goto cleanup;
|
|
}
|
|
file->namelen = strlen(name);
|
|
file->start_offset = FILEDATA_LEN + file->namelen;
|
|
|
|
if (is_tmp || ((res == E_DOES_NOT_EXIST) && ((op_flags & OP_FLAG_WRITE) != 0))) {
|
|
res = pfs_open_handle_create_request(fd, file_type, start_size);
|
|
} else if ((op_flags & OP_FLAG_READ) != 0) {
|
|
res = pfs_open_handle_read_request(fd, page);
|
|
} else { // unexpected situation
|
|
res = E_INTERNAL;
|
|
}
|
|
|
|
cleanup:
|
|
if (res < S_SUCCESS) {
|
|
mark_fd_free(fd);
|
|
} else {
|
|
PFS_FD(fd).fd_status = FD_STATUS_IN_USE;
|
|
}
|
|
return (res);
|
|
}
|
|
|
|
int pfs_open(const char *name, uint8_t op_flags, uint8_t file_type,
|
|
size_t start_size) {
|
|
|
|
size_t namelen = (name == NULL) ? 0 : strlen(name);
|
|
if ((namelen < 1) || (namelen > FILE_MAX_NAME_LEN)) {
|
|
return (E_INVALID_ARGUMENT);
|
|
}
|
|
|
|
mutex_lock_recursive(s_pfs_mutex);
|
|
int res;
|
|
// if the file is in the cache or we encountered a failure we are done
|
|
if (file_found_in_cache(name, op_flags, &res) || (res < S_SUCCESS)) {
|
|
goto cleanup;
|
|
}
|
|
|
|
// The file is not in the cache, let's see if it's on the filesystem
|
|
int fd = res;
|
|
if ((res = file_found_or_added_to_pfs(fd, name, op_flags, file_type,
|
|
start_size)) >= S_SUCCESS) {
|
|
res = fd; // success so return the fd
|
|
}
|
|
|
|
cleanup:
|
|
if (res >= S_SUCCESS) {
|
|
// we are returning a valid file handle so if the user has asked for the
|
|
// page translations to be cached let's do that now
|
|
if ((op_flags & OP_FLAG_USE_PAGE_CACHE) != 0) {
|
|
allocate_page_cache(res);
|
|
}
|
|
// check to see if we should update the gc block
|
|
prv_update_gc_reserved_region();
|
|
}
|
|
|
|
mutex_unlock_recursive(s_pfs_mutex);
|
|
return (res);
|
|
}
|
|
|
|
static int pfs_open_gc_file(uint32_t space_needed, bool create) {
|
|
int fd = GC_FD_HANDLE_ID; // the gc fd follows the avail fd
|
|
File *file = &PFS_FD(fd).file;
|
|
|
|
// settings for cached & new fds
|
|
file->op_flags = OP_FLAG_READ;
|
|
if (create) {
|
|
file->op_flags |= OP_FLAG_WRITE;
|
|
}
|
|
file->offset = 0; // (re)set seek position
|
|
file->is_tmp = false;
|
|
|
|
if (s_gc_block.block_valid && create) {
|
|
prv_flash_erase_sector(s_gc_block.gc_start_page);
|
|
}
|
|
|
|
int res = file_found_or_added_to_pfs(fd, GC_FILE_NAME, file->op_flags,
|
|
FILE_TYPE_STATIC, space_needed);
|
|
|
|
PBL_ASSERTN(!create || res >= 0); // we are toast if we cannot create the file
|
|
return (res >= 0) ? fd : res;
|
|
}
|
|
|
|
static status_t copy_or_recover_gc_data(int fd, GCData *gcdata, bool do_copy) {
|
|
//////
|
|
//
|
|
// GC File Format
|
|
//
|
|
// GCData
|
|
// Page 0 Header | Data Len | Data
|
|
// ...
|
|
// Page N Header | Data Len | Data
|
|
//
|
|
//////
|
|
uint32_t sector_start_page = gcdata->gc_start_page;
|
|
uint32_t sectors_active = gcdata->page_mask;
|
|
|
|
// copy the entire block to file (could use bss?)
|
|
const size_t copy_buf_size = 256;
|
|
uint8_t *buf = kernel_malloc_check(copy_buf_size);
|
|
|
|
for (uint16_t pg = 0; pg < PFS_PAGES_PER_ERASE_SECTOR; pg++) {
|
|
uint32_t base_addr = prv_page_to_flash_offset(sector_start_page + pg);
|
|
|
|
uint32_t data_len;
|
|
PageHeader hdr;
|
|
if (do_copy) {
|
|
// if the sector is not active we only need to copy the page header info
|
|
data_len = (((sectors_active >> pg) & 0x1) == 0) ?
|
|
0 : PFS_PAGE_SIZE - sizeof(PageHeader);
|
|
if (data_len == 0) {
|
|
get_updated_erase_hdr(&hdr, sector_start_page + pg);
|
|
} else {
|
|
prv_flash_read((uint8_t *)&hdr, sizeof(hdr), base_addr);
|
|
}
|
|
}
|
|
|
|
// Write Page Header + DataLen
|
|
if (do_copy) {
|
|
pfs_write(fd, &hdr, sizeof(hdr));
|
|
pfs_write(fd, &data_len, sizeof(data_len));
|
|
} else { // recover
|
|
pfs_read(fd, &hdr, sizeof(hdr));
|
|
pfs_read(fd, &data_len, sizeof(data_len));
|
|
prv_flash_write(&hdr, sizeof(hdr), base_addr);
|
|
}
|
|
|
|
base_addr += sizeof(PageHeader);
|
|
for (int i = 0; i < (int)data_len; i += copy_buf_size) {
|
|
size_t to_copy = MIN(data_len - i, copy_buf_size);
|
|
if (do_copy) {
|
|
prv_flash_read(buf, to_copy, base_addr + i);
|
|
pfs_write(fd, buf, to_copy);
|
|
} else {
|
|
pfs_read(fd, buf, to_copy);
|
|
prv_flash_write(buf, to_copy, base_addr + i);
|
|
}
|
|
}
|
|
}
|
|
|
|
kernel_free(buf);
|
|
return (S_SUCCESS);
|
|
}
|
|
|
|
static void recover_region_from_file(int fd) {
|
|
GCData gcdata;
|
|
|
|
pfs_seek(fd, 0, FSeekSet);
|
|
pfs_read(fd, &gcdata, sizeof(gcdata));
|
|
|
|
if (!GCDATA_VALID(gcdata.flags)) {
|
|
// we never completed setting up the migration
|
|
goto done;
|
|
}
|
|
|
|
// at this point we can erase the block
|
|
prv_handle_sector_erase(gcdata.gc_start_page, false);
|
|
|
|
copy_or_recover_gc_data(fd, &gcdata, false);
|
|
|
|
done:
|
|
pfs_close_and_remove(fd);
|
|
}
|
|
|
|
static int prv_copy_sector_to_gc_file(uint16_t *free_page,
|
|
uint16_t sector_start_page, uint32_t sectors_active) {
|
|
size_t num_entries = __builtin_popcount(sectors_active);
|
|
|
|
// We need space to store all the data for active pages, the
|
|
// page header for all pages, and the GCData struct
|
|
size_t space_needed = 0;
|
|
space_needed += (num_entries * (PFS_PAGE_SIZE - AVAIL_BYTES_OFFSET));
|
|
space_needed += (PFS_PAGES_PER_ERASE_SECTOR * (sizeof(PageHeader) + 4));
|
|
space_needed += sizeof(GCData);
|
|
|
|
// we rely on having 1 page to store some metadata so make sure
|
|
// we always have enough space based on our block & erase size
|
|
_Static_assert((PFS_PAGES_PER_ERASE_SECTOR * (sizeof(PageHeader) + 4)) <
|
|
(PFS_PAGE_SIZE - AVAIL_BYTES_OFFSET), "Too many pages per Erase sector");
|
|
PBL_ASSERTN(num_entries < PFS_PAGES_PER_ERASE_SECTOR);
|
|
|
|
int fd = pfs_open_gc_file(space_needed, true);
|
|
GCData gcdata = {
|
|
.version = 0, // Version 0 for now, bump if we change
|
|
.flags = 0xff,
|
|
.gc_start_page = sector_start_page,
|
|
.num_entries = num_entries,
|
|
.page_mask = sectors_active
|
|
};
|
|
|
|
// write out the GCData to the file
|
|
pfs_write(fd, &gcdata, sizeof(gcdata));
|
|
|
|
// copy all the data we need to the file
|
|
copy_or_recover_gc_data(fd, &gcdata, true);
|
|
|
|
// mark our data as valid
|
|
gcdata.flags &= ~GC_DATA_VALID;
|
|
pfs_seek(fd, offsetof(GCData, flags), FSeekSet);
|
|
pfs_write(fd, &gcdata.flags, sizeof(gcdata.flags));
|
|
|
|
return (fd);
|
|
}
|
|
|
|
static NOINLINE status_t garbage_collect_sector(uint16_t *free_page,
|
|
uint16_t sector_start_page, uint32_t sectors_active) {
|
|
|
|
// if no sectors are active in the region, just erase it!
|
|
if (sectors_active == 0) {
|
|
prv_handle_sector_erase(sector_start_page, true);
|
|
goto done;
|
|
}
|
|
|
|
int fd = prv_copy_sector_to_gc_file(free_page, sector_start_page,
|
|
sectors_active);
|
|
|
|
recover_region_from_file(fd);
|
|
|
|
// we used the gc block
|
|
s_gc_block.block_writes++;
|
|
|
|
done:
|
|
for (uint16_t pg = 0; pg < PFS_PAGES_PER_ERASE_SECTOR; pg++) {
|
|
if (((sectors_active >> pg) & 0x1) == 0) {
|
|
*free_page = pg + sector_start_page;
|
|
return (S_SUCCESS);
|
|
}
|
|
}
|
|
|
|
return (E_INTERNAL);
|
|
}
|
|
|
|
status_t pfs_init(bool run_filesystem_check) {
|
|
if (s_pfs_mutex == NULL) {
|
|
s_pfs_mutex = mutex_create_recursive();
|
|
}
|
|
|
|
for (int fd = FD_INDEX_OFFSET; fd < FD_INDEX_OFFSET+MAX_FD_HANDLES; fd++) {
|
|
PFS_FD(fd) = (FileDesc) { .fd_status = FD_STATUS_FREE };
|
|
}
|
|
|
|
ftl_populate_region_list();
|
|
|
|
if (run_filesystem_check) {
|
|
if (!pfs_active()) {
|
|
// either we have downgraded or there is no data on the flash
|
|
PBL_LOG(LOG_LEVEL_INFO, "PFS not active ... formatting");
|
|
pfs_format(true /* write erase headers */);
|
|
}
|
|
}
|
|
|
|
// we need to run this before reserving a new GC region so that we don't
|
|
// think a region is free when in reality we just rebooted in the middle of it
|
|
// being re-written
|
|
int fd;
|
|
if ((fd = pfs_open_gc_file(0, false)) >= S_SUCCESS) {
|
|
// we rebooted while we were in the middle of a garbage collection
|
|
PBL_LOG(LOG_LEVEL_INFO, "Recovering flash region from GC file");
|
|
recover_region_from_file(fd);
|
|
}
|
|
|
|
// find a free region
|
|
if (!prv_update_gc_reserved_region()) {
|
|
PBL_LOG(LOG_LEVEL_ERROR, "No free flash erase units!");
|
|
// Note: It should not be possible for this to happen since start of day no
|
|
// files will be written on then flash. We could also try to force apps to
|
|
// be flushed out of the FS in an attempt to free up space since they are
|
|
// only being cached on the FS
|
|
pfs_format(true);
|
|
}
|
|
|
|
// get us off to a good start by ensuring there is some pre-erased space on
|
|
// the filesystem. We do a lot of initialization from different threads early
|
|
// during boot flow. This prevents those threads from blocking each other
|
|
uint32_t bytes_to_free = ((s_pfs_page_count * PFS_PAGE_SIZE) * 4) / 100;
|
|
PBL_LOG(LOG_LEVEL_DEBUG, "Preparing %"PRIu32" bytes of flash for filesystem use",
|
|
bytes_to_free);
|
|
|
|
pfs_prepare_for_file_creation(bytes_to_free, 15 * RTC_TICKS_HZ);
|
|
|
|
return (S_SUCCESS);
|
|
}
|
|
|
|
void pfs_format(bool write_erase_headers) {
|
|
PBL_LOG(LOG_LEVEL_INFO, "FS-Format Start");
|
|
mutex_lock_recursive(s_pfs_mutex);
|
|
|
|
for (int i = FD_INDEX_OFFSET; i < FD_INDEX_OFFSET+PFS_FD_SET_SIZE; i++) {
|
|
mark_fd_free(i);
|
|
}
|
|
|
|
// clear out all pages
|
|
filesystem_regions_erase_all();
|
|
prv_invalidate_page_flags_cache_all();
|
|
|
|
if (write_erase_headers) {
|
|
prv_write_erased_header_on_page_range(0, s_pfs_page_count, 1);
|
|
}
|
|
|
|
mutex_unlock_recursive(s_pfs_mutex);
|
|
PBL_LOG(LOG_LEVEL_INFO, "FS-Format Done");
|
|
}
|
|
|
|
|
|
int pfs_sector_optimal_size(int min_size, int namelen) {
|
|
min_size += sizeof(FileHeader);
|
|
min_size += sizeof(FileMetaData);
|
|
min_size += namelen;
|
|
|
|
int bytes_per_sector = PFS_PAGE_SIZE - sizeof(PageHeader);
|
|
int num_pages = min_size / bytes_per_sector;
|
|
if ((min_size % bytes_per_sector) > 0) {
|
|
num_pages++;
|
|
}
|
|
int optimal_size = num_pages * bytes_per_sector;
|
|
|
|
optimal_size -= sizeof(FileHeader);
|
|
optimal_size -= sizeof(FileMetaData);
|
|
optimal_size -= namelen;
|
|
return optimal_size;
|
|
}
|
|
|
|
uint32_t get_available_pfs_space(void) {
|
|
uint32_t allocated_space = 0;
|
|
|
|
for (uint16_t pg = 0; pg < s_pfs_page_count; pg++) {
|
|
uint8_t page_flags = prv_get_page_flags(pg);
|
|
|
|
if ((IS_PAGE_TYPE(page_flags, PAGE_FLAG_START_PAGE)) ||
|
|
(IS_PAGE_TYPE(page_flags, PAGE_FLAG_CONT_PAGE))) {
|
|
allocated_space += free_bytes_in_page(pg);
|
|
}
|
|
}
|
|
|
|
// A full filesystem is bad for wear leveling since the same sectors will
|
|
// wind up getting written repeatedly. We should really be enforcing this
|
|
// within pfs_open but for now we will just let external callers use this
|
|
// routine before allocating large files
|
|
uint32_t tot_capacity = (pfs_get_size() * 8) / 10;
|
|
|
|
return ((allocated_space >= tot_capacity) ?
|
|
0 : (tot_capacity - allocated_space));
|
|
}
|
|
|
|
uint32_t pfs_crc_calculate_file(int fd, uint32_t offset, uint32_t num_bytes) {
|
|
LegacyChecksum checksum;
|
|
legacy_defective_checksum_init(&checksum);
|
|
|
|
// grab the pfs lock to prevent lock inversion with crc lock
|
|
mutex_lock_recursive(s_pfs_mutex);
|
|
|
|
// go to offset
|
|
pfs_seek(fd, offset, FSeekSet);
|
|
const unsigned int chunk_size = 128;
|
|
uint8_t buffer[chunk_size];
|
|
|
|
while (num_bytes > chunk_size) {
|
|
pfs_read(fd, buffer, chunk_size);
|
|
legacy_defective_checksum_update(&checksum, buffer, chunk_size);
|
|
num_bytes -= chunk_size;
|
|
}
|
|
|
|
pfs_read(fd, buffer, num_bytes);
|
|
legacy_defective_checksum_update(&checksum, buffer, num_bytes);
|
|
uint32_t crc = legacy_defective_checksum_finish(&checksum);
|
|
|
|
mutex_unlock_recursive(s_pfs_mutex);
|
|
|
|
return (crc);
|
|
}
|
|
|
|
void analytics_external_collect_pfs_stats(void) {
|
|
uint16_t avail_kilobytes = (uint16_t)(get_available_pfs_space() / 1024);
|
|
analytics_set(ANALYTICS_DEVICE_METRIC_PFS_SPACE_FREE_KB,
|
|
avail_kilobytes, AnalyticsClient_System);
|
|
}
|
|
|
|
/*
|
|
* Debug Utilities
|
|
*/
|
|
|
|
// TODO: Remove once we figure out PBL-20973
|
|
void pfs_collect_diagnostic_data(int fd, void *diagnostic_buf, size_t diagnostic_buf_len) {
|
|
mutex_lock_recursive(s_pfs_mutex);
|
|
memcpy(diagnostic_buf, &PFS_FD(fd), MIN(diagnostic_buf_len, sizeof(FileDesc)));
|
|
mutex_unlock_recursive(s_pfs_mutex);
|
|
}
|
|
|
|
// pass in either 0 or 1 to as argument
|
|
void pfs_command_fs_format(const char *erase_headers) {
|
|
int write_erase_headers = atoi(erase_headers);
|
|
if (write_erase_headers == 1) {
|
|
pfs_format(true /* write erase headers */);
|
|
} else {
|
|
pfs_format(false /* write erase headers */);
|
|
}
|
|
}
|
|
|
|
void pfs_command_dump_hdr(const char *page) {
|
|
uint16_t pg = (uint16_t) atoi(page);
|
|
if (pg > s_pfs_page_count) {
|
|
prompt_send_response("ERROR");
|
|
return;
|
|
}
|
|
|
|
uint8_t hdr[FILE_NAME_OFFSET + 10];
|
|
prv_flash_read((uint8_t *)&hdr, sizeof(hdr), prv_page_to_flash_offset(pg));
|
|
|
|
PBL_HEXDUMP_D_SERIAL(LOG_LEVEL_DEBUG, hdr, sizeof(hdr));
|
|
}
|
|
|
|
void pfs_command_fs_ls(void) {
|
|
char display_buf[80];
|
|
int pages_in_use = 0;
|
|
|
|
prompt_send_response("Page:\tFilename\tFile Size\tFile Info\tErase Count\n");
|
|
|
|
for (uint16_t pg = 0; pg < s_pfs_page_count; pg++) {
|
|
PageHeader pg_hdr;
|
|
FileHeader file_hdr;
|
|
pg_hdr.page_flags = prv_get_page_flags(pg);
|
|
|
|
if (!IS_PAGE_TYPE(pg_hdr.page_flags, PAGE_FLAG_START_PAGE)) {
|
|
pages_in_use += IS_PAGE_TYPE(pg_hdr.page_flags,
|
|
PAGE_FLAG_CONT_PAGE) ? 1 : 0;
|
|
continue; // only start pages contain file name info
|
|
}
|
|
pages_in_use++;
|
|
|
|
if (read_header(pg, &pg_hdr, &file_hdr) != PageAndFileHdrValid) {
|
|
snprintf(display_buf, sizeof(display_buf), "%3d: Corrupt Sector", pg);
|
|
prompt_send_response(display_buf);
|
|
}
|
|
|
|
char file_name[file_hdr.file_namelen + 1];
|
|
file_name[file_hdr.file_namelen] = '\0';
|
|
|
|
prv_flash_read((uint8_t *)file_name, file_hdr.file_namelen,
|
|
prv_page_to_flash_offset(pg) + FILE_NAME_OFFSET);
|
|
|
|
snprintf(display_buf, sizeof(display_buf), "%3d:\t%8s%s\t%5d\t\t0x%x\t%15d",
|
|
pg, file_name, is_tmp_file(pg) ? "(tmp)" : "", (int)file_hdr.file_size,
|
|
file_hdr.file_type, (int)pg_hdr.erase_count);
|
|
prompt_send_response(display_buf);
|
|
}
|
|
|
|
snprintf(display_buf, sizeof(display_buf), "\n---\n%d / %d pages in use "
|
|
"(%"PRIu32" kB available)", pages_in_use, s_pfs_page_count,
|
|
get_available_pfs_space() / 1024);
|
|
prompt_send_response(display_buf);
|
|
}
|
|
|
|
// Dump the first n bytes of a file (from current seek position)
|
|
void pfs_debug_dump(int fd, int num_bytes) {
|
|
char buf[16];
|
|
uint8_t *bytes = kernel_malloc(num_bytes);
|
|
|
|
if (bytes == NULL) {
|
|
prompt_send_response("malloc error");
|
|
goto cleanup;
|
|
}
|
|
|
|
memset(bytes, 0x00, num_bytes);
|
|
if ((num_bytes = pfs_read(fd, bytes, num_bytes)) < 0) {
|
|
prompt_send_response_fmt(buf, sizeof(buf), "rd err: %d", num_bytes);
|
|
goto cleanup;
|
|
}
|
|
|
|
PBL_HEXDUMP_D_SERIAL(LOG_LEVEL_DEBUG, bytes, num_bytes);
|
|
|
|
prompt_send_response("DONE");
|
|
cleanup:
|
|
kernel_free(bytes);
|
|
}
|
|
|
|
void pfs_command_cat(const char *filename, const char *num_chars) {
|
|
int fd = pfs_open(filename, OP_FLAG_READ, 0, 0);
|
|
char buf[16];
|
|
if (fd < 0) {
|
|
prompt_send_response_fmt(buf, sizeof(buf), "fd open err: %d", fd);
|
|
return;
|
|
}
|
|
int num_bytes = atoi(num_chars);
|
|
pfs_debug_dump(fd, num_bytes);
|
|
pfs_close(fd);
|
|
}
|
|
|
|
void pfs_command_crc(const char *filename) {
|
|
int fd = pfs_open(filename, OP_FLAG_READ, 0, 0);
|
|
char buffer[32];
|
|
if (fd < 0) {
|
|
prompt_send_response_fmt(buffer, sizeof(buffer), "fd open err: %d", fd);
|
|
return;
|
|
}
|
|
size_t num_bytes = pfs_get_file_size(fd);
|
|
uint32_t crc = pfs_crc_calculate_file(fd, 0, num_bytes);
|
|
pfs_close(fd);
|
|
prompt_send_response_fmt(buffer, sizeof(buffer), "CRC: %"PRIx32, crc);
|
|
}
|
|
|
|
/*
|
|
* Routines to facilitate unit testing
|
|
*/
|
|
#if UNITTEST
|
|
uint16_t test_get_file_start_page(int fd) {
|
|
return (PFS_FD(fd).file.start_page);
|
|
}
|
|
|
|
void test_force_garbage_collection(uint16_t start_page) {
|
|
start_page = (start_page / PFS_PAGES_PER_ERASE_SECTOR) * PFS_PAGES_PER_ERASE_SECTOR;
|
|
|
|
uint16_t free_page;
|
|
uint32_t active_sectors =
|
|
prv_get_sector_page_status(start_page / PFS_PAGES_PER_ERASE_SECTOR , &free_page);
|
|
|
|
garbage_collect_sector(&free_page, start_page, active_sectors);
|
|
}
|
|
|
|
status_t test_scan_for_last_written(void) {
|
|
for (uint16_t pg = 0; pg < s_pfs_page_count; pg++) {
|
|
PageHeader hdr;
|
|
prv_flash_read((uint8_t *)&hdr.last_written, sizeof(hdr.last_written),
|
|
prv_page_to_flash_offset(pg) + offsetof(PageHeader, last_written));
|
|
if (hdr.last_written == LAST_WRITTEN_TAG) {
|
|
return (pg);
|
|
}
|
|
}
|
|
|
|
return (-1);
|
|
}
|
|
|
|
void test_force_recalc_of_gc_region(void) {
|
|
s_gc_block.block_valid = false;
|
|
prv_update_gc_reserved_region();
|
|
}
|
|
|
|
void test_force_reboot_during_garbage_collection(uint16_t start_page) {
|
|
start_page =
|
|
(start_page / PFS_PAGES_PER_ERASE_SECTOR) * PFS_PAGES_PER_ERASE_SECTOR;
|
|
|
|
uint16_t free_page;
|
|
uint32_t active_sectors = prv_get_sector_page_status(start_page, &free_page);
|
|
|
|
prv_copy_sector_to_gc_file(&free_page, start_page, active_sectors);
|
|
|
|
// blow away the sector
|
|
prv_handle_sector_erase(s_gc_block.gc_start_page, false);
|
|
}
|
|
|
|
void test_override_last_written_page(uint16_t start_page) {
|
|
s_test_last_page_written_override = s_last_page_written;
|
|
}
|
|
#endif
|