zephyr/subsys/settings/src/settings_file.c
Miika Karanki b84bab36b9 settings: file: do not unlink the settings file unnecessarily
fs_rename can handle the file move atomically without unlinking
the file first if the filesystem supports that. This change makes
the settings file more power cut resilient so that the whole
settings file is not lost with poorly timed power cut.

Signed-off-by: Miika Karanki <miika.karanki@vaisala.com>
2023-12-12 13:02:15 +01:00

591 lines
12 KiB
C

/*
* 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);
#ifdef CONFIG_SETTINGS_FS
#define SETTINGS_FILE_MAX_LINES CONFIG_SETTINGS_FS_MAX_LINES
#define SETTINGS_FILE_PATH CONFIG_SETTINGS_FS_FILE
#else
#define SETTINGS_FILE_MAX_LINES CONFIG_SETTINGS_FILE_MAX_LINES
#define SETTINGS_FILE_PATH CONFIG_SETTINGS_FILE_PATH
#endif
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;
}