| /* |
| * Copyright (c) 2018 Nordic Semiconductor ASA |
| * Copyright (c) 2015 Runtime Inc |
| * |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| |
| #include <string.h> |
| #include <stdbool.h> |
| #include <zephyr/kernel.h> |
| |
| #include <zephyr/fs/fs.h> |
| |
| #include <zephyr/settings/settings.h> |
| #include "settings/settings_file.h" |
| #include "settings_priv.h" |
| |
| #include <zephyr/logging/log.h> |
| |
| LOG_MODULE_DECLARE(settings, CONFIG_SETTINGS_LOG_LEVEL); |
| |
| #define SETTINGS_FILE_MAX_LINES CONFIG_SETTINGS_FILE_MAX_LINES |
| #define SETTINGS_FILE_PATH CONFIG_SETTINGS_FILE_PATH |
| |
| int settings_backend_init(void); |
| |
| static int settings_file_load(struct settings_store *cs, |
| const struct settings_load_arg *arg); |
| static int settings_file_save(struct settings_store *cs, const char *name, |
| const char *value, size_t val_len); |
| static void *settings_file_storage_get(struct settings_store *cs); |
| |
| static const struct settings_store_itf settings_file_itf = { |
| .csi_load = settings_file_load, |
| .csi_save = settings_file_save, |
| .csi_storage_get = settings_file_storage_get |
| }; |
| |
| /* |
| * Register a file to be a source of configuration. |
| */ |
| int settings_file_src(struct settings_file *cf) |
| { |
| if (!cf->cf_name) { |
| return -EINVAL; |
| } |
| cf->cf_store.cs_itf = &settings_file_itf; |
| settings_src_register(&cf->cf_store); |
| |
| return 0; |
| } |
| |
| /* |
| * Register a file to be a destination of configuration. |
| */ |
| int settings_file_dst(struct settings_file *cf) |
| { |
| if (!cf->cf_name) { |
| return -EINVAL; |
| } |
| cf->cf_store.cs_itf = &settings_file_itf; |
| settings_dst_register(&cf->cf_store); |
| |
| return 0; |
| } |
| |
| /** |
| * @brief Check if there is any duplicate of the current setting |
| * |
| * This function checks if there is any duplicated data further in the buffer. |
| * |
| * @param entry_ctx Current entry context |
| * @param name The name of the current entry |
| * |
| * @retval false No duplicates found |
| * @retval true Duplicate found |
| */ |
| static bool settings_file_check_duplicate( |
| const struct line_entry_ctx *entry_ctx, |
| const char * const name) |
| { |
| struct line_entry_ctx entry2_ctx = *entry_ctx; |
| |
| /* Searching the duplicates */ |
| while (settings_next_line_ctx(&entry2_ctx) == 0) { |
| char name2[SETTINGS_MAX_NAME_LEN + SETTINGS_EXTRA_LEN + 1]; |
| size_t name2_len; |
| |
| if (entry2_ctx.len == 0) { |
| break; |
| } |
| |
| if (settings_line_name_read(name2, sizeof(name2), &name2_len, |
| &entry2_ctx)) { |
| continue; |
| } |
| name2[name2_len] = '\0'; |
| |
| if (!strcmp(name, name2)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| static int read_entry_len(const struct line_entry_ctx *entry_ctx, off_t off) |
| { |
| if (off >= entry_ctx->len) { |
| return 0; |
| } |
| return entry_ctx->len - off; |
| } |
| |
| static int settings_file_load_priv(struct settings_store *cs, line_load_cb cb, |
| void *cb_arg, bool filter_duplicates) |
| { |
| struct settings_file *cf = CONTAINER_OF(cs, struct settings_file, cf_store); |
| struct fs_file_t file; |
| int lines; |
| int rc; |
| |
| struct line_entry_ctx entry_ctx = { |
| .stor_ctx = (void *)&file, |
| .seek = 0, |
| .len = 0 /* unknown length */ |
| }; |
| |
| lines = 0; |
| |
| fs_file_t_init(&file); |
| |
| rc = fs_open(&file, cf->cf_name, FS_O_READ); |
| if (rc != 0) { |
| if (rc == -ENOENT) { |
| return -ENOENT; |
| } |
| |
| return -EINVAL; |
| } |
| |
| while (1) { |
| char name[SETTINGS_MAX_NAME_LEN + SETTINGS_EXTRA_LEN + 1]; |
| size_t name_len; |
| bool pass_entry = true; |
| |
| rc = settings_next_line_ctx(&entry_ctx); |
| if (rc || entry_ctx.len == 0) { |
| break; |
| } |
| |
| rc = settings_line_name_read(name, sizeof(name), &name_len, |
| &entry_ctx); |
| |
| if (rc || name_len == 0) { |
| break; |
| } |
| name[name_len] = '\0'; |
| |
| if (filter_duplicates && |
| (!read_entry_len(&entry_ctx, name_len+1) || |
| settings_file_check_duplicate(&entry_ctx, name))) { |
| pass_entry = false; |
| } |
| /*name, val-read_cb-ctx, val-off*/ |
| /* take into account '=' separator after the name */ |
| if (pass_entry) { |
| cb(name, (void *)&entry_ctx, name_len + 1, cb_arg); |
| } |
| lines++; |
| } |
| |
| rc = fs_close(&file); |
| cf->cf_lines = lines; |
| |
| return rc; |
| } |
| |
| /* |
| * Called to load configuration items. |
| */ |
| static int settings_file_load(struct settings_store *cs, |
| const struct settings_load_arg *arg) |
| { |
| return settings_file_load_priv(cs, |
| settings_line_load_cb, |
| (void *)arg, |
| true); |
| } |
| |
| static void settings_tmpfile(char *dst, const char *src, char *pfx) |
| { |
| int len; |
| int pfx_len; |
| |
| len = strlen(src); |
| pfx_len = strlen(pfx); |
| if (len + pfx_len >= SETTINGS_FILE_NAME_MAX) { |
| len = SETTINGS_FILE_NAME_MAX - pfx_len - 1; |
| } |
| memcpy(dst, src, len); |
| memcpy(dst + len, pfx, pfx_len); |
| dst[len + pfx_len] = '\0'; |
| } |
| |
| static int settings_file_create_or_replace(struct fs_file_t *zfp, |
| const char *file_name) |
| { |
| struct fs_dirent entry; |
| |
| if (fs_stat(file_name, &entry) == 0) { |
| if (entry.type == FS_DIR_ENTRY_FILE) { |
| if (fs_unlink(file_name)) { |
| return -EIO; |
| } |
| } else { |
| return -EISDIR; |
| } |
| } |
| |
| return fs_open(zfp, file_name, FS_O_CREATE | FS_O_RDWR); |
| } |
| |
| /* |
| * Try to compress configuration file by keeping unique names only. |
| */ |
| static int settings_file_save_and_compress(struct settings_file *cf, |
| const char *name, const char *value, |
| size_t val_len) |
| { |
| int rc, rc2; |
| struct fs_file_t rf; |
| struct fs_file_t wf; |
| char tmp_file[SETTINGS_FILE_NAME_MAX]; |
| char name1[SETTINGS_MAX_NAME_LEN + SETTINGS_EXTRA_LEN]; |
| char name2[SETTINGS_MAX_NAME_LEN + SETTINGS_EXTRA_LEN]; |
| struct line_entry_ctx loc1 = { |
| .stor_ctx = &rf, |
| .seek = 0, |
| .len = 0 /* unknown length */ |
| }; |
| |
| struct line_entry_ctx loc2; |
| |
| struct line_entry_ctx loc3 = { |
| .stor_ctx = &wf |
| }; |
| |
| int copy; |
| int lines; |
| size_t new_name_len; |
| size_t val1_off; |
| |
| fs_file_t_init(&rf); |
| fs_file_t_init(&wf); |
| |
| if (fs_open(&rf, cf->cf_name, FS_O_CREATE | FS_O_RDWR) != 0) { |
| return -ENOEXEC; |
| } |
| |
| settings_tmpfile(tmp_file, cf->cf_name, ".cmp"); |
| |
| if (settings_file_create_or_replace(&wf, tmp_file)) { |
| fs_close(&rf); |
| return -ENOEXEC; |
| } |
| |
| lines = 0; |
| new_name_len = strlen(name); |
| |
| while (1) { |
| rc = settings_next_line_ctx(&loc1); |
| |
| if (rc || loc1.len == 0) { |
| /* try to amend new value to the compressed file */ |
| break; |
| } |
| |
| rc = settings_line_name_read(name1, sizeof(name1), &val1_off, |
| &loc1); |
| if (rc) { |
| /* try to process next line */ |
| continue; |
| } |
| |
| if (val1_off + 1 == loc1.len) { |
| /* Lack of a value so the record is a deletion-record */ |
| /* No sense to copy empty entry from */ |
| /* the oldest sector */ |
| continue; |
| } |
| |
| /* avoid copping value which will be overwritten by new value*/ |
| if ((val1_off == new_name_len) && |
| !memcmp(name1, name, val1_off)) { |
| continue; |
| } |
| |
| loc2 = loc1; |
| |
| copy = 1; |
| while (1) { |
| size_t val2_off; |
| |
| rc = settings_next_line_ctx(&loc2); |
| |
| if (rc || loc2.len == 0) { |
| /* try to amend new value to */ |
| /* the compressed file */ |
| break; |
| } |
| |
| rc = settings_line_name_read(name2, sizeof(name2), |
| &val2_off, &loc2); |
| if (rc) { |
| /* try to process next line */ |
| continue; |
| } |
| if ((val1_off == val2_off) && |
| !memcmp(name1, name2, val1_off)) { |
| copy = 0; /* newer version doesn't exist */ |
| break; |
| } |
| } |
| if (!copy) { |
| continue; |
| } |
| |
| loc2 = loc1; |
| loc2.len += 2; |
| loc2.seek -= 2; |
| rc = settings_line_entry_copy(&loc3, 0, &loc2, 0, loc2.len); |
| if (rc) { |
| /* compressed file might be corrupted */ |
| goto end_rolback; |
| } |
| |
| lines++; |
| } |
| |
| /* at last store the new value */ |
| rc = settings_line_write(name, value, val_len, 0, &loc3); |
| if (rc) { |
| /* compressed file might be corrupted */ |
| goto end_rolback; |
| } |
| |
| rc = fs_close(&wf); |
| rc2 = fs_close(&rf); |
| if (rc == 0 && rc2 == 0) { |
| if (fs_rename(tmp_file, cf->cf_name)) { |
| return -ENOENT; |
| } |
| cf->cf_lines = lines + 1; |
| } else { |
| rc = -EIO; |
| } |
| /* |
| * XXX at settings_file_load(), look for .cmp if actual file does not |
| * exist. |
| */ |
| return 0; |
| end_rolback: |
| (void)fs_close(&wf); |
| if (fs_close(&rf) == 0) { |
| (void)fs_unlink(tmp_file); |
| } |
| return -EIO; |
| |
| } |
| |
| static int settings_file_save_priv(struct settings_store *cs, const char *name, |
| const char *value, size_t val_len) |
| { |
| struct settings_file *cf = CONTAINER_OF(cs, struct settings_file, cf_store); |
| struct line_entry_ctx entry_ctx; |
| struct fs_file_t file; |
| int rc2; |
| int rc; |
| |
| if (!name) { |
| return -EINVAL; |
| } |
| |
| fs_file_t_init(&file); |
| |
| if (cf->cf_maxlines && (cf->cf_lines + 1 >= cf->cf_maxlines)) { |
| /* |
| * Compress before config file size exceeds |
| * the max number of lines. |
| */ |
| return settings_file_save_and_compress(cf, name, value, |
| val_len); |
| } |
| |
| /* |
| * Open the file to add this one value. |
| */ |
| rc = fs_open(&file, cf->cf_name, FS_O_CREATE | FS_O_RDWR); |
| if (rc == 0) { |
| rc = fs_seek(&file, 0, FS_SEEK_END); |
| if (rc == 0) { |
| entry_ctx.stor_ctx = &file; |
| rc = settings_line_write(name, value, val_len, 0, |
| (void *)&entry_ctx); |
| if (rc == 0) { |
| cf->cf_lines++; |
| } |
| } |
| |
| rc2 = fs_close(&file); |
| if (rc == 0) { |
| rc = rc2; |
| } |
| } |
| |
| return rc; |
| } |
| |
| |
| /* |
| * Called to save configuration. |
| */ |
| static int settings_file_save(struct settings_store *cs, const char *name, |
| const char *value, size_t val_len) |
| { |
| struct settings_line_dup_check_arg cdca; |
| |
| if (val_len > 0 && value == NULL) { |
| return -EINVAL; |
| } |
| |
| /* |
| * Check if we're writing the same value again. |
| */ |
| cdca.name = name; |
| cdca.val = (char *)value; |
| cdca.is_dup = 0; |
| cdca.val_len = val_len; |
| settings_file_load_priv(cs, settings_line_dup_check_cb, &cdca, false); |
| if (cdca.is_dup == 1) { |
| return 0; |
| } |
| return settings_file_save_priv(cs, name, value, val_len); |
| } |
| |
| static int read_handler(void *ctx, off_t off, char *buf, size_t *len) |
| { |
| struct line_entry_ctx *entry_ctx = ctx; |
| struct fs_file_t *file = entry_ctx->stor_ctx; |
| ssize_t r_len; |
| int rc; |
| |
| /* 0 is reserved for reading the length-field only */ |
| if (entry_ctx->len != 0) { |
| if (off >= entry_ctx->len) { |
| *len = 0; |
| return 0; |
| } |
| |
| if ((off + *len) > entry_ctx->len) { |
| *len = entry_ctx->len - off; |
| } |
| } |
| |
| rc = fs_seek(file, entry_ctx->seek + off, FS_SEEK_SET); |
| if (rc) { |
| goto end; |
| } |
| |
| r_len = fs_read(file, buf, *len); |
| |
| if (r_len >= 0) { |
| *len = r_len; |
| rc = 0; |
| } else { |
| rc = r_len; |
| } |
| end: |
| return rc; |
| } |
| |
| static size_t get_len_cb(void *ctx) |
| { |
| struct line_entry_ctx *entry_ctx = ctx; |
| |
| return entry_ctx->len; |
| } |
| |
| static int write_handler(void *ctx, off_t off, char const *buf, size_t len) |
| { |
| struct line_entry_ctx *entry_ctx = ctx; |
| struct fs_file_t *file = entry_ctx->stor_ctx; |
| int rc; |
| |
| /* append to file only */ |
| rc = fs_seek(file, 0, FS_SEEK_END); |
| |
| if (rc == 0) { |
| rc = fs_write(file, buf, len); |
| |
| if (rc > 0) { |
| rc = 0; |
| } |
| } |
| |
| return rc; |
| } |
| |
| void settings_mount_file_backend(struct settings_file *cf) |
| { |
| settings_line_io_init(read_handler, write_handler, get_len_cb, 1); |
| } |
| |
| static int mkdir_if_not_exists(const char *path) |
| { |
| struct fs_dirent entry; |
| int err; |
| |
| err = fs_stat(path, &entry); |
| if (err == -ENOENT) { |
| return fs_mkdir(path); |
| } else if (err) { |
| return err; |
| } |
| |
| if (entry.type != FS_DIR_ENTRY_DIR) { |
| return -EEXIST; |
| } |
| |
| return 0; |
| } |
| |
| static int mkdir_for_file(const char *file_path) |
| { |
| char dir_path[SETTINGS_FILE_NAME_MAX]; |
| int err; |
| |
| for (size_t i = 0; file_path[i] != '\0'; i++) { |
| if (i > 0 && file_path[i] == '/') { |
| dir_path[i] = '\0'; |
| |
| err = mkdir_if_not_exists(dir_path); |
| if (err) { |
| return err; |
| } |
| } |
| |
| dir_path[i] = file_path[i]; |
| } |
| |
| return 0; |
| } |
| |
| int settings_backend_init(void) |
| { |
| static struct settings_file config_init_settings_file = { |
| .cf_name = SETTINGS_FILE_PATH, |
| .cf_maxlines = SETTINGS_FILE_MAX_LINES |
| }; |
| int rc; |
| |
| rc = settings_file_src(&config_init_settings_file); |
| if (rc) { |
| return rc; |
| } |
| |
| rc = settings_file_dst(&config_init_settings_file); |
| if (rc) { |
| return rc; |
| } |
| |
| settings_mount_file_backend(&config_init_settings_file); |
| |
| /* |
| * Must be called after root FS has been initialized. |
| */ |
| return mkdir_for_file(config_init_settings_file.cf_name); |
| } |
| |
| static void *settings_file_storage_get(struct settings_store *cs) |
| { |
| struct settings_file *cf = CONTAINER_OF(cs, struct settings_file, cf_store); |
| |
| return (void *)cf->cf_name; |
| } |