Compare commits

..

3 Commits

Author SHA1 Message Date
Christopher Haster
97b5d04bf4 Switched to separate-tag encoding of forward-looking CRCs
Previously forward-looking CRCs was just two new CRC types, one for
commits with forward-looking CRCs, one without. These both contained the
CRC needed to complete the current commit (note that the commit CRC
must come last!).

         [--   32   --|--   32   --|--   32   --|--   32   --]
with:    [  crc3 tag  | nprog size |  nprog crc | commit crc ]
without: [  crc2 tag  | commit crc ]

This meant there had to be several checks for the two possible structure
sizes, messying up the implementation.

         [--   32   --|--   32   --|--   32   --|--   32   --|--   32   --]
with:    [nprogcrc tag| nprog size |  nprog crc | commit tag | commit crc ]
without: [ commit tag | commit crc ]

But we already have a mechanism for storing optional metadata! The
different metadata tags! So why not use a separate tage for the
forward-looking CRC, separate from the commit CRC?

I wasn't sure this would actually help that much, there are still
necessary conditions for wether or not a forward-looking CRC is there,
but in the end it simplified the code quite nicely, and resulted in a ~200 byte
code-cost saving.
2021-01-15 02:00:01 -06:00
Christopher Haster
7535795a44 Cleaned up a few additional commit corner cases
- General cleanup from integration, including cleaning up some older
  commit code
- Partial-prog tests do not make sense when prog_size == block_size
  (there can't be partial-progs!)
- Fixed signed-comparison issue in modified filebd
2020-12-07 01:10:28 -06:00
Christopher Haster
01a3b1f5f7 Initial implementation of forward-looking erase-state CRCs
This change is necessary to handle out-of-order writes found by pjsg's
fuzzing work.

The problem is that it is possible for (non-NOR) block devices to write
pages in any order, or to even write random data in the case of a
power-loss. This breaks littlefs's use of the first bit in a page to
indicate the erase-state.

pjsg notes this behavior is documented in the W25Q here:
https://community.cypress.com/docs/DOC-10507

---

The basic idea here is to CRC the next page, and use this "erase-state CRC" to
check if the next page is erased and ready to accept programs.

.------------------. \   commit
|     metadata     | |
|                  | +---.
|                  | |   |
|------------------| |   |
| erase-state CRC -----. |
|------------------| | | |
|   commit CRC    ---|-|-'
|------------------| / |
|     padding      |   | padding (doesn't need CRC)
|                  |   |
|------------------| \ | next prog
|     erased?      | +-'
|        |         | |
|        v         | /
|                  |
|                  |
'------------------'

This is made a bit annoying since littlefs doesn't actually store the
page (prog_size) in the superblock, since it doesn't need to know the
size for any other operation. We can work around this by storing both
the CRC and size of the next page when necessary.

Another interesting note is that we don't need to any bit tweaking
information, since we read the next page every time we would need to
know how to clobber the erase-state CRC. And since we only read
prog_size, this works really well with our caching, since the caches
must be a multiple of prog_size.

This also brings back the internal lfs_bd_crc function, in which we can
use some optimizations added to lfs_bd_cmp.

Needs some cleanup but the idea is passing most relevant tests.
2020-12-07 00:06:17 -06:00
8 changed files with 565 additions and 574 deletions

View File

@@ -6,7 +6,6 @@ endif
CC ?= gcc
AR ?= ar
SIZE ?= size
NM ?= nm
SRC += $(wildcard *.c bd/*.c)
OBJ := $(SRC:.c=.o)
@@ -30,7 +29,6 @@ override CFLAGS += -Wextra -Wshadow -Wjump-misses-init -Wundef
ifdef VERBOSE
override TFLAGS += -v
override SFLAGS += -v
endif
@@ -41,9 +39,6 @@ asm: $(ASM)
size: $(OBJ)
$(SIZE) -t $^
code_size:
./scripts/code_size.py $(SFLAGS)
test:
./scripts/test.py $(TFLAGS)
.SECONDEXPANSION:
@@ -70,4 +65,3 @@ clean:
rm -f $(DEP)
rm -f $(ASM)
rm -f tests/*.toml.*
rm -f sizes/*

View File

@@ -80,11 +80,6 @@ int lfs_filebd_read(const struct lfs_config *cfg, lfs_block_t block,
LFS_ASSERT(size % cfg->read_size == 0);
LFS_ASSERT(block < cfg->block_count);
// zero for reproducability (in case file is truncated)
if (bd->cfg->erase_value != -1) {
memset(buffer, bd->cfg->erase_value, size);
}
// read
off_t res1 = lseek(bd->fd,
(off_t)block*cfg->block_size + (off_t)off, SEEK_SET);
@@ -101,6 +96,11 @@ int lfs_filebd_read(const struct lfs_config *cfg, lfs_block_t block,
return err;
}
// file truncated? zero for reproducability
if ((lfs_size_t)res2 < size) {
memset((uint8_t*)buffer + res2, 0, size-res2);
}
LFS_FILEBD_TRACE("lfs_filebd_read -> %d", 0);
return 0;
}

View File

@@ -32,11 +32,8 @@ int lfs_rambd_createcfg(const struct lfs_config *cfg,
}
}
// zero for reproducability?
if (bd->cfg->erase_value != -1) {
memset(bd->buffer, bd->cfg->erase_value,
cfg->block_size * cfg->block_count);
}
// zero for reproducability (this matches filebd)
memset(bd->buffer, 0, cfg->block_size * cfg->block_count);
LFS_RAMBD_TRACE("lfs_rambd_createcfg -> %d", 0);
return 0;

551
lfs.c

File diff suppressed because it is too large Load Diff

2
lfs.h
View File

@@ -113,6 +113,8 @@ enum lfs_type {
LFS_TYPE_SOFTTAIL = 0x600,
LFS_TYPE_HARDTAIL = 0x601,
LFS_TYPE_MOVESTATE = 0x7ff,
LFS_TYPE_COMMITCRC = 0x502,
LFS_TYPE_NPROGCRC = 0x5ff,
// internal chip sources
LFS_FROM_NOOP = 0x000,

View File

@@ -1,328 +0,0 @@
#!/usr/bin/env python3
#
# This script finds the code size at the function level, with/without
# static functions, and has some conveniences for comparing different
# versions. It's basically one big wrapper around nm, and may or may
# not have been written out of jealousy of Linux's Bloat-O-Meter.
#
# Here's a useful bash script to use while developing:
# ./scripts/code_size.py -qo old.csv
# while true ; do ./code_scripts/size.py -d old.csv ; inotifywait -rqe modify * ; done
#
# Or even better, to automatically update results on commit:
# ./scripts/code_size.py -qo commit.csv
# while true ; do ./scripts/code_size.py -d commit.csv -o current.csv ; git diff --exit-code --quiet && cp current.csv commit.csv ; inotifywait -rqe modify * ; done
#
# Or my personal favorite:
# ./scripts/code_size.py -qo master.csv && cp master.csv commit.csv
# while true ; do ( ./scripts/code_size.py -i commit.csv -d master.csv -s ; ./scripts/code_size.py -i current.csv -d master.csv -s ; ./scripts/code_size.py -d master.csv -o current.csv -s ) | awk 'BEGIN {printf "%-16s %7s %7s %7s\n","","old","new","diff"} (NR==2 && $1="commit") || (NR==4 && $1="prev") || (NR==6 && $1="current") {printf "%-16s %7s %7s %7s %s\n",$1,$2,$3,$5,$6}' ; git diff --exit-code --quiet && cp current.csv commit.csv ; inotifywait -rqe modify * ; done
#
import os
import itertools as it
import subprocess as sp
import shlex
import re
import csv
import collections as co
SIZEDIR = 'sizes'
RULES = """
define FLATTEN
%(sizedir)s/%(build)s.$(subst /,.,$(target)): $(target)
( echo "#line 1 \\"$$<\\"" ; %(cat)s $$< ) > $$@
%(sizedir)s/%(build)s.$(subst /,.,$(target:.c=.size)): \\
%(sizedir)s/%(build)s.$(subst /,.,$(target:.c=.o))
$(NM) --size-sort $$^ | sed 's/^/$(subst /,\\/,$(target:.c=.o)):/' > $$@
endef
$(foreach target,$(SRC),$(eval $(FLATTEN)))
-include %(sizedir)s/*.d
.SECONDARY:
%%.size: $(foreach t,$(subst /,.,$(SRC:.c=.size)),%%.$t)
cat $^ > $@
"""
CATS = {
'code': 'cat',
'code_inlined': 'sed \'s/^static\( inline\)\?//\'',
}
def build(**args):
# mkdir -p sizedir
os.makedirs(args['sizedir'], exist_ok=True)
if args.get('inlined', False):
builds = ['code', 'code_inlined']
else:
builds = ['code']
# write makefiles for the different types of builds
makefiles = []
targets = []
for build in builds:
path = args['sizedir'] + '/' + build
with open(path + '.mk', 'w') as mk:
mk.write(RULES.replace(4*' ', '\t') % dict(
sizedir=args['sizedir'],
build=build,
cat=CATS[build]))
mk.write('\n')
# pass on defines
for d in args['D']:
mk.write('%s: override CFLAGS += -D%s\n' % (
path+'.size', d))
makefiles.append(path + '.mk')
targets.append(path + '.size')
# build in parallel
cmd = (['make', '-f', 'Makefile'] +
list(it.chain.from_iterable(['-f', m] for m in makefiles)) +
[target for target in targets])
if args.get('verbose', False):
print(' '.join(shlex.quote(c) for c in cmd))
proc = sp.Popen(cmd,
stdout=sp.DEVNULL if not args.get('verbose', False) else None)
proc.wait()
if proc.returncode != 0:
sys.exit(-1)
# find results
build_results = co.defaultdict(lambda: 0)
# notes
# - filters type
# - discards internal/debug functions (leading __)
pattern = re.compile(
'^(?P<file>[^:]+)' +
':(?P<size>[0-9a-fA-F]+)' +
' (?P<type>[%s])' % re.escape(args['type']) +
' (?!__)(?P<name>.+?)$')
for build in builds:
path = args['sizedir'] + '/' + build
with open(path + '.size') as size:
for line in size:
match = pattern.match(line)
if match:
file = match.group('file')
# discard .8449 suffixes created by optimizer
name = re.sub('\.[0-9]+', '', match.group('name'))
size = int(match.group('size'), 16)
build_results[(build, file, name)] += size
results = []
for (build, file, name), size in build_results.items():
if build == 'code':
results.append((file, name, size, False))
elif (build == 'code_inlined' and
('inlined', file, name) not in results):
results.append((file, name, size, True))
return results
def main(**args):
# find results
if not args.get('input', None):
results = build(**args)
else:
with open(args['input']) as f:
r = csv.DictReader(f)
results = [
( result['file'],
result['name'],
int(result['size']),
bool(int(result.get('inlined', 0))))
for result in r
if (not bool(int(result.get('inlined', 0))) or
args.get('inlined', False))]
total = 0
for _, _, size, inlined in results:
if not inlined:
total += size
# find previous results?
if args.get('diff', None):
with open(args['diff']) as f:
r = csv.DictReader(f)
prev_results = [
( result['file'],
result['name'],
int(result['size']),
bool(int(result.get('inlined', 0))))
for result in r
if (not bool(int(result.get('inlined', 0))) or
args.get('inlined', False))]
prev_total = 0
for _, _, size, inlined in prev_results:
if not inlined:
prev_total += size
# write results to CSV
if args.get('output', None):
results.sort(key=lambda x: (-x[2], x))
with open(args['output'], 'w') as f:
w = csv.writer(f)
if args.get('inlined', False):
w.writerow(['file', 'name', 'size', 'inlined'])
for file, name, size, inlined in results:
w.writerow((file, name, size, int(inlined)))
else:
w.writerow(['file', 'name', 'size'])
for file, name, size, inlined in results:
w.writerow((file, name, size))
# print results
def dedup_functions(results):
functions = co.defaultdict(lambda: (0, True))
for _, name, size, inlined in results:
if not inlined:
functions[name] = (functions[name][0] + size, False)
for _, name, size, inlined in results:
if inlined and functions[name][1]:
functions[name] = (functions[name][0] + size, True)
return functions
def dedup_files(results):
files = co.defaultdict(lambda: 0)
for file, _, size, inlined in results:
if not inlined:
files[file] += size
return files
def diff_sizes(olds, news):
diff = co.defaultdict(lambda: (None, None, None))
for name, new in news.items():
diff[name] = (None, new, new)
for name, old in olds.items():
new = diff[name][1] or 0
diff[name] = (old, new, new-old)
return diff
def print_header(name=''):
if not args.get('diff', False):
print('%-40s %7s' % (name, 'size'))
else:
print('%-40s %7s %7s %7s' % (name, 'old', 'new', 'diff'))
def print_functions():
functions = dedup_functions(results)
functions = {
name+' (inlined)' if inlined else name: size
for name, (size, inlined) in functions.items()}
if not args.get('diff', None):
print_header('function')
for name, size in sorted(functions.items(),
key=lambda x: (-x[1], x)):
print("%-40s %7d" % (name, size))
else:
prev_functions = dedup_functions(prev_results)
prev_functions = {
name+' (inlined)' if inlined else name: size
for name, (size, inlined) in prev_functions.items()}
diff = diff_sizes(functions, prev_functions)
print_header('function (%d added, %d removed)' % (
sum(1 for old, _, _ in diff.values() if not old),
sum(1 for _, new, _ in diff.values() if not new)))
for name, (old, new, diff) in sorted(diff.items(),
key=lambda x: (-(x[1][2] or 0), x)):
if diff or args.get('all', False):
print("%-40s %7s %7s %+7d%s" % (
name, old or "-", new or "-", diff,
' (%+.2f%%)' % (100*((new-old)/old))
if old and new else
''))
def print_files():
files = dedup_files(results)
if not args.get('diff', None):
print_header('file')
for file, size in sorted(files.items(),
key=lambda x: (-x[1], x)):
print("%-40s %7d" % (file, size))
else:
prev_files = dedup_files(prev_results)
diff = diff_sizes(files, prev_files)
print_header('file (%d added, %d removed)' % (
sum(1 for old, _, _ in diff.values() if not old),
sum(1 for _, new, _ in diff.values() if not new)))
for name, (old, new, diff) in sorted(diff.items(),
key=lambda x: (-(x[1][2] or 0), x)):
if diff or args.get('all', False):
print("%-40s %7s %7s %+7d%s" % (
name, old or "-", new or "-", diff,
' (%+.2f%%)' % (100*((new-old)/old))
if old and new else
''))
def print_totals():
if not args.get('diff', None):
print("%-40s %7d" % ('TOTALS', total))
else:
print("%-40s %7s %7s %+7d%s" % (
'TOTALS', prev_total, total, total-prev_total,
' (%+.2f%%)' % (100*((total-prev_total)/total))
if prev_total and total else
''))
def print_status():
if not args.get('diff', None):
print(total)
else:
print("%d (%+.2f%%)" % (total, 100*((total-prev_total)/total)))
if args.get('quiet', False):
pass
elif args.get('status', False):
print_status()
elif args.get('summary', False):
print_header()
print_totals()
elif args.get('files', False):
print_files()
print_totals()
else:
print_functions()
print_totals()
if __name__ == "__main__":
import argparse
import sys
parser = argparse.ArgumentParser(
description="Find code size at the function level.")
parser.add_argument('sizedir', nargs='?', default=SIZEDIR,
help="Directory to store intermediary results. Defaults "
"to \"%s\"." % SIZEDIR)
parser.add_argument('-D', action='append', default=[],
help="Specify compile-time define.")
parser.add_argument('-v', '--verbose', action='store_true',
help="Output commands that run behind the scenes.")
parser.add_argument('-i', '--input',
help="Don't compile and find code sizes, instead use this CSV file.")
parser.add_argument('-o', '--output',
help="Specify CSV file to store results.")
parser.add_argument('-d', '--diff',
help="Specify CSV file to diff code size against.")
parser.add_argument('-a', '--all', action='store_true',
help="Show all functions, not just the ones that changed.")
parser.add_argument('--inlined', action='store_true',
help="Run a second compilation to find the sizes of functions normally "
"removed by optimizations. These will be shown as \"*.inlined\" "
"functions, and will not be included in the total.")
parser.add_argument('--files', action='store_true',
help="Show file-level code sizes. Note this does not include padding! "
"So sizes may differ from other tools.")
parser.add_argument('-s', '--summary', action='store_true',
help="Only show the total code size.")
parser.add_argument('-S', '--status', action='store_true',
help="Show minimum info useful for a single-line status.")
parser.add_argument('-q', '--quiet', action='store_true',
help="Don't show anything, useful with -o.")
parser.add_argument('--type', default='tTrRdDbB',
help="Type of symbols to report, this uses the same single-character "
"type-names emitted by nm. Defaults to %(default)r.")
sys.exit(main(**vars(parser.parse_args())))

View File

@@ -24,6 +24,7 @@ TAG_TYPES = {
'gstate': (0x700, 0x700),
'movestate': (0x7ff, 0x7ff),
'crc': (0x700, 0x500),
'nprogcrc': (0x7ff, 0x5ff),
}
class Tag:
@@ -99,7 +100,16 @@ class Tag:
return struct.unpack('b', struct.pack('B', self.chunk))[0]
def is_(self, type):
return (self.type & TAG_TYPES[type][0]) == TAG_TYPES[type][1]
try:
if ' ' in type:
type1, type3 = type.split()
return (self.is_(type1) and
(self.type & ~TAG_TYPES[type1][0]) == int(type3, 0))
return self.type == int(type, 0)
except (ValueError, KeyError):
return (self.type & TAG_TYPES[type][0]) == TAG_TYPES[type][1]
def mkmask(self):
return Tag(
@@ -109,14 +119,20 @@ class Tag:
def chid(self, nid):
ntag = Tag(self.type, nid, self.size)
if hasattr(self, 'off'): ntag.off = self.off
if hasattr(self, 'data'): ntag.data = self.data
if hasattr(self, 'crc'): ntag.crc = self.crc
if hasattr(self, 'off'): ntag.off = self.off
if hasattr(self, 'data'): ntag.data = self.data
if hasattr(self, 'crc'): ntag.crc = self.crc
if hasattr(self, 'erased'): ntag.erased = self.erased
return ntag
def typerepr(self):
if self.is_('crc') and getattr(self, 'crc', 0xffffffff) != 0xffffffff:
return 'crc (bad)'
if (self.is_('crc') and not self.is_('nprogcrc') and
getattr(self, 'crc', 0xffffffff) != 0xffffffff):
crc_status = ' (bad)'
elif self.is_('nprogcrc') and getattr(self, 'erased', False):
crc_status = ' (era)'
else:
crc_status = ''
reverse_types = {v: k for k, v in TAG_TYPES.items()}
for prefix in range(12):
@@ -124,12 +140,12 @@ class Tag:
if (mask, self.type & mask) in reverse_types:
type = reverse_types[mask, self.type & mask]
if prefix > 0:
return '%s %#0*x' % (
type, prefix//4, self.type & ((1 << prefix)-1))
return '%s %#x%s' % (
type, self.type & ((1 << prefix)-1), crc_status)
else:
return type
return '%s%s' % (type, crc_status)
else:
return '%02x' % self.type
return '%02x%s' % (self.type, crc_status)
def idrepr(self):
return repr(self.id) if self.id != 0x3ff else '.'
@@ -172,6 +188,8 @@ class MetadataPair:
self.rev, = struct.unpack('<I', block[0:4])
crc = binascii.crc32(block[0:4])
etag = None
estate = None
# parse tags
corrupt = False
@@ -182,11 +200,11 @@ class MetadataPair:
while len(block) - off >= 4:
ntag, = struct.unpack('>I', block[off:off+4])
tag = Tag(int(tag) ^ ntag)
tag = Tag((int(tag) ^ ntag) & 0x7fffffff)
tag.off = off + 4
tag.data = block[off+4:off+tag.dsize]
if tag.is_('crc'):
crc = binascii.crc32(block[off:off+4+4], crc)
if tag.is_('crc') and not tag.is_('nprogcrc'):
crc = binascii.crc32(block[off:off+2*4], crc)
else:
crc = binascii.crc32(block[off:off+tag.dsize], crc)
tag.crc = crc
@@ -194,16 +212,29 @@ class MetadataPair:
self.all_.append(tag)
if tag.is_('crc'):
if tag.is_('nprogcrc') and len(tag.data) == 8:
etag = tag
estate = struct.unpack('<II', tag.data)
elif tag.is_('crc'):
# is valid commit?
if crc != 0xffffffff:
corrupt = True
if not corrupt:
self.log = self.all_.copy()
# end of commit?
if estate:
esize, ecrc = estate
dcrc = 0xffffffff ^ binascii.crc32(block[off:off+esize])
if ecrc == dcrc:
etag.erased = True
corrupt = True
elif not (tag.is_('crc 0x0') or tag.is_('crc 0x1')):
corrupt = True
# reset tag parsing
crc = 0
tag = Tag(int(tag) ^ ((tag.type & 1) << 31))
etag = None
estate = None
# find active ids
self.ids = list(it.takewhile(
@@ -280,7 +311,7 @@ class MetadataPair:
f.write('\n')
for tag in tags:
f.write("%08x: %08x %-13s %4s %4s" % (
f.write("%08x: %08x %-14s %3s %4s" % (
tag.off, tag,
tag.typerepr(), tag.idrepr(), tag.sizerepr()))
if truncate:

172
tests/test_powerloss.toml Normal file
View File

@@ -0,0 +1,172 @@
# There are already a number of tests that test general operations under
# power-loss (see the reentrant attribute). These tests are for explicitly
# testing specific corner cases.
[[case]] # only a revision count
code = '''
lfs_format(&lfs, &cfg) => 0;
lfs_mount(&lfs, &cfg) => 0;
lfs_mkdir(&lfs, "notebook") => 0;
lfs_file_open(&lfs, &file, "notebook/paper",
LFS_O_WRONLY | LFS_O_CREAT | LFS_O_APPEND) => 0;
strcpy((char*)buffer, "hello");
size = strlen("hello");
for (int i = 0; i < 5; i++) {
lfs_file_write(&lfs, &file, buffer, size) => size;
lfs_file_sync(&lfs, &file) => 0;
}
lfs_file_close(&lfs, &file) => 0;
char rbuffer[256];
lfs_file_open(&lfs, &file, "notebook/paper", LFS_O_RDONLY) => 0;
for (int i = 0; i < 5; i++) {
lfs_file_read(&lfs, &file, rbuffer, size) => size;
assert(memcmp(rbuffer, buffer, size) == 0);
}
lfs_file_close(&lfs, &file) => 0;
lfs_unmount(&lfs) => 0;
// get pair/rev count
lfs_mount(&lfs, &cfg) => 0;
lfs_dir_open(&lfs, &dir, "notebook") => 0;
lfs_block_t pair[2] = {dir.m.pair[0], dir.m.pair[1]};
uint32_t rev = dir.m.rev;
lfs_dir_close(&lfs, &dir) => 0;
lfs_unmount(&lfs) => 0;
// write just the revision count
uint8_t bbuffer[LFS_BLOCK_SIZE];
cfg.read(&cfg, pair[1], 0, bbuffer, LFS_BLOCK_SIZE) => 0;
memcpy(bbuffer, &(uint32_t){lfs_tole32(rev+1)}, sizeof(uint32_t));
cfg.erase(&cfg, pair[1]) => 0;
cfg.prog(&cfg, pair[1], 0, bbuffer, LFS_BLOCK_SIZE) => 0;
lfs_mount(&lfs, &cfg) => 0;
// can read?
lfs_file_open(&lfs, &file, "notebook/paper", LFS_O_RDONLY) => 0;
for (int i = 0; i < 5; i++) {
lfs_file_read(&lfs, &file, rbuffer, size) => size;
assert(memcmp(rbuffer, buffer, size) == 0);
}
lfs_file_close(&lfs, &file) => 0;
// can write?
lfs_file_open(&lfs, &file, "notebook/paper",
LFS_O_WRONLY | LFS_O_APPEND) => 0;
strcpy((char*)buffer, "goodbye");
size = strlen("goodbye");
for (int i = 0; i < 5; i++) {
lfs_file_write(&lfs, &file, buffer, size) => size;
lfs_file_sync(&lfs, &file) => 0;
}
lfs_file_close(&lfs, &file) => 0;
lfs_file_open(&lfs, &file, "notebook/paper", LFS_O_RDONLY) => 0;
strcpy((char*)buffer, "hello");
size = strlen("hello");
for (int i = 0; i < 5; i++) {
lfs_file_read(&lfs, &file, rbuffer, size) => size;
assert(memcmp(rbuffer, buffer, size) == 0);
}
strcpy((char*)buffer, "goodbye");
size = strlen("goodbye");
for (int i = 0; i < 5; i++) {
lfs_file_read(&lfs, &file, rbuffer, size) => size;
assert(memcmp(rbuffer, buffer, size) == 0);
}
lfs_file_close(&lfs, &file) => 0;
lfs_unmount(&lfs) => 0;
'''
[[case]] # partial prog, may not be byte in order!
if = "LFS_PROG_SIZE < LFS_BLOCK_SIZE"
define.BYTE_OFF = ["0", "LFS_PROG_SIZE-1", "LFS_PROG_SIZE/2"]
define.BYTE_VALUE = [0x33, 0xcc]
in = "lfs.c"
code = '''
lfs_format(&lfs, &cfg) => 0;
lfs_mount(&lfs, &cfg) => 0;
lfs_mkdir(&lfs, "notebook") => 0;
lfs_file_open(&lfs, &file, "notebook/paper",
LFS_O_WRONLY | LFS_O_CREAT | LFS_O_APPEND) => 0;
strcpy((char*)buffer, "hello");
size = strlen("hello");
for (int i = 0; i < 5; i++) {
lfs_file_write(&lfs, &file, buffer, size) => size;
lfs_file_sync(&lfs, &file) => 0;
}
lfs_file_close(&lfs, &file) => 0;
char rbuffer[256];
lfs_file_open(&lfs, &file, "notebook/paper", LFS_O_RDONLY) => 0;
for (int i = 0; i < 5; i++) {
lfs_file_read(&lfs, &file, rbuffer, size) => size;
assert(memcmp(rbuffer, buffer, size) == 0);
}
lfs_file_close(&lfs, &file) => 0;
lfs_unmount(&lfs) => 0;
// imitate a partial prog, value should not matter, if littlefs
// doesn't notice the partial prog testbd will assert
// get offset to next prog
lfs_mount(&lfs, &cfg) => 0;
lfs_dir_open(&lfs, &dir, "notebook") => 0;
lfs_block_t block = dir.m.pair[0];
lfs_off_t off = dir.m.off;
lfs_dir_close(&lfs, &dir) => 0;
lfs_unmount(&lfs) => 0;
// tweak byte
uint8_t bbuffer[LFS_BLOCK_SIZE];
cfg.read(&cfg, block, 0, bbuffer, LFS_BLOCK_SIZE) => 0;
bbuffer[off + BYTE_OFF] = BYTE_VALUE;
cfg.erase(&cfg, block) => 0;
cfg.prog(&cfg, block, 0, bbuffer, LFS_BLOCK_SIZE) => 0;
lfs_mount(&lfs, &cfg) => 0;
// can read?
lfs_file_open(&lfs, &file, "notebook/paper", LFS_O_RDONLY) => 0;
for (int i = 0; i < 5; i++) {
lfs_file_read(&lfs, &file, rbuffer, size) => size;
assert(memcmp(rbuffer, buffer, size) == 0);
}
lfs_file_close(&lfs, &file) => 0;
// can write?
lfs_file_open(&lfs, &file, "notebook/paper",
LFS_O_WRONLY | LFS_O_APPEND) => 0;
strcpy((char*)buffer, "goodbye");
size = strlen("goodbye");
for (int i = 0; i < 5; i++) {
lfs_file_write(&lfs, &file, buffer, size) => size;
lfs_file_sync(&lfs, &file) => 0;
}
lfs_file_close(&lfs, &file) => 0;
lfs_file_open(&lfs, &file, "notebook/paper", LFS_O_RDONLY) => 0;
strcpy((char*)buffer, "hello");
size = strlen("hello");
for (int i = 0; i < 5; i++) {
lfs_file_read(&lfs, &file, rbuffer, size) => size;
assert(memcmp(rbuffer, buffer, size) == 0);
}
strcpy((char*)buffer, "goodbye");
size = strlen("goodbye");
for (int i = 0; i < 5; i++) {
lfs_file_read(&lfs, &file, rbuffer, size) => size;
assert(memcmp(rbuffer, buffer, size) == 0);
}
lfs_file_close(&lfs, &file) => 0;
lfs_unmount(&lfs) => 0;
'''