From f80db15c7e9ee86f24cc6ca000a873d05e5f45bd Mon Sep 17 00:00:00 2001 From: Christopher Haster Date: Thu, 22 Aug 2024 16:41:06 -0500 Subject: [PATCH] attrs: (Re)implemented file-attached custom attributes Unlike lfsr_setattr/getattr/etc, file-attached custom attributes are RAM-backed snapshots attached to, well, files, that can be committed atomically along with the file's contents. Great for power-loss resilience, but boy does it make a mess of an API. This API was really where custom attributes needed some TLC. The biggest change is how file-attached custom attributes interact with file sync broadcasting. A common complaint from users is that setting custom attributes did not update attributes in open file handles. This behavior is _very_ inconsistent with other filesystems and created a lot of confusion. Since we're nailing down littlefs's snapshot/broadcasting model as a part of larger changes, it makes sense to also nail down how custom attributes interact. In the new model: - Custom attributes are still in-RAM snapshots. Updates do not immediately take effect, even across write calls. - On lfsr_file_sync or lfsr_file_close, custom attributes are written atomically to disk and broadcasted to all open file handles. - lfsr_setattr/removeattr also take part in attribute broadcasting. When called, lfsr_setattr/removeattr updates the attribute on disk and broadcasts the attribute changes to all open file handles. - Desynced files do _not_ recieve any attribute broadcasts in the same way they do not recieve any data broadcasts. This should hopefully make littlefs behave much more consistently with other filesystems, while still maintaining a well-defined snapshot and power-loss properties. --- The lfs_attr struct also gained several new fields: // Custom attribute structure, used to describe custom attributes // committed atomically during file writes. struct lfs_attr { // Type of attribute // // Note some of this range is reserved: // 0x00-0x7f - Free for custom attributes // 0x80-0xff - May be assigned a standard attribute uint8_t type; // Flags that control how attr is read/written/removed uint8_t flags; // Pointer the buffer where the attr will be read/written void *buffer; // Size of the attr buffer in bytes, this can be set to // LFS_ERR_NOATTR to remove the attr lfs_ssize_t buffer_size; // Optional pointer to a mutable attr size, updated on read/write, // set to LFS_ERR_NOATTR if attr does not exist // // Defaults to buffer_size if NULL lfs_ssize_t *size; }; Which are useful for several new features: - lfs_attr now supports LFS_A_RDONLY/WRONLY/RDWR modes. One of the blockers for attribute broadcasting was in-ROM attributes, where broadcast updates would hard-fault. But now if you mark in-ROM attributes as WRONLY, and in-RAM attributes as RDWR, this problem goes away. - When opened, lfs_attr now optionally writes the attribute size to the indirect size field. No more hacky zero padding and not knowing an attribute's size. Note this follows the same rules as lfsr_getattr, so it does truncate if the buffer is too small. The size field can also be set to NULL, in which case lfs_attr defaults to the buffer_size. This can be quite useful for pure ROM-backed attributes. - Missing attributes are now represented with size=LFS_ERR_NOATTR. No more zero-sized vs missing attribute ambiguity. This also makes it possible to remove attributes via lfs_attr, by setting the size to LFS_ERR_NOATTR manually. This does lead to a bit of a quirk where buffer_size can be LFS_ERR_NOATTR, which is a bit weird but at least consistent. - Changes to lfs_attrs will now always trigger file syncs by default. Previously, if you changed an attribute, you had to also change the file's contents for it to get written to disk. As pointed out by users this is both surprising and difficult to work around. Solving this is quite tricky since there's no real signalling mechanism between attribute buffers and littlefs. The best I could come up with is to read attributes from disk during lfsr_file_sync to see if anything changed. At the very least, the new flag LFS_A_LAZY restores the old behavior in case the extra reads in lfsr_file_sync are problematic. Though I suspect _most_ calls to lfsr_file_sync immediately follow intentional changes to a file. It would be interesting to know of examples where this is not the case... These new fields do increase the size of lfs_attr, which is a downside, but thanks to flags fitting in type's padding, this is only an increase from 3 words (12 bytes) -> 4 words (16 bytes). --- Other implementation notes: - I did try to implement LFS_A_CREAT/EXCL in lfs_attr but this proved to be too messy and inconsistent, so I dropped the idea for now. The idea was to error with NOATTR/EXIST if the lfs_attr flag in incompatible with what's on disk, but this led to a lot of complexity for what is a pretty niche use case. It's also inconsistent with rdonly attrs, which do _not_ error with NOATTR during lfsr_file_opencfg, because that would be kind of annoying. - Having both `struct lfs_attr` and `lfsr_attr_t` to represent different things in the codebase is both fragile and confusing. One of these needs to change, probably `lfsr_attr_t`. If only I could think of a good name... One of the nice side-effects of the now-dropped uattr/sattr split was avoiding this conflict. - We still need more tests related to how custom attributes interact with other filesystem operations, but I wanted to get what is currently working committed, see the TODOs in test_attrs.toml. All of the new bells and whistles unfortunately do add up. lfsr_file_sync is also the root of our current stack hot-path, so the additional attr also adds a bit of stack: code stack before: 37116 2608 after: 38104 (+2.7%) 2624 (+0.6%) Still, having a consistent and flexible API is well worth it. Though I do think at some point we should add a compile-time option to opt-out of custom attributes (LFS_NO_ATTR?). --- lfs.c | 327 +++++++++++-- lfs.h | 40 ++ tests/test_attrs.toml | 1044 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 1360 insertions(+), 51 deletions(-) diff --git a/lfs.c b/lfs.c index 3fb8e31b..d79aaf52 100644 --- a/lfs.c +++ b/lfs.c @@ -1419,6 +1419,7 @@ enum lfsr_tag { LFSR_TAG_SHRUBCOMMIT = 0x0801, LFSR_TAG_SHRUBTRUNK = 0x0802, LFSR_TAG_MOVE = 0x0803, + LFSR_TAG_UATTRS = 0x0804, // some in-device only tag modifiers LFSR_TAG_RM = 0x8000, @@ -2394,6 +2395,11 @@ typedef struct lfsr_data_name { #define LFSR_ATTR_ATTRS(_tag, _weight, _attrs, _attr_count) \ LFSR_ATTR_(_tag, _weight, (const lfsr_attr_t*){_attrs}, _attr_count) +// chain a list of user attributes, these require a bit of last-minute +// reencoding during commits +#define LFSR_ATTR_UATTRS(_tag, _weight, _attrs, _attr_count) \ + LFSR_ATTR_(_tag, _weight, (const struct lfs_attr*){_attrs}, _attr_count) + // a move of all attrs from an mdir entry #define LFSR_ATTR_MOVE(_tag, _weight, _mdir) \ LFSR_ATTR_(_tag, _weight, (const lfsr_mdir_t*){_mdir}, 0) @@ -2419,6 +2425,41 @@ typedef struct lfsr_shrubcommit lfsr_shrubcommit_t; LFSR_ATTR_(_tag, _weight, (const lfsr_shrub_t*){_shrub}, 0) +// operations on custom attribute lists +// +// a slightly different struct because it's user facing + +static inline lfs_ssize_t lfsr_uattr_size(const struct lfs_attr *attr) { + // we default to the buffer_size if a mutable size is not provided + if (attr->size) { + return *attr->size; + } else { + return attr->buffer_size; + } +} + +static inline bool lfsr_uattr_isnoattr(const struct lfs_attr *attr) { + return lfsr_uattr_size(attr) == LFS_ERR_NOATTR; +} + +static lfs_scmp_t lfsr_uattr_cmp(lfs_t *lfs, const struct lfs_attr *attr, + const lfsr_data_t *data) { + // note data=NULL => NOATTR + if (!data) { + return (lfsr_uattr_isnoattr(attr)) ? LFS_CMP_EQ : LFS_CMP_GT; + } else { + if (lfsr_uattr_isnoattr(attr)) { + return LFS_CMP_LT; + } else { + return lfsr_data_cmp(lfs, *data, + attr->buffer, + lfsr_uattr_size(attr)); + } + } +} + + + //struct lfsr_attr_from { // const lfsr_rbyd_t *rbyd; // const struct lfsr_attr *attrs; @@ -7117,6 +7158,11 @@ static inline bool lfsr_o_iszombie(uint32_t flags) { return flags & LFS_O_ZOMBIE; } +// custom attr flags +static inline bool lfsr_a_islazy(uint32_t flags) { + return flags & LFS_A_LAZY; +} + // traversal flags static inline bool lfsr_t_ismtreeonly(uint32_t flags) { return flags & LFS_T_MTREEONLY; @@ -8135,6 +8181,57 @@ static int lfsr_mdir_commit__(lfs_t *lfs, lfsr_mdir_t *mdir, } } + // custom attributes need to be reencoded into our tag format + } else if (lfsr_tag_key(attrs[i].tag) == LFSR_TAG_UATTRS) { + const struct lfs_attr *attrs_ = attrs[i].cat; + lfs_size_t attr_count_ = attrs[i].count; + + for (lfs_size_t j = 0; j < attr_count_; j++) { + // skip readonly attrs and lazy attrs + if (lfsr_o_isrdonly(attrs_[j].flags)) { + continue; + } + + // first lets check if the attr changed, we don't want + // to append attrs unless we have to + lfsr_data_t data; + int err = lfsr_mdir_lookup(lfs, mdir, + LFSR_TAG_ATTR(attrs_[j].type), + &data); + if (err && err != LFS_ERR_NOENT) { + return err; + } + + // does disk match our attr? + lfs_scmp_t cmp = lfsr_uattr_cmp(lfs, &attrs_[j], + (err != LFS_ERR_NOENT) ? &data : NULL); + if (cmp < 0) { + return cmp; + } + + if (cmp == LFS_CMP_EQ) { + continue; + } + + // append the custom attr + err = lfsr_rbyd_appendattr(lfs, &mdir->rbyd, + rid - lfs_smax(start_rid, 0), + // removing or updating? + (lfsr_uattr_isnoattr(&attrs_[j])) + ? LFSR_ATTR( + LFSR_TAG_RM + | LFSR_TAG_ATTR(attrs_[j].type), 0, + LFSR_DATA_NULL()) + : LFSR_ATTR( + LFSR_TAG_ATTR(attrs_[j].type), 0, + LFSR_DATA_BUF( + attrs_[j].buffer, + lfsr_uattr_size(&attrs_[j])))); + if (err) { + return err; + } + } + // write out normal tags normally } else { LFS_ASSERT(!lfsr_tag_isinternal(attrs[i].tag)); @@ -11149,10 +11246,39 @@ int lfsr_setattr(lfs_t *lfs, const char *path, uint8_t type, // commit our attr lfs_alloc_ckpoint(lfs); - return lfsr_mdir_commit(lfs, &mdir, LFSR_ATTRS( + err = lfsr_mdir_commit(lfs, &mdir, LFSR_ATTRS( LFSR_ATTR( LFSR_TAG_ATTR(type), 0, LFSR_DATA_BUF(buffer, size)))); + if (err) { + return err; + } + + // update any opened files tracking custom attrs + for (lfsr_omdir_t *o = lfs->omdirs; o; o = o->next) { + if (!(lfsr_o_type(o->flags) == LFS_TYPE_REG + && o->mdir.mid == mdir.mid + && !lfsr_o_isdesync(o->flags))) { + continue; + } + + lfsr_file_t *file = (lfsr_file_t*)o; + for (lfs_size_t i = 0; i < file->cfg->attr_count; i++) { + if (!(file->cfg->attrs[i].type == type + && !lfsr_o_iswronly(file->cfg->attrs[i].flags))) { + continue; + } + + lfs_size_t d = lfs_min(size, file->cfg->attrs[i].buffer_size); + memcpy(file->cfg->attrs[i].buffer, buffer, d); + if (file->cfg->attrs[i].size) { + *file->cfg->attrs[i].size = d; + } + } + } + + + return 0; } int lfsr_removeattr(lfs_t *lfs, const char *path, uint8_t type) { @@ -11172,10 +11298,36 @@ int lfsr_removeattr(lfs_t *lfs, const char *path, uint8_t type) { // commit our removal lfs_alloc_ckpoint(lfs); - return lfsr_mdir_commit(lfs, &mdir, LFSR_ATTRS( + err = lfsr_mdir_commit(lfs, &mdir, LFSR_ATTRS( LFSR_ATTR( LFSR_TAG_RM | LFSR_TAG_ATTR(type), 0, LFSR_DATA_NULL()))); + if (err) { + return err; + } + + // update any opened files tracking custom attrs + for (lfsr_omdir_t *o = lfs->omdirs; o; o = o->next) { + if (!(lfsr_o_type(o->flags) == LFS_TYPE_REG + && o->mdir.mid == mdir.mid + && !lfsr_o_isdesync(o->flags))) { + continue; + } + + lfsr_file_t *file = (lfsr_file_t*)o; + for (lfs_size_t i = 0; i < file->cfg->attr_count; i++) { + if (!(file->cfg->attrs[i].type == type + && !lfsr_o_iswronly(file->cfg->attrs[i].flags))) { + continue; + } + + if (file->cfg->attrs[i].size) { + *file->cfg->attrs[i].size = LFS_ERR_NOATTR; + } + } + } + + return 0; } @@ -11292,6 +11444,43 @@ static int lfsr_file_fetch(lfs_t *lfs, lfsr_file_t *file) { file->buffer.size = size; } + // try to fetch any custom attributes + for (lfs_size_t i = 0; i < file->cfg->attr_count; i++) { + // skip writeonly attrs + if (lfsr_o_iswronly(file->cfg->attrs[i].flags)) { + continue; + } + + // lookup the attr + lfsr_data_t data; + int err = lfsr_mdir_lookup(lfs, &file->o.o.mdir, + LFSR_TAG_ATTR(file->cfg->attrs[i].type), + &data); + if (err && err != LFS_ERR_NOENT) { + return err; + } + + // read the attr, if it exists + if (err == LFS_ERR_NOENT + // awkward case here if buffer_size is LFS_ERR_NOATTR + || file->cfg->attrs[i].buffer_size == LFS_ERR_NOATTR) { + if (file->cfg->attrs[i].size) { + *file->cfg->attrs[i].size = LFS_ERR_NOATTR; + } + } else { + lfs_ssize_t d = lfsr_data_read(lfs, &data, + file->cfg->attrs[i].buffer, + file->cfg->attrs[i].buffer_size); + if (d < 0) { + return d; + } + + if (file->cfg->attrs[i].size) { + *file->cfg->attrs[i].size = d; + } + } + } + return 0; } @@ -11324,6 +11513,13 @@ int lfsr_file_opencfg(lfs_t *lfs, lfsr_file_t *file, LFS_ASSERT(!lfsr_o_isrdonly(flags) || !lfsr_o_iscreat(flags)); LFS_ASSERT(!lfsr_o_isrdonly(flags) || !lfsr_o_isexcl(flags)); LFS_ASSERT(!lfsr_o_isrdonly(flags) || !lfsr_o_istrunc(flags)); + for (lfs_size_t i = 0; i < cfg->attr_count; i++) { + // these flags require a writable attr + LFS_ASSERT(!lfsr_o_isrdonly(cfg->attrs[i].flags) + || !lfsr_o_iscreat(cfg->attrs[i].flags)); + LFS_ASSERT(!lfsr_o_isrdonly(cfg->attrs[i].flags) + || !lfsr_o_isexcl(cfg->attrs[i].flags)); + } if (!lfsr_o_isrdonly(flags)) { // prepare our filesystem for writing @@ -12810,30 +13006,34 @@ int lfsr_file_sync(lfs_t *lfs, lfsr_file_t *file) { LFS_ASSERT(!lfsr_o_isorphan(file->o.o.flags) || lfsr_o_isunsync(file->o.o.flags)); - // don't write to disk if already in-sync - if (lfsr_o_isunsync(file->o.o.flags)) { - // commit any changes to our file's metadata - lfsr_attr_t attrs[2]; - lfs_size_t attr_count = 0; - lfsr_data_t name_data; - uint8_t buf[LFSR_BTREE_DSIZE]; + // build a commit of any pending file metadata + lfsr_attr_t attrs[3]; + lfs_size_t attr_count = 0; + lfsr_data_t name_data; + uint8_t buf[LFSR_BTREE_DSIZE]; - // not created yet? need to convert orphan to normal file - if (lfsr_o_isorphan(file->o.o.flags)) { - err = lfsr_mdir_lookup(lfs, &file->o.o.mdir, LFSR_TAG_ORPHAN, - &name_data); - if (err) { - // we must have an orphan at this point - LFS_ASSERT(err != LFS_ERR_NOENT); - goto failed; - } - - attrs[attr_count++] = LFSR_ATTR_CAT_( - LFSR_TAG_SUB | LFSR_TAG_REG, 0, - &name_data, 1); + // not created yet? need to convert orphan to normal file + if (lfsr_o_isorphan(file->o.o.flags)) { + err = lfsr_mdir_lookup(lfs, &file->o.o.mdir, LFSR_TAG_ORPHAN, + &name_data); + if (err) { + // orphan flag but no orphan tag? + LFS_ASSERT(err != LFS_ERR_NOENT); + goto failed; } - // commit the file state + attrs[attr_count++] = LFSR_ATTR_CAT_( + LFSR_TAG_SUB | LFSR_TAG_REG, 0, + &name_data, 1); + } + + // pending file changes? + if (lfsr_o_isunsync(file->o.o.flags)) { + // make sure data is on-disk before committing metadata + err = lfsr_bd_sync(lfs); + if (err) { + goto failed; + } // null? no attr? if (lfsr_o_isunflush(file->o.o.flags) && file->buffer.size == 0) { @@ -12858,13 +13058,54 @@ int lfsr_file_sync(lfs_t *lfs, lfsr_file_t *file) { } else { LFS_UNREACHABLE(); } + } - // make sure data is on-disk before committing metadata - err = lfsr_bd_sync(lfs); - if (err) { - goto failed; + // pending custom attributes? + // + // this gets real messy, since users can change custom attributes + // whenever they want without informing littlefs, the best we can do + // is read from disk to manually check if any attributes changed + bool uattrs = lfsr_o_isunsync(file->o.o.flags); + if (!uattrs) { + for (lfs_size_t i = 0; i < file->cfg->attr_count; i++) { + // skip readonly attrs and lazy attrs + if (lfsr_o_isrdonly(file->cfg->attrs[i].flags) + || lfsr_a_islazy(file->cfg->attrs[i].flags)) { + continue; + } + + // lookup the attr + lfsr_data_t data; + err = lfsr_mdir_lookup(lfs, &file->o.o.mdir, + LFSR_TAG_ATTR(file->cfg->attrs[i].type), + &data); + if (err && err != LFS_ERR_NOENT) { + goto failed; + } + + // does disk match our attr? + lfs_scmp_t cmp = lfsr_uattr_cmp(lfs, &file->cfg->attrs[i], + (err != LFS_ERR_NOENT) ? &data : NULL); + if (cmp < 0) { + err = cmp; + goto failed; + } + + if (cmp != LFS_CMP_EQ) { + uattrs = true; + break; + } } + } + if (uattrs) { + // need to append custom attributes + attrs[attr_count++] = LFSR_ATTR_UATTRS( + LFSR_TAG_UATTRS, 0, + file->cfg->attrs, file->cfg->attr_count); + } + // pending metadata? looks like we need to write to disk + if (attr_count > 0) { // checkpoint the allocator lfs_alloc_ckpoint(lfs); @@ -12907,6 +13148,38 @@ int lfsr_file_sync(lfs_t *lfs, lfsr_file_t *file) { file->buffer.buffer, file->buffer.size); file_->buffer.size = file->buffer.size; + + // update any custom attrs + for (lfs_size_t i = 0; i < file->cfg->attr_count; i++) { + if (lfsr_o_isrdonly(file->cfg->attrs[i].flags)) { + continue; + } + + for (lfs_size_t j = 0; j < file_->cfg->attr_count; j++) { + if (!(file_->cfg->attrs[j].type + == file->cfg->attrs[i].type + && !lfsr_o_iswronly( + file_->cfg->attrs[j].flags))) { + continue; + } + + if (lfsr_uattr_isnoattr(&file->cfg->attrs[i])) { + if (file_->cfg->attrs[j].size) { + *file_->cfg->attrs[j].size = LFS_ERR_NOATTR; + } + } else { + lfs_size_t d = lfs_min( + lfsr_uattr_size(&file->cfg->attrs[i]), + file_->cfg->attrs[j].buffer_size); + memcpy(file_->cfg->attrs[j].buffer, + file->cfg->attrs[i].buffer, + d); + if (file_->cfg->attrs[j].size) { + *file_->cfg->attrs[j].size = d; + } + } + } + } } // clobber entangled traversals diff --git a/lfs.h b/lfs.h index 9b43feaa..7f3dbe97 100644 --- a/lfs.h +++ b/lfs.h @@ -152,8 +152,12 @@ enum lfs_type { #define LFS_SEEK_END 2 // Seek relative to the end of the file // Custom attribute flags +#define LFS_A_RDONLY 0 // Open an attr as read only +#define LFS_A_WRONLY 1 // Open an attr as write only +#define LFS_A_RDWR 2 // Open an attr as read and write #define LFS_A_CREAT 0x04 // Create an attr if it does not exist #define LFS_A_EXCL 0x08 // Fail if an attr already exists +#define LFS_A_LAZY 0x10 // Only write attr if file changed // Filesystem format flags #define LFS_F_RDWR 0 // Format the filesystem as read and write @@ -481,6 +485,33 @@ struct lfs_tinfo { // lfs_size_t size; //}; +// Custom attribute structure, used to describe custom attributes +// committed atomically during file writes. +struct lfs_attr { + // Type of attribute + // + // Note some of this range is reserved: + // 0x00-0x7f - Free for custom attributes + // 0x80-0xff - May be assigned a standard attribute + uint8_t type; + + // Flags that control how attr is read/written/removed + uint8_t flags; + + // Pointer the buffer where the attr will be read/written + void *buffer; + + // Size of the attr buffer in bytes, this can be set to + // LFS_ERR_NOATTR to remove the attr + lfs_ssize_t buffer_size; + + // Optional pointer to a mutable attr size, updated on read/write, + // set to LFS_ERR_NOATTR if attr does not exist + // + // Defaults to buffer_size if NULL + lfs_ssize_t *size; +}; + // Optional configuration provided during lfs_file_opencfg struct lfs_file_config { // Optional statically allocated file buffer. Must be buffer_size. @@ -507,6 +538,15 @@ struct lfs_file_config { // // // Number of custom attributes in the list // lfs_size_t attr_count; + + // Optional list of custom attributes attached to the file. If readable, + // these attributes will be kept up to date with the attributes on-disk. + // If writeable, these attributes will be written to disk atomically on + // every file sync or close. + struct lfs_attr *attrs; + + // Number of custom attributes in the list + lfs_size_t attr_count; }; diff --git a/tests/test_attrs.toml b/tests/test_attrs.toml index cca7ebb4..2dbdfa5d 100644 --- a/tests/test_attrs.toml +++ b/tests/test_attrs.toml @@ -2,6 +2,8 @@ after = ['test_files', 'test_fsync', 'test_forphans'] +## General setattr/getattr tests + # test some simple attr operations [cases.test_attrs_setattr] # type of file to attach attrs to @@ -68,13 +70,14 @@ code = ''' lfsr_unmount(&lfs) => 0; ''' -# test some simple attr operations +# test truncated getattr calls still work [cases.test_attrs_setattr_trunc] # type of file to attach attrs to # FILETYPE=0 => regular file # FILETYPE=1 => directory # FILETYPE=2 => root defines.FILETYPE = [0, 1, 2] +defines.BUFSIZE = [1, 4, 7] code = ''' lfs_t lfs; lfsr_format(&lfs, LFS_F_RDWR, CFG) => 0; @@ -124,30 +127,17 @@ code = ''' // mark rbuf so we can detect overflow uint8_t rbuf[256]; - rbuf[1] = '!'; + rbuf[BUFSIZE] = '!'; // try reading truncated attrs - lfsr_getattr(&lfs, path, 'a', rbuf, 1) => 1; - assert(memcmp(rbuf, a, 1) == 0); - assert(rbuf[1] == '!'); - lfsr_getattr(&lfs, path, 'b', rbuf, 1) => 1; - assert(memcmp(rbuf, b, 1) == 0); - assert(rbuf[1] == '!'); - lfsr_getattr(&lfs, path, 'c', rbuf, 1) => 1; - assert(memcmp(rbuf, c, 1) == 0); - assert(rbuf[1] == '!'); - - // mark rbuf so we can detect overflow - rbuf[4] = '!'; - // try reading truncated attrs - lfsr_getattr(&lfs, path, 'a', rbuf, 4) => 4; - assert(memcmp(rbuf, a, 4) == 0); - assert(rbuf[4] == '!'); - lfsr_getattr(&lfs, path, 'b', rbuf, 4) => 4; - assert(memcmp(rbuf, b, 4) == 0); - assert(rbuf[4] == '!'); - lfsr_getattr(&lfs, path, 'c', rbuf, 4) => 4; - assert(memcmp(rbuf, c, 4) == 0); - assert(rbuf[4] == '!'); + lfsr_getattr(&lfs, path, 'a', rbuf, BUFSIZE) => BUFSIZE; + assert(memcmp(rbuf, a, BUFSIZE) == 0); + assert(rbuf[BUFSIZE] == '!'); + lfsr_getattr(&lfs, path, 'b', rbuf, BUFSIZE) => BUFSIZE; + assert(memcmp(rbuf, b, BUFSIZE) == 0); + assert(rbuf[BUFSIZE] == '!'); + lfsr_getattr(&lfs, path, 'c', rbuf, BUFSIZE) => BUFSIZE; + assert(memcmp(rbuf, c, BUFSIZE) == 0); + assert(rbuf[BUFSIZE] == '!'); // try reading the full attrs lfsr_getattr(&lfs, path, 'a', rbuf, sizeof(rbuf)) => strlen(a); @@ -1515,21 +1505,1027 @@ code = ''' ''' +## Tests involving file-attached attrs +# test that file-attached attrs are read correctly +[cases.test_attrs_fattr_get] +defines.MODE = ['LFS_A_RDONLY', 'LFS_A_RDWR'] +defines.MUTSIZE = [false, true] +code = ''' + lfs_t lfs; + lfsr_format(&lfs, LFS_F_RDWR, CFG) => 0; + lfsr_mount(&lfs, LFS_M_RDWR, CFG) => 0; + // create a file + lfsr_file_t file; + lfsr_file_open(&lfs, &file, "cat", LFS_O_WRONLY | LFS_O_CREAT) => 0; + lfsr_file_write(&lfs, &file, "meow", strlen("meow")) => strlen("meow"); + lfsr_file_close(&lfs, &file) => 0; + // create some attrs + const char *a = "One 18.25 ounce package chocolate cake mix."; + lfsr_setattr(&lfs, "cat", 'a', a, strlen(a), + LFS_A_CREAT | LFS_A_EXCL) => 0; + const char *b = "One can prepared coconut pecan frosting."; + lfsr_setattr(&lfs, "cat", 'b', b, strlen(b), + LFS_A_CREAT | LFS_A_EXCL) => 0; + const char *c = "Three slash four cup vegetable oil."; + lfsr_setattr(&lfs, "cat", 'c', c, strlen(c), + LFS_A_CREAT | LFS_A_EXCL) => 0; + // try opening a file with these attrs + uint8_t a_buf[256]; + lfs_ssize_t a_size = -1; + uint8_t b_buf[256]; + lfs_ssize_t b_size = -1; + uint8_t c_buf[256]; + lfs_ssize_t c_size = -1; + struct lfs_attr attrs[] = { + { + .type = 'a', + .flags = MODE, + .buffer = a_buf, + .buffer_size = sizeof(a_buf), + .size = (MUTSIZE) ? &a_size : NULL, + }, + { + .type = 'b', + .flags = MODE, + .buffer = b_buf, + .buffer_size = sizeof(b_buf), + .size = (MUTSIZE) ? &b_size : NULL, + }, + { + .type = 'c', + .flags = MODE, + .buffer = c_buf, + .buffer_size = sizeof(c_buf), + .size = (MUTSIZE) ? &c_size : NULL, + } + }; + struct lfs_file_config filecfg = { + .attrs = attrs, + .attr_count = 3, + }; + lfsr_file_opencfg(&lfs, &file, "cat", MODE, &filecfg) => 0; + // did we read the attrs correctly? + if (MUTSIZE) { + assert(a_size == strlen(a)); + } + assert(memcmp(a_buf, a, strlen(a)) == 0); + if (MUTSIZE) { + assert(b_size == strlen(b)); + } + assert(memcmp(b_buf, b, strlen(b)) == 0); + if (MUTSIZE) { + assert(c_size == strlen(c)); + } + assert(memcmp(c_buf, c, strlen(c)) == 0); + lfsr_file_close(&lfs, &file) => 0; + lfsr_unmount(&lfs) => 0; +''' +# test that truncate attrs will work +[cases.test_attrs_fattr_trunc] +defines.MODE = ['LFS_A_RDONLY', 'LFS_A_RDWR'] +defines.MUTSIZE = [false, true] +defines.BUFSIZE = [1, 4, 7] +code = ''' + lfs_t lfs; + lfsr_format(&lfs, LFS_F_RDWR, CFG) => 0; + lfsr_mount(&lfs, LFS_M_RDWR, CFG) => 0; + // create a file + lfsr_file_t file; + lfsr_file_open(&lfs, &file, "cat", LFS_O_WRONLY | LFS_O_CREAT) => 0; + lfsr_file_write(&lfs, &file, "meow", strlen("meow")) => strlen("meow"); + lfsr_file_close(&lfs, &file) => 0; + // create some attrs + const char *a = "One 18.25 ounce package chocolate cake mix."; + lfsr_setattr(&lfs, "cat", 'a', a, strlen(a), + LFS_A_CREAT | LFS_A_EXCL) => 0; + const char *b = "One can prepared coconut pecan frosting."; + lfsr_setattr(&lfs, "cat", 'b', b, strlen(b), + LFS_A_CREAT | LFS_A_EXCL) => 0; + const char *c = "Three slash four cup vegetable oil."; + lfsr_setattr(&lfs, "cat", 'c', c, strlen(c), + LFS_A_CREAT | LFS_A_EXCL) => 0; + // try opening a file with these attrs, marking bufs so we can + // detect overflow + uint8_t a_buf[256]; + a_buf[BUFSIZE] = '!'; + lfs_ssize_t a_size = -1; + uint8_t b_buf[256]; + b_buf[BUFSIZE] = '!'; + lfs_ssize_t b_size = -1; + uint8_t c_buf[256]; + c_buf[BUFSIZE] = '!'; + lfs_ssize_t c_size = -1; + struct lfs_attr attrs[] = { + { + .type = 'a', + .flags = MODE, + .buffer = a_buf, + .buffer_size = BUFSIZE, + .size = (MUTSIZE) ? &a_size : NULL, + }, + { + .type = 'b', + .flags = MODE, + .buffer = b_buf, + .buffer_size = BUFSIZE, + .size = (MUTSIZE) ? &b_size : NULL, + }, + { + .type = 'c', + .flags = MODE, + .buffer = c_buf, + .buffer_size = BUFSIZE, + .size = (MUTSIZE) ? &c_size : NULL, + } + }; + struct lfs_file_config filecfg = { + .attrs = attrs, + .attr_count = 3, + }; + lfsr_file_opencfg(&lfs, &file, "cat", MODE, &filecfg) => 0; + // did we read the attrs correctly? no overflow? + if (MUTSIZE) { + assert(a_size == BUFSIZE); + } + assert(memcmp(a_buf, a, BUFSIZE) == 0); + assert(a_buf[BUFSIZE] == '!'); + if (MUTSIZE) { + assert(b_size == BUFSIZE); + } + assert(memcmp(b_buf, b, BUFSIZE) == 0); + assert(a_buf[BUFSIZE] == '!'); + if (MUTSIZE) { + assert(c_size == BUFSIZE); + } + assert(memcmp(c_buf, c, BUFSIZE) == 0); + assert(a_buf[BUFSIZE] == '!'); + lfsr_file_close(&lfs, &file) => 0; + lfsr_unmount(&lfs) => 0; +''' +# test that missing attrs are read correctly +[cases.test_attrs_fattr_noattr] +defines.MODE = ['LFS_A_RDONLY', 'LFS_A_RDWR'] +defines.MUTSIZE = [false, true] +code = ''' + lfs_t lfs; + lfsr_format(&lfs, LFS_F_RDWR, CFG) => 0; + lfsr_mount(&lfs, LFS_M_RDWR, CFG) => 0; + // create a file + lfsr_file_t file; + lfsr_file_open(&lfs, &file, "cat", LFS_O_WRONLY | LFS_O_CREAT) => 0; + lfsr_file_write(&lfs, &file, "meow", strlen("meow")) => strlen("meow"); + lfsr_file_close(&lfs, &file) => 0; + // try opening a file with missing attrs + uint8_t a_buf[256]; + lfs_ssize_t a_size = -1; + uint8_t b_buf[256]; + lfs_ssize_t b_size = -1; + uint8_t c_buf[256]; + lfs_ssize_t c_size = -1; + struct lfs_attr attrs[] = { + { + .type = 'a', + .flags = MODE, + .buffer = a_buf, + .buffer_size = sizeof(a_buf), + .size = (MUTSIZE) ? &a_size : NULL, + }, + { + .type = 'b', + .flags = MODE, + .buffer = b_buf, + .buffer_size = sizeof(b_buf), + .size = (MUTSIZE) ? &b_size : NULL, + }, + { + .type = 'c', + .flags = MODE, + .buffer = c_buf, + .buffer_size = sizeof(c_buf), + .size = (MUTSIZE) ? &c_size : NULL, + } + }; + struct lfs_file_config filecfg = { + .attrs = attrs, + .attr_count = 3, + }; + lfsr_file_opencfg(&lfs, &file, "cat", MODE, &filecfg) => 0; + + // did we read the attrs correctly? + if (MUTSIZE) { + assert(a_size == LFS_ERR_NOATTR); + } + if (MUTSIZE) { + assert(b_size == LFS_ERR_NOATTR); + } + if (MUTSIZE) { + assert(c_size == LFS_ERR_NOATTR); + } + + lfsr_file_close(&lfs, &file) => 0; + lfsr_unmount(&lfs) => 0; +''' + +# test that file-attached attrs are written correctly +[cases.test_attrs_fattr_set] +defines.MODE = ['LFS_A_WRONLY', 'LFS_A_RDWR'] +defines.MUTSIZE = [false, true] +code = ''' + lfs_t lfs; + lfsr_format(&lfs, LFS_F_RDWR, CFG) => 0; + lfsr_mount(&lfs, LFS_M_RDWR, CFG) => 0; + + // create a file + lfsr_file_t file; + lfsr_file_open(&lfs, &file, "cat", LFS_O_WRONLY | LFS_O_CREAT) => 0; + lfsr_file_write(&lfs, &file, "meow", strlen("meow")) => strlen("meow"); + lfsr_file_close(&lfs, &file) => 0; + + // create some attrs + const char *a = "One 18.25 ounce package chocolate cake mix."; + const char *b = "One can prepared coconut pecan frosting."; + const char *c = "Three slash four cup vegetable oil."; + + // try opening a file with attrs + uint8_t a_buf[256]; + lfs_ssize_t a_size; + uint8_t b_buf[256]; + lfs_ssize_t b_size; + uint8_t c_buf[256]; + lfs_ssize_t c_size; + struct lfs_attr attrs[] = { + { + .type = 'a', + .flags = MODE, + .buffer = a_buf, + .buffer_size = (MUTSIZE) ? sizeof(a_buf) : strlen(a), + .size = (MUTSIZE) ? &a_size : NULL, + }, + { + .type = 'b', + .flags = MODE, + .buffer = b_buf, + .buffer_size = (MUTSIZE) ? sizeof(b_buf) : strlen(b), + .size = (MUTSIZE) ? &b_size : NULL, + }, + { + .type = 'c', + .flags = MODE, + .buffer = c_buf, + .buffer_size = (MUTSIZE) ? sizeof(c_buf) : strlen(c), + .size = (MUTSIZE) ? &c_size : NULL, + } + }; + struct lfs_file_config filecfg = { + .attrs = attrs, + .attr_count = 3, + }; + lfsr_file_opencfg(&lfs, &file, "cat", MODE, &filecfg) => 0; + + // set the attrs + memcpy(a_buf, a, strlen(a)); + memcpy(b_buf, b, strlen(b)); + memcpy(c_buf, c, strlen(c)); + a_size = strlen(a); + b_size = strlen(b); + c_size = strlen(c); + + // write and close our file to write the attrs out to disk + lfsr_file_write(&lfs, &file, "miao", strlen("miao")) => strlen("miao"); + lfsr_file_close(&lfs, &file) => 0; + + for (int remount = 0; remount < 2; remount++) { + // remount? + if (remount) { + lfsr_unmount(&lfs) => 0; + lfsr_mount(&lfs, LFS_M_RDWR, CFG) => 0; + } + + // try getting the attr sizes + lfsr_sizeattr(&lfs, "cat", 'a') => strlen(a); + lfsr_sizeattr(&lfs, "cat", 'b') => strlen(b); + lfsr_sizeattr(&lfs, "cat", 'c') => strlen(c); + // try reading the attrs + uint8_t rbuf[256]; + lfsr_getattr(&lfs, "cat", 'a', rbuf, sizeof(rbuf)) => strlen(a); + assert(memcmp(rbuf, a, strlen(a)) == 0); + lfsr_getattr(&lfs, "cat", 'b', rbuf, sizeof(rbuf)) => strlen(b); + assert(memcmp(rbuf, b, strlen(b)) == 0); + lfsr_getattr(&lfs, "cat", 'c', rbuf, sizeof(rbuf)) => strlen(c); + assert(memcmp(rbuf, c, strlen(c)) == 0); + } + + lfsr_unmount(&lfs) => 0; +''' + +# test that we can update existing attrs +[cases.test_attrs_fattr_update] +defines.MODE = ['LFS_A_WRONLY', 'LFS_A_RDWR'] +defines.MUTSIZE = [false, true] +code = ''' + lfs_t lfs; + lfsr_format(&lfs, LFS_F_RDWR, CFG) => 0; + lfsr_mount(&lfs, LFS_M_RDWR, CFG) => 0; + + // create a file + lfsr_file_t file; + lfsr_file_open(&lfs, &file, "cat", LFS_O_WRONLY | LFS_O_CREAT) => 0; + lfsr_file_write(&lfs, &file, "meow", strlen("meow")) => strlen("meow"); + lfsr_file_close(&lfs, &file) => 0; + + // create some attrs + const char *a = "One 18.25 ounce package chocolate cake mix."; + lfsr_setattr(&lfs, "cat", 'a', a, strlen(a), + LFS_A_CREAT | LFS_A_EXCL) => 0; + const char *b = "One can prepared coconut pecan frosting."; + lfsr_setattr(&lfs, "cat", 'b', b, strlen(b), + LFS_A_CREAT | LFS_A_EXCL) => 0; + const char *c = "Three slash four cup vegetable oil."; + lfsr_setattr(&lfs, "cat", 'c', c, strlen(c), + LFS_A_CREAT | LFS_A_EXCL) => 0; + + // try opening a file with attrs + const char *a_ = "Four large eggs. One cup semi-sweet chocolate chips."; + const char *b_ = "Three slash four cup butter or margarine."; + const char *c_ = "One and two third cups granulated sugar."; + uint8_t a_buf[256]; + lfs_ssize_t a_size; + uint8_t b_buf[256]; + lfs_ssize_t b_size; + uint8_t c_buf[256]; + lfs_ssize_t c_size; + struct lfs_attr attrs[] = { + { + .type = 'a', + .flags = MODE, + .buffer = a_buf, + .buffer_size = (MUTSIZE) ? sizeof(a_buf) : strlen(a_), + .size = (MUTSIZE) ? &a_size : NULL, + }, + { + .type = 'b', + .flags = MODE, + .buffer = b_buf, + .buffer_size = (MUTSIZE) ? sizeof(b_buf) : strlen(b_), + .size = (MUTSIZE) ? &b_size : NULL, + }, + { + .type = 'c', + .flags = MODE, + .buffer = c_buf, + .buffer_size = (MUTSIZE) ? sizeof(c_buf) : strlen(c_), + .size = (MUTSIZE) ? &c_size : NULL, + } + }; + struct lfs_file_config filecfg = { + .attrs = attrs, + .attr_count = 3, + }; + lfsr_file_opencfg(&lfs, &file, "cat", MODE, &filecfg) => 0; + + if (MODE == LFS_A_RDWR && MUTSIZE) { + // did we read the attrs correctly? + assert(a_size == strlen(a)); + assert(memcmp(a_buf, a, strlen(a)) == 0); + assert(b_size == strlen(b)); + assert(memcmp(b_buf, b, strlen(b)) == 0); + assert(c_size == strlen(c)); + assert(memcmp(c_buf, c, strlen(c)) == 0); + } + + // update the attrs with new values + memcpy(a_buf, a_, strlen(a_)); + memcpy(b_buf, b_, strlen(b_)); + memcpy(c_buf, c_, strlen(c_)); + a_size = strlen(a_); + b_size = strlen(b_); + c_size = strlen(c_); + + // write and close our file to write the attrs out to disk + lfsr_file_write(&lfs, &file, "miao", strlen("miao")) => strlen("miao"); + lfsr_file_close(&lfs, &file) => 0; + + for (int remount = 0; remount < 2; remount++) { + // remount? + if (remount) { + lfsr_unmount(&lfs) => 0; + lfsr_mount(&lfs, LFS_M_RDWR, CFG) => 0; + } + + // try getting the attr sizes + lfsr_sizeattr(&lfs, "cat", 'a') => strlen(a_); + lfsr_sizeattr(&lfs, "cat", 'b') => strlen(b_); + lfsr_sizeattr(&lfs, "cat", 'c') => strlen(c_); + // try reading the attrs + uint8_t rbuf[256]; + lfsr_getattr(&lfs, "cat", 'a', rbuf, sizeof(rbuf)) => strlen(a_); + assert(memcmp(rbuf, a_, strlen(a_)) == 0); + lfsr_getattr(&lfs, "cat", 'b', rbuf, sizeof(rbuf)) => strlen(b_); + assert(memcmp(rbuf, b_, strlen(b_)) == 0); + lfsr_getattr(&lfs, "cat", 'c', rbuf, sizeof(rbuf)) => strlen(c_); + assert(memcmp(rbuf, c_, strlen(c_)) == 0); + } + + lfsr_unmount(&lfs) => 0; +''' + +# test that we can remove attrs +[cases.test_attrs_fattr_remove] +defines.MODE = ['LFS_A_WRONLY', 'LFS_A_RDWR'] +defines.MUTSIZE = [false, true] +code = ''' + lfs_t lfs; + lfsr_format(&lfs, LFS_F_RDWR, CFG) => 0; + lfsr_mount(&lfs, LFS_M_RDWR, CFG) => 0; + + // create a file + lfsr_file_t file; + lfsr_file_open(&lfs, &file, "cat", LFS_O_WRONLY | LFS_O_CREAT) => 0; + lfsr_file_write(&lfs, &file, "meow", strlen("meow")) => strlen("meow"); + lfsr_file_close(&lfs, &file) => 0; + + // create some attrs + const char *a = "One 18.25 ounce package chocolate cake mix."; + lfsr_setattr(&lfs, "cat", 'a', a, strlen(a), + LFS_A_CREAT | LFS_A_EXCL) => 0; + const char *b = "One can prepared coconut pecan frosting."; + lfsr_setattr(&lfs, "cat", 'b', b, strlen(b), + LFS_A_CREAT | LFS_A_EXCL) => 0; + const char *c = "Three slash four cup vegetable oil."; + lfsr_setattr(&lfs, "cat", 'c', c, strlen(c), + LFS_A_CREAT | LFS_A_EXCL) => 0; + + // try opening a file with attrs + uint8_t a_buf[256]; + lfs_ssize_t a_size; + uint8_t b_buf[256]; + lfs_ssize_t b_size; + uint8_t c_buf[256]; + lfs_ssize_t c_size; + struct lfs_attr attrs[] = { + { + .type = 'a', + .flags = MODE, + .buffer = a_buf, + .buffer_size = (MUTSIZE) + ? (lfs_ssize_t)sizeof(a_buf) + : LFS_ERR_NOATTR, + .size = (MUTSIZE) ? &a_size : NULL, + }, + { + .type = 'b', + .flags = MODE, + .buffer = b_buf, + .buffer_size = (MUTSIZE) + ? (lfs_ssize_t)sizeof(b_buf) + : LFS_ERR_NOATTR, + .size = (MUTSIZE) ? &b_size : NULL, + }, + { + .type = 'c', + .flags = MODE, + .buffer = c_buf, + .buffer_size = (MUTSIZE) + ? (lfs_ssize_t)sizeof(c_buf) + : LFS_ERR_NOATTR, + .size = (MUTSIZE) ? &c_size : NULL, + } + }; + struct lfs_file_config filecfg = { + .attrs = attrs, + .attr_count = 3, + }; + lfsr_file_opencfg(&lfs, &file, "cat", MODE, &filecfg) => 0; + + if (MODE == LFS_A_RDWR && MUTSIZE) { + // did we read the attrs correctly? + assert(a_size == strlen(a)); + assert(memcmp(a_buf, a, strlen(a)) == 0); + assert(b_size == strlen(b)); + assert(memcmp(b_buf, b, strlen(b)) == 0); + assert(c_size == strlen(c)); + assert(memcmp(c_buf, c, strlen(c)) == 0); + } + + // mark the attrs as removed + a_size = LFS_ERR_NOATTR; + b_size = LFS_ERR_NOATTR; + c_size = LFS_ERR_NOATTR; + + // write and close our file to write the attrs out to disk + lfsr_file_write(&lfs, &file, "miao", strlen("miao")) => strlen("miao"); + lfsr_file_close(&lfs, &file) => 0; + + for (int remount = 0; remount < 2; remount++) { + // remount? + if (remount) { + lfsr_unmount(&lfs) => 0; + lfsr_mount(&lfs, LFS_M_RDWR, CFG) => 0; + } + + // try getting the attr sizes + lfsr_sizeattr(&lfs, "cat", 'a') => LFS_ERR_NOATTR; + lfsr_sizeattr(&lfs, "cat", 'b') => LFS_ERR_NOATTR; + lfsr_sizeattr(&lfs, "cat", 'c') => LFS_ERR_NOATTR; + // try reading the attrs + uint8_t rbuf[256]; + lfsr_getattr(&lfs, "cat", 'a', rbuf, sizeof(rbuf)) => LFS_ERR_NOATTR; + lfsr_getattr(&lfs, "cat", 'b', rbuf, sizeof(rbuf)) => LFS_ERR_NOATTR; + lfsr_getattr(&lfs, "cat", 'c', rbuf, sizeof(rbuf)) => LFS_ERR_NOATTR; + } + + lfsr_unmount(&lfs) => 0; +''' + +# test that wronly attrs are not read from disk +[cases.test_attrs_fattr_wronly] +defines.MODE = ['LFS_A_WRONLY'] +defines.MUTSIZE = [false, true] +code = ''' + lfs_t lfs; + lfsr_format(&lfs, LFS_F_RDWR, CFG) => 0; + lfsr_mount(&lfs, LFS_M_RDWR, CFG) => 0; + + // create a file + lfsr_file_t file; + lfsr_file_open(&lfs, &file, "cat", LFS_O_WRONLY | LFS_O_CREAT) => 0; + lfsr_file_write(&lfs, &file, "meow", strlen("meow")) => strlen("meow"); + lfsr_file_close(&lfs, &file) => 0; + + // create some attrs + const char *a = "One 18.25 ounce package chocolate cake mix."; + lfsr_setattr(&lfs, "cat", 'a', a, strlen(a), + LFS_A_CREAT | LFS_A_EXCL) => 0; + const char *b = "One can prepared coconut pecan frosting."; + lfsr_setattr(&lfs, "cat", 'b', b, strlen(b), + LFS_A_CREAT | LFS_A_EXCL) => 0; + const char *c = "Three slash four cup vegetable oil."; + lfsr_setattr(&lfs, "cat", 'c', c, strlen(c), + LFS_A_CREAT | LFS_A_EXCL) => 0; + + // try opening a file with new wronly attrs + const char *a_ = "Four large eggs. One cup semi-sweet chocolate chips."; + const char *b_ = "Three slash four cup butter or margarine."; + const char *c_ = "One and two third cups granulated sugar."; + uint8_t a_buf[256]; + memcpy(a_buf, a_, strlen(a_)); + lfs_ssize_t a_size = strlen(a_); + uint8_t b_buf[256]; + memcpy(b_buf, b_, strlen(b_)); + lfs_ssize_t b_size = strlen(b_); + uint8_t c_buf[256]; + memcpy(c_buf, c_, strlen(c_)); + lfs_ssize_t c_size = strlen(c_); + struct lfs_attr attrs[] = { + { + .type = 'a', + .flags = MODE, + .buffer = a_buf, + .buffer_size = (MUTSIZE) ? sizeof(a_buf) : strlen(a_), + .size = (MUTSIZE) ? &a_size : NULL, + }, + { + .type = 'b', + .flags = MODE, + .buffer = b_buf, + .buffer_size = (MUTSIZE) ? sizeof(b_buf) : strlen(b_), + .size = (MUTSIZE) ? &b_size : NULL, + }, + { + .type = 'c', + .flags = MODE, + .buffer = c_buf, + .buffer_size = (MUTSIZE) ? sizeof(c_buf) : strlen(c_), + .size = (MUTSIZE) ? &c_size : NULL, + } + }; + struct lfs_file_config filecfg = { + .attrs = attrs, + .attr_count = 3, + }; + lfsr_file_opencfg(&lfs, &file, "cat", LFS_O_RDWR, &filecfg) => 0; + + // open should have had no effect on our wronly attrs + assert(a_size == strlen(a_)); + assert(memcmp(a_buf, a_, strlen(a_)) == 0); + assert(b_size == strlen(b_)); + assert(memcmp(b_buf, b_, strlen(b_)) == 0); + assert(c_size == strlen(c_)); + assert(memcmp(c_buf, c_, strlen(c_)) == 0); + + // write and close our file to write the attrs out to disk + lfsr_file_write(&lfs, &file, "miao", strlen("miao")) => strlen("miao"); + lfsr_file_close(&lfs, &file) => 0; + + for (int remount = 0; remount < 2; remount++) { + // remount? + if (remount) { + lfsr_unmount(&lfs) => 0; + lfsr_mount(&lfs, LFS_M_RDWR, CFG) => 0; + } + + // try getting the attr sizes + lfsr_sizeattr(&lfs, "cat", 'a') => strlen(a_); + lfsr_sizeattr(&lfs, "cat", 'b') => strlen(b_); + lfsr_sizeattr(&lfs, "cat", 'c') => strlen(c_); + // try reading the attrs + uint8_t rbuf[256]; + lfsr_getattr(&lfs, "cat", 'a', rbuf, sizeof(rbuf)) => strlen(a_); + assert(memcmp(rbuf, a_, strlen(a_)) == 0); + lfsr_getattr(&lfs, "cat", 'b', rbuf, sizeof(rbuf)) => strlen(b_); + assert(memcmp(rbuf, b_, strlen(b_)) == 0); + lfsr_getattr(&lfs, "cat", 'c', rbuf, sizeof(rbuf)) => strlen(c_); + assert(memcmp(rbuf, c_, strlen(c_)) == 0); + } + + lfsr_unmount(&lfs) => 0; +''' + +# test that rdonly attrs have no effect on disk +[cases.test_attrs_fattr_rdonly] +defines.MODE = ['LFS_A_RDONLY'] +defines.MUTSIZE = [false, true] +code = ''' + lfs_t lfs; + lfsr_format(&lfs, LFS_F_RDWR, CFG) => 0; + lfsr_mount(&lfs, LFS_M_RDWR, CFG) => 0; + + // create a file + lfsr_file_t file; + lfsr_file_open(&lfs, &file, "cat", LFS_O_WRONLY | LFS_O_CREAT) => 0; + lfsr_file_write(&lfs, &file, "meow", strlen("meow")) => strlen("meow"); + lfsr_file_close(&lfs, &file) => 0; + + // create some attrs + const char *a = "One 18.25 ounce package chocolate cake mix."; + lfsr_setattr(&lfs, "cat", 'a', a, strlen(a), + LFS_A_CREAT | LFS_A_EXCL) => 0; + const char *b = "One can prepared coconut pecan frosting."; + lfsr_setattr(&lfs, "cat", 'b', b, strlen(b), + LFS_A_CREAT | LFS_A_EXCL) => 0; + const char *c = "Three slash four cup vegetable oil."; + lfsr_setattr(&lfs, "cat", 'c', c, strlen(c), + LFS_A_CREAT | LFS_A_EXCL) => 0; + + // try opening a file with rdonly attrs + const char *a_ = "Four large eggs. One cup semi-sweet chocolate chips."; + const char *b_ = "Three slash four cup butter or margarine."; + const char *c_ = "One and two third cups granulated sugar."; + uint8_t a_buf[256]; + lfs_ssize_t a_size; + uint8_t b_buf[256]; + lfs_ssize_t b_size; + uint8_t c_buf[256]; + lfs_ssize_t c_size; + struct lfs_attr attrs[] = { + { + .type = 'a', + .flags = MODE, + .buffer = a_buf, + .buffer_size = (MUTSIZE) ? sizeof(a_buf) : strlen(a_), + .size = (MUTSIZE) ? &a_size : NULL, + }, + { + .type = 'b', + .flags = MODE, + .buffer = b_buf, + .buffer_size = (MUTSIZE) ? sizeof(b_buf) : strlen(b_), + .size = (MUTSIZE) ? &b_size : NULL, + }, + { + .type = 'c', + .flags = MODE, + .buffer = c_buf, + .buffer_size = (MUTSIZE) ? sizeof(c_buf) : strlen(c_), + .size = (MUTSIZE) ? &c_size : NULL, + } + }; + struct lfs_file_config filecfg = { + .attrs = attrs, + .attr_count = 3, + }; + lfsr_file_opencfg(&lfs, &file, "cat", LFS_O_RDWR, &filecfg) => 0; + + if (MUTSIZE) { + // did we read the attrs correctly? + assert(a_size == strlen(a)); + assert(memcmp(a_buf, a, strlen(a)) == 0); + assert(b_size == strlen(b)); + assert(memcmp(b_buf, b, strlen(b)) == 0); + assert(c_size == strlen(c)); + assert(memcmp(c_buf, c, strlen(c)) == 0); + } + + // update the attrs with new values + memcpy(a_buf, a_, strlen(a_)); + memcpy(b_buf, b_, strlen(b_)); + memcpy(c_buf, c_, strlen(c_)); + a_size = strlen(a_); + b_size = strlen(b_); + c_size = strlen(c_); + + // write and close our file, this should have no effect on attrs + lfsr_file_write(&lfs, &file, "miao", strlen("miao")) => strlen("miao"); + lfsr_file_close(&lfs, &file) => 0; + + for (int remount = 0; remount < 2; remount++) { + // remount? + if (remount) { + lfsr_unmount(&lfs) => 0; + lfsr_mount(&lfs, LFS_M_RDWR, CFG) => 0; + } + + // try getting the attr sizes + lfsr_sizeattr(&lfs, "cat", 'a') => strlen(a); + lfsr_sizeattr(&lfs, "cat", 'b') => strlen(b); + lfsr_sizeattr(&lfs, "cat", 'c') => strlen(c); + // try reading the attrs + uint8_t rbuf[256]; + lfsr_getattr(&lfs, "cat", 'a', rbuf, sizeof(rbuf)) => strlen(a); + assert(memcmp(rbuf, a, strlen(a)) == 0); + lfsr_getattr(&lfs, "cat", 'b', rbuf, sizeof(rbuf)) => strlen(b); + assert(memcmp(rbuf, b, strlen(b)) == 0); + lfsr_getattr(&lfs, "cat", 'c', rbuf, sizeof(rbuf)) => strlen(c); + assert(memcmp(rbuf, c, strlen(c)) == 0); + } + + lfsr_unmount(&lfs) => 0; +''' + +# test that attrs are written correctly even if there are no file changes +[cases.test_attrs_fattr_noop] +defines.MODE = ['LFS_A_WRONLY', 'LFS_A_RDWR'] +defines.MUTSIZE = [false, true] +code = ''' + lfs_t lfs; + lfsr_format(&lfs, LFS_F_RDWR, CFG) => 0; + lfsr_mount(&lfs, LFS_M_RDWR, CFG) => 0; + + // create a file + lfsr_file_t file; + lfsr_file_open(&lfs, &file, "cat", LFS_O_WRONLY | LFS_O_CREAT) => 0; + lfsr_file_write(&lfs, &file, "meow", strlen("meow")) => strlen("meow"); + lfsr_file_close(&lfs, &file) => 0; + + // create some attrs + const char *a = "One 18.25 ounce package chocolate cake mix."; + lfsr_setattr(&lfs, "cat", 'a', a, strlen(a), + LFS_A_CREAT | LFS_A_EXCL) => 0; + const char *b = "One can prepared coconut pecan frosting."; + lfsr_setattr(&lfs, "cat", 'b', b, strlen(b), + LFS_A_CREAT | LFS_A_EXCL) => 0; + const char *c = "Three slash four cup vegetable oil."; + lfsr_setattr(&lfs, "cat", 'c', c, strlen(c), + LFS_A_CREAT | LFS_A_EXCL) => 0; + + // try opening a file with attrs + const char *a_ = "Four large eggs. One cup semi-sweet chocolate chips."; + const char *b_ = "Three slash four cup butter or margarine."; + const char *c_ = "One and two third cups granulated sugar."; + uint8_t a_buf[256]; + lfs_ssize_t a_size; + uint8_t b_buf[256]; + lfs_ssize_t b_size; + uint8_t c_buf[256]; + lfs_ssize_t c_size; + struct lfs_attr attrs[] = { + { + .type = 'a', + .flags = MODE, + .buffer = a_buf, + .buffer_size = (MUTSIZE) ? sizeof(a_buf) : strlen(a_), + .size = (MUTSIZE) ? &a_size : NULL, + }, + { + .type = 'b', + .flags = MODE, + .buffer = b_buf, + .buffer_size = (MUTSIZE) ? sizeof(b_buf) : strlen(b_), + .size = (MUTSIZE) ? &b_size : NULL, + }, + { + .type = 'c', + .flags = MODE, + .buffer = c_buf, + .buffer_size = (MUTSIZE) ? sizeof(c_buf) : strlen(c_), + .size = (MUTSIZE) ? &c_size : NULL, + } + }; + struct lfs_file_config filecfg = { + .attrs = attrs, + .attr_count = 3, + }; + lfsr_file_opencfg(&lfs, &file, "cat", MODE, &filecfg) => 0; + + if (MODE == LFS_A_RDWR && MUTSIZE) { + // did we read the attrs correctly? + assert(a_size == strlen(a)); + assert(memcmp(a_buf, a, strlen(a)) == 0); + assert(b_size == strlen(b)); + assert(memcmp(b_buf, b, strlen(b)) == 0); + assert(c_size == strlen(c)); + assert(memcmp(c_buf, c, strlen(c)) == 0); + } + + // update the attrs with new values + memcpy(a_buf, a_, strlen(a_)); + memcpy(b_buf, b_, strlen(b_)); + memcpy(c_buf, c_, strlen(c_)); + a_size = strlen(a_); + b_size = strlen(b_); + c_size = strlen(c_); + + // close our file without any data changes, this should still update + // our attrs! + lfsr_file_close(&lfs, &file) => 0; + + for (int remount = 0; remount < 2; remount++) { + // remount? + if (remount) { + lfsr_unmount(&lfs) => 0; + lfsr_mount(&lfs, LFS_M_RDWR, CFG) => 0; + } + + // try getting the attr sizes + lfsr_sizeattr(&lfs, "cat", 'a') => strlen(a_); + lfsr_sizeattr(&lfs, "cat", 'b') => strlen(b_); + lfsr_sizeattr(&lfs, "cat", 'c') => strlen(c_); + // try reading the attrs + uint8_t rbuf[256]; + lfsr_getattr(&lfs, "cat", 'a', rbuf, sizeof(rbuf)) => strlen(a_); + assert(memcmp(rbuf, a_, strlen(a_)) == 0); + lfsr_getattr(&lfs, "cat", 'b', rbuf, sizeof(rbuf)) => strlen(b_); + assert(memcmp(rbuf, b_, strlen(b_)) == 0); + lfsr_getattr(&lfs, "cat", 'c', rbuf, sizeof(rbuf)) => strlen(c_); + assert(memcmp(rbuf, c_, strlen(c_)) == 0); + } + + lfsr_unmount(&lfs) => 0; +''' + +# test that lazy attrs aren't written _until_ there are file changes +[cases.test_attrs_fattr_lazy] +defines.MODE = ['LFS_A_WRONLY', 'LFS_A_RDWR'] +defines.MUTSIZE = [false, true] +code = ''' + lfs_t lfs; + lfsr_format(&lfs, LFS_F_RDWR, CFG) => 0; + lfsr_mount(&lfs, LFS_M_RDWR, CFG) => 0; + + // create a file + lfsr_file_t file; + lfsr_file_open(&lfs, &file, "cat", LFS_O_WRONLY | LFS_O_CREAT) => 0; + lfsr_file_write(&lfs, &file, "meow", strlen("meow")) => strlen("meow"); + lfsr_file_close(&lfs, &file) => 0; + + // create some attrs + const char *a = "One 18.25 ounce package chocolate cake mix."; + lfsr_setattr(&lfs, "cat", 'a', a, strlen(a), + LFS_A_CREAT | LFS_A_EXCL) => 0; + const char *b = "One can prepared coconut pecan frosting."; + lfsr_setattr(&lfs, "cat", 'b', b, strlen(b), + LFS_A_CREAT | LFS_A_EXCL) => 0; + const char *c = "Three slash four cup vegetable oil."; + lfsr_setattr(&lfs, "cat", 'c', c, strlen(c), + LFS_A_CREAT | LFS_A_EXCL) => 0; + + // try opening a file with attrs + const char *a_ = "Four large eggs. One cup semi-sweet chocolate chips."; + const char *b_ = "Three slash four cup butter or margarine."; + const char *c_ = "One and two third cups granulated sugar."; + uint8_t a_buf[256]; + lfs_ssize_t a_size; + uint8_t b_buf[256]; + lfs_ssize_t b_size; + uint8_t c_buf[256]; + lfs_ssize_t c_size; + struct lfs_attr attrs[] = { + { + .type = 'a', + .flags = MODE | LFS_A_LAZY, + .buffer = a_buf, + .buffer_size = (MUTSIZE) ? sizeof(a_buf) : strlen(a_), + .size = (MUTSIZE) ? &a_size : NULL, + }, + { + .type = 'b', + .flags = MODE | LFS_A_LAZY, + .buffer = b_buf, + .buffer_size = (MUTSIZE) ? sizeof(b_buf) : strlen(b_), + .size = (MUTSIZE) ? &b_size : NULL, + }, + { + .type = 'c', + .flags = MODE | LFS_A_LAZY, + .buffer = c_buf, + .buffer_size = (MUTSIZE) ? sizeof(c_buf) : strlen(c_), + .size = (MUTSIZE) ? &c_size : NULL, + } + }; + struct lfs_file_config filecfg = { + .attrs = attrs, + .attr_count = 3, + }; + lfsr_file_opencfg(&lfs, &file, "cat", MODE, &filecfg) => 0; + + if (MODE == LFS_A_RDWR && MUTSIZE) { + // did we read the attrs correctly? + assert(a_size == strlen(a)); + assert(memcmp(a_buf, a, strlen(a)) == 0); + assert(b_size == strlen(b)); + assert(memcmp(b_buf, b, strlen(b)) == 0); + assert(c_size == strlen(c)); + assert(memcmp(c_buf, c, strlen(c)) == 0); + } + + // update the attrs with new values + memcpy(a_buf, a_, strlen(a_)); + memcpy(b_buf, b_, strlen(b_)); + memcpy(c_buf, c_, strlen(c_)); + a_size = strlen(a_); + b_size = strlen(b_); + c_size = strlen(c_); + + // sync our file without any data changes, this should have no + // effect because our attrs are lazy + lfsr_file_sync(&lfs, &file) => 0; + + // try getting the attr sizes + lfsr_sizeattr(&lfs, "cat", 'a') => strlen(a); + lfsr_sizeattr(&lfs, "cat", 'b') => strlen(b); + lfsr_sizeattr(&lfs, "cat", 'c') => strlen(c); + // try reading the attrs + uint8_t rbuf[256]; + lfsr_getattr(&lfs, "cat", 'a', rbuf, sizeof(rbuf)) => strlen(a); + assert(memcmp(rbuf, a, strlen(a)) == 0); + lfsr_getattr(&lfs, "cat", 'b', rbuf, sizeof(rbuf)) => strlen(b); + assert(memcmp(rbuf, b, strlen(b)) == 0); + lfsr_getattr(&lfs, "cat", 'c', rbuf, sizeof(rbuf)) => strlen(c); + assert(memcmp(rbuf, c, strlen(c)) == 0); + + // now write some data and close our file, this should update + // the attrs now + lfsr_file_write(&lfs, &file, "miao", strlen("miao")) => strlen("miao"); + lfsr_file_close(&lfs, &file) => 0; + + for (int remount = 0; remount < 2; remount++) { + // remount? + if (remount) { + lfsr_unmount(&lfs) => 0; + lfsr_mount(&lfs, LFS_M_RDWR, CFG) => 0; + } + + // try getting the attr sizes + lfsr_sizeattr(&lfs, "cat", 'a') => strlen(a_); + lfsr_sizeattr(&lfs, "cat", 'b') => strlen(b_); + lfsr_sizeattr(&lfs, "cat", 'c') => strlen(c_); + // try reading the attrs + uint8_t rbuf[256]; + lfsr_getattr(&lfs, "cat", 'a', rbuf, sizeof(rbuf)) => strlen(a_); + assert(memcmp(rbuf, a_, strlen(a_)) == 0); + lfsr_getattr(&lfs, "cat", 'b', rbuf, sizeof(rbuf)) => strlen(b_); + assert(memcmp(rbuf, b_, strlen(b_)) == 0); + lfsr_getattr(&lfs, "cat", 'c', rbuf, sizeof(rbuf)) => strlen(c_); + assert(memcmp(rbuf, c_, strlen(c_)) == 0); + } + + lfsr_unmount(&lfs) => 0; +''' + +# TODO +#[cases.test_attrs_fattr_all] +#[cases.test_attrs_fattr_many] +#[cases.test_attrs_fattr_many_many] +#[cases.test_attrs_fattr_fuzz] +#[cases.test_attrs_fattr_fuzz_fuzz] + +#[cases.test_attrs_fattr_broadcast] +#[cases.test_attrs_fattr_remove_broadcast] +#[cases.test_attrs_fattr_setattr_broadcast] +#[cases.test_attrs_fattr_removeattr_broadcast] +#[cases.test_attrs_fattr_wronly_broadcast] +#[cases.test_attrs_fattr_rdonly_broadcast] +#[cases.test_attrs_fattr_desync_broadcast] + +#[cases.test_attrs_fattr_resync] +#[cases.test_attrs_fattr_zombie_resync] TODO do we test file zombie resync? + +#[cases.test_attrs_fattr_pl] ?