diff --git a/.gitignore b/.gitignore index 09707c6bd..e9938232d 100644 --- a/.gitignore +++ b/.gitignore @@ -9,24 +9,26 @@ *.gcno *.gcda *.perf -lfs -liblfs.a +lfs3 +liblfs3.a # Testing things runners/test_runner runners/bench_runner -lfs.code.csv -lfs.data.csv -lfs.stack.csv -lfs.structs.csv -lfs.cov.csv -lfs.perf.csv -lfs.perfbd.csv -lfs.test.csv -lfs.bench.csv +lfs3.code.csv +lfs3.data.csv +lfs3.stack.csv +lfs3.structs.csv +lfs3.cov.csv +lfs3.perf.csv +lfs3.perfbd.csv +lfs3.test.csv +lfs3.bench.csv # Misc tags +clip +disk .gdb_history scripts/__pycache__ diff --git a/Makefile b/Makefile index 501671564..8c887ac48 100644 --- a/Makefile +++ b/Makefile @@ -1,78 +1,80 @@ -ifdef BUILDDIR -# bit of a hack, but we want to make sure BUILDDIR directory structure -# is correct before any commands -$(if $(findstring n,$(MAKEFLAGS)),, $(shell mkdir -p \ - $(BUILDDIR)/ \ - $(BUILDDIR)/bd \ - $(BUILDDIR)/runners \ - $(BUILDDIR)/tests \ - $(BUILDDIR)/benches)) -endif +# overrideable build dir, default is in-place BUILDDIR ?= . - -# overridable target/src/tools/flags/etc +# overrideable target, default to building a library ifneq ($(wildcard test.c main.c),) -TARGET ?= $(BUILDDIR)/lfs +TARGET ?= $(BUILDDIR)/lfs3 else -TARGET ?= $(BUILDDIR)/liblfs.a +TARGET ?= $(BUILDDIR)/liblfs3.a endif -CC ?= gcc -AR ?= ar -SIZE ?= size -CTAGS ?= ctags -NM ?= nm -OBJDUMP ?= objdump -VALGRIND ?= valgrind -GDB ?= gdb -PERF ?= perf - -SRC ?= $(filter-out $(wildcard *.t.* *.b.*),$(wildcard *.c)) +# find source files +SRC ?= $(filter-out %.t.c %.b.c %.a.c,$(wildcard *.c)) OBJ := $(SRC:%.c=$(BUILDDIR)/%.o) DEP := $(SRC:%.c=$(BUILDDIR)/%.d) ASM := $(SRC:%.c=$(BUILDDIR)/%.s) CI := $(SRC:%.c=$(BUILDDIR)/%.ci) -GCDA := $(SRC:%.c=$(BUILDDIR)/%.t.gcda) +GCDA := $(SRC:%.c=$(BUILDDIR)/%.t.a.gcda) TESTS ?= $(wildcard tests/*.toml) -TEST_SRC ?= $(SRC) \ - $(filter-out $(wildcard bd/*.t.* bd/*.b.*),$(wildcard bd/*.c)) \ +TEST_SRC ?= \ + $(SRC) \ + $(filter-out %.t.c %.b.c %.a.c,$(wildcard bd/*.c)) \ runners/test_runner.c TEST_RUNNER ?= $(BUILDDIR)/runners/test_runner -TEST_A := $(TESTS:%.toml=$(BUILDDIR)/%.t.a.c) \ - $(TEST_SRC:%.c=$(BUILDDIR)/%.t.a.c) -TEST_C := $(TEST_A:%.t.a.c=%.t.c) -TEST_OBJ := $(TEST_C:%.t.c=%.t.o) -TEST_DEP := $(TEST_C:%.t.c=%.t.d) -TEST_CI := $(TEST_C:%.t.c=%.t.ci) -TEST_GCNO := $(TEST_C:%.t.c=%.t.gcno) -TEST_GCDA := $(TEST_C:%.t.c=%.t.gcda) +TEST_C := \ + $(TESTS:%.toml=$(BUILDDIR)/%.t.c) \ + $(TEST_SRC:%.c=$(BUILDDIR)/%.t.c) +TEST_A := $(TEST_C:%.t.c=%.t.a.c) +TEST_OBJ := $(TEST_A:%.t.a.c=%.t.a.o) +TEST_DEP := $(TEST_A:%.t.a.c=%.t.a.d) +TEST_CI := $(TEST_A:%.t.a.c=%.t.a.ci) +TEST_GCNO := $(TEST_A:%.t.a.c=%.t.a.gcno) +TEST_GCDA := $(TEST_A:%.t.a.c=%.t.a.gcda) TEST_PERF := $(TEST_RUNNER:%=%.perf) TEST_TRACE := $(TEST_RUNNER:%=%.trace) TEST_CSV := $(TEST_RUNNER:%=%.csv) BENCHES ?= $(wildcard benches/*.toml) -BENCH_SRC ?= $(SRC) \ - $(filter-out $(wildcard bd/*.t.* bd/*.b.*),$(wildcard bd/*.c)) \ +BENCH_SRC ?= \ + $(SRC) \ + $(filter-out %.t.c %.b.c %.a.c,$(wildcard bd/*.c)) \ runners/bench_runner.c BENCH_RUNNER ?= $(BUILDDIR)/runners/bench_runner -BENCH_A := $(BENCHES:%.toml=$(BUILDDIR)/%.b.a.c) \ - $(BENCH_SRC:%.c=$(BUILDDIR)/%.b.a.c) -BENCH_C := $(BENCH_A:%.b.a.c=%.b.c) -BENCH_OBJ := $(BENCH_C:%.b.c=%.b.o) -BENCH_DEP := $(BENCH_C:%.b.c=%.b.d) -BENCH_CI := $(BENCH_C:%.b.c=%.b.ci) -BENCH_GCNO := $(BENCH_C:%.b.c=%.b.gcno) -BENCH_GCDA := $(BENCH_C:%.b.c=%.b.gcda) +BENCH_C := \ + $(BENCHES:%.toml=$(BUILDDIR)/%.b.c) \ + $(BENCH_SRC:%.c=$(BUILDDIR)/%.b.c) +BENCH_A := $(BENCH_C:%.b.c=%.b.a.c) +BENCH_OBJ := $(BENCH_A:%.b.a.c=%.b.a.o) +BENCH_DEP := $(BENCH_A:%.b.a.c=%.b.a.d) +BENCH_CI := $(BENCH_A:%.b.a.c=%.b.a.ci) +BENCH_GCNO := $(BENCH_A:%.b.a.c=%.b.a.gcno) +BENCH_GCDA := $(BENCH_A:%.b.a.c=%.b.a.gcda) BENCH_PERF := $(BENCH_RUNNER:%=%.perf) BENCH_TRACE := $(BENCH_RUNNER:%=%.trace) BENCH_CSV := $(BENCH_RUNNER:%=%.csv) +# overridable tools/flags +CC ?= gcc +AR ?= ar +SIZE ?= size +CTAGS ?= ctags +OBJDUMP ?= objdump +VALGRIND ?= valgrind +GDB ?= gdb +PERF ?= perf +PRETTYASSERTS ?= ./scripts/prettyasserts.py + CFLAGS += -fcallgraph-info=su CFLAGS += -g3 CFLAGS += -I. CFLAGS += -std=c99 -Wall -Wextra -pedantic +# labels are useful for debugging, in-function organization, etc +CFLAGS += -Wno-unused-label +# life's too short to not use this flag +CFLAGS += -Wno-unused-function +# compiler bug: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=101854 +CFLAGS += -Wno-stringop-overflow CFLAGS += -ftrack-macro-expansion=0 ifdef DEBUG CFLAGS += -O0 @@ -80,22 +82,32 @@ else CFLAGS += -Os endif ifdef TRACE -CFLAGS += -DLFS_YES_TRACE +CFLAGS += -DLFS3_YES_TRACE endif -ifdef YES_COV +ifdef COVGEN CFLAGS += --coverage endif -ifdef YES_PERF +ifdef PERFGEN CFLAGS += -fno-omit-frame-pointer endif -ifdef YES_PERFBD +ifdef PERFBDGEN CFLAGS += -fno-omit-frame-pointer endif +# also forward all LFS3_* environment variables +CFLAGS += $(foreach d,$(filter LFS3_%,$(.VARIABLES)),-D$d=$($d)) + +TEST_CFLAGS += -Wno-unused-function +TEST_CFLAGS += -Wno-format-overflow + +BENCH_CFLAGS += -Wno-unused-function +BENCH_CFLAGS += -Wno-format-overflow + ifdef VERBOSE CODEFLAGS += -v DATAFLAGS += -v STACKFLAGS += -v +CTXFLAGS += -v STRUCTSFLAGS += -v COVFLAGS += -v PERFFLAGS += -v @@ -104,13 +116,11 @@ endif # forward -j flag PERFFLAGS += $(filter -j%,$(MAKEFLAGS)) PERFBDFLAGS += $(filter -j%,$(MAKEFLAGS)) -ifneq ($(NM),nm) -CODEFLAGS += --nm-path="$(NM)" -DATAFLAGS += --nm-path="$(NM)" -endif ifneq ($(OBJDUMP),objdump) CODEFLAGS += --objdump-path="$(OBJDUMP)" DATAFLAGS += --objdump-path="$(OBJDUMP)" +STACKFLAGS += --objdump-path="$(OBJDUMP)" +CTXFLAGS += --objdump-path="$(OBJDUMP)" STRUCTSFLAGS += --objdump-path="$(OBJDUMP)" PERFFLAGS += --objdump-path="$(OBJDUMP)" PERFBDFLAGS += --objdump-path="$(OBJDUMP)" @@ -124,21 +134,19 @@ BENCHFLAGS += -b # forward -j flag TESTFLAGS += $(filter -j%,$(MAKEFLAGS)) BENCHFLAGS += $(filter -j%,$(MAKEFLAGS)) -ifdef YES_PERF -TESTFLAGS += -p $(TEST_PERF) -BENCHFLAGS += -p $(BENCH_PERF) -endif -ifdef YES_PERFBD -TESTFLAGS += -t $(TEST_TRACE) --trace-backtrace --trace-freq=100 +ifdef PERFGEN +TESTFLAGS += -p$(TEST_PERF) +BENCHFLAGS += -p$(BENCH_PERF) endif -ifndef NO_PERFBD -BENCHFLAGS += -t $(BENCH_TRACE) --trace-backtrace --trace-freq=100 +ifdef PERFBDGEN +TESTFLAGS += -t$(TEST_TRACE) --trace-backtrace --trace-freq=100 +BENCHFLAGS += -t$(BENCH_TRACE) --trace-backtrace --trace-freq=100 endif -ifdef YES_TESTMARKS -TESTFLAGS += -o $(TEST_CSV) +ifdef TESTMARKS +TESTFLAGS += -o$(TEST_CSV) endif -ifndef NO_BENCHMARKS -BENCHFLAGS += -o $(BENCH_CSV) +ifdef BENCHMARKS +BENCHFLAGS += -o$(BENCH_CSV) endif ifdef VERBOSE TESTFLAGS += -v @@ -163,8 +171,21 @@ TESTFLAGS += --perf-path="$(PERF)" BENCHFLAGS += --perf-path="$(PERF)" endif +# this is a bit of a hack, but we want to make sure the BUILDDIR +# directory structure is correct before we run any commands +ifneq ($(BUILDDIR),.) +$(if $(findstring n,$(MAKEFLAGS)),, $(shell mkdir -p \ + $(BUILDDIR) \ + $(addprefix $(BUILDDIR)/,$(dir \ + $(SRC) \ + $(TESTS) \ + $(TEST_SRC) \ + $(BENCHES) \ + $(BENCH_SRC))))) +endif + -# commands +# top-level commands ## Build littlefs .PHONY: all build @@ -174,15 +195,22 @@ all build: $(TARGET) .PHONY: asm asm: $(ASM) -## Find the total size +## Find total section sizes .PHONY: size size: $(OBJ) $(SIZE) -t $^ ## Generate a ctags file -.PHONY: tags -tags: - $(CTAGS) --totals --c-types=+p $(shell find -H -name '*.h') $(SRC) +# +# run twice to only include prototypes in header files +.PHONY: tags ctags +tags ctags: + $(strip $(CTAGS) \ + --totals --fields=+n --c-types=+p \ + $(shell find -H -name '*.h')) + $(strip $(CTAGS) \ + --totals --append --fields=+n \ + $(SRC)) ## Show this help text .PHONY: help @@ -192,363 +220,471 @@ help: getline rule; \ while (rule ~ /^(#|\.PHONY|ifdef|ifndef)/) getline rule; \ gsub(/:.*/, "", rule); \ - printf " "" %-25s %s\n", rule, $$0 \ + if (length(rule) <= 21) { \ + printf "%2s%-21s %s\n", "", rule, $$0; \ + } else { \ + printf "%2s%s\n", "", rule; \ + printf "%24s%s\n", "", $$0; \ + } \ }' $(MAKEFILE_LIST)) ## Find the per-function code size .PHONY: code code: CODEFLAGS+=-S -code: $(OBJ) $(BUILDDIR)/lfs.code.csv - ./scripts/code.py $(OBJ) $(CODEFLAGS) +code: $(OBJ) + ./scripts/code.py $^ $(CODEFLAGS) + +## Save the per-function code size +.PHONY: code-csv +code-csv: $(BUILDDIR)/lfs3.code.csv ## Compare per-function code size .PHONY: code-diff code-diff: $(OBJ) - ./scripts/code.py $^ $(CODEFLAGS) -d $(BUILDDIR)/lfs.code.csv + ./scripts/code.py $^ $(CODEFLAGS) -d $(BUILDDIR)/lfs3.code.csv ## Find the per-function data size .PHONY: data data: DATAFLAGS+=-S -data: $(OBJ) $(BUILDDIR)/lfs.data.csv - ./scripts/data.py $(OBJ) $(DATAFLAGS) +data: $(OBJ) + ./scripts/data.py $^ $(DATAFLAGS) + +## Save the per-function data size +.PHONY: data-csv +data-csv: $(BUILDDIR)/lfs3.data.csv ## Compare per-function data size .PHONY: data-diff data-diff: $(OBJ) - ./scripts/data.py $^ $(DATAFLAGS) -d $(BUILDDIR)/lfs.data.csv + ./scripts/data.py $^ $(DATAFLAGS) -d $(BUILDDIR)/lfs3.data.csv ## Find the per-function stack usage .PHONY: stack stack: STACKFLAGS+=-S -stack: $(CI) $(BUILDDIR)/lfs.stack.csv - ./scripts/stack.py $(CI) $(STACKFLAGS) +stack: $(CI) + ./scripts/stack.py $^ $(STACKFLAGS) + +## Save the per-function stack usage +.PHONY: stack-csv +stack-csv: $(BUILDDIR)/lfs3.stack.csv ## Compare per-function stack usage .PHONY: stack-diff stack-diff: $(CI) - ./scripts/stack.py $^ $(STACKFLAGS) -d $(BUILDDIR)/lfs.stack.csv + ./scripts/stack.py $^ $(STACKFLAGS) -d $(BUILDDIR)/lfs3.stack.csv + +## Find the per-function context +.PHONY: ctx +ctx: CTXFLAGS+=-S +ctx: $(OBJ) + ./scripts/ctx.py $^ $(CTXFLAGS) + +## Save the per-function context +.PHONY: ctx-csv +ctx-csv: $(BUILDDIR)/lfs3.ctx.csv + +## Compare per-function context +.PHONY: ctx-diff +ctx-diff: $(CI) + ./scripts/ctx.py $^ $(CTXFLAGS) -d $(BUILDDIR)/lfs3.ctx.csv ## Find function sizes .PHONY: funcs funcs: SUMMARYFLAGS+=-S -funcs: \ - $(BUILDDIR)/lfs.code.csv \ - $(BUILDDIR)/lfs.data.csv \ - $(BUILDDIR)/lfs.stack.csv - $(strip ./scripts/summary.py $^ \ +funcs: SHELL=/bin/bash +funcs: $(OBJ) $(CI) + $(strip ./scripts/csv.py \ + <(./scripts/code.py $(OBJ) $(CODEFLAGS) -o-) \ + <(./scripts/stack.py $(CI) $(STACKFLAGS) -o-) \ + <(./scripts/ctx.py $(OBJ) $(CTXFLAGS) -o-) \ -bfunction \ -fcode=code_size \ - -fdata=data_size \ - -fstack=stack_limit --max=stack \ + -fstack='max(stack_limit)' \ + -fctx='max(ctx_size)' \ $(SUMMARYFLAGS)) +## Save function sizes +.PHONY: funcs-csv +funcs-csv: SHELL=/bin/bash +funcs-csv: \ + $(BUILDDIR)/lfs3.code.csv \ + $(BUILDDIR)/lfs3.stack.csv \ + $(BUILDDIR)/lfs3.ctx.csv + ## Compare function sizes .PHONY: funcs-diff funcs-diff: SHELL=/bin/bash funcs-diff: $(OBJ) $(CI) - $(strip ./scripts/summary.py \ - <(./scripts/code.py $(OBJ) -q $(CODEFLAGS) -o-) \ - <(./scripts/data.py $(OBJ) -q $(DATAFLAGS) -o-) \ - <(./scripts/stack.py $(CI) -q $(STACKFLAGS) -o-) \ + $(strip ./scripts/csv.py \ + <(./scripts/code.py $(OBJ) $(CODEFLAGS) -o-) \ + <(./scripts/stack.py $(CI) $(STACKFLAGS) -o-) \ + <(./scripts/ctx.py $(OBJ) $(CTXFLAGS) -o-) \ -bfunction \ -fcode=code_size \ - -fdata=data_size \ - -fstack=stack_limit --max=stack \ - $(SUMMARYFLAGS) -d <(./scripts/summary.py \ - $(BUILDDIR)/lfs.code.csv \ - $(BUILDDIR)/lfs.data.csv \ - $(BUILDDIR)/lfs.stack.csv \ - -q $(SUMMARYFLAGS) -o-)) + -fstack='max(stack_limit)' \ + -fctx='max(ctx_size)' \ + -d <(./scripts/csv.py \ + $(BUILDDIR)/lfs3.code.csv \ + $(BUILDDIR)/lfs3.stack.csv \ + $(BUILDDIR)/lfs3.ctx.csv \ + -fcode_size=code_size \ + -fstack_limit='max(stack_limit)' \ + -fctx_size='max(ctx_size)' \ + -o-) \ + $(SUMMARYFLAGS)) ## Find struct sizes .PHONY: structs structs: STRUCTSFLAGS+=-S -structs: $(OBJ) $(BUILDDIR)/lfs.structs.csv - ./scripts/structs.py $(OBJ) $(STRUCTSFLAGS) +structs: $(OBJ) + ./scripts/structs.py $^ $(STRUCTSFLAGS) + +## Save struct sizes +.PHONY: structs-csv +structs-csv: $(BUILDDIR)/lfs3.structs.csv ## Compare struct sizes .PHONY: structs-diff structs-diff: $(OBJ) - ./scripts/structs.py $^ $(STRUCTSFLAGS) -d $(BUILDDIR)/lfs.structs.csv + ./scripts/structs.py $^ $(STRUCTSFLAGS) -d $(BUILDDIR)/lfs3.structs.csv ## Find the line/branch coverage after a test run .PHONY: cov cov: COVFLAGS+=-s -cov: $(GCDA) $(BUILDDIR)/lfs.cov.csv - $(strip ./scripts/cov.py $(GCDA) \ +cov: $(GCDA) + $(strip ./scripts/cov.py $^ \ $(patsubst %,-F%,$(SRC)) \ $(COVFLAGS)) +## Save line/branch coverage +.PHONY: cov-csv +cov-csv: $(BUILDDIR)/lfs3.cov.csv + ## Compare line/branch coverage .PHONY: cov-diff cov-diff: $(GCDA) $(strip ./scripts/cov.py $^ \ $(patsubst %,-F%,$(SRC)) \ - $(COVFLAGS) -d $(BUILDDIR)/lfs.cov.csv) + $(COVFLAGS) -d $(BUILDDIR)/lfs3.cov.csv) -## Find the perf results after bench run with YES_PERF +## Find the perf results after bench run with PERFGEN .PHONY: perf perf: PERFFLAGS+=-S -perf: $(BENCH_PERF) $(BUILDDIR)/lfs.perf.csv - $(strip ./scripts/perf.py $(BENCH_PERF) \ +perf: $(BENCH_PERF) + $(strip ./scripts/perf.py $^ \ $(patsubst %,-F%,$(SRC)) \ $(PERFFLAGS)) +## Save perf results +.PHONY: perf-csv +perf-csv: $(BUILDDIR)/lfs3.perf.csv + ## Compare perf results .PHONY: perf-diff perf-diff: $(BENCH_PERF) $(strip ./scripts/perf.py $^ \ $(patsubst %,-F%,$(SRC)) \ - $(PERFFLAGS) -d $(BUILDDIR)/lfs.perf.csv) + $(PERFFLAGS) -d $(BUILDDIR)/lfs3.perf.csv) ## Find the perfbd results after a bench run .PHONY: perfbd perfbd: PERFBDFLAGS+=-S -perfbd: $(BENCH_TRACE) $(BUILDDIR)/lfs.perfbd.csv - $(strip ./scripts/perfbd.py $(BENCH_RUNNER) $(BENCH_TRACE) \ +perfbd: $(BENCH_TRACE) + $(strip ./scripts/perfbd.py $(BENCH_RUNNER) $^ \ $(patsubst %,-F%,$(SRC)) \ $(PERFBDFLAGS)) +## Save perfbd results +.PHONY: perfbd-csv +perfbd-csv: $(BUILDDIR)/lfs3.perfbd.csv + ## Compare perfbd results .PHONY: perfbd-diff perfbd-diff: $(BENCH_TRACE) $(strip ./scripts/perfbd.py $(BENCH_RUNNER) $^ \ $(patsubst %,-F%,$(SRC)) \ - $(PERFBDFLAGS) -d $(BUILDDIR)/lfs.perfbd.csv) + $(PERFBDFLAGS) -d $(BUILDDIR)/lfs3.perfbd.csv) ## Find a summary of compile-time sizes .PHONY: summary sizes -summary sizes: \ - $(BUILDDIR)/lfs.code.csv \ - $(BUILDDIR)/lfs.data.csv \ - $(BUILDDIR)/lfs.stack.csv \ - $(BUILDDIR)/lfs.structs.csv - $(strip ./scripts/summary.py $^ \ +summary sizes: SHELL=/bin/bash +summary sizes: $(OBJ) $(CI) + $(strip ./scripts/csv.py \ + <(./scripts/code.py $(OBJ) $(CODEFLAGS) -o-) \ + <(./scripts/data.py $(OBJ) $(DATAFLAGS) -o-) \ + <(./scripts/stack.py $(CI) $(STACKFLAGS) -o-) \ + <(./scripts/ctx.py $(OBJ) $(CTXFLAGS) -o-) \ + -bfunction \ -fcode=code_size \ -fdata=data_size \ - -fstack=stack_limit --max=stack \ - -fstructs=struct_size \ + -fstack='max(stack_limit)' \ + -fctx='max(ctx_size)' \ -Y $(SUMMARYFLAGS)) +## Save compile-time sizes +.PHONY: summary-csv sizes-csv +summary-csv sizes-csv: SHELL=/bin/bash +summary-csv sizes-csv: \ + $(BUILDDIR)/lfs3.code.csv \ + $(BUILDDIR)/lfs3.data.csv \ + $(BUILDDIR)/lfs3.stack.csv \ + $(BUILDDIR)/lfs3.ctx.csv + ## Compare compile-time sizes .PHONY: summary-diff sizes-diff summary-diff sizes-diff: SHELL=/bin/bash summary-diff sizes-diff: $(OBJ) $(CI) - $(strip ./scripts/summary.py \ - <(./scripts/code.py $(OBJ) -q $(CODEFLAGS) -o-) \ - <(./scripts/data.py $(OBJ) -q $(DATAFLAGS) -o-) \ - <(./scripts/stack.py $(CI) -q $(STACKFLAGS) -o-) \ - <(./scripts/structs.py $(OBJ) -q $(STRUCTSFLAGS) -o-) \ - -fcode=code_size \ - -fdata=data_size \ - -fstack=stack_limit --max=stack \ - -fstructs=struct_size \ - -Y $(SUMMARYFLAGS) -d <(./scripts/summary.py \ - $(BUILDDIR)/lfs.code.csv \ - $(BUILDDIR)/lfs.data.csv \ - $(BUILDDIR)/lfs.stack.csv \ - $(BUILDDIR)/lfs.structs.csv \ - -q $(SUMMARYFLAGS) -o-)) + $(strip ./scripts/csv.py \ + <(./scripts/csv.py \ + <(./scripts/code.py $(OBJ) $(CODEFLAGS) -o-) \ + <(./scripts/data.py $(OBJ) $(DATAFLAGS) -o-) \ + <(./scripts/stack.py $(CI) $(STACKFLAGS) -o-) \ + <(./scripts/ctx.py $(OBJ) $(CTXFLAGS) -o-) \ + -bbuild=AFTER \ + -fcode=code_size \ + -fdata=data_size \ + -fstack='max(stack_limit)' \ + -fctx='max(ctx_size)' \ + -o-) \ + <(./scripts/csv.py \ + $(BUILDDIR)/lfs3.code.csv \ + $(BUILDDIR)/lfs3.data.csv \ + $(BUILDDIR)/lfs3.stack.csv \ + $(BUILDDIR)/lfs3.ctx.csv \ + -bbuild=BEFORE \ + -fcode=code_size \ + -fdata=data_size \ + -fstack='max(stack_limit)' \ + -fctx='max(ctx_size)' \ + -o-) \ + -bbuild -cBEFORE -Q $(SUMMARYFLAGS)) + + +## Generate a codemap svg +.PHONY: codemap +codemap: CODEMAPFLAGS+=-W1125 -H525 --dark +codemap: $(BUILDDIR)/lfs3.codemap.svg + +## Generate a tiny codemap, where 1 pixel ~= 1 byte +.PHONY: codemap-tiny +codemap-tiny: CODEMAPFLAGS+=--dark +codemap-tiny: $(BUILDDIR)/lfs3.codemap_tiny.svg + ## Build the test-runner -.PHONY: test-runner build-test -ifndef NO_COV -test-runner build-test: CFLAGS+=--coverage -endif -ifdef YES_PERF -test-runner build-test: CFLAGS+=-fno-omit-frame-pointer -endif -ifdef YES_PERFBD -test-runner build-test: CFLAGS+=-fno-omit-frame-pointer -endif +.PHONY: test-runner build-tests +test-runner build-tests: CFLAGS+=$(TEST_CFLAGS) # note we remove some binary dependent files during compilation, # otherwise it's way to easy to end up with outdated results -test-runner build-test: $(TEST_RUNNER) -ifndef NO_COV +test-runner build-tests: $(TEST_RUNNER) +ifdef COVGEN rm -f $(TEST_GCDA) endif -ifdef YES_PERF +ifdef PERFGEN rm -f $(TEST_PERF) endif -ifdef YES_PERFBD +ifdef PERFBDGEN rm -f $(TEST_TRACE) endif ## Run the tests, -j enables parallel tests .PHONY: test test: test-runner - ./scripts/test.py $(TEST_RUNNER) $(TESTFLAGS) + ./scripts/test.py -R$(TEST_RUNNER) $(TESTFLAGS) ## List the tests -.PHONY: test-list -test-list: test-runner - ./scripts/test.py $(TEST_RUNNER) $(TESTFLAGS) -l +.PHONY: test-list list-tests +test-list list-tests: test-runner + ./scripts/test.py -R$(TEST_RUNNER) $(TESTFLAGS) -l -## Summarize the testmarks +## Summarize the test results .PHONY: testmarks -testmarks: SUMMARYFLAGS+=-spassed -testmarks: $(TEST_CSV) $(BUILDDIR)/lfs.test.csv - $(strip ./scripts/summary.py $(TEST_CSV) \ +testmarks: SUMMARYFLAGS+=-spassed -Stime +testmarks: $(TEST_CSV) + $(strip ./scripts/csv.py $^ \ -bsuite \ -fpassed=test_passed \ + -ftime=test_time \ $(SUMMARYFLAGS)) -## Compare testmarks against a previous run +## Save the test results +.PHONY: testmarks-csv +testmarks-csv: $(BUILDDIR)/lfs3.test.csv + +## Compare test results against a previous run .PHONY: testmarks-diff testmarks-diff: $(TEST_CSV) - $(strip ./scripts/summary.py $^ \ + $(strip ./scripts/csv.py $^ \ -bsuite \ -fpassed=test_passed \ - $(SUMMARYFLAGS) -d $(BUILDDIR)/lfs.test.csv) + -ftime=test_time \ + $(SUMMARYFLAGS) -d $(BUILDDIR)/lfs3.test.csv) ## Build the bench-runner -.PHONY: bench-runner build-bench -ifdef YES_COV -bench-runner build-bench: CFLAGS+=--coverage -endif -ifdef YES_PERF -bench-runner build-bench: CFLAGS+=-fno-omit-frame-pointer -endif -ifndef NO_PERFBD -bench-runner build-bench: CFLAGS+=-fno-omit-frame-pointer -endif +.PHONY: bench-runner build-benches +bench-runner build-benches: CFLAGS+=$(BENCH_CFLAGS) # note we remove some binary dependent files during compilation, # otherwise it's way to easy to end up with outdated results -bench-runner build-bench: $(BENCH_RUNNER) -ifdef YES_COV +bench-runner build-benches: $(BENCH_RUNNER) +ifdef COVGEN rm -f $(BENCH_GCDA) endif -ifdef YES_PERF +ifdef PERFGEN rm -f $(BENCH_PERF) endif -ifndef NO_PERFBD +ifdef PERFBDGEN rm -f $(BENCH_TRACE) endif -## Run the benchmarks, -j enables parallel benchmarks +## Run the benches, -j enables parallel benches .PHONY: bench bench: bench-runner - ./scripts/bench.py $(BENCH_RUNNER) $(BENCHFLAGS) + ./scripts/bench.py -R$(BENCH_RUNNER) $(BENCHFLAGS) -## List the benchmarks -.PHONY: bench-list -bench-list: bench-runner - ./scripts/bench.py $(BENCH_RUNNER) $(BENCHFLAGS) -l +## List the benches +.PHONY: bench-list list-benches +bench-list list-benches: bench-runner + ./scripts/bench.py -R$(BENCH_RUNNER) $(BENCHFLAGS) -l -## Summarize the benchmarks +## Summarize the bench results .PHONY: benchmarks benchmarks: SUMMARYFLAGS+=-Serased -Sproged -Sreaded -benchmarks: $(BENCH_CSV) $(BUILDDIR)/lfs.bench.csv - $(strip ./scripts/summary.py $(BENCH_CSV) \ +benchmarks: $(BENCH_CSV) + $(strip ./scripts/csv.py $^ \ -bsuite \ -freaded=bench_readed \ -fproged=bench_proged \ -ferased=bench_erased \ $(SUMMARYFLAGS)) -## Compare benchmarks against a previous run +## Save the bench results +.PHONY: benchmarks-csv +benchmarks-csv: $(BUILDDIR)/lfs3.bench.csv + +## Compare bench results against a previous run .PHONY: benchmarks-diff benchmarks-diff: $(BENCH_CSV) - $(strip ./scripts/summary.py $^ \ + $(strip ./scripts/csv.py $^ \ -bsuite \ -freaded=bench_readed \ -fproged=bench_proged \ -ferased=bench_erased \ - $(SUMMARYFLAGS) -d $(BUILDDIR)/lfs.bench.csv) + $(SUMMARYFLAGS) -d $(BUILDDIR)/lfs3.bench.csv) -# rules +# low-level rules -include $(DEP) -include $(TEST_DEP) +-include $(BENCH_DEP) .SUFFIXES: .SECONDARY: -$(BUILDDIR)/lfs: $(OBJ) - $(CC) $(CFLAGS) $^ $(LFLAGS) -o $@ +$(BUILDDIR)/lfs3: $(OBJ) + $(CC) $(CFLAGS) $^ $(LFLAGS) -o$@ -$(BUILDDIR)/liblfs.a: $(OBJ) +$(BUILDDIR)/liblfs3.a: $(OBJ) $(AR) rcs $@ $^ -$(BUILDDIR)/lfs.code.csv: $(OBJ) - ./scripts/code.py $^ -q $(CODEFLAGS) -o $@ +$(BUILDDIR)/lfs3.code.csv: $(OBJ) + ./scripts/code.py $^ $(CODEFLAGS) -o$@ -$(BUILDDIR)/lfs.data.csv: $(OBJ) - ./scripts/data.py $^ -q $(DATAFLAGS) -o $@ +$(BUILDDIR)/lfs3.data.csv: $(OBJ) + ./scripts/data.py $^ $(DATAFLAGS) -o$@ -$(BUILDDIR)/lfs.stack.csv: $(CI) - ./scripts/stack.py $^ -q $(STACKFLAGS) -o $@ +$(BUILDDIR)/lfs3.stack.csv: $(CI) + ./scripts/stack.py $^ $(STACKFLAGS) -o$@ -$(BUILDDIR)/lfs.structs.csv: $(OBJ) - ./scripts/structs.py $^ -q $(STRUCTSFLAGS) -o $@ +$(BUILDDIR)/lfs3.ctx.csv: $(OBJ) + ./scripts/ctx.py $^ $(CTXFLAGS) -o$@ -$(BUILDDIR)/lfs.cov.csv: $(GCDA) +$(BUILDDIR)/lfs3.structs.csv: $(OBJ) + ./scripts/structs.py $^ $(STRUCTSFLAGS) -o$@ + +$(BUILDDIR)/lfs3.cov.csv: $(GCDA) $(strip ./scripts/cov.py $^ \ $(patsubst %,-F%,$(SRC)) \ - -q $(COVFLAGS) -o $@) + $(COVFLAGS) -o$@) -$(BUILDDIR)/lfs.perf.csv: $(BENCH_PERF) +$(BUILDDIR)/lfs3.perf.csv: $(BENCH_PERF) $(strip ./scripts/perf.py $^ \ $(patsubst %,-F%,$(SRC)) \ - -q $(PERFFLAGS) -o $@) + $(PERFFLAGS) -o$@) -$(BUILDDIR)/lfs.perfbd.csv: $(BENCH_TRACE) +$(BUILDDIR)/lfs3.perfbd.csv: $(BENCH_TRACE) $(strip ./scripts/perfbd.py $(BENCH_RUNNER) $^ \ $(patsubst %,-F%,$(SRC)) \ - -q $(PERFBDFLAGS) -o $@) + $(PERFBDFLAGS) -o$@) + +$(BUILDDIR)/lfs3.codemap.svg: $(OBJ) $(CI) + $(strip ./scripts/codemapsvg.py $^ $(CODEMAPFLAGS) -o$@ \ + && ./scripts/codemap.py $^ --no-header) + +$(BUILDDIR)/lfs3.codemap_tiny.svg: $(OBJ) $(CI) + $(strip ./scripts/codemapsvg.py $^ --tiny $(CODEMAPFLAGS) -o$@ \ + && ./scripts/codemap.py $^ --no-header) -$(BUILDDIR)/lfs.test.csv: $(TEST_CSV) +$(BUILDDIR)/lfs3.test.csv: $(TEST_CSV) cp $^ $@ -$(BUILDDIR)/lfs.bench.csv: $(BENCH_CSV) +$(BUILDDIR)/lfs3.bench.csv: $(BENCH_CSV) cp $^ $@ $(BUILDDIR)/runners/test_runner: $(TEST_OBJ) - $(CC) $(CFLAGS) $^ $(LFLAGS) -o $@ + $(CC) $(CFLAGS) $^ $(LFLAGS) -o$@ $(BUILDDIR)/runners/bench_runner: $(BENCH_OBJ) - $(CC) $(CFLAGS) $^ $(LFLAGS) -o $@ + $(CC) $(CFLAGS) $^ $(LFLAGS) -o$@ # our main build rule generates .o, .d, and .ci files, the latter # used for stack analysis $(BUILDDIR)/%.o $(BUILDDIR)/%.ci: %.c $(CC) -c -MMD $(CFLAGS) $< -o $(BUILDDIR)/$*.o +$(BUILDDIR)/%.o $(BUILDDIR)/%.ci: $(BUILDDIR)/%.c + $(CC) -c -MMD $(CFLAGS) $< -o $(BUILDDIR)/$*.o + $(BUILDDIR)/%.s: %.c - $(CC) -S $(CFLAGS) $< -o $@ + $(CC) -S $(CFLAGS) $< -o$@ + +$(BUILDDIR)/%.s: $(BUILDDIR)/%.c + $(CC) -S $(CFLAGS) $< -o$@ -$(BUILDDIR)/%.c: %.a.c - ./scripts/prettyasserts.py -p LFS_ASSERT $< -o $@ +$(BUILDDIR)/%.a.c: %.c + $(PRETTYASSERTS) -Plfs3_ $< -o$@ -$(BUILDDIR)/%.c: $(BUILDDIR)/%.a.c - ./scripts/prettyasserts.py -p LFS_ASSERT $< -o $@ +$(BUILDDIR)/%.a.c: $(BUILDDIR)/%.c + $(PRETTYASSERTS) -Plfs3_ $< -o$@ -$(BUILDDIR)/%.t.a.c: %.toml - ./scripts/test.py -c $< $(TESTCFLAGS) -o $@ +$(BUILDDIR)/%.t.c: %.toml + ./scripts/test.py -c $< $(TESTCFLAGS) -o$@ -$(BUILDDIR)/%.t.a.c: %.c $(TESTS) - ./scripts/test.py -c $(TESTS) -s $< $(TESTCFLAGS) -o $@ +$(BUILDDIR)/%.t.c: %.c $(TESTS) + ./scripts/test.py -c $(TESTS) -s $< $(TESTCFLAGS) -o$@ -$(BUILDDIR)/%.b.a.c: %.toml - ./scripts/bench.py -c $< $(BENCHCFLAGS) -o $@ +$(BUILDDIR)/%.b.c: %.toml + ./scripts/bench.py -c $< $(BENCHCFLAGS) -o$@ -$(BUILDDIR)/%.b.a.c: %.c $(BENCHES) - ./scripts/bench.py -c $(BENCHES) -s $< $(BENCHCFLAGS) -o $@ +$(BUILDDIR)/%.b.c: %.c $(BENCHES) + ./scripts/bench.py -c $(BENCHES) -s $< $(BENCHCFLAGS) -o$@ ## Clean everything .PHONY: clean clean: - rm -f $(BUILDDIR)/lfs - rm -f $(BUILDDIR)/liblfs.a - rm -f $(BUILDDIR)/lfs.code.csv - rm -f $(BUILDDIR)/lfs.data.csv - rm -f $(BUILDDIR)/lfs.stack.csv - rm -f $(BUILDDIR)/lfs.structs.csv - rm -f $(BUILDDIR)/lfs.cov.csv - rm -f $(BUILDDIR)/lfs.perf.csv - rm -f $(BUILDDIR)/lfs.perfbd.csv - rm -f $(BUILDDIR)/lfs.test.csv - rm -f $(BUILDDIR)/lfs.bench.csv + rm -f $(BUILDDIR)/lfs3 + rm -f $(BUILDDIR)/liblfs3.a + rm -f $(BUILDDIR)/lfs3.code.csv + rm -f $(BUILDDIR)/lfs3.data.csv + rm -f $(BUILDDIR)/lfs3.stack.csv + rm -f $(BUILDDIR)/lfs3.ctx.csv + rm -f $(BUILDDIR)/lfs3.structs.csv + rm -f $(BUILDDIR)/lfs3.cov.csv + rm -f $(BUILDDIR)/lfs3.perf.csv + rm -f $(BUILDDIR)/lfs3.perfbd.csv + rm -f $(BUILDDIR)/lfs3.codemap.svg + rm -f $(BUILDDIR)/lfs3.codemap_tiny.svg + rm -f $(BUILDDIR)/lfs3.test.csv + rm -f $(BUILDDIR)/lfs3.bench.csv rm -f $(OBJ) rm -f $(DEP) rm -f $(ASM) diff --git a/bd/lfs3_emubd.c b/bd/lfs3_emubd.c new file mode 100644 index 000000000..f72d5fe90 --- /dev/null +++ b/bd/lfs3_emubd.c @@ -0,0 +1,1489 @@ +/* + * emubd - High-level emulating block device with many bells and + * whistles for testing powerloss, wear, etc. + * + * Note emubd always backs the block device in RAM. Consider using + * kiwibd if you need a block device larger than the available RAM on + * the system. + * + * Copyright (c) 2022, The littlefs authors. + * Copyright (c) 2017, Arm Limited. All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + */ + +#ifndef _POSIX_C_SOURCE +#define _POSIX_C_SOURCE 199309L +#endif + +#include "bd/lfs3_emubd.h" + +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#endif + + +// low-level flash memory emulation + +// read data +static inline void lfs3_emubd_memread(const struct lfs3_cfg *cfg, + void *restrict dst, const void *restrict src, size_t size) { + (void)cfg; + memcpy(dst, src, size); +} + +static inline void lfs3_emubd_memprog(const struct lfs3_cfg *cfg, + void *restrict dst, const void *restrict src, size_t size) { + lfs3_emubd_t *bd = cfg->context; + // emulating nor-masking? + if (bd->cfg->erase_value == -2) { + uint8_t *dst_ = dst; + const uint8_t *src_ = src; + for (size_t i = 0; i < size; i++) { + dst_[i] &= src_[i]; + } + } else { + memcpy(dst, src, size); + } +} + +static inline void lfs3_emubd_memerase(const struct lfs3_cfg *cfg, + void *restrict dst, size_t size) { + lfs3_emubd_t *bd = cfg->context; + // emulating erase value? + if (bd->cfg->erase_value != -1) { + memset(dst, + (bd->cfg->erase_value >= 0) + ? bd->cfg->erase_value + : 0xff, + size); + } +} + +// this is slightly different from lfs3_emubd_memerase in that we use +// lfs3_emubd_memzero when we need to unconditionally zero memory +static inline void lfs3_emubd_memzero(const struct lfs3_cfg *cfg, + void *restrict dst, size_t size) { + lfs3_emubd_t *bd = cfg->context; + memset(dst, + (bd->cfg->erase_value == -1) ? 0 + : (bd->cfg->erase_value >= 0) ? bd->cfg->erase_value + : (bd->cfg->erase_value == -2) ? 0xff + : 0, + size); +} + + +// access to lazily-allocated/copy-on-write blocks +// +// note we can only modify a block if we have exclusive access to +// it (rc == 1) +// + +static lfs3_emubd_block_t *lfs3_emubd_incblock(lfs3_emubd_block_t *block) { + if (block) { + block->rc += 1; + } + return block; +} + +static void lfs3_emubd_decblock(lfs3_emubd_block_t *block) { + if (block) { + block->rc -= 1; + if (block->rc == 0) { + free(block); + } + } +} + +static lfs3_emubd_block_t *lfs3_emubd_mutblock( + const struct lfs3_cfg *cfg, + lfs3_emubd_block_t *block) { + if (block && block->rc == 1) { + // rc == 1? can modify + return block; + + } else if (block) { + // rc > 1? need to create a copy + lfs3_emubd_block_t *block_ = malloc( + sizeof(lfs3_emubd_block_t) + cfg->block_size); + if (!block_) { + return NULL; + } + + memcpy(block_, block, + sizeof(lfs3_emubd_block_t) + cfg->block_size); + block_->rc = 1; + + lfs3_emubd_decblock(block); + return block_; + + } else { + // no block? need to allocate + lfs3_emubd_block_t *block_ = malloc( + sizeof(lfs3_emubd_block_t) + cfg->block_size); + if (!block_) { + return NULL; + } + + block_->rc = 1; + block_->wear = 0; + block_->metastable = false; + block_->bad_bit = 0; + + // zero for consistency + lfs3_emubd_memzero(cfg, block_->data, cfg->block_size); + + return block_; + } +} + + +// prng used for some emulation things +static uint32_t lfs3_emubd_prng_(uint32_t *state) { + // A simple xorshift32 generator, easily reproducible. Keep in mind + // determinism is much more important than actual randomness here. + uint32_t x = *state; + // must be non-zero, use uintmax here so that seed=0 is different + // from seed=1 and seed=range(0,n) makes a bit more sense + if (x == 0) { + x = -1; + } + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + *state = x; + return x; +} + + +// emubd create/destroy + +int lfs3_emubd_createcfg(const struct lfs3_cfg *cfg, const char *path, + const struct lfs3_emubd_cfg *bdcfg) { + LFS3_EMUBD_TRACE("lfs3_emubd_createcfg(" + "%p {" + ".context=%p, " + ".read=%p, " + ".prog=%p, " + ".erase=%p, " + ".sync=%p, " + ".read_size=%"PRIu32", " + ".prog_size=%"PRIu32", " + ".block_size=%"PRIu32", " + ".block_count=%"PRIu32"}, " + "\"%s\", " + "%p {.erase_value=%"PRId32", " + ".erase_cycles=%"PRIu32", " + ".badblock_behavior=%"PRIu8", " + ".power_cycles=%"PRIu32", " + ".powerloss_behavior=%"PRIu8", " + ".powerloss_cb=%p, " + ".powerloss_data=%p, " + ".seed=%"PRIu32", " + ".read_sleep=%"PRIu64", " + ".prog_sleep=%"PRIu64", " + ".erase_sleep=%"PRIu64"})", + (void*)cfg, + cfg->context, + (void*)(uintptr_t)cfg->read, + (void*)(uintptr_t)cfg->prog, + (void*)(uintptr_t)cfg->erase, + (void*)(uintptr_t)cfg->sync, + cfg->read_size, + cfg->prog_size, + cfg->block_size, + cfg->block_count, + path, + (void*)bdcfg, + bdcfg->erase_value, + bdcfg->erase_cycles, + bdcfg->badblock_behavior, + bdcfg->power_cycles, + bdcfg->powerloss_behavior, + (void*)(uintptr_t)bdcfg->powerloss_cb, + bdcfg->powerloss_data, + bdcfg->seed, + bdcfg->read_sleep, + bdcfg->prog_sleep, + bdcfg->erase_sleep); + lfs3_emubd_t *bd = cfg->context; + bd->cfg = bdcfg; + + // setup testing things + bd->blocks = NULL; + bd->readed = 0; + bd->proged = 0; + bd->erased = 0; + bd->prng = bd->cfg->seed; + bd->power_cycles = bd->cfg->power_cycles; + bd->ooo_before = NULL; + bd->ooo_after = NULL; + bd->disk = NULL; + + // allocate our block array, all blocks start as uninitialized + bd->blocks = malloc( + cfg->block_count * sizeof(lfs3_emubd_block_t*)); + int err; + if (!bd->blocks) { + err = LFS3_ERR_NOMEM; + goto failed; + } + memset(bd->blocks, 0, + cfg->block_count * sizeof(lfs3_emubd_block_t*)); + + // allocate extra block arrays to hold our ooo snapshots + if (bd->cfg->powerloss_behavior == LFS3_EMUBD_POWERLOSS_OOO) { + bd->ooo_before = malloc( + cfg->block_count * sizeof(lfs3_emubd_block_t*)); + if (!bd->ooo_before) { + err = LFS3_ERR_NOMEM; + goto failed; + } + memset(bd->ooo_before, 0, + cfg->block_count * sizeof(lfs3_emubd_block_t*)); + + bd->ooo_after = malloc( + cfg->block_count * sizeof(lfs3_emubd_block_t*)); + if (!bd->ooo_after) { + err = LFS3_ERR_NOMEM; + goto failed; + } + memset(bd->ooo_after, 0, + cfg->block_count * sizeof(lfs3_emubd_block_t*)); + } + + if (path) { + bd->disk = malloc(sizeof(lfs3_emubd_disk_t)); + if (!bd->disk) { + err = LFS3_ERR_NOMEM; + goto failed; + } + bd->disk->rc = 1; + bd->disk->fd = -1; + bd->disk->scratch = NULL; + + #ifdef _WIN32 + bd->disk->fd = open(path, O_RDWR | O_CREAT | O_BINARY, 0666); + #else + bd->disk->fd = open(path, O_RDWR | O_CREAT, 0666); + #endif + if (bd->disk->fd < 0) { + err = -errno; + goto failed; + } + + bd->disk->scratch = malloc(cfg->block_size); + if (!bd->disk->scratch) { + err = LFS3_ERR_NOMEM; + goto failed; + } + lfs3_emubd_memzero(cfg, bd->disk->scratch, cfg->block_size); + + // go ahead and erase all of the disk, otherwise the file will not + // match our internal representation + for (size_t i = 0; i < cfg->block_count; i++) { + ssize_t res = write(bd->disk->fd, + bd->disk->scratch, + cfg->block_size); + if (res < 0) { + err = -errno; + goto failed; + } + } + } + + LFS3_EMUBD_TRACE("lfs3_emubd_createcfg -> %d", 0); + return 0; + +failed:; + LFS3_EMUBD_TRACE("lfs3_emubd_createcfg -> %d", err); + // clean up memory + free(bd->blocks); + if (bd->cfg->powerloss_behavior == LFS3_EMUBD_POWERLOSS_OOO) { + free(bd->ooo_before); + free(bd->ooo_after); + } + if (bd->disk) { + if (bd->disk->fd != -1) { + close(bd->disk->fd); + } + free(bd->disk->scratch); + free(bd->disk); + } + return err; +} + +int lfs3_emubd_create(const struct lfs3_cfg *cfg, const char *path) { + LFS3_EMUBD_TRACE("lfs3_emubd_create(" + "%p {" + ".context=%p, " + ".read=%p, " + ".prog=%p, " + ".erase=%p, " + ".sync=%p, " + ".read_size=%"PRIu32", " + ".prog_size=%"PRIu32", " + ".block_size=%"PRIu32", " + ".block_count=%"PRIu32"}, " + "\"%s\")", + (void*)cfg, + cfg->context, + (void*)(uintptr_t)cfg->read, + (void*)(uintptr_t)cfg->prog, + (void*)(uintptr_t)cfg->erase, + (void*)(uintptr_t)cfg->sync, + cfg->read_size, + cfg->prog_size, + cfg->block_size, + cfg->block_count, + path); + static const struct lfs3_emubd_cfg defaults = {.erase_value=-1}; + int err = lfs3_emubd_createcfg(cfg, path, &defaults); + LFS3_EMUBD_TRACE("lfs3_emubd_create -> %d", err); + return err; +} + +int lfs3_emubd_destroy(const struct lfs3_cfg *cfg) { + LFS3_EMUBD_TRACE("lfs3_emubd_destroy(%p)", (void*)cfg); + lfs3_emubd_t *bd = cfg->context; + + // decrement reference counts + for (lfs3_block_t i = 0; i < cfg->block_count; i++) { + lfs3_emubd_decblock(bd->blocks[i]); + } + free(bd->blocks); + + if (bd->cfg->powerloss_behavior == LFS3_EMUBD_POWERLOSS_OOO) { + for (lfs3_block_t i = 0; i < cfg->block_count; i++) { + lfs3_emubd_decblock(bd->ooo_before[i]); + } + free(bd->ooo_before); + + for (lfs3_block_t i = 0; i < cfg->block_count; i++) { + lfs3_emubd_decblock(bd->ooo_after[i]); + } + free(bd->ooo_after); + } + + // clean up other resources + if (bd->disk) { + bd->disk->rc -= 1; + if (bd->disk->rc == 0) { + close(bd->disk->fd); + free(bd->disk->scratch); + free(bd->disk); + } + } + + LFS3_EMUBD_TRACE("lfs3_emubd_destroy -> %d", 0); + return 0; +} + + +// block device API + +int lfs3_emubd_read(const struct lfs3_cfg *cfg, lfs3_block_t block, + lfs3_off_t off, void *buffer, lfs3_size_t size) { + LFS3_EMUBD_TRACE("lfs3_emubd_read(%p, " + "0x%"PRIx32", %"PRIu32", %p, %"PRIu32")", + (void*)cfg, block, off, buffer, size); + lfs3_emubd_t *bd = cfg->context; + + // check if read is valid + LFS3_ASSERT(block < cfg->block_count); + LFS3_ASSERT(off % cfg->read_size == 0); + LFS3_ASSERT(size % cfg->read_size == 0); + LFS3_ASSERT(off+size <= cfg->block_size); + + // get the block + const lfs3_emubd_block_t *b = bd->blocks[block]; + if (b) { + // block bad? + if (b->wear > bd->cfg->erase_cycles) { + // erroring reads? error + if (bd->cfg->badblock_behavior + == LFS3_EMUBD_BADBLOCK_READERROR) { + LFS3_EMUBD_TRACE("lfs3_emubd_read -> %d", LFS3_ERR_CORRUPT); + return LFS3_ERR_CORRUPT; + } + } + + // read data + lfs3_emubd_memread(cfg, buffer, &b->data[off], size); + + // metastable? randomly decide if our bad bit flips + if (b->metastable) { + lfs3_size_t bit = b->bad_bit & 0x7fffffff; + if (bit/8 >= off + && bit/8 < off+size + && (lfs3_emubd_prng_(&bd->prng) & 1)) { + ((uint8_t*)buffer)[(bit/8) - off] ^= 1 << (bit%8); + } + } + + // no block yet + } else { + // zero for consistency + lfs3_emubd_memzero(cfg, buffer, size); + } + + // track reads + bd->readed += size; + if (bd->cfg->read_sleep) { + int err = nanosleep(&(struct timespec){ + .tv_sec=bd->cfg->read_sleep/1000000000, + .tv_nsec=bd->cfg->read_sleep%1000000000}, + NULL); + if (err) { + err = -errno; + LFS3_EMUBD_TRACE("lfs3_emubd_read -> %d", err); + return err; + } + } + + LFS3_EMUBD_TRACE("lfs3_emubd_read -> %d", 0); + return 0; +} + +int lfs3_emubd_prog(const struct lfs3_cfg *cfg, lfs3_block_t block, + lfs3_off_t off, const void *buffer, lfs3_size_t size) { + LFS3_EMUBD_TRACE("lfs3_emubd_prog(%p, " + "0x%"PRIx32", %"PRIu32", %p, %"PRIu32")", + (void*)cfg, block, off, buffer, size); + lfs3_emubd_t *bd = cfg->context; + + // check if write is valid + LFS3_ASSERT(block < cfg->block_count); + LFS3_ASSERT(off % cfg->prog_size == 0); + LFS3_ASSERT(size % cfg->prog_size == 0); + LFS3_ASSERT(off+size <= cfg->block_size); + + // were we erased properly? + LFS3_ASSERT(bd->blocks[block]); + if (bd->cfg->erase_value >= 0 + && bd->blocks[block]->wear <= bd->cfg->erase_cycles) { + for (lfs3_off_t i = 0; i < size; i++) { + LFS3_ASSERT(bd->blocks[block]->data[off+i] == bd->cfg->erase_value); + } + } + + // losing power? + if (bd->power_cycles > 0) { + bd->power_cycles -= 1; + if (bd->power_cycles == 0) { + // emulating some bits? choose a random bit to flip + if (bd->cfg->powerloss_behavior + == LFS3_EMUBD_POWERLOSS_SOMEBITS) { + // mutate the block + lfs3_emubd_block_t *b = lfs3_emubd_mutblock(cfg, + bd->blocks[block]); + if (!b) { + LFS3_EMUBD_TRACE("lfs3_emubd_prog -> %d", LFS3_ERR_NOMEM); + return LFS3_ERR_NOMEM; + } + bd->blocks[block] = b; + + // flip bit + lfs3_size_t bit = lfs3_emubd_prng_(&bd->prng) + % (cfg->prog_size*8); + b->data[off + (bit/8)] ^= 1 << (bit%8); + + // mirror to disk file? + if (bd->disk) { + off_t res1 = lseek(bd->disk->fd, + (off_t)block*cfg->block_size + (off_t)off, + SEEK_SET); + if (res1 < 0) { + int err = -errno; + LFS3_EMUBD_TRACE("lfs3_emubd_prog -> %d", err); + return err; + } + + ssize_t res2 = write(bd->disk->fd, &b->data[off], size); + if (res2 < 0) { + int err = -errno; + LFS3_EMUBD_TRACE("lfs3_emubd_prog -> %d", err); + return err; + } + } + + // emulating most bits? prog data and choose a random bit + // to flip + } else if (bd->cfg->powerloss_behavior + == LFS3_EMUBD_POWERLOSS_MOSTBITS) { + // mutate the block + lfs3_emubd_block_t *b = lfs3_emubd_mutblock(cfg, + bd->blocks[block]); + if (!b) { + LFS3_EMUBD_TRACE("lfs3_emubd_prog -> %d", LFS3_ERR_NOMEM); + return LFS3_ERR_NOMEM; + } + bd->blocks[block] = b; + + // prog data + lfs3_emubd_memprog(cfg, &b->data[off], buffer, size); + + // flip bit + lfs3_size_t bit = lfs3_emubd_prng_(&bd->prng) + % (cfg->prog_size*8); + b->data[off + (bit/8)] ^= 1 << (bit%8); + + // mirror to disk file? + if (bd->disk) { + off_t res1 = lseek(bd->disk->fd, + (off_t)block*cfg->block_size + (off_t)off, + SEEK_SET); + if (res1 < 0) { + int err = -errno; + LFS3_EMUBD_TRACE("lfs3_emubd_prog -> %d", err); + return err; + } + + ssize_t res2 = write(bd->disk->fd, &b->data[off], size); + if (res2 < 0) { + int err = -errno; + LFS3_EMUBD_TRACE("lfs3_emubd_prog -> %d", err); + return err; + } + } + + // emulating out-of-order writes? revert everything unsynced + // except for our current block + } else if (bd->cfg->powerloss_behavior + == LFS3_EMUBD_POWERLOSS_OOO) { + for (lfs3_block_t i = 0; i < cfg->block_count; i++) { + lfs3_emubd_decblock(bd->ooo_after[i]); + bd->ooo_after[i] = lfs3_emubd_incblock(bd->blocks[i]); + + if (i != block && bd->blocks[i] != bd->ooo_before[i]) { + lfs3_emubd_decblock(bd->blocks[i]); + bd->blocks[i] = lfs3_emubd_incblock(bd->ooo_before[i]); + + // mirror to disk file? + if (bd->disk) { + off_t res1 = lseek(bd->disk->fd, + (off_t)i*cfg->block_size, + SEEK_SET); + if (res1 < 0) { + int err = -errno; + LFS3_EMUBD_TRACE("lfs3_emubd_prog -> %d", err); + return err; + } + + ssize_t res2 = write(bd->disk->fd, + (bd->blocks[i]) + ? bd->blocks[i]->data + : bd->disk->scratch, + cfg->block_size); + if (res2 < 0) { + int err = -errno; + LFS3_EMUBD_TRACE("lfs3_emubd_prog -> %d", err); + return err; + } + } + } + } + + // emulating metastability? prog data, choose a random bad bit, + // and mark as metastable + } else if (bd->cfg->powerloss_behavior + == LFS3_EMUBD_POWERLOSS_METASTABLE) { + // mutate the block + lfs3_emubd_block_t *b = lfs3_emubd_mutblock(cfg, + bd->blocks[block]); + if (!b) { + LFS3_EMUBD_TRACE("lfs3_emubd_prog -> %d", LFS3_ERR_NOMEM); + return LFS3_ERR_NOMEM; + } + bd->blocks[block] = b; + + // prog data + lfs3_emubd_memprog(cfg, &b->data[off], buffer, size); + + // choose a new bad bit unless overridden + if (!(0x80000000 & b->bad_bit)) { + b->bad_bit = lfs3_emubd_prng_(&bd->prng) + % (cfg->block_size*8); + } + + // mark as metastable + b->metastable = true; + + // mirror to disk file? + if (bd->disk) { + off_t res1 = lseek(bd->disk->fd, + (off_t)block*cfg->block_size + (off_t)off, + SEEK_SET); + if (res1 < 0) { + int err = -errno; + LFS3_EMUBD_TRACE("lfs3_emubd_prog -> %d", err); + return err; + } + + ssize_t res2 = write(bd->disk->fd, &b->data[off], size); + if (res2 < 0) { + int err = -errno; + LFS3_EMUBD_TRACE("lfs3_emubd_prog -> %d", err); + return err; + } + } + } + + // powerloss! + bd->cfg->powerloss_cb(bd->cfg->powerloss_data); + + // oh, continuing? undo out-of-order write emulation + if (bd->cfg->powerloss_behavior == LFS3_EMUBD_POWERLOSS_OOO) { + for (lfs3_block_t i = 0; i < cfg->block_count; i++) { + if (bd->blocks[i] != bd->ooo_after[i]) { + lfs3_emubd_decblock(bd->blocks[i]); + bd->blocks[i] = lfs3_emubd_incblock(bd->ooo_after[i]); + + // mirror to disk file? + if (bd->disk) { + off_t res1 = lseek(bd->disk->fd, + (off_t)i*cfg->block_size, + SEEK_SET); + if (res1 < 0) { + int err = -errno; + LFS3_EMUBD_TRACE("lfs3_emubd_prog -> %d", err); + return err; + } + + ssize_t res2 = write(bd->disk->fd, + (bd->blocks[i]) + ? bd->blocks[i]->data + : bd->disk->scratch, + cfg->block_size); + if (res2 < 0) { + int err = -errno; + LFS3_EMUBD_TRACE("lfs3_emubd_prog -> %d", err); + return err; + } + } + } + } + } + } + } + + // mutate the block + lfs3_emubd_block_t *b = lfs3_emubd_mutblock(cfg, bd->blocks[block]); + if (!b) { + LFS3_EMUBD_TRACE("lfs3_emubd_prog -> %d", LFS3_ERR_NOMEM); + return LFS3_ERR_NOMEM; + } + bd->blocks[block] = b; + + // block bad? + if (b->wear > bd->cfg->erase_cycles) { + // erroring progs? error + if (bd->cfg->badblock_behavior + == LFS3_EMUBD_BADBLOCK_PROGERROR) { + LFS3_EMUBD_TRACE("lfs3_emubd_prog -> %d", LFS3_ERR_CORRUPT); + return LFS3_ERR_CORRUPT; + + // noop progs? skip + } else if (bd->cfg->badblock_behavior + == LFS3_EMUBD_BADBLOCK_PROGNOOP + || bd->cfg->badblock_behavior + == LFS3_EMUBD_BADBLOCK_ERASENOOP) { + goto progged; + + // progs flipping bits? flip our bad bit, exactly which bit + // is chosen during erase + } else if (bd->cfg->badblock_behavior + == LFS3_EMUBD_BADBLOCK_PROGFLIP) { + lfs3_size_t bit = b->bad_bit & 0x7fffffff; + if (bit/8 >= off && bit/8 < off+size) { + // prog data + lfs3_emubd_memprog(cfg, &b->data[off], buffer, size); + b->data[bit/8] ^= 1 << (bit%8); + goto progged; + } + + // reads flipping bits? prog as normal but mark as metastable + } else if (bd->cfg->badblock_behavior + == LFS3_EMUBD_BADBLOCK_READFLIP) { + // prog data + lfs3_emubd_memprog(cfg, &b->data[off], buffer, size); + b->metastable = true; + goto progged; + } + } + + // prog data + lfs3_emubd_memprog(cfg, &b->data[off], buffer, size); + + // clear any metastability + b->metastable = false; + +progged:; + // mirror to disk file? + if (bd->disk) { + off_t res1 = lseek(bd->disk->fd, + (off_t)block*cfg->block_size + (off_t)off, + SEEK_SET); + if (res1 < 0) { + int err = -errno; + LFS3_EMUBD_TRACE("lfs3_emubd_prog -> %d", err); + return err; + } + + ssize_t res2 = write(bd->disk->fd, &b->data[off], size); + if (res2 < 0) { + int err = -errno; + LFS3_EMUBD_TRACE("lfs3_emubd_prog -> %d", err); + return err; + } + } + + // track progs + bd->proged += size; + if (bd->cfg->prog_sleep) { + int err = nanosleep(&(struct timespec){ + .tv_sec=bd->cfg->prog_sleep/1000000000, + .tv_nsec=bd->cfg->prog_sleep%1000000000}, + NULL); + if (err) { + err = -errno; + LFS3_EMUBD_TRACE("lfs3_emubd_prog -> %d", err); + return err; + } + } + + LFS3_EMUBD_TRACE("lfs3_emubd_prog -> %d", 0); + return 0; +} + +int lfs3_emubd_erase(const struct lfs3_cfg *cfg, lfs3_block_t block) { + LFS3_EMUBD_TRACE("lfs3_emubd_erase(%p, 0x%"PRIx32" (%"PRIu32"))", + (void*)cfg, block, cfg->block_size); + lfs3_emubd_t *bd = cfg->context; + + // check if erase is valid + LFS3_ASSERT(block < cfg->block_count); + + // losing power? + if (bd->power_cycles > 0) { + bd->power_cycles -= 1; + if (bd->power_cycles == 0) { + // emulating some bits? choose a random bit to flip + if (bd->cfg->powerloss_behavior + == LFS3_EMUBD_POWERLOSS_SOMEBITS) { + // mutate the block + lfs3_emubd_block_t *b = lfs3_emubd_mutblock(cfg, + bd->blocks[block]); + if (!b) { + LFS3_EMUBD_TRACE("lfs3_emubd_erase -> %d", LFS3_ERR_NOMEM); + return LFS3_ERR_NOMEM; + } + bd->blocks[block] = b; + + // flip bit + lfs3_size_t bit = lfs3_emubd_prng_(&bd->prng) + % (cfg->block_size*8); + b->data[(bit/8)] ^= 1 << (bit%8); + + // mirror to disk file? + if (bd->disk) { + off_t res1 = lseek(bd->disk->fd, + (off_t)block*cfg->block_size, + SEEK_SET); + if (res1 < 0) { + int err = -errno; + LFS3_EMUBD_TRACE("lfs3_emubd_erase -> %d", err); + return err; + } + + ssize_t res2 = write(bd->disk->fd, + b->data, cfg->block_size); + if (res2 < 0) { + int err = -errno; + LFS3_EMUBD_TRACE("lfs3_emubd_erase -> %d", err); + return err; + } + } + + // emulating most bits? erase data and choose a random bit + // to flip + } else if (bd->cfg->powerloss_behavior + == LFS3_EMUBD_POWERLOSS_MOSTBITS) { + // mutate the block + lfs3_emubd_block_t *b = lfs3_emubd_mutblock(cfg, + bd->blocks[block]); + if (!b) { + LFS3_EMUBD_TRACE("lfs3_emubd_erase -> %d", LFS3_ERR_NOMEM); + return LFS3_ERR_NOMEM; + } + bd->blocks[block] = b; + + // emulate an erase value? + if (bd->cfg->erase_value != -1) { + lfs3_emubd_memerase(cfg, b->data, cfg->block_size); + } + + // flip bit + lfs3_size_t bit = lfs3_emubd_prng_(&bd->prng) + % (cfg->block_size*8); + b->data[(bit/8)] ^= 1 << (bit%8); + + // mirror to disk file? + if (bd->disk) { + off_t res1 = lseek(bd->disk->fd, + (off_t)block*cfg->block_size, + SEEK_SET); + if (res1 < 0) { + int err = -errno; + LFS3_EMUBD_TRACE("lfs3_emubd_erase -> %d", err); + return err; + } + + ssize_t res2 = write(bd->disk->fd, + b->data, cfg->block_size); + if (res2 < 0) { + int err = -errno; + LFS3_EMUBD_TRACE("lfs3_emubd_erase -> %d", err); + return err; + } + } + + // emulating out-of-order writes? revert everything unsynced + // except for our current block + } else if (bd->cfg->powerloss_behavior + == LFS3_EMUBD_POWERLOSS_OOO) { + for (lfs3_block_t i = 0; i < cfg->block_count; i++) { + if (i != block && bd->blocks[i] != bd->ooo_before[i]) { + lfs3_emubd_decblock(bd->blocks[i]); + bd->blocks[i] = lfs3_emubd_incblock(bd->ooo_before[i]); + + // mirror to disk file? + if (bd->disk) { + off_t res1 = lseek(bd->disk->fd, + (off_t)i*cfg->block_size, + SEEK_SET); + if (res1 < 0) { + int err = -errno; + LFS3_EMUBD_TRACE("lfs3_emubd_erase -> %d", err); + return err; + } + + ssize_t res2 = write(bd->disk->fd, + (bd->blocks[i]) + ? bd->blocks[i]->data + : bd->disk->scratch, + cfg->block_size); + if (res2 < 0) { + int err = -errno; + LFS3_EMUBD_TRACE("lfs3_emubd_erase -> %d", err); + return err; + } + } + } + } + + // emulating metastability? erase data, choose a random bad bit, + // and mark as metastable + } else if (bd->cfg->powerloss_behavior + == LFS3_EMUBD_POWERLOSS_METASTABLE) { + // mutate the block + lfs3_emubd_block_t *b = lfs3_emubd_mutblock(cfg, + bd->blocks[block]); + if (!b) { + LFS3_EMUBD_TRACE("lfs3_emubd_erase -> %d", LFS3_ERR_NOMEM); + return LFS3_ERR_NOMEM; + } + bd->blocks[block] = b; + + // emulate an erase value? + if (bd->cfg->erase_value != -1) { + lfs3_emubd_memerase(cfg, b->data, cfg->block_size); + } + + // choose a new bad bit unless overridden + if (!(0x80000000 & b->bad_bit)) { + b->bad_bit = lfs3_emubd_prng_(&bd->prng) + % (cfg->block_size*8); + } + + // mark as metastable + b->metastable = true; + + // mirror to disk file? + if (bd->disk) { + off_t res1 = lseek(bd->disk->fd, + (off_t)block*cfg->block_size, + SEEK_SET); + if (res1 < 0) { + int err = -errno; + LFS3_EMUBD_TRACE("lfs3_emubd_erase -> %d", err); + return err; + } + + ssize_t res2 = write(bd->disk->fd, + b->data, cfg->block_size); + if (res2 < 0) { + int err = -errno; + LFS3_EMUBD_TRACE("lfs3_emubd_erase -> %d", err); + return err; + } + } + } + + // powerloss! + bd->cfg->powerloss_cb(bd->cfg->powerloss_data); + + // oh, continuing? undo out-of-order write emulation + if (bd->cfg->powerloss_behavior == LFS3_EMUBD_POWERLOSS_OOO) { + for (lfs3_block_t i = 0; i < cfg->block_count; i++) { + if (bd->blocks[i] != bd->ooo_after[i]) { + lfs3_emubd_decblock(bd->blocks[i]); + bd->blocks[i] = lfs3_emubd_incblock(bd->ooo_after[i]); + + // mirror to disk file? + if (bd->disk) { + off_t res1 = lseek(bd->disk->fd, + (off_t)i*cfg->block_size, + SEEK_SET); + if (res1 < 0) { + int err = -errno; + LFS3_EMUBD_TRACE("lfs3_emubd_erase -> %d", err); + return err; + } + + ssize_t res2 = write(bd->disk->fd, + (bd->blocks[i]) + ? bd->blocks[i]->data + : bd->disk->scratch, + cfg->block_size); + if (res2 < 0) { + int err = -errno; + LFS3_EMUBD_TRACE("lfs3_emubd_erase -> %d", err); + return err; + } + } + } + } + } + } + } + + // mutate the block + lfs3_emubd_block_t *b = lfs3_emubd_mutblock(cfg, bd->blocks[block]); + if (!b) { + LFS3_EMUBD_TRACE("lfs3_emubd_erase -> %d", LFS3_ERR_NOMEM); + return LFS3_ERR_NOMEM; + } + bd->blocks[block] = b; + + // keep track of wear + if (bd->cfg->erase_cycles && b->wear <= bd->cfg->erase_cycles) { + b->wear += 1; + } + + // block bad? + if (b->wear > bd->cfg->erase_cycles) { + // erroring erases? error + if (bd->cfg->badblock_behavior + == LFS3_EMUBD_BADBLOCK_ERASEERROR) { + LFS3_EMUBD_TRACE("lfs3_emubd_erase -> %d", LFS3_ERR_CORRUPT); + return LFS3_ERR_CORRUPT; + + // noop erases? skip + } else if (bd->cfg->badblock_behavior + == LFS3_EMUBD_BADBLOCK_ERASENOOP) { + goto erased; + + // flipping bits? if we're not manually overridden, choose a + // new bad bit on erase, this makes it more likely to + // eventually cause problems + } else { + if (!(0x80000000 & b->bad_bit)) { + b->bad_bit = lfs3_emubd_prng_(&bd->prng) + % (cfg->block_size*8); + } + } + } + + // emulate an erase value? + if (bd->cfg->erase_value != -1) { + lfs3_emubd_memerase(cfg, b->data, cfg->block_size); + + // mirror to disk file? + if (bd->disk) { + off_t res1 = lseek(bd->disk->fd, + (off_t)block*cfg->block_size, + SEEK_SET); + if (res1 < 0) { + int err = -errno; + LFS3_EMUBD_TRACE("lfs3_emubd_erase -> %d", err); + return err; + } + + ssize_t res2 = write(bd->disk->fd, b->data, cfg->block_size); + if (res2 < 0) { + int err = -errno; + LFS3_EMUBD_TRACE("lfs3_emubd_erase -> %d", err); + return err; + } + } + } + + // clear any metastability + b->metastable = false; + +erased:; + // track erases + bd->erased += cfg->block_size; + if (bd->cfg->erase_sleep) { + int err = nanosleep(&(struct timespec){ + .tv_sec=bd->cfg->erase_sleep/1000000000, + .tv_nsec=bd->cfg->erase_sleep%1000000000}, + NULL); + if (err) { + err = -errno; + LFS3_EMUBD_TRACE("lfs3_emubd_erase -> %d", err); + return err; + } + } + + LFS3_EMUBD_TRACE("lfs3_emubd_erase -> %d", 0); + return 0; +} + +int lfs3_emubd_sync(const struct lfs3_cfg *cfg) { + LFS3_EMUBD_TRACE("lfs3_emubd_sync(%p)", (void*)cfg); + lfs3_emubd_t *bd = cfg->context; + + // emulate out-of-order writes? save a snapshot on sync + if (bd->cfg->powerloss_behavior == LFS3_EMUBD_POWERLOSS_OOO) { + for (size_t i = 0; i < cfg->block_count; i++) { + lfs3_emubd_decblock(bd->ooo_before[i]); + bd->ooo_before[i] = lfs3_emubd_incblock(bd->blocks[i]); + } + } + + LFS3_EMUBD_TRACE("lfs3_emubd_sync -> %d", 0); + return 0; +} + + +/// Additional emubd features for testing /// + +void lfs3_emubd_seed(const struct lfs3_cfg *cfg, uint32_t seed) { + LFS3_EMUBD_TRACE("lfs3_emubd_seed(%p, 0x%08"PRIx32")", + (void*)cfg, seed); + lfs3_emubd_t *bd = cfg->context; + + bd->prng = seed; + + LFS3_EMUBD_TRACE("lfs3_emubd_seed -> _"); +} + +uint32_t lfs3_emubd_prng(const struct lfs3_cfg *cfg) { + LFS3_EMUBD_TRACE("lfs3_emubd_prng(%p)", (void*)cfg); + lfs3_emubd_t *bd = cfg->context; + + uint32_t x = lfs3_emubd_prng_(&bd->prng); + + LFS3_EMUBD_TRACE("lfs3_emubd_prng -> 0x%08"PRIx32, x); + return x; +} + +lfs3_emubd_sio_t lfs3_emubd_readed(const struct lfs3_cfg *cfg) { + LFS3_EMUBD_TRACE("lfs3_emubd_readed(%p)", (void*)cfg); + lfs3_emubd_t *bd = cfg->context; + LFS3_EMUBD_TRACE("lfs3_emubd_readed -> %"PRIu64, bd->readed); + return bd->readed; +} + +lfs3_emubd_sio_t lfs3_emubd_proged(const struct lfs3_cfg *cfg) { + LFS3_EMUBD_TRACE("lfs3_emubd_proged(%p)", (void*)cfg); + lfs3_emubd_t *bd = cfg->context; + LFS3_EMUBD_TRACE("lfs3_emubd_proged -> %"PRIu64, bd->proged); + return bd->proged; +} + +lfs3_emubd_sio_t lfs3_emubd_erased(const struct lfs3_cfg *cfg) { + LFS3_EMUBD_TRACE("lfs3_emubd_erased(%p)", (void*)cfg); + lfs3_emubd_t *bd = cfg->context; + LFS3_EMUBD_TRACE("lfs3_emubd_erased -> %"PRIu64, bd->erased); + return bd->erased; +} + +int lfs3_emubd_setreaded(const struct lfs3_cfg *cfg, + lfs3_emubd_io_t readed) { + LFS3_EMUBD_TRACE("lfs3_emubd_setreaded(%p, %"PRIu64")", (void*)cfg, readed); + lfs3_emubd_t *bd = cfg->context; + bd->readed = readed; + LFS3_EMUBD_TRACE("lfs3_emubd_setreaded -> %d", 0); + return 0; +} + +int lfs3_emubd_setproged(const struct lfs3_cfg *cfg, + lfs3_emubd_io_t proged) { + LFS3_EMUBD_TRACE("lfs3_emubd_setproged(%p, %"PRIu64")", (void*)cfg, proged); + lfs3_emubd_t *bd = cfg->context; + bd->proged = proged; + LFS3_EMUBD_TRACE("lfs3_emubd_setproged -> %d", 0); + return 0; +} + +int lfs3_emubd_seterased(const struct lfs3_cfg *cfg, + lfs3_emubd_io_t erased) { + LFS3_EMUBD_TRACE("lfs3_emubd_seterased(%p, %"PRIu64")", (void*)cfg, erased); + lfs3_emubd_t *bd = cfg->context; + bd->erased = erased; + LFS3_EMUBD_TRACE("lfs3_emubd_seterased -> %d", 0); + return 0; +} + +lfs3_emubd_swear_t lfs3_emubd_wear(const struct lfs3_cfg *cfg, + lfs3_block_t block) { + LFS3_EMUBD_TRACE("lfs3_emubd_wear(%p, %"PRIu32")", (void*)cfg, block); + lfs3_emubd_t *bd = cfg->context; + + // check if block is valid + LFS3_ASSERT(block < cfg->block_count); + + // get the wear + lfs3_emubd_wear_t wear; + const lfs3_emubd_block_t *b = bd->blocks[block]; + if (b) { + wear = b->wear; + } else { + wear = 0; + } + + LFS3_EMUBD_TRACE("lfs3_emubd_wear -> %"PRIi32, wear); + return wear; +} + +int lfs3_emubd_setwear(const struct lfs3_cfg *cfg, + lfs3_block_t block, lfs3_emubd_wear_t wear) { + LFS3_EMUBD_TRACE("lfs3_emubd_setwear(%p, %"PRIu32", %"PRIi32")", + (void*)cfg, block, wear); + lfs3_emubd_t *bd = cfg->context; + + // check if block is valid + LFS3_ASSERT(block < cfg->block_count); + + // mutate the block + lfs3_emubd_block_t *b = lfs3_emubd_mutblock(cfg, bd->blocks[block]); + if (!b) { + LFS3_EMUBD_TRACE("lfs3_emubd_setwear -> %d", LFS3_ERR_NOMEM); + return LFS3_ERR_NOMEM; + } + bd->blocks[block] = b; + + // set the wear + b->wear = wear; + + LFS3_EMUBD_TRACE("lfs3_emubd_setwear -> %d", 0); + return 0; +} + +int lfs3_emubd_markbad(const struct lfs3_cfg *cfg, + lfs3_block_t block) { + LFS3_EMUBD_TRACE("lfs3_emubd_markbad(%p, %"PRIu32")", + (void*)cfg, block); + lfs3_emubd_t *bd = cfg->context; + + // check if block is valid + LFS3_ASSERT(block < cfg->block_count); + + // mutate the block + lfs3_emubd_block_t *b = lfs3_emubd_mutblock(cfg, bd->blocks[block]); + if (!b) { + LFS3_EMUBD_TRACE("lfs3_emubd_markbad -> %d", LFS3_ERR_NOMEM); + return LFS3_ERR_NOMEM; + } + bd->blocks[block] = b; + + // set the wear + b->wear = -1; + + // choose a bad bit now in case this block is never erased + if (!(0x80000000 & b->bad_bit)) { + b->bad_bit = lfs3_emubd_prng_(&bd->prng) + % (cfg->block_size*8); + } + + LFS3_EMUBD_TRACE("lfs3_emubd_markbad -> %d", 0); + return 0; +} + +int lfs3_emubd_markgood(const struct lfs3_cfg *cfg, + lfs3_block_t block) { + LFS3_EMUBD_TRACE("lfs3_emubd_markgood(%p, %"PRIu32")", + (void*)cfg, block); + lfs3_emubd_t *bd = cfg->context; + + // check if block is valid + LFS3_ASSERT(block < cfg->block_count); + + // mutate the block + lfs3_emubd_block_t *b = lfs3_emubd_mutblock(cfg, bd->blocks[block]); + if (!b) { + LFS3_EMUBD_TRACE("lfs3_emubd_markgood -> %d", LFS3_ERR_NOMEM); + return LFS3_ERR_NOMEM; + } + bd->blocks[block] = b; + + // set the wear + b->wear = 0; + + LFS3_EMUBD_TRACE("lfs3_emubd_markgood -> %d", 0); + return 0; +} + +lfs3_ssize_t lfs3_emubd_badbit(const struct lfs3_cfg *cfg, + lfs3_block_t block) { + LFS3_EMUBD_TRACE("lfs3_emubd_badbit(%p, %"PRIu32")", (void*)cfg, block); + lfs3_emubd_t *bd = cfg->context; + + // check if block is valid + LFS3_ASSERT(block < cfg->block_count); + + // get the bad bit + lfs3_size_t bad_bit; + const lfs3_emubd_block_t *b = bd->blocks[block]; + if (b) { + bad_bit = 0x7fffffff & b->bad_bit; + } else { + bad_bit = 0; + } + + LFS3_EMUBD_TRACE("lfs3_emubd_badbit -> %"PRIi32, bad_bit); + return bad_bit; +} + +int lfs3_emubd_setbadbit(const struct lfs3_cfg *cfg, + lfs3_block_t block, lfs3_size_t bit) { + LFS3_EMUBD_TRACE("lfs3_emubd_setbadbit(%p, %"PRIu32", %"PRIu32")", + (void*)cfg, block, bit); + lfs3_emubd_t *bd = cfg->context; + + // check if block is valid + LFS3_ASSERT(block < cfg->block_count); + + // mutate the block + lfs3_emubd_block_t *b = lfs3_emubd_mutblock(cfg, bd->blocks[block]); + if (!b) { + LFS3_EMUBD_TRACE("lfs3_emubd_setbadbit -> %d", LFS3_ERR_NOMEM); + return LFS3_ERR_NOMEM; + } + bd->blocks[block] = b; + + // set the bad bit and mark as fixed + b->bad_bit = 0x80000000 | bit; + + LFS3_EMUBD_TRACE("lfs3_emubd_setbadbit -> %d", 0); + return 0; +} + +int lfs3_emubd_randomizebadbit(const struct lfs3_cfg *cfg, + lfs3_block_t block) { + LFS3_EMUBD_TRACE("lfs3_emubd_randomizebadbit(%p, %"PRIu32")", + (void*)cfg, block); + lfs3_emubd_t *bd = cfg->context; + + // check if block is valid + LFS3_ASSERT(block < cfg->block_count); + + // mutate the block + lfs3_emubd_block_t *b = lfs3_emubd_mutblock(cfg, bd->blocks[block]); + if (!b) { + LFS3_EMUBD_TRACE("lfs3_emubd_randomizebadbit -> %d", LFS3_ERR_NOMEM); + return LFS3_ERR_NOMEM; + } + bd->blocks[block] = b; + + // mark the bad bit as randomized + b->bad_bit &= ~0x80000000; + + LFS3_EMUBD_TRACE("lfs3_emubd_randomizebadbit -> %d", 0); + return 0; +} + +int lfs3_emubd_markbadbit(const struct lfs3_cfg *cfg, + lfs3_block_t block, lfs3_size_t bit) { + LFS3_EMUBD_TRACE("lfs3_emubd_markbadbit(%p, %"PRIu32", %"PRIu32")", + (void*)cfg, block, bit); + lfs3_emubd_t *bd = cfg->context; + + // check if block is valid + LFS3_ASSERT(block < cfg->block_count); + + // mutate the block + lfs3_emubd_block_t *b = lfs3_emubd_mutblock(cfg, bd->blocks[block]); + if (!b) { + LFS3_EMUBD_TRACE("lfs3_emubd_markbadbit -> %d", LFS3_ERR_NOMEM); + return LFS3_ERR_NOMEM; + } + bd->blocks[block] = b; + + // set the wear + b->wear = -1; + // set the bad bit and mark as fixed + b->bad_bit = 0x80000000 | bit; + + LFS3_EMUBD_TRACE("lfs3_emubd_markbadbit -> %d", 0); + return 0; +} + +int lfs3_emubd_flipbit_(const struct lfs3_cfg *cfg, + lfs3_block_t block, lfs3_size_t bit) { + lfs3_emubd_t *bd = cfg->context; + + // check if block is valid + LFS3_ASSERT(block < cfg->block_count); + + // mutate the block + lfs3_emubd_block_t *b = lfs3_emubd_mutblock(cfg, bd->blocks[block]); + if (!b) { + return LFS3_ERR_NOMEM; + } + bd->blocks[block] = b; + + // flip the bit + b->data[bit/8] ^= 1 << (bit%8); + + // mirror to disk file? + if (bd->disk) { + off_t res1 = lseek(bd->disk->fd, + (off_t)block*cfg->block_size + (off_t)(bit/8), + SEEK_SET); + if (res1 < 0) { + int err = -errno; + return err; + } + + ssize_t res2 = write(bd->disk->fd, &b->data[bit/8], 1); + if (res2 < 0) { + int err = -errno; + return err; + } + } + + return 0; +} + + +int lfs3_emubd_flipbit(const struct lfs3_cfg *cfg, + lfs3_block_t block, lfs3_size_t bit) { + LFS3_EMUBD_TRACE("lfs3_emubd_flipbit(%p, %"PRIu32", %"PRIu32")", + (void*)cfg, block, bit); + + // flip the bit + int err = lfs3_emubd_flipbit_(cfg, block, bit); + if (err) { + LFS3_EMUBD_TRACE("lfs3_emubd_flipbit -> %d", err); + return err; + } + + LFS3_EMUBD_TRACE("lfs3_emubd_flipbit -> %d", 0); + return 0; +} + +int lfs3_emubd_flip(const struct lfs3_cfg *cfg) { + LFS3_EMUBD_TRACE("lfs3_emubd_flip(%p)", (void*)cfg); + lfs3_emubd_t *bd = cfg->context; + + // flip all bits in bad blocks, make sure not to allocate blocks we + // don't need + for (lfs3_block_t i = 0; i < cfg->block_count; i++) { + const lfs3_emubd_block_t *b = bd->blocks[i]; + if (b && b->wear > bd->cfg->erase_cycles) { + int err = lfs3_emubd_flipbit_(cfg, i, b->bad_bit & 0x7fffffff); + if (err) { + LFS3_EMUBD_TRACE("lfs3_emubd_flip -> %d", err); + return err; + } + } + } + + LFS3_EMUBD_TRACE("lfs3_emubd_flip -> %d", 0); + return 0; +} + +lfs3_emubd_spowercycles_t lfs3_emubd_powercycles( + const struct lfs3_cfg *cfg) { + LFS3_EMUBD_TRACE("lfs3_emubd_powercycles(%p)", (void*)cfg); + lfs3_emubd_t *bd = cfg->context; + + LFS3_EMUBD_TRACE("lfs3_emubd_powercycles -> %"PRIi32, bd->power_cycles); + return bd->power_cycles; +} + +int lfs3_emubd_setpowercycles(const struct lfs3_cfg *cfg, + lfs3_emubd_powercycles_t power_cycles) { + LFS3_EMUBD_TRACE("lfs3_emubd_setpowercycles(%p, %"PRIi32")", + (void*)cfg, power_cycles); + lfs3_emubd_t *bd = cfg->context; + + bd->power_cycles = power_cycles; + + LFS3_EMUBD_TRACE("lfs3_emubd_powercycles -> %d", 0); + return 0; +} + +int lfs3_emubd_cpy(const struct lfs3_cfg *cfg, lfs3_emubd_t *copy) { + LFS3_EMUBD_TRACE("lfs3_emubd_cpy(%p, %p)", (void*)cfg, (void*)copy); + lfs3_emubd_t *bd = cfg->context; + + // lazily copy over our block array + copy->blocks = malloc( + cfg->block_count * sizeof(lfs3_emubd_block_t*)); + if (!copy->blocks) { + LFS3_EMUBD_TRACE("lfs3_emubd_cpy -> %d", LFS3_ERR_NOMEM); + return LFS3_ERR_NOMEM; + } + for (lfs3_block_t i = 0; i < cfg->block_count; i++) { + copy->blocks[i] = lfs3_emubd_incblock(bd->blocks[i]); + } + + if (bd->cfg->powerloss_behavior == LFS3_EMUBD_POWERLOSS_OOO) { + copy->ooo_before = malloc( + cfg->block_count * sizeof(lfs3_emubd_block_t*)); + if (!copy->ooo_before) { + LFS3_EMUBD_TRACE("lfs3_emubd_cpy -> %d", LFS3_ERR_NOMEM); + return LFS3_ERR_NOMEM; + } + for (lfs3_block_t i = 0; i < cfg->block_count; i++) { + copy->ooo_before[i] = lfs3_emubd_incblock(bd->ooo_before[i]); + } + + copy->ooo_after = malloc( + cfg->block_count * sizeof(lfs3_emubd_block_t*)); + if (!copy->ooo_after) { + LFS3_EMUBD_TRACE("lfs3_emubd_cpy -> %d", LFS3_ERR_NOMEM); + return LFS3_ERR_NOMEM; + } + for (lfs3_block_t i = 0; i < cfg->block_count; i++) { + copy->ooo_after[i] = lfs3_emubd_incblock(bd->ooo_after[i]); + } + } + + // other state + copy->readed = bd->readed; + copy->proged = bd->proged; + copy->erased = bd->erased; + copy->prng = bd->prng; + copy->power_cycles = bd->power_cycles; + copy->disk = bd->disk; + if (copy->disk) { + copy->disk->rc += 1; + } + copy->cfg = bd->cfg; + + LFS3_EMUBD_TRACE("lfs3_emubd_cpy -> %d", 0); + return 0; +} + diff --git a/bd/lfs3_emubd.h b/bd/lfs3_emubd.h new file mode 100644 index 000000000..f1b67bc7b --- /dev/null +++ b/bd/lfs3_emubd.h @@ -0,0 +1,267 @@ +/* + * emubd - High-level emulating block device with many bells and + * whistles for testing powerloss, wear, etc. + * + * Note emubd always backs the block device in RAM. Consider using + * kiwibd if you need a block device larger than the available RAM on + * the system. + * + * Copyright (c) 2022, The littlefs authors. + * Copyright (c) 2017, Arm Limited. All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + */ +#ifndef LFS3_EMUBD_H +#define LFS3_EMUBD_H + +#include "lfs3.h" +#include "lfs3_util.h" + + +// Block device specific tracing +#ifndef LFS3_EMUBD_TRACE +#ifdef LFS3_EMUBD_YES_TRACE +#define LFS3_EMUBD_TRACE(...) LFS3_TRACE(__VA_ARGS__) +#else +#define LFS3_EMUBD_TRACE(...) +#endif +#endif + +// Mode determining how "bad-blocks" behave during testing. This simulates +// some real-world circumstances such as progs not sticking (prog-noop), +// a readonly disk (erase-noop), ECC failures (read-error), and of course, +// random bit failures (prog-flip, read-flip) +typedef enum lfs3_emubd_badblock_behavior { + LFS3_EMUBD_BADBLOCK_PROGERROR = 0, // Error on prog + LFS3_EMUBD_BADBLOCK_ERASEERROR = 1, // Error on erase + LFS3_EMUBD_BADBLOCK_READERROR = 2, // Error on read + LFS3_EMUBD_BADBLOCK_PROGNOOP = 3, // Prog does nothing silently + LFS3_EMUBD_BADBLOCK_ERASENOOP = 4, // Erase does nothing silently + LFS3_EMUBD_BADBLOCK_PROGFLIP = 5, // Prog flips a bit + LFS3_EMUBD_BADBLOCK_READFLIP = 6, // Read flips a bit sometimes + LFS3_EMUBD_BADBLOCK_MANUAL = 7, // Bits require manual flipping +} lfs3_emubd_badblock_behavior_t; + +// Mode determining how power-loss behaves during testing. +typedef enum lfs3_emubd_powerloss_behavior { + LFS3_EMUBD_POWERLOSS_ATOMIC = 0, // Progs are atomic + LFS3_EMUBD_POWERLOSS_SOMEBITS = 1, // One bit is progged + LFS3_EMUBD_POWERLOSS_MOSTBITS = 2, // All-but-one bit is progged + LFS3_EMUBD_POWERLOSS_OOO = 3, // Blocks are written out-of-order + LFS3_EMUBD_POWERLOSS_METASTABLE = 4, // Reads may flip a bit +} lfs3_emubd_powerloss_behavior_t; + +// Type for measuring read/program/erase operations +typedef uint64_t lfs3_emubd_io_t; +typedef int64_t lfs3_emubd_sio_t; + +// Type for measuring wear +typedef uint32_t lfs3_emubd_wear_t; +typedef int32_t lfs3_emubd_swear_t; + +// Type for tracking power-cycles +typedef uint32_t lfs3_emubd_powercycles_t; +typedef int32_t lfs3_emubd_spowercycles_t; + +// Type for delays in nanoseconds +typedef uint64_t lfs3_emubd_sleep_t; +typedef int64_t lfs3_emubd_ssleep_t; + +// emubd config, this is required for testing +struct lfs3_emubd_cfg { + // 8-bit erase value to use for simulating erases. -1 simulates a noop + // erase, which is faster than simulating a fixed erase value. -2 emulates + // nor-masking, which is useful for testing other filesystems (littlefs + // does _not_ rely on this!). + int32_t erase_value; + + // Number of erase cycles before a block becomes "bad". The exact behavior + // of bad blocks is controlled by badblock_behavior. + uint32_t erase_cycles; + + // The mode determining how bad-blocks fail + lfs3_emubd_badblock_behavior_t badblock_behavior; + + // Number of write operations (erase/prog) before triggering a power-loss. + // power_cycles=0 disables this. The exact behavior of power-loss is + // controlled by a combination of powerloss_behavior and powerloss_cb. + lfs3_emubd_powercycles_t power_cycles; + + // The mode determining how power-loss affects disk + lfs3_emubd_powerloss_behavior_t powerloss_behavior; + + // Function to call to emulate power-loss. The exact behavior of power-loss + // is up to the runner to provide. + void (*powerloss_cb)(void*); + + // Data for power-loss callback + void *powerloss_data; + + // Seed for prng, which may be used for emulating failed progs. This does + // not affect normal operation. + uint32_t seed; + + // Artificial delay in nanoseconds, there is no purpose for this other + // than slowing down the simulation. + lfs3_emubd_sleep_t read_sleep; + + // Artificial delay in nanoseconds, there is no purpose for this other + // than slowing down the simulation. + lfs3_emubd_sleep_t prog_sleep; + + // Artificial delay in nanoseconds, there is no purpose for this other + // than slowing down the simulation. + lfs3_emubd_sleep_t erase_sleep; +}; + +// A reference counted block +typedef struct lfs3_emubd_block { + uint32_t rc; + lfs3_emubd_wear_t wear; + bool metastable; + // sign(bad_bit)=0 => randomized on erase + // sign(bad_bit)=1 => fixed + lfs3_size_t bad_bit; + + uint8_t data[]; +} lfs3_emubd_block_t; + +// Disk mirror +typedef struct lfs3_emubd_disk { + uint32_t rc; + int fd; + uint8_t *scratch; +} lfs3_emubd_disk_t; + +// emubd state +typedef struct lfs3_emubd { + // array of copy-on-write blocks + lfs3_emubd_block_t **blocks; + + // some other test state + lfs3_emubd_io_t readed; + lfs3_emubd_io_t proged; + lfs3_emubd_io_t erased; + uint32_t prng; + lfs3_emubd_powercycles_t power_cycles; + lfs3_emubd_block_t **ooo_before; + lfs3_emubd_block_t **ooo_after; + lfs3_emubd_disk_t *disk; + + const struct lfs3_emubd_cfg *cfg; +} lfs3_emubd_t; + + +/// Block device API /// + +// Create an emulating block device using the geometry in lfs3_cfg +// +// If path is provided, emubd will mirror the block device in the file. +// This provides a way to view the current state of the block device, +// but does not eliminate the RAM requirement. +// +int lfs3_emubd_create(const struct lfs3_cfg *cfg, const char *path); +int lfs3_emubd_createcfg(const struct lfs3_cfg *cfg, const char *path, + const struct lfs3_emubd_cfg *bdcfg); + +// Clean up memory associated with block device +int lfs3_emubd_destroy(const struct lfs3_cfg *cfg); + +// Read a block +int lfs3_emubd_read(const struct lfs3_cfg *cfg, lfs3_block_t block, + lfs3_off_t off, void *buffer, lfs3_size_t size); + +// Program a block +// +// The block must have previously been erased. +int lfs3_emubd_prog(const struct lfs3_cfg *cfg, lfs3_block_t block, + lfs3_off_t off, const void *buffer, lfs3_size_t size); + +// Erase a block +// +// A block must be erased before being programmed. The +// state of an erased block is undefined. +int lfs3_emubd_erase(const struct lfs3_cfg *cfg, lfs3_block_t block); + +// Sync the block device +int lfs3_emubd_sync(const struct lfs3_cfg *cfg); + + +/// Additional emubd features for testing /// + +// Set the current prng state +void lfs3_emubd_seed(const struct lfs3_cfg *cfg, uint32_t seed); + +// Get a pseudo-random number from emubd's internal prng +uint32_t lfs3_emubd_prng(const struct lfs3_cfg *cfg); + +// Get total amount of bytes read +lfs3_emubd_sio_t lfs3_emubd_readed(const struct lfs3_cfg *cfg); + +// Get total amount of bytes programmed +lfs3_emubd_sio_t lfs3_emubd_proged(const struct lfs3_cfg *cfg); + +// Get total amount of bytes erased +lfs3_emubd_sio_t lfs3_emubd_erased(const struct lfs3_cfg *cfg); + +// Manually set amount of bytes read +int lfs3_emubd_setreaded(const struct lfs3_cfg *cfg, + lfs3_emubd_io_t readed); + +// Manually set amount of bytes programmed +int lfs3_emubd_setproged(const struct lfs3_cfg *cfg, + lfs3_emubd_io_t proged); + +// Manually set amount of bytes erased +int lfs3_emubd_seterased(const struct lfs3_cfg *cfg, + lfs3_emubd_io_t erased); + +// Get simulated wear on a given block +lfs3_emubd_swear_t lfs3_emubd_wear(const struct lfs3_cfg *cfg, + lfs3_block_t block); + +// Manually set simulated wear on a given block +int lfs3_emubd_setwear(const struct lfs3_cfg *cfg, + lfs3_block_t block, lfs3_emubd_wear_t wear); + +// Mark a block as bad, this is equivalent to setting wear to maximum +int lfs3_emubd_markbad(const struct lfs3_cfg *cfg, lfs3_block_t block); + +// Clear any simulated wear on a given block +int lfs3_emubd_markgood(const struct lfs3_cfg *cfg, lfs3_block_t block); + +// Get which bit failed, this changes on erase/power-loss unless manually set +lfs3_ssize_t lfs3_emubd_badbit(const struct lfs3_cfg *cfg, + lfs3_block_t block); + +// Set which bit should fail in a given block +int lfs3_emubd_setbadbit(const struct lfs3_cfg *cfg, + lfs3_block_t block, lfs3_size_t bit); + +// Randomize the bad bit on erase (the default) +int lfs3_emubd_randomizebadbit(const struct lfs3_cfg *cfg, + lfs3_block_t block); + +// Mark a block as bad and which bit should fail +int lfs3_emubd_markbadbit(const struct lfs3_cfg *cfg, + lfs3_block_t block, lfs3_size_t bit); + +// Flip a bit in a given block, intended for emulating bit errors +int lfs3_emubd_flipbit(const struct lfs3_cfg *cfg, + lfs3_block_t block, lfs3_size_t bit); + +// Flip all bits marked as bad +int lfs3_emubd_flip(const struct lfs3_cfg *cfg); + +// Get the remaining power-cycles +lfs3_emubd_spowercycles_t lfs3_emubd_powercycles( + const struct lfs3_cfg *cfg); + +// Manually set the remaining power-cycles +int lfs3_emubd_setpowercycles(const struct lfs3_cfg *cfg, + lfs3_emubd_powercycles_t power_cycles); + +// Create a copy-on-write copy of the state of this block device +int lfs3_emubd_cpy(const struct lfs3_cfg *cfg, lfs3_emubd_t *copy); + + +#endif diff --git a/bd/lfs_filebd.c b/bd/lfs3_filebd.c similarity index 54% rename from bd/lfs_filebd.c rename to bd/lfs3_filebd.c index 780c8f902..5caefbd08 100644 --- a/bd/lfs_filebd.c +++ b/bd/lfs3_filebd.c @@ -5,7 +5,7 @@ * Copyright (c) 2017, Arm Limited. All rights reserved. * SPDX-License-Identifier: BSD-3-Clause */ -#include "bd/lfs_filebd.h" +#include "bd/lfs3_filebd.h" #include #include @@ -15,8 +15,8 @@ #include #endif -int lfs_filebd_create(const struct lfs_config *cfg, const char *path) { - LFS_FILEBD_TRACE("lfs_filebd_create(%p {.context=%p, " +int lfs3_filebd_create(const struct lfs3_cfg *cfg, const char *path) { + LFS3_FILEBD_TRACE("lfs3_filebd_create(%p {.context=%p, " ".read=%p, .prog=%p, .erase=%p, .sync=%p, " ".read_size=%"PRIu32", .prog_size=%"PRIu32", " ".block_size=%"PRIu32", .block_count=%"PRIu32"}, " @@ -26,7 +26,7 @@ int lfs_filebd_create(const struct lfs_config *cfg, const char *path) { (void*)(uintptr_t)cfg->erase, (void*)(uintptr_t)cfg->sync, cfg->read_size, cfg->prog_size, cfg->block_size, cfg->block_count, path, (void*)bdcfg, bdcfg->erase_value); - lfs_filebd_t *bd = cfg->context; + lfs3_filebd_t *bd = cfg->context; // open file #ifdef _WIN32 @@ -37,39 +37,39 @@ int lfs_filebd_create(const struct lfs_config *cfg, const char *path) { if (bd->fd < 0) { int err = -errno; - LFS_FILEBD_TRACE("lfs_filebd_create -> %d", err); + LFS3_FILEBD_TRACE("lfs3_filebd_create -> %d", err); return err; } - LFS_FILEBD_TRACE("lfs_filebd_create -> %d", 0); + LFS3_FILEBD_TRACE("lfs3_filebd_create -> %d", 0); return 0; } -int lfs_filebd_destroy(const struct lfs_config *cfg) { - LFS_FILEBD_TRACE("lfs_filebd_destroy(%p)", (void*)cfg); - lfs_filebd_t *bd = cfg->context; +int lfs3_filebd_destroy(const struct lfs3_cfg *cfg) { + LFS3_FILEBD_TRACE("lfs3_filebd_destroy(%p)", (void*)cfg); + lfs3_filebd_t *bd = cfg->context; int err = close(bd->fd); if (err < 0) { err = -errno; - LFS_FILEBD_TRACE("lfs_filebd_destroy -> %d", err); + LFS3_FILEBD_TRACE("lfs3_filebd_destroy -> %d", err); return err; } - LFS_FILEBD_TRACE("lfs_filebd_destroy -> %d", 0); + LFS3_FILEBD_TRACE("lfs3_filebd_destroy -> %d", 0); return 0; } -int lfs_filebd_read(const struct lfs_config *cfg, lfs_block_t block, - lfs_off_t off, void *buffer, lfs_size_t size) { - LFS_FILEBD_TRACE("lfs_filebd_read(%p, " +int lfs3_filebd_read(const struct lfs3_cfg *cfg, lfs3_block_t block, + lfs3_off_t off, void *buffer, lfs3_size_t size) { + LFS3_FILEBD_TRACE("lfs3_filebd_read(%p, " "0x%"PRIx32", %"PRIu32", %p, %"PRIu32")", (void*)cfg, block, off, buffer, size); - lfs_filebd_t *bd = cfg->context; + lfs3_filebd_t *bd = cfg->context; // check if read is valid - LFS_ASSERT(block < cfg->block_count); - LFS_ASSERT(off % cfg->read_size == 0); - LFS_ASSERT(size % cfg->read_size == 0); - LFS_ASSERT(off+size <= cfg->block_size); + LFS3_ASSERT(block < cfg->block_count); + LFS3_ASSERT(off % cfg->read_size == 0); + LFS3_ASSERT(size % cfg->read_size == 0); + LFS3_ASSERT(off+size <= cfg->block_size); // zero for reproducibility (in case file is truncated) memset(buffer, 0, size); @@ -79,73 +79,73 @@ int lfs_filebd_read(const struct lfs_config *cfg, lfs_block_t block, (off_t)block*cfg->block_size + (off_t)off, SEEK_SET); if (res1 < 0) { int err = -errno; - LFS_FILEBD_TRACE("lfs_filebd_read -> %d", err); + LFS3_FILEBD_TRACE("lfs3_filebd_read -> %d", err); return err; } ssize_t res2 = read(bd->fd, buffer, size); if (res2 < 0) { int err = -errno; - LFS_FILEBD_TRACE("lfs_filebd_read -> %d", err); + LFS3_FILEBD_TRACE("lfs3_filebd_read -> %d", err); return err; } - LFS_FILEBD_TRACE("lfs_filebd_read -> %d", 0); + LFS3_FILEBD_TRACE("lfs3_filebd_read -> %d", 0); return 0; } -int lfs_filebd_prog(const struct lfs_config *cfg, lfs_block_t block, - lfs_off_t off, const void *buffer, lfs_size_t size) { - LFS_FILEBD_TRACE("lfs_filebd_prog(%p, " +int lfs3_filebd_prog(const struct lfs3_cfg *cfg, lfs3_block_t block, + lfs3_off_t off, const void *buffer, lfs3_size_t size) { + LFS3_FILEBD_TRACE("lfs3_filebd_prog(%p, " "0x%"PRIx32", %"PRIu32", %p, %"PRIu32")", (void*)cfg, block, off, buffer, size); - lfs_filebd_t *bd = cfg->context; + lfs3_filebd_t *bd = cfg->context; // check if write is valid - LFS_ASSERT(block < cfg->block_count); - LFS_ASSERT(off % cfg->prog_size == 0); - LFS_ASSERT(size % cfg->prog_size == 0); - LFS_ASSERT(off+size <= cfg->block_size); + LFS3_ASSERT(block < cfg->block_count); + LFS3_ASSERT(off % cfg->prog_size == 0); + LFS3_ASSERT(size % cfg->prog_size == 0); + LFS3_ASSERT(off+size <= cfg->block_size); // program data off_t res1 = lseek(bd->fd, (off_t)block*cfg->block_size + (off_t)off, SEEK_SET); if (res1 < 0) { int err = -errno; - LFS_FILEBD_TRACE("lfs_filebd_prog -> %d", err); + LFS3_FILEBD_TRACE("lfs3_filebd_prog -> %d", err); return err; } ssize_t res2 = write(bd->fd, buffer, size); if (res2 < 0) { int err = -errno; - LFS_FILEBD_TRACE("lfs_filebd_prog -> %d", err); + LFS3_FILEBD_TRACE("lfs3_filebd_prog -> %d", err); return err; } - LFS_FILEBD_TRACE("lfs_filebd_prog -> %d", 0); + LFS3_FILEBD_TRACE("lfs3_filebd_prog -> %d", 0); return 0; } -int lfs_filebd_erase(const struct lfs_config *cfg, lfs_block_t block) { - LFS_FILEBD_TRACE("lfs_filebd_erase(%p, 0x%"PRIx32" (%"PRIu32"))", +int lfs3_filebd_erase(const struct lfs3_cfg *cfg, lfs3_block_t block) { + LFS3_FILEBD_TRACE("lfs3_filebd_erase(%p, 0x%"PRIx32" (%"PRIu32"))", (void*)cfg, block, cfg->block_size); // check if erase is valid - LFS_ASSERT(block < cfg->block_count); + LFS3_ASSERT(block < cfg->block_count); // erase is a noop (void)block; - LFS_FILEBD_TRACE("lfs_filebd_erase -> %d", 0); + LFS3_FILEBD_TRACE("lfs3_filebd_erase -> %d", 0); return 0; } -int lfs_filebd_sync(const struct lfs_config *cfg) { - LFS_FILEBD_TRACE("lfs_filebd_sync(%p)", (void*)cfg); +int lfs3_filebd_sync(const struct lfs3_cfg *cfg) { + LFS3_FILEBD_TRACE("lfs3_filebd_sync(%p)", (void*)cfg); // file sync - lfs_filebd_t *bd = cfg->context; + lfs3_filebd_t *bd = cfg->context; #ifdef _WIN32 int err = FlushFileBuffers((HANDLE) _get_osfhandle(bd->fd)) ? 0 : -1; #else @@ -153,10 +153,10 @@ int lfs_filebd_sync(const struct lfs_config *cfg) { #endif if (err) { err = -errno; - LFS_FILEBD_TRACE("lfs_filebd_sync -> %d", 0); + LFS3_FILEBD_TRACE("lfs3_filebd_sync -> %d", 0); return err; } - LFS_FILEBD_TRACE("lfs_filebd_sync -> %d", 0); + LFS3_FILEBD_TRACE("lfs3_filebd_sync -> %d", 0); return 0; } diff --git a/bd/lfs3_filebd.h b/bd/lfs3_filebd.h new file mode 100644 index 000000000..0d24b5081 --- /dev/null +++ b/bd/lfs3_filebd.h @@ -0,0 +1,56 @@ +/* + * Block device emulated in a file + * + * Copyright (c) 2022, The littlefs authors. + * Copyright (c) 2017, Arm Limited. All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + */ +#ifndef LFS3_FILEBD_H +#define LFS3_FILEBD_H + +#include "lfs3.h" +#include "lfs3_util.h" + + +// Block device specific tracing +#ifndef LFS3_FILEBD_TRACE +#ifdef LFS3_FILEBD_YES_TRACE +#define LFS3_FILEBD_TRACE(...) LFS3_TRACE(__VA_ARGS__) +#else +#define LFS3_FILEBD_TRACE(...) +#endif +#endif + +// filebd state +typedef struct lfs3_filebd { + int fd; +} lfs3_filebd_t; + + +// Create a file block device using the geometry in lfs3_cfg +int lfs3_filebd_create(const struct lfs3_cfg *cfg, const char *path); + +// Clean up memory associated with block device +int lfs3_filebd_destroy(const struct lfs3_cfg *cfg); + +// Read a block +int lfs3_filebd_read(const struct lfs3_cfg *cfg, lfs3_block_t block, + lfs3_off_t off, void *buffer, lfs3_size_t size); + +// Program a block +// +// The block must have previously been erased. +int lfs3_filebd_prog(const struct lfs3_cfg *cfg, lfs3_block_t block, + lfs3_off_t off, const void *buffer, lfs3_size_t size); + +// Erase a block +// +// A block must be erased before being programmed. The +// state of an erased block is undefined. +int lfs3_filebd_erase(const struct lfs3_cfg *cfg, lfs3_block_t block); + +// Sync the block device +int lfs3_filebd_sync(const struct lfs3_cfg *cfg); + + +#endif diff --git a/bd/lfs3_kiwibd.c b/bd/lfs3_kiwibd.c new file mode 100644 index 000000000..53dec9bed --- /dev/null +++ b/bd/lfs3_kiwibd.c @@ -0,0 +1,554 @@ +/* + * kiwibd - A lightweight variant of emubd, useful for emulating large + * disks backed by a file or in RAM. + * + * Unlike emubd, file-backed disks are _not_ mirrored in RAM. kiwibd has + * fewer features than emubd, prioritizing speed for benchmarking. + * + * + */ + +#ifndef _POSIX_C_SOURCE +#define _POSIX_C_SOURCE 199309L +#endif + +#include "bd/lfs3_kiwibd.h" + +#include +#include +#include +#include +#include + + + +// low-level flash memory emulation + +// read data +static inline void lfs3_kiwibd_memread(const struct lfs3_cfg *cfg, + void *restrict dst, const void *restrict src, size_t size) { + (void)cfg; + memcpy(dst, src, size); +} + +static inline void lfs3_kiwibd_memprog(const struct lfs3_cfg *cfg, + void *restrict dst, const void *restrict src, size_t size) { + lfs3_kiwibd_t *bd = cfg->context; + // emulating nor-masking? + if (bd->cfg->erase_value == -2) { + uint8_t *dst_ = dst; + const uint8_t *src_ = src; + for (size_t i = 0; i < size; i++) { + dst_[i] &= src_[i]; + } + } else { + memcpy(dst, src, size); + } +} + +static inline void lfs3_kiwibd_memerase(const struct lfs3_cfg *cfg, + void *restrict dst, size_t size) { + lfs3_kiwibd_t *bd = cfg->context; + // emulating erase value? + if (bd->cfg->erase_value != -1) { + memset(dst, + (bd->cfg->erase_value >= 0) + ? bd->cfg->erase_value + : 0xff, + size); + } +} + +// this is slightly different from lfs3_kiwibd_memerase in that we use +// lfs3_kiwibd_memzero when we need to unconditionally zero memory +static inline void lfs3_kiwibd_memzero(const struct lfs3_cfg *cfg, + void *restrict dst, size_t size) { + lfs3_kiwibd_t *bd = cfg->context; + memset(dst, + (bd->cfg->erase_value == -1) ? 0 + : (bd->cfg->erase_value >= 0) ? bd->cfg->erase_value + : (bd->cfg->erase_value == -2) ? 0xff + : 0, + size); +} + + + +// kiwibd create/destroy + +int lfs3_kiwibd_createcfg(const struct lfs3_cfg *cfg, const char *path, + const struct lfs3_kiwibd_cfg *bdcfg) { + LFS3_KIWIBD_TRACE("lfs3_kiwibd_createcfg(" + "%p {" + ".context=%p, " + ".read=%p, " + ".prog=%p, " + ".erase=%p, " + ".sync=%p, " + ".read_size=%"PRIu32", " + ".prog_size=%"PRIu32", " + ".block_size=%"PRIu32", " + ".block_count=%"PRIu32"}, " + "\"%s\", " + "%p {" + ".erase_value=%"PRId32", " + ".buffer=%p, " + ".read_sleep=%"PRIu64", " + ".prog_sleep=%"PRIu64", " + ".erase_sleep=%"PRIu64"})", + (void*)cfg, + cfg->context, + (void*)(uintptr_t)cfg->read, + (void*)(uintptr_t)cfg->prog, + (void*)(uintptr_t)cfg->erase, + (void*)(uintptr_t)cfg->sync, + cfg->read_size, + cfg->prog_size, + cfg->block_size, + cfg->block_count, + path, + (void*)bdcfg, + bdcfg->erase_value, + bdcfg->buffer, + bdcfg->read_sleep, + bdcfg->prog_sleep, + bdcfg->erase_sleep); + lfs3_kiwibd_t *bd = cfg->context; + bd->cfg = bdcfg; + + // setup some initial state + bd->readed = 0; + bd->proged = 0; + bd->erased = 0; + bd->fd = -1; + if (path) { + bd->u.scratch = NULL; + } else { + bd->u.mem = NULL; + } + int err; + + // if we have a path, try to open the backing file + if (path) { + bd->fd = open(path, O_RDWR | O_CREAT, 0666); + if (bd->fd < 0) { + err = -errno; + goto failed; + } + + // allocate a scratch buffer to help with zeroing/masking/etc + bd->u.scratch = malloc(cfg->block_size); + if (!bd->u.scratch) { + err = LFS3_ERR_NOMEM; + goto failed; + } + + // zero for reproducibility + lfs3_kiwibd_memzero(cfg, bd->u.scratch, cfg->block_size); + for (lfs3_block_t i = 0; i < cfg->block_count; i++) { + ssize_t res = write(bd->fd, + bd->u.scratch, + cfg->block_size); + if (res < 0) { + err = -errno; + goto failed; + } + } + + // otherwise, try to malloc a big memory array + } else { + bd->u.mem = malloc((size_t)cfg->block_size * cfg->block_count); + if (!bd->u.mem) { + err = LFS3_ERR_NOMEM; + goto failed; + } + + // zero for reproducibility + lfs3_kiwibd_memzero(cfg, bd->u.mem, + (size_t)cfg->block_size * cfg->block_count); + } + + LFS3_KIWIBD_TRACE("lfs3_kiwibd_createcfg -> %d", 0); + return 0; + +failed:; + LFS3_KIWIBD_TRACE("lfs3_kiwibd_createcfg -> %d", err); + // clean up memory + if (bd->fd >= 0) { + close(bd->fd); + free(bd->u.scratch); + } else { + free(bd->u.mem); + } + return err; +} + +int lfs3_kiwibd_create(const struct lfs3_cfg *cfg, const char *path) { + LFS3_KIWIBD_TRACE("lfs3_kiwibd_create(" + "%p {" + ".context=%p, " + ".read=%p, " + ".prog=%p, " + ".erase=%p, " + ".sync=%p, " + ".read_size=%"PRIu32", " + ".prog_size=%"PRIu32", " + ".block_size=%"PRIu32", " + ".block_count=%"PRIu32"}, " + "\"%s\")", + (void*)cfg, + cfg->context, + (void*)(uintptr_t)cfg->read, + (void*)(uintptr_t)cfg->prog, + (void*)(uintptr_t)cfg->erase, + (void*)(uintptr_t)cfg->sync, + cfg->read_size, + cfg->prog_size, + cfg->block_size, + cfg->block_count, + path); + static const struct lfs3_kiwibd_cfg defaults = {.erase_value=-1}; + int err = lfs3_kiwibd_createcfg(cfg, path, &defaults); + LFS3_KIWIBD_TRACE("lfs3_kiwibd_create -> %d", err); + return err; +} + +int lfs3_kiwibd_destroy(const struct lfs3_cfg *cfg) { + LFS3_KIWIBD_TRACE("lfs3_kiwibd_destroy(%p)", (void*)cfg); + lfs3_kiwibd_t *bd = cfg->context; + + // clean up memory + if (bd->fd >= 0) { + close(bd->fd); + free(bd->u.scratch); + } else { + free(bd->u.mem); + } + + LFS3_KIWIBD_TRACE("lfs3_kiwibd_destroy -> %d", 0); + return 0; +} + + +// block device API + +int lfs3_kiwibd_read(const struct lfs3_cfg *cfg, lfs3_block_t block, + lfs3_off_t off, void *buffer, lfs3_size_t size) { + LFS3_KIWIBD_TRACE("lfs3_kiwibd_read(%p, " + "0x%"PRIx32", %"PRIu32", %p, %"PRIu32")", + (void*)cfg, block, off, buffer, size); + lfs3_kiwibd_t *bd = cfg->context; + + // check if read is valid + LFS3_ASSERT(block < cfg->block_count); + LFS3_ASSERT(off % cfg->read_size == 0); + LFS3_ASSERT(size % cfg->read_size == 0); + LFS3_ASSERT(off+size <= cfg->block_size); + + // read in file? + if (bd->fd >= 0) { + lfs3_kiwibd_memerase(cfg, + bd->u.scratch, + cfg->block_size); + + off_t res = lseek(bd->fd, + (off_t)block*cfg->block_size + (off_t)off, + SEEK_SET); + if (res < 0) { + int err = -errno; + LFS3_KIWIBD_TRACE("lfs3_kiwibd_read -> %d", err); + return err; + } + + ssize_t res_ = read(bd->fd, buffer, size); + if (res_ < 0) { + int err = -errno; + LFS3_KIWIBD_TRACE("lfs3_kiwibd_read -> %d", err); + return err; + } + + // read in RAM? + } else { + lfs3_kiwibd_memread(cfg, + buffer, + &bd->u.mem[(size_t)block*cfg->block_size + (size_t)off], + size); + } + + // track reads + bd->readed += size; + if (bd->cfg->read_sleep) { + int err = nanosleep(&(struct timespec){ + .tv_sec=bd->cfg->read_sleep/1000000000, + .tv_nsec=bd->cfg->read_sleep%1000000000}, + NULL); + if (err) { + err = -errno; + LFS3_KIWIBD_TRACE("lfs3_kiwibd_read -> %d", err); + return err; + } + } + + LFS3_KIWIBD_TRACE("lfs3_kiwibd_read -> %d", 0); + return 0; +} + +int lfs3_kiwibd_prog(const struct lfs3_cfg *cfg, lfs3_block_t block, + lfs3_off_t off, const void *buffer, lfs3_size_t size) { + LFS3_KIWIBD_TRACE("lfs3_kiwibd_prog(%p, " + "0x%"PRIx32", %"PRIu32", %p, %"PRIu32")", + (void*)cfg, block, off, buffer, size); + lfs3_kiwibd_t *bd = cfg->context; + + // check if write is valid + LFS3_ASSERT(block < cfg->block_count); + LFS3_ASSERT(off % cfg->prog_size == 0); + LFS3_ASSERT(size % cfg->prog_size == 0); + LFS3_ASSERT(off+size <= cfg->block_size); + + // prog in file? + if (bd->fd >= 0) { + // were we erased properly? + if (bd->cfg->erase_value >= 0) { + off_t res = lseek(bd->fd, + (off_t)block*cfg->block_size + (off_t)off, + SEEK_SET); + if (res < 0) { + int err = -errno; + LFS3_KIWIBD_TRACE("lfs3_kiwibd_prog -> %d", err); + return err; + } + + ssize_t res_ = read(bd->fd, bd->u.scratch, size); + if (res_ < 0) { + int err = -errno; + LFS3_KIWIBD_TRACE("lfs3_kiwibd_prog -> %d", err); + return err; + } + + for (lfs3_off_t i = 0; i < size; i++) { + LFS3_ASSERT(bd->u.scratch[i] == bd->cfg->erase_value); + } + } + + // masking progs? + if (bd->cfg->erase_value == -2) { + off_t res = lseek(bd->fd, + (off_t)block*cfg->block_size + (off_t)off, + SEEK_SET); + if (res < 0) { + int err = -errno; + LFS3_KIWIBD_TRACE("lfs3_kiwibd_prog -> %d", err); + return err; + } + + ssize_t res_ = read(bd->fd, bd->u.scratch, size); + if (res_ < 0) { + int err = -errno; + LFS3_KIWIBD_TRACE("lfs3_kiwibd_prog -> %d", err); + return err; + } + + lfs3_kiwibd_memprog(cfg, bd->u.scratch, buffer, size); + + res = lseek(bd->fd, + (off_t)block*cfg->block_size + (off_t)off, + SEEK_SET); + if (res < 0) { + int err = -errno; + LFS3_KIWIBD_TRACE("lfs3_kiwibd_prog -> %d", err); + return err; + } + + res_ = write(bd->fd, bd->u.scratch, size); + if (res_ < 0) { + int err = -errno; + LFS3_KIWIBD_TRACE("lfs3_kiwibd_prog -> %d", err); + return err; + } + + // normal progs? + } else { + off_t res = lseek(bd->fd, + (off_t)block*cfg->block_size + (off_t)off, + SEEK_SET); + if (res < 0) { + int err = -errno; + LFS3_KIWIBD_TRACE("lfs3_kiwibd_prog -> %d", err); + return err; + } + + ssize_t res_ = write(bd->fd, buffer, size); + if (res_ < 0) { + int err = -errno; + LFS3_KIWIBD_TRACE("lfs3_kiwibd_prog -> %d", err); + return err; + } + } + + // prog in RAM? + } else { + // were we erased properly? + if (bd->cfg->erase_value >= 0) { + for (lfs3_off_t i = 0; i < size; i++) { + LFS3_ASSERT( + bd->u.mem[(size_t)block*cfg->block_size + (size_t)off] + == bd->cfg->erase_value); + } + } + + lfs3_kiwibd_memprog(cfg, + &bd->u.mem[(size_t)block*cfg->block_size + (size_t)off], + buffer, + size); + } + + // track progs + bd->proged += size; + if (bd->cfg->prog_sleep) { + int err = nanosleep(&(struct timespec){ + .tv_sec=bd->cfg->prog_sleep/1000000000, + .tv_nsec=bd->cfg->prog_sleep%1000000000}, + NULL); + if (err) { + err = -errno; + LFS3_KIWIBD_TRACE("lfs3_kiwibd_prog -> %d", err); + return err; + } + } + + LFS3_KIWIBD_TRACE("lfs3_kiwibd_prog -> %d", 0); + return 0; +} + +int lfs3_kiwibd_erase(const struct lfs3_cfg *cfg, lfs3_block_t block) { + LFS3_KIWIBD_TRACE("lfs3_kiwibd_erase(%p, 0x%"PRIx32" (%"PRIu32"))", + (void*)cfg, block, cfg->block_size); + lfs3_kiwibd_t *bd = cfg->context; + + // check if erase is valid + LFS3_ASSERT(block < cfg->block_count); + + // emulate an erase value? + if (bd->cfg->erase_value != -1) { + // erase in file? + if (bd->fd >= 0) { + off_t res = lseek(bd->fd, + (off_t)block*cfg->block_size, + SEEK_SET); + if (res < 0) { + int err = -errno; + LFS3_KIWIBD_TRACE("lfs3_kiwibd_erase -> %d", err); + return err; + } + + lfs3_kiwibd_memerase(cfg, + bd->u.scratch, + cfg->block_size); + + ssize_t res_ = write(bd->fd, + bd->u.scratch, + cfg->block_size); + if (res_ < 0) { + int err = -errno; + LFS3_KIWIBD_TRACE("lfs3_kiwibd_erase -> %d", err); + return err; + } + + // erase in RAM? + } else { + lfs3_kiwibd_memerase(cfg, + &bd->u.mem[(size_t)block*cfg->block_size], + cfg->block_size); + } + } + +erased:; + // track erases + bd->erased += cfg->block_size; + if (bd->cfg->erase_sleep) { + int err = nanosleep(&(struct timespec){ + .tv_sec=bd->cfg->erase_sleep/1000000000, + .tv_nsec=bd->cfg->erase_sleep%1000000000}, + NULL); + if (err) { + err = -errno; + LFS3_KIWIBD_TRACE("lfs3_kiwibd_erase -> %d", err); + return err; + } + } + + LFS3_KIWIBD_TRACE("lfs3_kiwibd_erase -> %d", 0); + return 0; +} + +int lfs3_kiwibd_sync(const struct lfs3_cfg *cfg) { + LFS3_KIWIBD_TRACE("lfs3_kiwibd_sync(%p)", (void*)cfg); + + // in theory we could actually sync here, but if our goal is + // performance, why bother? + // + // filebd may be a better block device is your goal is actual + // storage + + // sync is a noop + (void)cfg; + + LFS3_KIWIBD_TRACE("lfs3_kiwibd_sync -> %d", 0); + return 0; +} + + +/// Additional kiwibd features /// + +lfs3_kiwibd_sio_t lfs3_kiwibd_readed(const struct lfs3_cfg *cfg) { + LFS3_KIWIBD_TRACE("lfs3_kiwibd_readed(%p)", (void*)cfg); + lfs3_kiwibd_t *bd = cfg->context; + LFS3_KIWIBD_TRACE("lfs3_kiwibd_readed -> %"PRIu64, bd->readed); + return bd->readed; +} + +lfs3_kiwibd_sio_t lfs3_kiwibd_proged(const struct lfs3_cfg *cfg) { + LFS3_KIWIBD_TRACE("lfs3_kiwibd_proged(%p)", (void*)cfg); + lfs3_kiwibd_t *bd = cfg->context; + LFS3_KIWIBD_TRACE("lfs3_kiwibd_proged -> %"PRIu64, bd->proged); + return bd->proged; +} + +lfs3_kiwibd_sio_t lfs3_kiwibd_erased(const struct lfs3_cfg *cfg) { + LFS3_KIWIBD_TRACE("lfs3_kiwibd_erased(%p)", (void*)cfg); + lfs3_kiwibd_t *bd = cfg->context; + LFS3_KIWIBD_TRACE("lfs3_kiwibd_erased -> %"PRIu64, bd->erased); + return bd->erased; +} + +int lfs3_kiwibd_setreaded(const struct lfs3_cfg *cfg, + lfs3_kiwibd_io_t readed) { + LFS3_KIWIBD_TRACE("lfs3_kiwibd_setreaded(%p, %"PRIu64")", + (void*)cfg, readed); + lfs3_kiwibd_t *bd = cfg->context; + bd->readed = readed; + LFS3_KIWIBD_TRACE("lfs3_kiwibd_setreaded -> %d", 0); + return 0; +} + +int lfs3_kiwibd_setproged(const struct lfs3_cfg *cfg, + lfs3_kiwibd_io_t proged) { + LFS3_KIWIBD_TRACE("lfs3_kiwibd_setproged(%p, %"PRIu64")", + (void*)cfg, proged); + lfs3_kiwibd_t *bd = cfg->context; + bd->proged = proged; + LFS3_KIWIBD_TRACE("lfs3_kiwibd_setproged -> %d", 0); + return 0; +} + +int lfs3_kiwibd_seterased(const struct lfs3_cfg *cfg, + lfs3_kiwibd_io_t erased) { + LFS3_KIWIBD_TRACE("lfs3_kiwibd_seterased(%p, %"PRIu64")", + (void*)cfg, erased); + lfs3_kiwibd_t *bd = cfg->context; + bd->erased = erased; + LFS3_KIWIBD_TRACE("lfs3_kiwibd_seterased -> %d", 0); + return 0; +} + diff --git a/bd/lfs3_kiwibd.h b/bd/lfs3_kiwibd.h new file mode 100644 index 000000000..0cd367282 --- /dev/null +++ b/bd/lfs3_kiwibd.h @@ -0,0 +1,136 @@ +/* + * kiwibd - A lightweight variant of emubd, useful for emulating large + * disks backed by a file or in RAM. + * + * Unlike emubd, file-backed disks are _not_ mirrored in RAM. kiwibd has + * fewer features than emubd, prioritizing speed for benchmarking. + * + * + */ +#ifndef LFS3_KIWIBD_H +#define LFS3_KIWIBD_H + +#include "lfs3.h" +#include "lfs3_util.h" + + +// Block device specific tracing +#ifndef LFS3_KIWIBD_TRACE +#ifdef LFS3_KIWIBD_YES_TRACE +#define LFS3_KIWIBD_TRACE(...) LFS3_TRACE(__VA_ARGS__) +#else +#define LFS3_KIWIBD_TRACE(...) +#endif +#endif + +// Type for measuring read/program/erase operations +typedef uint64_t lfs3_kiwibd_io_t; +typedef int64_t lfs3_kiwibd_sio_t; + +// Type for delays in nanoseconds +typedef uint64_t lfs3_kiwibd_sleep_t; +typedef int64_t lfs3_kiwibd_ssleep_t; + +// kiwibd config, this is required for testing +struct lfs3_kiwibd_cfg { + // 8-bit erase value to use for simulating erases. -1 simulates a noop + // erase, which is faster than simulating a fixed erase value. -2 emulates + // nor-masking, which is useful for testing other filesystems (littlefs + // does _not_ rely on this!). + int32_t erase_value; + + // Optional statically allocated buffer for the block device. Ignored + // if disk_path is provided. + void *buffer; + + // Artificial delay in nanoseconds, there is no purpose for this other + // than slowing down the simulation. + lfs3_kiwibd_sleep_t read_sleep; + + // Artificial delay in nanoseconds, there is no purpose for this other + // than slowing down the simulation. + lfs3_kiwibd_sleep_t prog_sleep; + + // Artificial delay in nanoseconds, there is no purpose for this other + // than slowing down the simulation. + lfs3_kiwibd_sleep_t erase_sleep; +}; + +// kiwibd state +typedef struct lfs3_kiwibd { + // backing disk + int fd; + union { + uint8_t *scratch; + uint8_t *mem; + } u; + + // amount read/progged/erased + lfs3_kiwibd_io_t readed; + lfs3_kiwibd_io_t proged; + lfs3_kiwibd_io_t erased; + + const struct lfs3_kiwibd_cfg *cfg; +} lfs3_kiwibd_t; + + +/// Block device API /// + +// Create a kiwibd using the geometry in lfs3_cfg +// +// If path is provided, kiwibd will use the file to back the block +// device, allowing emulation of block devices > available RAM. +// +int lfs3_kiwibd_create(const struct lfs3_cfg *cfg, const char *path); +int lfs3_kiwibd_createcfg(const struct lfs3_cfg *cfg, const char *path, + const struct lfs3_kiwibd_cfg *bdcfg); + +// Clean up memory associated with block device +int lfs3_kiwibd_destroy(const struct lfs3_cfg *cfg); + +// Read a block +int lfs3_kiwibd_read(const struct lfs3_cfg *cfg, lfs3_block_t block, + lfs3_off_t off, void *buffer, lfs3_size_t size); + +// Program a block +// +// The block must have previously been erased. +int lfs3_kiwibd_prog(const struct lfs3_cfg *cfg, lfs3_block_t block, + lfs3_off_t off, const void *buffer, lfs3_size_t size); + +// Erase a block +// +// A block must be erased before being programmed. The +// state of an erased block is undefined. +int lfs3_kiwibd_erase(const struct lfs3_cfg *cfg, lfs3_block_t block); + +// Sync the block device +int lfs3_kiwibd_sync(const struct lfs3_cfg *cfg); + + +/// Additional kiwibd features /// + +// Get total amount of bytes read +lfs3_kiwibd_sio_t lfs3_kiwibd_readed(const struct lfs3_cfg *cfg); + +// Get total amount of bytes programmed +lfs3_kiwibd_sio_t lfs3_kiwibd_proged(const struct lfs3_cfg *cfg); + +// Get total amount of bytes erased +lfs3_kiwibd_sio_t lfs3_kiwibd_erased(const struct lfs3_cfg *cfg); + +// Manually set amount of bytes read +int lfs3_kiwibd_setreaded(const struct lfs3_cfg *cfg, + lfs3_kiwibd_io_t readed); + +// Manually set amount of bytes programmed +int lfs3_kiwibd_setproged(const struct lfs3_cfg *cfg, + lfs3_kiwibd_io_t proged); + +// Manually set amount of bytes erased +int lfs3_kiwibd_seterased(const struct lfs3_cfg *cfg, + lfs3_kiwibd_io_t erased); + + +#endif + diff --git a/bd/lfs3_rambd.c b/bd/lfs3_rambd.c new file mode 100644 index 000000000..2df32ebfa --- /dev/null +++ b/bd/lfs3_rambd.c @@ -0,0 +1,131 @@ +/* + * Block device emulated in RAM + * + * Copyright (c) 2022, The littlefs authors. + * Copyright (c) 2017, Arm Limited. All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + */ +#include "bd/lfs3_rambd.h" + +int lfs3_rambd_createcfg(const struct lfs3_cfg *cfg, + const struct lfs3_rambd_cfg *bdcfg) { + LFS3_RAMBD_TRACE("lfs3_rambd_createcfg(%p {.context=%p, " + ".read=%p, .prog=%p, .erase=%p, .sync=%p, " + ".read_size=%"PRIu32", .prog_size=%"PRIu32", " + ".block_size=%"PRIu32", .block_count=%"PRIu32"}, " + "%p {.buffer=%p})", + (void*)cfg, cfg->context, + (void*)(uintptr_t)cfg->read, (void*)(uintptr_t)cfg->prog, + (void*)(uintptr_t)cfg->erase, (void*)(uintptr_t)cfg->sync, + cfg->read_size, cfg->prog_size, cfg->block_size, cfg->block_count, + (void*)bdcfg, bdcfg->buffer); + lfs3_rambd_t *bd = cfg->context; + bd->cfg = bdcfg; + + // allocate buffer? + if (bd->cfg->buffer) { + bd->mem = bd->cfg->buffer; + } else { + bd->mem = lfs3_malloc(cfg->block_size * cfg->block_count); + if (!bd->mem) { + LFS3_RAMBD_TRACE("lfs3_rambd_createcfg -> %d", LFS3_ERR_NOMEM); + return LFS3_ERR_NOMEM; + } + } + + // zero for reproducibility + memset(bd->mem, 0, cfg->block_size * cfg->block_count); + + LFS3_RAMBD_TRACE("lfs3_rambd_createcfg -> %d", 0); + return 0; +} + +int lfs3_rambd_create(const struct lfs3_cfg *cfg) { + LFS3_RAMBD_TRACE("lfs3_rambd_create(%p {.context=%p, " + ".read=%p, .prog=%p, .erase=%p, .sync=%p, " + ".read_size=%"PRIu32", .prog_size=%"PRIu32", " + ".block_size=%"PRIu32", .block_count=%"PRIu32"})", + (void*)cfg, cfg->context, + (void*)(uintptr_t)cfg->read, (void*)(uintptr_t)cfg->prog, + (void*)(uintptr_t)cfg->erase, (void*)(uintptr_t)cfg->sync, + cfg->read_size, cfg->prog_size, cfg->block_size, cfg->block_count); + static const struct lfs3_rambd_cfg defaults = {0}; + int err = lfs3_rambd_createcfg(cfg, &defaults); + LFS3_RAMBD_TRACE("lfs3_rambd_create -> %d", err); + return err; +} + +int lfs3_rambd_destroy(const struct lfs3_cfg *cfg) { + LFS3_RAMBD_TRACE("lfs3_rambd_destroy(%p)", (void*)cfg); + // clean up memory + lfs3_rambd_t *bd = cfg->context; + if (!bd->cfg->buffer) { + lfs3_free(bd->mem); + } + LFS3_RAMBD_TRACE("lfs3_rambd_destroy -> %d", 0); + return 0; +} + +int lfs3_rambd_read(const struct lfs3_cfg *cfg, lfs3_block_t block, + lfs3_off_t off, void *buffer, lfs3_size_t size) { + LFS3_RAMBD_TRACE("lfs3_rambd_read(%p, " + "0x%"PRIx32", %"PRIu32", %p, %"PRIu32")", + (void*)cfg, block, off, buffer, size); + lfs3_rambd_t *bd = cfg->context; + + // check if read is valid + LFS3_ASSERT(block < cfg->block_count); + LFS3_ASSERT(off % cfg->read_size == 0); + LFS3_ASSERT(size % cfg->read_size == 0); + LFS3_ASSERT(off+size <= cfg->block_size); + + // read data + memcpy(buffer, &bd->mem[block*cfg->block_size + off], size); + + LFS3_RAMBD_TRACE("lfs3_rambd_read -> %d", 0); + return 0; +} + +int lfs3_rambd_prog(const struct lfs3_cfg *cfg, lfs3_block_t block, + lfs3_off_t off, const void *buffer, lfs3_size_t size) { + LFS3_RAMBD_TRACE("lfs3_rambd_prog(%p, " + "0x%"PRIx32", %"PRIu32", %p, %"PRIu32")", + (void*)cfg, block, off, buffer, size); + lfs3_rambd_t *bd = cfg->context; + + // check if write is valid + LFS3_ASSERT(block < cfg->block_count); + LFS3_ASSERT(off % cfg->prog_size == 0); + LFS3_ASSERT(size % cfg->prog_size == 0); + LFS3_ASSERT(off+size <= cfg->block_size); + + // program data + memcpy(&bd->mem[block*cfg->block_size + off], buffer, size); + + LFS3_RAMBD_TRACE("lfs3_rambd_prog -> %d", 0); + return 0; +} + +int lfs3_rambd_erase(const struct lfs3_cfg *cfg, lfs3_block_t block) { + LFS3_RAMBD_TRACE("lfs3_rambd_erase(%p, 0x%"PRIx32" (%"PRIu32"))", + (void*)cfg, block, cfg->block_size); + + // check if erase is valid + LFS3_ASSERT(block < cfg->block_count); + + // erase is a noop + (void)block; + + LFS3_RAMBD_TRACE("lfs3_rambd_erase -> %d", 0); + return 0; +} + +int lfs3_rambd_sync(const struct lfs3_cfg *cfg) { + LFS3_RAMBD_TRACE("lfs3_rambd_sync(%p)", (void*)cfg); + + // sync is a noop + (void)cfg; + + LFS3_RAMBD_TRACE("lfs3_rambd_sync -> %d", 0); + return 0; +} diff --git a/bd/lfs3_rambd.h b/bd/lfs3_rambd.h new file mode 100644 index 000000000..fbd0acba2 --- /dev/null +++ b/bd/lfs3_rambd.h @@ -0,0 +1,65 @@ +/* + * Block device emulated in RAM + * + * Copyright (c) 2022, The littlefs authors. + * Copyright (c) 2017, Arm Limited. All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + */ +#ifndef LFS3_RAMBD_H +#define LFS3_RAMBD_H + +#include "lfs3.h" +#include "lfs3_util.h" + + +// Block device specific tracing +#ifndef LFS3_RAMBD_TRACE +#ifdef LFS3_RAMBD_YES_TRACE +#define LFS3_RAMBD_TRACE(...) LFS3_TRACE(__VA_ARGS__) +#else +#define LFS3_RAMBD_TRACE(...) +#endif +#endif + +// rambd config (optional) +struct lfs3_rambd_cfg { + // Optional statically allocated buffer for the block device. + void *buffer; +}; + +// rambd state +typedef struct lfs3_rambd { + uint8_t *mem; + const struct lfs3_rambd_cfg *cfg; +} lfs3_rambd_t; + + +// Create a RAM block device using the geometry in lfs3_cfg +int lfs3_rambd_create(const struct lfs3_cfg *cfg); +int lfs3_rambd_createcfg(const struct lfs3_cfg *cfg, + const struct lfs3_rambd_cfg *bdcfg); + +// Clean up memory associated with block device +int lfs3_rambd_destroy(const struct lfs3_cfg *cfg); + +// Read a block +int lfs3_rambd_read(const struct lfs3_cfg *cfg, lfs3_block_t block, + lfs3_off_t off, void *buffer, lfs3_size_t size); + +// Program a block +// +// The block must have previously been erased. +int lfs3_rambd_prog(const struct lfs3_cfg *cfg, lfs3_block_t block, + lfs3_off_t off, const void *buffer, lfs3_size_t size); + +// Erase a block +// +// A block must be erased before being programmed. The +// state of an erased block is undefined. +int lfs3_rambd_erase(const struct lfs3_cfg *cfg, lfs3_block_t block); + +// Sync the block device +int lfs3_rambd_sync(const struct lfs3_cfg *cfg); + + +#endif diff --git a/bd/lfs_emubd.c b/bd/lfs_emubd.c deleted file mode 100644 index 299255386..000000000 --- a/bd/lfs_emubd.c +++ /dev/null @@ -1,662 +0,0 @@ -/* - * Emulating block device, wraps filebd and rambd while providing a bunch - * of hooks for testing littlefs in various conditions. - * - * Copyright (c) 2022, The littlefs authors. - * Copyright (c) 2017, Arm Limited. All rights reserved. - * SPDX-License-Identifier: BSD-3-Clause - */ - -#ifndef _POSIX_C_SOURCE -#define _POSIX_C_SOURCE 199309L -#endif - -#include "bd/lfs_emubd.h" - -#include -#include -#include -#include -#include - -#ifdef _WIN32 -#include -#endif - - -// access to lazily-allocated/copy-on-write blocks -// -// Note we can only modify a block if we have exclusive access to it (rc == 1) -// - -static lfs_emubd_block_t *lfs_emubd_incblock(lfs_emubd_block_t *block) { - if (block) { - block->rc += 1; - } - return block; -} - -static void lfs_emubd_decblock(lfs_emubd_block_t *block) { - if (block) { - block->rc -= 1; - if (block->rc == 0) { - free(block); - } - } -} - -static lfs_emubd_block_t *lfs_emubd_mutblock( - const struct lfs_config *cfg, - lfs_emubd_block_t **block) { - lfs_emubd_block_t *block_ = *block; - if (block_ && block_->rc == 1) { - // rc == 1? can modify - return block_; - - } else if (block_) { - // rc > 1? need to create a copy - lfs_emubd_block_t *nblock = malloc( - sizeof(lfs_emubd_block_t) + cfg->block_size); - if (!nblock) { - return NULL; - } - - memcpy(nblock, block_, - sizeof(lfs_emubd_block_t) + cfg->block_size); - nblock->rc = 1; - - lfs_emubd_decblock(block_); - *block = nblock; - return nblock; - - } else { - // no block? need to allocate - lfs_emubd_block_t *nblock = malloc( - sizeof(lfs_emubd_block_t) + cfg->block_size); - if (!nblock) { - return NULL; - } - - nblock->rc = 1; - nblock->wear = 0; - - // zero for consistency - lfs_emubd_t *bd = cfg->context; - memset(nblock->data, - (bd->cfg->erase_value != -1) ? bd->cfg->erase_value : 0, - cfg->block_size); - - *block = nblock; - return nblock; - } -} - - -// emubd create/destroy - -int lfs_emubd_createcfg(const struct lfs_config *cfg, const char *path, - const struct lfs_emubd_config *bdcfg) { - LFS_EMUBD_TRACE("lfs_emubd_createcfg(%p {.context=%p, " - ".read=%p, .prog=%p, .erase=%p, .sync=%p, " - ".read_size=%"PRIu32", .prog_size=%"PRIu32", " - ".block_size=%"PRIu32", .block_count=%"PRIu32"}, " - "\"%s\", " - "%p {.erase_value=%"PRId32", .erase_cycles=%"PRIu32", " - ".badblock_behavior=%"PRIu8", .power_cycles=%"PRIu32", " - ".powerloss_behavior=%"PRIu8", .powerloss_cb=%p, " - ".powerloss_data=%p, .track_branches=%d})", - (void*)cfg, cfg->context, - (void*)(uintptr_t)cfg->read, (void*)(uintptr_t)cfg->prog, - (void*)(uintptr_t)cfg->erase, (void*)(uintptr_t)cfg->sync, - cfg->read_size, cfg->prog_size, cfg->block_size, cfg->block_count, - path, (void*)bdcfg, bdcfg->erase_value, bdcfg->erase_cycles, - bdcfg->badblock_behavior, bdcfg->power_cycles, - bdcfg->powerloss_behavior, (void*)(uintptr_t)bdcfg->powerloss_cb, - bdcfg->powerloss_data, bdcfg->track_branches); - lfs_emubd_t *bd = cfg->context; - bd->cfg = bdcfg; - - // allocate our block array, all blocks start as uninitialized - bd->blocks = malloc(cfg->block_count * sizeof(lfs_emubd_block_t*)); - if (!bd->blocks) { - LFS_EMUBD_TRACE("lfs_emubd_createcfg -> %d", LFS_ERR_NOMEM); - return LFS_ERR_NOMEM; - } - memset(bd->blocks, 0, cfg->block_count * sizeof(lfs_emubd_block_t*)); - - // setup testing things - bd->readed = 0; - bd->proged = 0; - bd->erased = 0; - bd->power_cycles = bd->cfg->power_cycles; - bd->disk = NULL; - - if (bd->cfg->disk_path) { - bd->disk = malloc(sizeof(lfs_emubd_disk_t)); - if (!bd->disk) { - LFS_EMUBD_TRACE("lfs_emubd_createcfg -> %d", LFS_ERR_NOMEM); - return LFS_ERR_NOMEM; - } - bd->disk->rc = 1; - bd->disk->scratch = NULL; - - #ifdef _WIN32 - bd->disk->fd = open(bd->cfg->disk_path, - O_RDWR | O_CREAT | O_BINARY, 0666); - #else - bd->disk->fd = open(bd->cfg->disk_path, - O_RDWR | O_CREAT, 0666); - #endif - if (bd->disk->fd < 0) { - int err = -errno; - LFS_EMUBD_TRACE("lfs_emubd_create -> %d", err); - return err; - } - - // if we're emulating erase values, we can keep a block around in - // memory of just the erase state to speed up emulated erases - if (bd->cfg->erase_value != -1) { - bd->disk->scratch = malloc(cfg->block_size); - if (!bd->disk->scratch) { - LFS_EMUBD_TRACE("lfs_emubd_createcfg -> %d", LFS_ERR_NOMEM); - return LFS_ERR_NOMEM; - } - memset(bd->disk->scratch, - bd->cfg->erase_value, - cfg->block_size); - - // go ahead and erase all of the disk, otherwise the file will not - // match our internal representation - for (size_t i = 0; i < cfg->block_count; i++) { - ssize_t res = write(bd->disk->fd, - bd->disk->scratch, - cfg->block_size); - if (res < 0) { - int err = -errno; - LFS_EMUBD_TRACE("lfs_emubd_create -> %d", err); - return err; - } - } - } - } - - LFS_EMUBD_TRACE("lfs_emubd_createcfg -> %d", 0); - return 0; -} - -int lfs_emubd_create(const struct lfs_config *cfg, const char *path) { - LFS_EMUBD_TRACE("lfs_emubd_create(%p {.context=%p, " - ".read=%p, .prog=%p, .erase=%p, .sync=%p, " - ".read_size=%"PRIu32", .prog_size=%"PRIu32", " - ".block_size=%"PRIu32", .block_count=%"PRIu32"}, " - "\"%s\")", - (void*)cfg, cfg->context, - (void*)(uintptr_t)cfg->read, (void*)(uintptr_t)cfg->prog, - (void*)(uintptr_t)cfg->erase, (void*)(uintptr_t)cfg->sync, - cfg->read_size, cfg->prog_size, cfg->block_size, cfg->block_count, - path); - static const struct lfs_emubd_config defaults = {.erase_value=-1}; - int err = lfs_emubd_createcfg(cfg, path, &defaults); - LFS_EMUBD_TRACE("lfs_emubd_create -> %d", err); - return err; -} - -int lfs_emubd_destroy(const struct lfs_config *cfg) { - LFS_EMUBD_TRACE("lfs_emubd_destroy(%p)", (void*)cfg); - lfs_emubd_t *bd = cfg->context; - - // decrement reference counts - for (lfs_block_t i = 0; i < cfg->block_count; i++) { - lfs_emubd_decblock(bd->blocks[i]); - } - free(bd->blocks); - - // clean up other resources - if (bd->disk) { - bd->disk->rc -= 1; - if (bd->disk->rc == 0) { - close(bd->disk->fd); - free(bd->disk->scratch); - free(bd->disk); - } - } - - LFS_EMUBD_TRACE("lfs_emubd_destroy -> %d", 0); - return 0; -} - - - -// block device API - -int lfs_emubd_read(const struct lfs_config *cfg, lfs_block_t block, - lfs_off_t off, void *buffer, lfs_size_t size) { - LFS_EMUBD_TRACE("lfs_emubd_read(%p, " - "0x%"PRIx32", %"PRIu32", %p, %"PRIu32")", - (void*)cfg, block, off, buffer, size); - lfs_emubd_t *bd = cfg->context; - - // check if read is valid - LFS_ASSERT(block < cfg->block_count); - LFS_ASSERT(off % cfg->read_size == 0); - LFS_ASSERT(size % cfg->read_size == 0); - LFS_ASSERT(off+size <= cfg->block_size); - - // get the block - const lfs_emubd_block_t *b = bd->blocks[block]; - if (b) { - // block bad? - if (bd->cfg->erase_cycles && b->wear >= bd->cfg->erase_cycles && - bd->cfg->badblock_behavior == LFS_EMUBD_BADBLOCK_READERROR) { - LFS_EMUBD_TRACE("lfs_emubd_read -> %d", LFS_ERR_CORRUPT); - return LFS_ERR_CORRUPT; - } - - // read data - memcpy(buffer, &b->data[off], size); - } else { - // zero for consistency - memset(buffer, - (bd->cfg->erase_value != -1) ? bd->cfg->erase_value : 0, - size); - } - - // track reads - bd->readed += size; - if (bd->cfg->read_sleep) { - int err = nanosleep(&(struct timespec){ - .tv_sec=bd->cfg->read_sleep/1000000000, - .tv_nsec=bd->cfg->read_sleep%1000000000}, - NULL); - if (err) { - err = -errno; - LFS_EMUBD_TRACE("lfs_emubd_read -> %d", err); - return err; - } - } - - LFS_EMUBD_TRACE("lfs_emubd_read -> %d", 0); - return 0; -} - -int lfs_emubd_prog(const struct lfs_config *cfg, lfs_block_t block, - lfs_off_t off, const void *buffer, lfs_size_t size) { - LFS_EMUBD_TRACE("lfs_emubd_prog(%p, " - "0x%"PRIx32", %"PRIu32", %p, %"PRIu32")", - (void*)cfg, block, off, buffer, size); - lfs_emubd_t *bd = cfg->context; - - // check if write is valid - LFS_ASSERT(block < cfg->block_count); - LFS_ASSERT(off % cfg->prog_size == 0); - LFS_ASSERT(size % cfg->prog_size == 0); - LFS_ASSERT(off+size <= cfg->block_size); - - // get the block - lfs_emubd_block_t *b = lfs_emubd_mutblock(cfg, &bd->blocks[block]); - if (!b) { - LFS_EMUBD_TRACE("lfs_emubd_prog -> %d", LFS_ERR_NOMEM); - return LFS_ERR_NOMEM; - } - - // block bad? - if (bd->cfg->erase_cycles && b->wear >= bd->cfg->erase_cycles) { - if (bd->cfg->badblock_behavior == - LFS_EMUBD_BADBLOCK_PROGERROR) { - LFS_EMUBD_TRACE("lfs_emubd_prog -> %d", LFS_ERR_CORRUPT); - return LFS_ERR_CORRUPT; - } else if (bd->cfg->badblock_behavior == - LFS_EMUBD_BADBLOCK_PROGNOOP || - bd->cfg->badblock_behavior == - LFS_EMUBD_BADBLOCK_ERASENOOP) { - LFS_EMUBD_TRACE("lfs_emubd_prog -> %d", 0); - return 0; - } - } - - // were we erased properly? - if (bd->cfg->erase_value != -1) { - for (lfs_off_t i = 0; i < size; i++) { - LFS_ASSERT(b->data[off+i] == bd->cfg->erase_value); - } - } - - // prog data - memcpy(&b->data[off], buffer, size); - - // mirror to disk file? - if (bd->disk) { - off_t res1 = lseek(bd->disk->fd, - (off_t)block*cfg->block_size + (off_t)off, - SEEK_SET); - if (res1 < 0) { - int err = -errno; - LFS_EMUBD_TRACE("lfs_emubd_prog -> %d", err); - return err; - } - - ssize_t res2 = write(bd->disk->fd, buffer, size); - if (res2 < 0) { - int err = -errno; - LFS_EMUBD_TRACE("lfs_emubd_prog -> %d", err); - return err; - } - } - - // track progs - bd->proged += size; - if (bd->cfg->prog_sleep) { - int err = nanosleep(&(struct timespec){ - .tv_sec=bd->cfg->prog_sleep/1000000000, - .tv_nsec=bd->cfg->prog_sleep%1000000000}, - NULL); - if (err) { - err = -errno; - LFS_EMUBD_TRACE("lfs_emubd_prog -> %d", err); - return err; - } - } - - // lose power? - if (bd->power_cycles > 0) { - bd->power_cycles -= 1; - if (bd->power_cycles == 0) { - // simulate power loss - bd->cfg->powerloss_cb(bd->cfg->powerloss_data); - } - } - - LFS_EMUBD_TRACE("lfs_emubd_prog -> %d", 0); - return 0; -} - -int lfs_emubd_erase(const struct lfs_config *cfg, lfs_block_t block) { - LFS_EMUBD_TRACE("lfs_emubd_erase(%p, 0x%"PRIx32" (%"PRIu32"))", - (void*)cfg, block, cfg->block_size); - lfs_emubd_t *bd = cfg->context; - - // check if erase is valid - LFS_ASSERT(block < cfg->block_count); - - // get the block - lfs_emubd_block_t *b = lfs_emubd_mutblock(cfg, &bd->blocks[block]); - if (!b) { - LFS_EMUBD_TRACE("lfs_emubd_prog -> %d", LFS_ERR_NOMEM); - return LFS_ERR_NOMEM; - } - - // block bad? - if (bd->cfg->erase_cycles) { - if (b->wear >= bd->cfg->erase_cycles) { - if (bd->cfg->badblock_behavior == - LFS_EMUBD_BADBLOCK_ERASEERROR) { - LFS_EMUBD_TRACE("lfs_emubd_erase -> %d", LFS_ERR_CORRUPT); - return LFS_ERR_CORRUPT; - } else if (bd->cfg->badblock_behavior == - LFS_EMUBD_BADBLOCK_ERASENOOP) { - LFS_EMUBD_TRACE("lfs_emubd_erase -> %d", 0); - return 0; - } - } else { - // mark wear - b->wear += 1; - } - } - - // emulate an erase value? - if (bd->cfg->erase_value != -1) { - memset(b->data, bd->cfg->erase_value, cfg->block_size); - - // mirror to disk file? - if (bd->disk) { - off_t res1 = lseek(bd->disk->fd, - (off_t)block*cfg->block_size, - SEEK_SET); - if (res1 < 0) { - int err = -errno; - LFS_EMUBD_TRACE("lfs_emubd_erase -> %d", err); - return err; - } - - ssize_t res2 = write(bd->disk->fd, - bd->disk->scratch, - cfg->block_size); - if (res2 < 0) { - int err = -errno; - LFS_EMUBD_TRACE("lfs_emubd_erase -> %d", err); - return err; - } - } - } - - // track erases - bd->erased += cfg->block_size; - if (bd->cfg->erase_sleep) { - int err = nanosleep(&(struct timespec){ - .tv_sec=bd->cfg->erase_sleep/1000000000, - .tv_nsec=bd->cfg->erase_sleep%1000000000}, - NULL); - if (err) { - err = -errno; - LFS_EMUBD_TRACE("lfs_emubd_erase -> %d", err); - return err; - } - } - - // lose power? - if (bd->power_cycles > 0) { - bd->power_cycles -= 1; - if (bd->power_cycles == 0) { - // simulate power loss - bd->cfg->powerloss_cb(bd->cfg->powerloss_data); - } - } - - LFS_EMUBD_TRACE("lfs_emubd_erase -> %d", 0); - return 0; -} - -int lfs_emubd_sync(const struct lfs_config *cfg) { - LFS_EMUBD_TRACE("lfs_emubd_sync(%p)", (void*)cfg); - - // do nothing - (void)cfg; - - LFS_EMUBD_TRACE("lfs_emubd_sync -> %d", 0); - return 0; -} - -/// Additional extended API for driving test features /// - -static int lfs_emubd_rawcrc(const struct lfs_config *cfg, - lfs_block_t block, uint32_t *crc) { - lfs_emubd_t *bd = cfg->context; - - // check if crc is valid - LFS_ASSERT(block < cfg->block_count); - - // crc the block - uint32_t crc_ = 0xffffffff; - const lfs_emubd_block_t *b = bd->blocks[block]; - if (b) { - crc_ = lfs_crc(crc_, b->data, cfg->block_size); - } else { - uint8_t erase_value = (bd->cfg->erase_value != -1) - ? bd->cfg->erase_value - : 0; - for (lfs_size_t i = 0; i < cfg->block_size; i++) { - crc_ = lfs_crc(crc_, &erase_value, 1); - } - } - *crc = 0xffffffff ^ crc_; - - return 0; -} - -int lfs_emubd_crc(const struct lfs_config *cfg, - lfs_block_t block, uint32_t *crc) { - LFS_EMUBD_TRACE("lfs_emubd_crc(%p, %"PRIu32", %p)", - (void*)cfg, block, crc); - int err = lfs_emubd_rawcrc(cfg, block, crc); - LFS_EMUBD_TRACE("lfs_emubd_crc -> %d", err); - return err; -} - -int lfs_emubd_bdcrc(const struct lfs_config *cfg, uint32_t *crc) { - LFS_EMUBD_TRACE("lfs_emubd_bdcrc(%p, %p)", (void*)cfg, crc); - - uint32_t crc_ = 0xffffffff; - for (lfs_block_t i = 0; i < cfg->block_count; i++) { - uint32_t i_crc; - int err = lfs_emubd_rawcrc(cfg, i, &i_crc); - if (err) { - LFS_EMUBD_TRACE("lfs_emubd_bdcrc -> %d", err); - return err; - } - - crc_ = lfs_crc(crc_, &i_crc, sizeof(uint32_t)); - } - *crc = 0xffffffff ^ crc_; - - LFS_EMUBD_TRACE("lfs_emubd_bdcrc -> %d", 0); - return 0; -} - -lfs_emubd_sio_t lfs_emubd_readed(const struct lfs_config *cfg) { - LFS_EMUBD_TRACE("lfs_emubd_readed(%p)", (void*)cfg); - lfs_emubd_t *bd = cfg->context; - LFS_EMUBD_TRACE("lfs_emubd_readed -> %"PRIu64, bd->readed); - return bd->readed; -} - -lfs_emubd_sio_t lfs_emubd_proged(const struct lfs_config *cfg) { - LFS_EMUBD_TRACE("lfs_emubd_proged(%p)", (void*)cfg); - lfs_emubd_t *bd = cfg->context; - LFS_EMUBD_TRACE("lfs_emubd_proged -> %"PRIu64, bd->proged); - return bd->proged; -} - -lfs_emubd_sio_t lfs_emubd_erased(const struct lfs_config *cfg) { - LFS_EMUBD_TRACE("lfs_emubd_erased(%p)", (void*)cfg); - lfs_emubd_t *bd = cfg->context; - LFS_EMUBD_TRACE("lfs_emubd_erased -> %"PRIu64, bd->erased); - return bd->erased; -} - -int lfs_emubd_setreaded(const struct lfs_config *cfg, lfs_emubd_io_t readed) { - LFS_EMUBD_TRACE("lfs_emubd_setreaded(%p, %"PRIu64")", (void*)cfg, readed); - lfs_emubd_t *bd = cfg->context; - bd->readed = readed; - LFS_EMUBD_TRACE("lfs_emubd_setreaded -> %d", 0); - return 0; -} - -int lfs_emubd_setproged(const struct lfs_config *cfg, lfs_emubd_io_t proged) { - LFS_EMUBD_TRACE("lfs_emubd_setproged(%p, %"PRIu64")", (void*)cfg, proged); - lfs_emubd_t *bd = cfg->context; - bd->proged = proged; - LFS_EMUBD_TRACE("lfs_emubd_setproged -> %d", 0); - return 0; -} - -int lfs_emubd_seterased(const struct lfs_config *cfg, lfs_emubd_io_t erased) { - LFS_EMUBD_TRACE("lfs_emubd_seterased(%p, %"PRIu64")", (void*)cfg, erased); - lfs_emubd_t *bd = cfg->context; - bd->erased = erased; - LFS_EMUBD_TRACE("lfs_emubd_seterased -> %d", 0); - return 0; -} - -lfs_emubd_swear_t lfs_emubd_wear(const struct lfs_config *cfg, - lfs_block_t block) { - LFS_EMUBD_TRACE("lfs_emubd_wear(%p, %"PRIu32")", (void*)cfg, block); - lfs_emubd_t *bd = cfg->context; - - // check if block is valid - LFS_ASSERT(block < cfg->block_count); - - // get the wear - lfs_emubd_wear_t wear; - const lfs_emubd_block_t *b = bd->blocks[block]; - if (b) { - wear = b->wear; - } else { - wear = 0; - } - - LFS_EMUBD_TRACE("lfs_emubd_wear -> %"PRIi32, wear); - return wear; -} - -int lfs_emubd_setwear(const struct lfs_config *cfg, - lfs_block_t block, lfs_emubd_wear_t wear) { - LFS_EMUBD_TRACE("lfs_emubd_setwear(%p, %"PRIu32", %"PRIi32")", - (void*)cfg, block, wear); - lfs_emubd_t *bd = cfg->context; - - // check if block is valid - LFS_ASSERT(block < cfg->block_count); - - // set the wear - lfs_emubd_block_t *b = lfs_emubd_mutblock(cfg, &bd->blocks[block]); - if (!b) { - LFS_EMUBD_TRACE("lfs_emubd_setwear -> %d", LFS_ERR_NOMEM); - return LFS_ERR_NOMEM; - } - b->wear = wear; - - LFS_EMUBD_TRACE("lfs_emubd_setwear -> %d", 0); - return 0; -} - -lfs_emubd_spowercycles_t lfs_emubd_powercycles( - const struct lfs_config *cfg) { - LFS_EMUBD_TRACE("lfs_emubd_powercycles(%p)", (void*)cfg); - lfs_emubd_t *bd = cfg->context; - - LFS_EMUBD_TRACE("lfs_emubd_powercycles -> %"PRIi32, bd->power_cycles); - return bd->power_cycles; -} - -int lfs_emubd_setpowercycles(const struct lfs_config *cfg, - lfs_emubd_powercycles_t power_cycles) { - LFS_EMUBD_TRACE("lfs_emubd_setpowercycles(%p, %"PRIi32")", - (void*)cfg, power_cycles); - lfs_emubd_t *bd = cfg->context; - - bd->power_cycles = power_cycles; - - LFS_EMUBD_TRACE("lfs_emubd_powercycles -> %d", 0); - return 0; -} - -int lfs_emubd_copy(const struct lfs_config *cfg, lfs_emubd_t *copy) { - LFS_EMUBD_TRACE("lfs_emubd_copy(%p, %p)", (void*)cfg, (void*)copy); - lfs_emubd_t *bd = cfg->context; - - // lazily copy over our block array - copy->blocks = malloc(cfg->block_count * sizeof(lfs_emubd_block_t*)); - if (!copy->blocks) { - LFS_EMUBD_TRACE("lfs_emubd_copy -> %d", LFS_ERR_NOMEM); - return LFS_ERR_NOMEM; - } - - for (size_t i = 0; i < cfg->block_count; i++) { - copy->blocks[i] = lfs_emubd_incblock(bd->blocks[i]); - } - - // other state - copy->readed = bd->readed; - copy->proged = bd->proged; - copy->erased = bd->erased; - copy->power_cycles = bd->power_cycles; - copy->disk = bd->disk; - if (copy->disk) { - copy->disk->rc += 1; - } - copy->cfg = bd->cfg; - - LFS_EMUBD_TRACE("lfs_emubd_copy -> %d", 0); - return 0; -} - diff --git a/bd/lfs_emubd.h b/bd/lfs_emubd.h deleted file mode 100644 index 35a411fec..000000000 --- a/bd/lfs_emubd.h +++ /dev/null @@ -1,233 +0,0 @@ -/* - * Emulating block device, wraps filebd and rambd while providing a bunch - * of hooks for testing littlefs in various conditions. - * - * Copyright (c) 2022, The littlefs authors. - * Copyright (c) 2017, Arm Limited. All rights reserved. - * SPDX-License-Identifier: BSD-3-Clause - */ -#ifndef LFS_EMUBD_H -#define LFS_EMUBD_H - -#include "lfs.h" -#include "lfs_util.h" -#include "bd/lfs_rambd.h" -#include "bd/lfs_filebd.h" - -#ifdef __cplusplus -extern "C" -{ -#endif - - -// Block device specific tracing -#ifndef LFS_EMUBD_TRACE -#ifdef LFS_EMUBD_YES_TRACE -#define LFS_EMUBD_TRACE(...) LFS_TRACE(__VA_ARGS__) -#else -#define LFS_EMUBD_TRACE(...) -#endif -#endif - -// Mode determining how "bad-blocks" behave during testing. This simulates -// some real-world circumstances such as progs not sticking (prog-noop), -// a readonly disk (erase-noop), and ECC failures (read-error). -// -// Not that read-noop is not allowed. Read _must_ return a consistent (but -// may be arbitrary) value on every read. -typedef enum lfs_emubd_badblock_behavior { - LFS_EMUBD_BADBLOCK_PROGERROR, - LFS_EMUBD_BADBLOCK_ERASEERROR, - LFS_EMUBD_BADBLOCK_READERROR, - LFS_EMUBD_BADBLOCK_PROGNOOP, - LFS_EMUBD_BADBLOCK_ERASENOOP, -} lfs_emubd_badblock_behavior_t; - -// Mode determining how power-loss behaves during testing. For now this -// only supports a noop behavior, leaving the data on-disk untouched. -typedef enum lfs_emubd_powerloss_behavior { - LFS_EMUBD_POWERLOSS_NOOP, -} lfs_emubd_powerloss_behavior_t; - -// Type for measuring read/program/erase operations -typedef uint64_t lfs_emubd_io_t; -typedef int64_t lfs_emubd_sio_t; - -// Type for measuring wear -typedef uint32_t lfs_emubd_wear_t; -typedef int32_t lfs_emubd_swear_t; - -// Type for tracking power-cycles -typedef uint32_t lfs_emubd_powercycles_t; -typedef int32_t lfs_emubd_spowercycles_t; - -// Type for delays in nanoseconds -typedef uint64_t lfs_emubd_sleep_t; -typedef int64_t lfs_emubd_ssleep_t; - -// emubd config, this is required for testing -struct lfs_emubd_config { - // 8-bit erase value to use for simulating erases. -1 does not simulate - // erases, which can speed up testing by avoiding the extra block-device - // operations to store the erase value. - int32_t erase_value; - - // Number of erase cycles before a block becomes "bad". The exact behavior - // of bad blocks is controlled by badblock_behavior. - uint32_t erase_cycles; - - // The mode determining how bad-blocks fail - lfs_emubd_badblock_behavior_t badblock_behavior; - - // Number of write operations (erase/prog) before triggering a power-loss. - // power_cycles=0 disables this. The exact behavior of power-loss is - // controlled by a combination of powerloss_behavior and powerloss_cb. - lfs_emubd_powercycles_t power_cycles; - - // The mode determining how power-loss affects disk - lfs_emubd_powerloss_behavior_t powerloss_behavior; - - // Function to call to emulate power-loss. The exact behavior of power-loss - // is up to the runner to provide. - void (*powerloss_cb)(void*); - - // Data for power-loss callback - void *powerloss_data; - - // True to track when power-loss could have occured. Note this involves - // heavy memory usage! - bool track_branches; - - // Path to file to use as a mirror of the disk. This provides a way to view - // the current state of the block device. - const char *disk_path; - - // Artificial delay in nanoseconds, there is no purpose for this other - // than slowing down the simulation. - lfs_emubd_sleep_t read_sleep; - - // Artificial delay in nanoseconds, there is no purpose for this other - // than slowing down the simulation. - lfs_emubd_sleep_t prog_sleep; - - // Artificial delay in nanoseconds, there is no purpose for this other - // than slowing down the simulation. - lfs_emubd_sleep_t erase_sleep; -}; - -// A reference counted block -typedef struct lfs_emubd_block { - uint32_t rc; - lfs_emubd_wear_t wear; - - uint8_t data[]; -} lfs_emubd_block_t; - -// Disk mirror -typedef struct lfs_emubd_disk { - uint32_t rc; - int fd; - uint8_t *scratch; -} lfs_emubd_disk_t; - -// emubd state -typedef struct lfs_emubd { - // array of copy-on-write blocks - lfs_emubd_block_t **blocks; - - // some other test state - lfs_emubd_io_t readed; - lfs_emubd_io_t proged; - lfs_emubd_io_t erased; - lfs_emubd_powercycles_t power_cycles; - lfs_emubd_disk_t *disk; - - const struct lfs_emubd_config *cfg; -} lfs_emubd_t; - - -/// Block device API /// - -// Create an emulating block device using the geometry in lfs_config -// -// Note that filebd is used if a path is provided, if path is NULL -// emubd will use rambd which can be much faster. -int lfs_emubd_create(const struct lfs_config *cfg, const char *path); -int lfs_emubd_createcfg(const struct lfs_config *cfg, const char *path, - const struct lfs_emubd_config *bdcfg); - -// Clean up memory associated with block device -int lfs_emubd_destroy(const struct lfs_config *cfg); - -// Read a block -int lfs_emubd_read(const struct lfs_config *cfg, lfs_block_t block, - lfs_off_t off, void *buffer, lfs_size_t size); - -// Program a block -// -// The block must have previously been erased. -int lfs_emubd_prog(const struct lfs_config *cfg, lfs_block_t block, - lfs_off_t off, const void *buffer, lfs_size_t size); - -// Erase a block -// -// A block must be erased before being programmed. The -// state of an erased block is undefined. -int lfs_emubd_erase(const struct lfs_config *cfg, lfs_block_t block); - -// Sync the block device -int lfs_emubd_sync(const struct lfs_config *cfg); - - -/// Additional extended API for driving test features /// - -// A CRC of a block for debugging purposes -int lfs_emubd_crc(const struct lfs_config *cfg, - lfs_block_t block, uint32_t *crc); - -// A CRC of the entire block device for debugging purposes -int lfs_emubd_bdcrc(const struct lfs_config *cfg, uint32_t *crc); - -// Get total amount of bytes read -lfs_emubd_sio_t lfs_emubd_readed(const struct lfs_config *cfg); - -// Get total amount of bytes programmed -lfs_emubd_sio_t lfs_emubd_proged(const struct lfs_config *cfg); - -// Get total amount of bytes erased -lfs_emubd_sio_t lfs_emubd_erased(const struct lfs_config *cfg); - -// Manually set amount of bytes read -int lfs_emubd_setreaded(const struct lfs_config *cfg, lfs_emubd_io_t readed); - -// Manually set amount of bytes programmed -int lfs_emubd_setproged(const struct lfs_config *cfg, lfs_emubd_io_t proged); - -// Manually set amount of bytes erased -int lfs_emubd_seterased(const struct lfs_config *cfg, lfs_emubd_io_t erased); - -// Get simulated wear on a given block -lfs_emubd_swear_t lfs_emubd_wear(const struct lfs_config *cfg, - lfs_block_t block); - -// Manually set simulated wear on a given block -int lfs_emubd_setwear(const struct lfs_config *cfg, - lfs_block_t block, lfs_emubd_wear_t wear); - -// Get the remaining power-cycles -lfs_emubd_spowercycles_t lfs_emubd_powercycles( - const struct lfs_config *cfg); - -// Manually set the remaining power-cycles -int lfs_emubd_setpowercycles(const struct lfs_config *cfg, - lfs_emubd_powercycles_t power_cycles); - -// Create a copy-on-write copy of the state of this block device -int lfs_emubd_copy(const struct lfs_config *cfg, lfs_emubd_t *copy); - - -#ifdef __cplusplus -} /* extern "C" */ -#endif - -#endif diff --git a/bd/lfs_filebd.h b/bd/lfs_filebd.h deleted file mode 100644 index 0f24996a5..000000000 --- a/bd/lfs_filebd.h +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Block device emulated in a file - * - * Copyright (c) 2022, The littlefs authors. - * Copyright (c) 2017, Arm Limited. All rights reserved. - * SPDX-License-Identifier: BSD-3-Clause - */ -#ifndef LFS_FILEBD_H -#define LFS_FILEBD_H - -#include "lfs.h" -#include "lfs_util.h" - -#ifdef __cplusplus -extern "C" -{ -#endif - - -// Block device specific tracing -#ifndef LFS_FILEBD_TRACE -#ifdef LFS_FILEBD_YES_TRACE -#define LFS_FILEBD_TRACE(...) LFS_TRACE(__VA_ARGS__) -#else -#define LFS_FILEBD_TRACE(...) -#endif -#endif - -// filebd state -typedef struct lfs_filebd { - int fd; -} lfs_filebd_t; - - -// Create a file block device using the geometry in lfs_config -int lfs_filebd_create(const struct lfs_config *cfg, const char *path); - -// Clean up memory associated with block device -int lfs_filebd_destroy(const struct lfs_config *cfg); - -// Read a block -int lfs_filebd_read(const struct lfs_config *cfg, lfs_block_t block, - lfs_off_t off, void *buffer, lfs_size_t size); - -// Program a block -// -// The block must have previously been erased. -int lfs_filebd_prog(const struct lfs_config *cfg, lfs_block_t block, - lfs_off_t off, const void *buffer, lfs_size_t size); - -// Erase a block -// -// A block must be erased before being programmed. The -// state of an erased block is undefined. -int lfs_filebd_erase(const struct lfs_config *cfg, lfs_block_t block); - -// Sync the block device -int lfs_filebd_sync(const struct lfs_config *cfg); - - -#ifdef __cplusplus -} /* extern "C" */ -#endif - -#endif diff --git a/bd/lfs_rambd.c b/bd/lfs_rambd.c deleted file mode 100644 index ab180b93e..000000000 --- a/bd/lfs_rambd.c +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Block device emulated in RAM - * - * Copyright (c) 2022, The littlefs authors. - * Copyright (c) 2017, Arm Limited. All rights reserved. - * SPDX-License-Identifier: BSD-3-Clause - */ -#include "bd/lfs_rambd.h" - -int lfs_rambd_createcfg(const struct lfs_config *cfg, - const struct lfs_rambd_config *bdcfg) { - LFS_RAMBD_TRACE("lfs_rambd_createcfg(%p {.context=%p, " - ".read=%p, .prog=%p, .erase=%p, .sync=%p, " - ".read_size=%"PRIu32", .prog_size=%"PRIu32", " - ".block_size=%"PRIu32", .block_count=%"PRIu32"}, " - "%p {.buffer=%p})", - (void*)cfg, cfg->context, - (void*)(uintptr_t)cfg->read, (void*)(uintptr_t)cfg->prog, - (void*)(uintptr_t)cfg->erase, (void*)(uintptr_t)cfg->sync, - cfg->read_size, cfg->prog_size, cfg->block_size, cfg->block_count, - (void*)bdcfg, bdcfg->buffer); - lfs_rambd_t *bd = cfg->context; - bd->cfg = bdcfg; - - // allocate buffer? - if (bd->cfg->buffer) { - bd->buffer = bd->cfg->buffer; - } else { - bd->buffer = lfs_malloc(cfg->block_size * cfg->block_count); - if (!bd->buffer) { - LFS_RAMBD_TRACE("lfs_rambd_createcfg -> %d", LFS_ERR_NOMEM); - return LFS_ERR_NOMEM; - } - } - - // zero for reproducibility - memset(bd->buffer, 0, cfg->block_size * cfg->block_count); - - LFS_RAMBD_TRACE("lfs_rambd_createcfg -> %d", 0); - return 0; -} - -int lfs_rambd_create(const struct lfs_config *cfg) { - LFS_RAMBD_TRACE("lfs_rambd_create(%p {.context=%p, " - ".read=%p, .prog=%p, .erase=%p, .sync=%p, " - ".read_size=%"PRIu32", .prog_size=%"PRIu32", " - ".block_size=%"PRIu32", .block_count=%"PRIu32"})", - (void*)cfg, cfg->context, - (void*)(uintptr_t)cfg->read, (void*)(uintptr_t)cfg->prog, - (void*)(uintptr_t)cfg->erase, (void*)(uintptr_t)cfg->sync, - cfg->read_size, cfg->prog_size, cfg->block_size, cfg->block_count); - static const struct lfs_rambd_config defaults = {0}; - int err = lfs_rambd_createcfg(cfg, &defaults); - LFS_RAMBD_TRACE("lfs_rambd_create -> %d", err); - return err; -} - -int lfs_rambd_destroy(const struct lfs_config *cfg) { - LFS_RAMBD_TRACE("lfs_rambd_destroy(%p)", (void*)cfg); - // clean up memory - lfs_rambd_t *bd = cfg->context; - if (!bd->cfg->buffer) { - lfs_free(bd->buffer); - } - LFS_RAMBD_TRACE("lfs_rambd_destroy -> %d", 0); - return 0; -} - -int lfs_rambd_read(const struct lfs_config *cfg, lfs_block_t block, - lfs_off_t off, void *buffer, lfs_size_t size) { - LFS_RAMBD_TRACE("lfs_rambd_read(%p, " - "0x%"PRIx32", %"PRIu32", %p, %"PRIu32")", - (void*)cfg, block, off, buffer, size); - lfs_rambd_t *bd = cfg->context; - - // check if read is valid - LFS_ASSERT(block < cfg->block_count); - LFS_ASSERT(off % cfg->read_size == 0); - LFS_ASSERT(size % cfg->read_size == 0); - LFS_ASSERT(off+size <= cfg->block_size); - - // read data - memcpy(buffer, &bd->buffer[block*cfg->block_size + off], size); - - LFS_RAMBD_TRACE("lfs_rambd_read -> %d", 0); - return 0; -} - -int lfs_rambd_prog(const struct lfs_config *cfg, lfs_block_t block, - lfs_off_t off, const void *buffer, lfs_size_t size) { - LFS_RAMBD_TRACE("lfs_rambd_prog(%p, " - "0x%"PRIx32", %"PRIu32", %p, %"PRIu32")", - (void*)cfg, block, off, buffer, size); - lfs_rambd_t *bd = cfg->context; - - // check if write is valid - LFS_ASSERT(block < cfg->block_count); - LFS_ASSERT(off % cfg->prog_size == 0); - LFS_ASSERT(size % cfg->prog_size == 0); - LFS_ASSERT(off+size <= cfg->block_size); - - // program data - memcpy(&bd->buffer[block*cfg->block_size + off], buffer, size); - - LFS_RAMBD_TRACE("lfs_rambd_prog -> %d", 0); - return 0; -} - -int lfs_rambd_erase(const struct lfs_config *cfg, lfs_block_t block) { - LFS_RAMBD_TRACE("lfs_rambd_erase(%p, 0x%"PRIx32" (%"PRIu32"))", - (void*)cfg, block, cfg->block_size); - - // check if erase is valid - LFS_ASSERT(block < cfg->block_count); - - // erase is a noop - (void)block; - - LFS_RAMBD_TRACE("lfs_rambd_erase -> %d", 0); - return 0; -} - -int lfs_rambd_sync(const struct lfs_config *cfg) { - LFS_RAMBD_TRACE("lfs_rambd_sync(%p)", (void*)cfg); - - // sync is a noop - (void)cfg; - - LFS_RAMBD_TRACE("lfs_rambd_sync -> %d", 0); - return 0; -} diff --git a/bd/lfs_rambd.h b/bd/lfs_rambd.h deleted file mode 100644 index 342468024..000000000 --- a/bd/lfs_rambd.h +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Block device emulated in RAM - * - * Copyright (c) 2022, The littlefs authors. - * Copyright (c) 2017, Arm Limited. All rights reserved. - * SPDX-License-Identifier: BSD-3-Clause - */ -#ifndef LFS_RAMBD_H -#define LFS_RAMBD_H - -#include "lfs.h" -#include "lfs_util.h" - -#ifdef __cplusplus -extern "C" -{ -#endif - - -// Block device specific tracing -#ifndef LFS_RAMBD_TRACE -#ifdef LFS_RAMBD_YES_TRACE -#define LFS_RAMBD_TRACE(...) LFS_TRACE(__VA_ARGS__) -#else -#define LFS_RAMBD_TRACE(...) -#endif -#endif - -// rambd config (optional) -struct lfs_rambd_config { - // Optional statically allocated buffer for the block device. - void *buffer; -}; - -// rambd state -typedef struct lfs_rambd { - uint8_t *buffer; - const struct lfs_rambd_config *cfg; -} lfs_rambd_t; - - -// Create a RAM block device using the geometry in lfs_config -int lfs_rambd_create(const struct lfs_config *cfg); -int lfs_rambd_createcfg(const struct lfs_config *cfg, - const struct lfs_rambd_config *bdcfg); - -// Clean up memory associated with block device -int lfs_rambd_destroy(const struct lfs_config *cfg); - -// Read a block -int lfs_rambd_read(const struct lfs_config *cfg, lfs_block_t block, - lfs_off_t off, void *buffer, lfs_size_t size); - -// Program a block -// -// The block must have previously been erased. -int lfs_rambd_prog(const struct lfs_config *cfg, lfs_block_t block, - lfs_off_t off, const void *buffer, lfs_size_t size); - -// Erase a block -// -// A block must be erased before being programmed. The -// state of an erased block is undefined. -int lfs_rambd_erase(const struct lfs_config *cfg, lfs_block_t block); - -// Sync the block device -int lfs_rambd_sync(const struct lfs_config *cfg); - - -#ifdef __cplusplus -} /* extern "C" */ -#endif - -#endif diff --git a/benches/bench_dir.toml b/benches/bench_dir.toml deleted file mode 100644 index 5f8cb490c..000000000 --- a/benches/bench_dir.toml +++ /dev/null @@ -1,270 +0,0 @@ -[cases.bench_dir_open] -# 0 = in-order -# 1 = reversed-order -# 2 = random-order -defines.ORDER = [0, 1, 2] -defines.N = 1024 -defines.FILE_SIZE = 8 -defines.CHUNK_SIZE = 8 -code = ''' - lfs_t lfs; - lfs_format(&lfs, cfg) => 0; - lfs_mount(&lfs, cfg) => 0; - - // first create the files - char name[256]; - uint8_t buffer[CHUNK_SIZE]; - for (lfs_size_t i = 0; i < N; i++) { - sprintf(name, "file%08x", i); - lfs_file_t file; - lfs_file_open(&lfs, &file, name, - LFS_O_WRONLY | LFS_O_CREAT | LFS_O_EXCL) => 0; - - uint32_t file_prng = i; - for (lfs_size_t j = 0; j < FILE_SIZE; j += CHUNK_SIZE) { - for (lfs_size_t k = 0; k < CHUNK_SIZE; k++) { - buffer[k] = BENCH_PRNG(&file_prng); - } - lfs_file_write(&lfs, &file, buffer, CHUNK_SIZE) => CHUNK_SIZE; - } - - lfs_file_close(&lfs, &file) => 0; - } - - // then read the files - BENCH_START(); - uint32_t prng = 42; - for (lfs_size_t i = 0; i < N; i++) { - lfs_off_t i_ - = (ORDER == 0) ? i - : (ORDER == 1) ? (N-1-i) - : BENCH_PRNG(&prng) % N; - sprintf(name, "file%08x", i_); - lfs_file_t file; - lfs_file_open(&lfs, &file, name, LFS_O_RDONLY) => 0; - - uint32_t file_prng = i_; - for (lfs_size_t j = 0; j < FILE_SIZE; j += CHUNK_SIZE) { - lfs_file_read(&lfs, &file, buffer, CHUNK_SIZE) => CHUNK_SIZE; - for (lfs_size_t k = 0; k < CHUNK_SIZE; k++) { - assert(buffer[k] == BENCH_PRNG(&file_prng)); - } - } - - lfs_file_close(&lfs, &file) => 0; - } - BENCH_STOP(); - - lfs_unmount(&lfs) => 0; -''' - -[cases.bench_dir_creat] -# 0 = in-order -# 1 = reversed-order -# 2 = random-order -defines.ORDER = [0, 1, 2] -defines.N = 1024 -defines.FILE_SIZE = 8 -defines.CHUNK_SIZE = 8 -code = ''' - lfs_t lfs; - lfs_format(&lfs, cfg) => 0; - lfs_mount(&lfs, cfg) => 0; - - BENCH_START(); - uint32_t prng = 42; - char name[256]; - uint8_t buffer[CHUNK_SIZE]; - for (lfs_size_t i = 0; i < N; i++) { - lfs_off_t i_ - = (ORDER == 0) ? i - : (ORDER == 1) ? (N-1-i) - : BENCH_PRNG(&prng) % N; - sprintf(name, "file%08x", i_); - lfs_file_t file; - lfs_file_open(&lfs, &file, name, - LFS_O_WRONLY | LFS_O_CREAT | LFS_O_TRUNC) => 0; - - uint32_t file_prng = i_; - for (lfs_size_t j = 0; j < FILE_SIZE; j += CHUNK_SIZE) { - for (lfs_size_t k = 0; k < CHUNK_SIZE; k++) { - buffer[k] = BENCH_PRNG(&file_prng); - } - lfs_file_write(&lfs, &file, buffer, CHUNK_SIZE) => CHUNK_SIZE; - } - - lfs_file_close(&lfs, &file) => 0; - } - BENCH_STOP(); - - lfs_unmount(&lfs) => 0; -''' - -[cases.bench_dir_remove] -# 0 = in-order -# 1 = reversed-order -# 2 = random-order -defines.ORDER = [0, 1, 2] -defines.N = 1024 -defines.FILE_SIZE = 8 -defines.CHUNK_SIZE = 8 -code = ''' - lfs_t lfs; - lfs_format(&lfs, cfg) => 0; - lfs_mount(&lfs, cfg) => 0; - - // first create the files - char name[256]; - uint8_t buffer[CHUNK_SIZE]; - for (lfs_size_t i = 0; i < N; i++) { - sprintf(name, "file%08x", i); - lfs_file_t file; - lfs_file_open(&lfs, &file, name, - LFS_O_WRONLY | LFS_O_CREAT | LFS_O_EXCL) => 0; - - uint32_t file_prng = i; - for (lfs_size_t j = 0; j < FILE_SIZE; j += CHUNK_SIZE) { - for (lfs_size_t k = 0; k < CHUNK_SIZE; k++) { - buffer[k] = BENCH_PRNG(&file_prng); - } - lfs_file_write(&lfs, &file, buffer, CHUNK_SIZE) => CHUNK_SIZE; - } - - lfs_file_close(&lfs, &file) => 0; - } - - // then remove the files - BENCH_START(); - uint32_t prng = 42; - for (lfs_size_t i = 0; i < N; i++) { - lfs_off_t i_ - = (ORDER == 0) ? i - : (ORDER == 1) ? (N-1-i) - : BENCH_PRNG(&prng) % N; - sprintf(name, "file%08x", i_); - int err = lfs_remove(&lfs, name); - assert(!err || err == LFS_ERR_NOENT); - } - BENCH_STOP(); - - lfs_unmount(&lfs) => 0; -''' - -[cases.bench_dir_read] -defines.N = 1024 -defines.FILE_SIZE = 8 -defines.CHUNK_SIZE = 8 -code = ''' - lfs_t lfs; - lfs_format(&lfs, cfg) => 0; - lfs_mount(&lfs, cfg) => 0; - - // first create the files - char name[256]; - uint8_t buffer[CHUNK_SIZE]; - for (lfs_size_t i = 0; i < N; i++) { - sprintf(name, "file%08x", i); - lfs_file_t file; - lfs_file_open(&lfs, &file, name, - LFS_O_WRONLY | LFS_O_CREAT | LFS_O_EXCL) => 0; - - uint32_t file_prng = i; - for (lfs_size_t j = 0; j < FILE_SIZE; j += CHUNK_SIZE) { - for (lfs_size_t k = 0; k < CHUNK_SIZE; k++) { - buffer[k] = BENCH_PRNG(&file_prng); - } - lfs_file_write(&lfs, &file, buffer, CHUNK_SIZE) => CHUNK_SIZE; - } - - lfs_file_close(&lfs, &file) => 0; - } - - // then read the directory - BENCH_START(); - lfs_dir_t dir; - lfs_dir_open(&lfs, &dir, "/") => 0; - struct lfs_info info; - lfs_dir_read(&lfs, &dir, &info) => 1; - assert(info.type == LFS_TYPE_DIR); - assert(strcmp(info.name, ".") == 0); - lfs_dir_read(&lfs, &dir, &info) => 1; - assert(info.type == LFS_TYPE_DIR); - assert(strcmp(info.name, "..") == 0); - for (int i = 0; i < N; i++) { - sprintf(name, "file%08x", i); - lfs_dir_read(&lfs, &dir, &info) => 1; - assert(info.type == LFS_TYPE_REG); - assert(strcmp(info.name, name) == 0); - } - lfs_dir_read(&lfs, &dir, &info) => 0; - lfs_dir_close(&lfs, &dir) => 0; - BENCH_STOP(); - - lfs_unmount(&lfs) => 0; -''' - -[cases.bench_dir_mkdir] -# 0 = in-order -# 1 = reversed-order -# 2 = random-order -defines.ORDER = [0, 1, 2] -defines.N = 8 -code = ''' - lfs_t lfs; - lfs_format(&lfs, cfg) => 0; - lfs_mount(&lfs, cfg) => 0; - - BENCH_START(); - uint32_t prng = 42; - char name[256]; - for (lfs_size_t i = 0; i < N; i++) { - lfs_off_t i_ - = (ORDER == 0) ? i - : (ORDER == 1) ? (N-1-i) - : BENCH_PRNG(&prng) % N; - printf("hm %d\n", i); - sprintf(name, "dir%08x", i_); - int err = lfs_mkdir(&lfs, name); - assert(!err || err == LFS_ERR_EXIST); - } - BENCH_STOP(); - - lfs_unmount(&lfs) => 0; -''' - -[cases.bench_dir_rmdir] -# 0 = in-order -# 1 = reversed-order -# 2 = random-order -defines.ORDER = [0, 1, 2] -defines.N = 8 -code = ''' - lfs_t lfs; - lfs_format(&lfs, cfg) => 0; - lfs_mount(&lfs, cfg) => 0; - - // first create the dirs - char name[256]; - for (lfs_size_t i = 0; i < N; i++) { - sprintf(name, "dir%08x", i); - lfs_mkdir(&lfs, name) => 0; - } - - // then remove the dirs - BENCH_START(); - uint32_t prng = 42; - for (lfs_size_t i = 0; i < N; i++) { - lfs_off_t i_ - = (ORDER == 0) ? i - : (ORDER == 1) ? (N-1-i) - : BENCH_PRNG(&prng) % N; - sprintf(name, "dir%08x", i_); - int err = lfs_remove(&lfs, name); - assert(!err || err == LFS_ERR_NOENT); - } - BENCH_STOP(); - - lfs_unmount(&lfs) => 0; -''' - - diff --git a/benches/bench_file.toml b/benches/bench_file.toml deleted file mode 100644 index 168eaad87..000000000 --- a/benches/bench_file.toml +++ /dev/null @@ -1,95 +0,0 @@ -[cases.bench_file_read] -# 0 = in-order -# 1 = reversed-order -# 2 = random-order -defines.ORDER = [0, 1, 2] -defines.SIZE = '128*1024' -defines.CHUNK_SIZE = 64 -code = ''' - lfs_t lfs; - lfs_format(&lfs, cfg) => 0; - lfs_mount(&lfs, cfg) => 0; - lfs_size_t chunks = (SIZE+CHUNK_SIZE-1)/CHUNK_SIZE; - - // first write the file - lfs_file_t file; - uint8_t buffer[CHUNK_SIZE]; - lfs_file_open(&lfs, &file, "file", - LFS_O_WRONLY | LFS_O_CREAT | LFS_O_EXCL) => 0; - for (lfs_size_t i = 0; i < chunks; i++) { - uint32_t chunk_prng = i; - for (lfs_size_t j = 0; j < CHUNK_SIZE; j++) { - buffer[j] = BENCH_PRNG(&chunk_prng); - } - - lfs_file_write(&lfs, &file, buffer, CHUNK_SIZE) => CHUNK_SIZE; - } - lfs_file_write(&lfs, &file, buffer, CHUNK_SIZE) => CHUNK_SIZE; - lfs_file_close(&lfs, &file) => 0; - - // then read the file - BENCH_START(); - lfs_file_open(&lfs, &file, "file", LFS_O_RDONLY) => 0; - - uint32_t prng = 42; - for (lfs_size_t i = 0; i < chunks; i++) { - lfs_off_t i_ - = (ORDER == 0) ? i - : (ORDER == 1) ? (chunks-1-i) - : BENCH_PRNG(&prng) % chunks; - lfs_file_seek(&lfs, &file, i_*CHUNK_SIZE, LFS_SEEK_SET) - => i_*CHUNK_SIZE; - lfs_file_read(&lfs, &file, buffer, CHUNK_SIZE) => CHUNK_SIZE; - - uint32_t chunk_prng = i_; - for (lfs_size_t j = 0; j < CHUNK_SIZE; j++) { - assert(buffer[j] == BENCH_PRNG(&chunk_prng)); - } - } - - lfs_file_close(&lfs, &file) => 0; - BENCH_STOP(); - - lfs_unmount(&lfs) => 0; -''' - -[cases.bench_file_write] -# 0 = in-order -# 1 = reversed-order -# 2 = random-order -defines.ORDER = [0, 1, 2] -defines.SIZE = '128*1024' -defines.CHUNK_SIZE = 64 -code = ''' - lfs_t lfs; - lfs_format(&lfs, cfg) => 0; - lfs_mount(&lfs, cfg) => 0; - lfs_size_t chunks = (SIZE+CHUNK_SIZE-1)/CHUNK_SIZE; - - BENCH_START(); - lfs_file_t file; - lfs_file_open(&lfs, &file, "file", - LFS_O_WRONLY | LFS_O_CREAT | LFS_O_EXCL) => 0; - - uint8_t buffer[CHUNK_SIZE]; - uint32_t prng = 42; - for (lfs_size_t i = 0; i < chunks; i++) { - lfs_off_t i_ - = (ORDER == 0) ? i - : (ORDER == 1) ? (chunks-1-i) - : BENCH_PRNG(&prng) % chunks; - uint32_t chunk_prng = i_; - for (lfs_size_t j = 0; j < CHUNK_SIZE; j++) { - buffer[j] = BENCH_PRNG(&chunk_prng); - } - - lfs_file_seek(&lfs, &file, i_*CHUNK_SIZE, LFS_SEEK_SET) - => i_*CHUNK_SIZE; - lfs_file_write(&lfs, &file, buffer, CHUNK_SIZE) => CHUNK_SIZE; - } - - lfs_file_close(&lfs, &file) => 0; - BENCH_STOP(); - - lfs_unmount(&lfs) => 0; -''' diff --git a/benches/bench_superblock.toml b/benches/bench_superblock.toml deleted file mode 100644 index 37659d477..000000000 --- a/benches/bench_superblock.toml +++ /dev/null @@ -1,56 +0,0 @@ -[cases.bench_superblocks_found] -# support benchmarking with files -defines.N = [0, 1024] -defines.FILE_SIZE = 8 -defines.CHUNK_SIZE = 8 -code = ''' - lfs_t lfs; - lfs_format(&lfs, cfg) => 0; - - // create files? - lfs_mount(&lfs, cfg) => 0; - char name[256]; - uint8_t buffer[CHUNK_SIZE]; - for (lfs_size_t i = 0; i < N; i++) { - sprintf(name, "file%08x", i); - lfs_file_t file; - lfs_file_open(&lfs, &file, name, - LFS_O_WRONLY | LFS_O_CREAT | LFS_O_EXCL) => 0; - - for (lfs_size_t j = 0; j < FILE_SIZE; j += CHUNK_SIZE) { - for (lfs_size_t k = 0; k < CHUNK_SIZE; k++) { - buffer[k] = i+j+k; - } - lfs_file_write(&lfs, &file, buffer, CHUNK_SIZE) => CHUNK_SIZE; - } - - lfs_file_close(&lfs, &file) => 0; - } - lfs_unmount(&lfs) => 0; - - BENCH_START(); - lfs_mount(&lfs, cfg) => 0; - BENCH_STOP(); - - lfs_unmount(&lfs) => 0; -''' - -[cases.bench_superblocks_missing] -code = ''' - lfs_t lfs; - - BENCH_START(); - int err = lfs_mount(&lfs, cfg); - assert(err != 0); - BENCH_STOP(); -''' - -[cases.bench_superblocks_format] -code = ''' - lfs_t lfs; - - BENCH_START(); - lfs_format(&lfs, cfg) => 0; - BENCH_STOP(); -''' - diff --git a/lfs.c b/lfs.c deleted file mode 100644 index 7a4b41a8e..000000000 --- a/lfs.c +++ /dev/null @@ -1,5948 +0,0 @@ -/* - * The little filesystem - * - * Copyright (c) 2022, The littlefs authors. - * Copyright (c) 2017, Arm Limited. All rights reserved. - * SPDX-License-Identifier: BSD-3-Clause - */ -#include "lfs.h" -#include "lfs_util.h" - - -// some constants used throughout the code -#define LFS_BLOCK_NULL ((lfs_block_t)-1) -#define LFS_BLOCK_INLINE ((lfs_block_t)-2) - -enum { - LFS_OK_RELOCATED = 1, - LFS_OK_DROPPED = 2, - LFS_OK_ORPHANED = 3, -}; - -enum { - LFS_CMP_EQ = 0, - LFS_CMP_LT = 1, - LFS_CMP_GT = 2, -}; - - -/// Caching block device operations /// - -static inline void lfs_cache_drop(lfs_t *lfs, lfs_cache_t *rcache) { - // do not zero, cheaper if cache is readonly or only going to be - // written with identical data (during relocates) - (void)lfs; - rcache->block = LFS_BLOCK_NULL; -} - -static inline void lfs_cache_zero(lfs_t *lfs, lfs_cache_t *pcache) { - // zero to avoid information leak - memset(pcache->buffer, 0xff, lfs->cfg->cache_size); - pcache->block = LFS_BLOCK_NULL; -} - -static int lfs_bd_read(lfs_t *lfs, - const lfs_cache_t *pcache, lfs_cache_t *rcache, lfs_size_t hint, - lfs_block_t block, lfs_off_t off, - void *buffer, lfs_size_t size) { - uint8_t *data = buffer; - if (block >= lfs->cfg->block_count || - off+size > lfs->cfg->block_size) { - return LFS_ERR_CORRUPT; - } - - while (size > 0) { - lfs_size_t diff = size; - - if (pcache && block == pcache->block && - off < pcache->off + pcache->size) { - if (off >= pcache->off) { - // is already in pcache? - diff = lfs_min(diff, pcache->size - (off-pcache->off)); - memcpy(data, &pcache->buffer[off-pcache->off], diff); - - data += diff; - off += diff; - size -= diff; - continue; - } - - // pcache takes priority - diff = lfs_min(diff, pcache->off-off); - } - - if (block == rcache->block && - off < rcache->off + rcache->size) { - if (off >= rcache->off) { - // is already in rcache? - diff = lfs_min(diff, rcache->size - (off-rcache->off)); - memcpy(data, &rcache->buffer[off-rcache->off], diff); - - data += diff; - off += diff; - size -= diff; - continue; - } - - // rcache takes priority - diff = lfs_min(diff, rcache->off-off); - } - - if (size >= hint && off % lfs->cfg->read_size == 0 && - size >= lfs->cfg->read_size) { - // bypass cache? - diff = lfs_aligndown(diff, lfs->cfg->read_size); - int err = lfs->cfg->read(lfs->cfg, block, off, data, diff); - if (err) { - return err; - } - - data += diff; - off += diff; - size -= diff; - continue; - } - - // load to cache, first condition can no longer fail - LFS_ASSERT(block < lfs->cfg->block_count); - rcache->block = block; - rcache->off = lfs_aligndown(off, lfs->cfg->read_size); - rcache->size = lfs_min( - lfs_min( - lfs_alignup(off+hint, lfs->cfg->read_size), - lfs->cfg->block_size) - - rcache->off, - lfs->cfg->cache_size); - int err = lfs->cfg->read(lfs->cfg, rcache->block, - rcache->off, rcache->buffer, rcache->size); - LFS_ASSERT(err <= 0); - if (err) { - return err; - } - } - - return 0; -} - -static int lfs_bd_cmp(lfs_t *lfs, - const lfs_cache_t *pcache, lfs_cache_t *rcache, lfs_size_t hint, - lfs_block_t block, lfs_off_t off, - const void *buffer, lfs_size_t size) { - const uint8_t *data = buffer; - lfs_size_t diff = 0; - - for (lfs_off_t i = 0; i < size; i += diff) { - uint8_t dat[8]; - - diff = lfs_min(size-i, sizeof(dat)); - int err = lfs_bd_read(lfs, - pcache, rcache, hint-i, - block, off+i, &dat, diff); - if (err) { - return err; - } - - int res = memcmp(dat, data + i, diff); - if (res) { - return res < 0 ? LFS_CMP_LT : LFS_CMP_GT; - } - } - - return LFS_CMP_EQ; -} - -static int lfs_bd_crc(lfs_t *lfs, - const lfs_cache_t *pcache, lfs_cache_t *rcache, lfs_size_t hint, - lfs_block_t block, lfs_off_t off, lfs_size_t size, uint32_t *crc) { - lfs_size_t diff = 0; - - for (lfs_off_t i = 0; i < size; i += diff) { - uint8_t dat[8]; - diff = lfs_min(size-i, sizeof(dat)); - int err = lfs_bd_read(lfs, - pcache, rcache, hint-i, - block, off+i, &dat, diff); - if (err) { - return err; - } - - *crc = lfs_crc(*crc, &dat, diff); - } - - return 0; -} - -#ifndef LFS_READONLY -static int lfs_bd_flush(lfs_t *lfs, - lfs_cache_t *pcache, lfs_cache_t *rcache, bool validate) { - if (pcache->block != LFS_BLOCK_NULL && pcache->block != LFS_BLOCK_INLINE) { - LFS_ASSERT(pcache->block < lfs->cfg->block_count); - lfs_size_t diff = lfs_alignup(pcache->size, lfs->cfg->prog_size); - int err = lfs->cfg->prog(lfs->cfg, pcache->block, - pcache->off, pcache->buffer, diff); - LFS_ASSERT(err <= 0); - if (err) { - return err; - } - - if (validate) { - // check data on disk - lfs_cache_drop(lfs, rcache); - int res = lfs_bd_cmp(lfs, - NULL, rcache, diff, - pcache->block, pcache->off, pcache->buffer, diff); - if (res < 0) { - return res; - } - - if (res != LFS_CMP_EQ) { - return LFS_ERR_CORRUPT; - } - } - - lfs_cache_zero(lfs, pcache); - } - - return 0; -} -#endif - -#ifndef LFS_READONLY -static int lfs_bd_sync(lfs_t *lfs, - lfs_cache_t *pcache, lfs_cache_t *rcache, bool validate) { - lfs_cache_drop(lfs, rcache); - - int err = lfs_bd_flush(lfs, pcache, rcache, validate); - if (err) { - return err; - } - - err = lfs->cfg->sync(lfs->cfg); - LFS_ASSERT(err <= 0); - return err; -} -#endif - -#ifndef LFS_READONLY -static int lfs_bd_prog(lfs_t *lfs, - lfs_cache_t *pcache, lfs_cache_t *rcache, bool validate, - lfs_block_t block, lfs_off_t off, - const void *buffer, lfs_size_t size) { - const uint8_t *data = buffer; - LFS_ASSERT(block == LFS_BLOCK_INLINE || block < lfs->cfg->block_count); - LFS_ASSERT(off + size <= lfs->cfg->block_size); - - while (size > 0) { - if (block == pcache->block && - off >= pcache->off && - off < pcache->off + lfs->cfg->cache_size) { - // already fits in pcache? - lfs_size_t diff = lfs_min(size, - lfs->cfg->cache_size - (off-pcache->off)); - memcpy(&pcache->buffer[off-pcache->off], data, diff); - - data += diff; - off += diff; - size -= diff; - - pcache->size = lfs_max(pcache->size, off - pcache->off); - if (pcache->size == lfs->cfg->cache_size) { - // eagerly flush out pcache if we fill up - int err = lfs_bd_flush(lfs, pcache, rcache, validate); - if (err) { - return err; - } - } - - continue; - } - - // pcache must have been flushed, either by programming and - // entire block or manually flushing the pcache - LFS_ASSERT(pcache->block == LFS_BLOCK_NULL); - - // prepare pcache, first condition can no longer fail - pcache->block = block; - pcache->off = lfs_aligndown(off, lfs->cfg->prog_size); - pcache->size = 0; - } - - return 0; -} -#endif - -#ifndef LFS_READONLY -static int lfs_bd_erase(lfs_t *lfs, lfs_block_t block) { - LFS_ASSERT(block < lfs->cfg->block_count); - int err = lfs->cfg->erase(lfs->cfg, block); - LFS_ASSERT(err <= 0); - return err; -} -#endif - - -/// Small type-level utilities /// -// operations on block pairs -static inline void lfs_pair_swap(lfs_block_t pair[2]) { - lfs_block_t t = pair[0]; - pair[0] = pair[1]; - pair[1] = t; -} - -static inline bool lfs_pair_isnull(const lfs_block_t pair[2]) { - return pair[0] == LFS_BLOCK_NULL || pair[1] == LFS_BLOCK_NULL; -} - -static inline int lfs_pair_cmp( - const lfs_block_t paira[2], - const lfs_block_t pairb[2]) { - return !(paira[0] == pairb[0] || paira[1] == pairb[1] || - paira[0] == pairb[1] || paira[1] == pairb[0]); -} - -#ifndef LFS_READONLY -static inline bool lfs_pair_sync( - const lfs_block_t paira[2], - const lfs_block_t pairb[2]) { - return (paira[0] == pairb[0] && paira[1] == pairb[1]) || - (paira[0] == pairb[1] && paira[1] == pairb[0]); -} -#endif - -static inline void lfs_pair_fromle32(lfs_block_t pair[2]) { - pair[0] = lfs_fromle32(pair[0]); - pair[1] = lfs_fromle32(pair[1]); -} - -#ifndef LFS_READONLY -static inline void lfs_pair_tole32(lfs_block_t pair[2]) { - pair[0] = lfs_tole32(pair[0]); - pair[1] = lfs_tole32(pair[1]); -} -#endif - -// operations on 32-bit entry tags -typedef uint32_t lfs_tag_t; -typedef int32_t lfs_stag_t; - -#define LFS_MKTAG(type, id, size) \ - (((lfs_tag_t)(type) << 20) | ((lfs_tag_t)(id) << 10) | (lfs_tag_t)(size)) - -#define LFS_MKTAG_IF(cond, type, id, size) \ - ((cond) ? LFS_MKTAG(type, id, size) : LFS_MKTAG(LFS_FROM_NOOP, 0, 0)) - -#define LFS_MKTAG_IF_ELSE(cond, type1, id1, size1, type2, id2, size2) \ - ((cond) ? LFS_MKTAG(type1, id1, size1) : LFS_MKTAG(type2, id2, size2)) - -static inline bool lfs_tag_isvalid(lfs_tag_t tag) { - return !(tag & 0x80000000); -} - -static inline bool lfs_tag_isdelete(lfs_tag_t tag) { - return ((int32_t)(tag << 22) >> 22) == -1; -} - -static inline uint16_t lfs_tag_type1(lfs_tag_t tag) { - return (tag & 0x70000000) >> 20; -} - -static inline uint16_t lfs_tag_type2(lfs_tag_t tag) { - return (tag & 0x78000000) >> 20; -} - -static inline uint16_t lfs_tag_type3(lfs_tag_t tag) { - return (tag & 0x7ff00000) >> 20; -} - -static inline uint8_t lfs_tag_chunk(lfs_tag_t tag) { - return (tag & 0x0ff00000) >> 20; -} - -static inline int8_t lfs_tag_splice(lfs_tag_t tag) { - return (int8_t)lfs_tag_chunk(tag); -} - -static inline uint16_t lfs_tag_id(lfs_tag_t tag) { - return (tag & 0x000ffc00) >> 10; -} - -static inline lfs_size_t lfs_tag_size(lfs_tag_t tag) { - return tag & 0x000003ff; -} - -static inline lfs_size_t lfs_tag_dsize(lfs_tag_t tag) { - return sizeof(tag) + lfs_tag_size(tag + lfs_tag_isdelete(tag)); -} - -// operations on attributes in attribute lists -struct lfs_mattr { - lfs_tag_t tag; - const void *buffer; -}; - -struct lfs_diskoff { - lfs_block_t block; - lfs_off_t off; -}; - -#define LFS_MKATTRS(...) \ - (struct lfs_mattr[]){__VA_ARGS__}, \ - sizeof((struct lfs_mattr[]){__VA_ARGS__}) / sizeof(struct lfs_mattr) - -// operations on global state -static inline void lfs_gstate_xor(lfs_gstate_t *a, const lfs_gstate_t *b) { - for (int i = 0; i < 3; i++) { - ((uint32_t*)a)[i] ^= ((const uint32_t*)b)[i]; - } -} - -static inline bool lfs_gstate_iszero(const lfs_gstate_t *a) { - for (int i = 0; i < 3; i++) { - if (((uint32_t*)a)[i] != 0) { - return false; - } - } - return true; -} - -#ifndef LFS_READONLY -static inline bool lfs_gstate_hasorphans(const lfs_gstate_t *a) { - return lfs_tag_size(a->tag); -} - -static inline uint8_t lfs_gstate_getorphans(const lfs_gstate_t *a) { - return lfs_tag_size(a->tag); -} - -static inline bool lfs_gstate_hasmove(const lfs_gstate_t *a) { - return lfs_tag_type1(a->tag); -} -#endif - -static inline bool lfs_gstate_hasmovehere(const lfs_gstate_t *a, - const lfs_block_t *pair) { - return lfs_tag_type1(a->tag) && lfs_pair_cmp(a->pair, pair) == 0; -} - -static inline void lfs_gstate_fromle32(lfs_gstate_t *a) { - a->tag = lfs_fromle32(a->tag); - a->pair[0] = lfs_fromle32(a->pair[0]); - a->pair[1] = lfs_fromle32(a->pair[1]); -} - -#ifndef LFS_READONLY -static inline void lfs_gstate_tole32(lfs_gstate_t *a) { - a->tag = lfs_tole32(a->tag); - a->pair[0] = lfs_tole32(a->pair[0]); - a->pair[1] = lfs_tole32(a->pair[1]); -} -#endif - -// operations on forward-CRCs used to track erased state -struct lfs_fcrc { - lfs_size_t size; - uint32_t crc; -}; - -static void lfs_fcrc_fromle32(struct lfs_fcrc *fcrc) { - fcrc->size = lfs_fromle32(fcrc->size); - fcrc->crc = lfs_fromle32(fcrc->crc); -} - -#ifndef LFS_READONLY -static void lfs_fcrc_tole32(struct lfs_fcrc *fcrc) { - fcrc->size = lfs_tole32(fcrc->size); - fcrc->crc = lfs_tole32(fcrc->crc); -} -#endif - -// other endianness operations -static void lfs_ctz_fromle32(struct lfs_ctz *ctz) { - ctz->head = lfs_fromle32(ctz->head); - ctz->size = lfs_fromle32(ctz->size); -} - -#ifndef LFS_READONLY -static void lfs_ctz_tole32(struct lfs_ctz *ctz) { - ctz->head = lfs_tole32(ctz->head); - ctz->size = lfs_tole32(ctz->size); -} -#endif - -static inline void lfs_superblock_fromle32(lfs_superblock_t *superblock) { - superblock->version = lfs_fromle32(superblock->version); - superblock->block_size = lfs_fromle32(superblock->block_size); - superblock->block_count = lfs_fromle32(superblock->block_count); - superblock->name_max = lfs_fromle32(superblock->name_max); - superblock->file_max = lfs_fromle32(superblock->file_max); - superblock->attr_max = lfs_fromle32(superblock->attr_max); -} - -#ifndef LFS_READONLY -static inline void lfs_superblock_tole32(lfs_superblock_t *superblock) { - superblock->version = lfs_tole32(superblock->version); - superblock->block_size = lfs_tole32(superblock->block_size); - superblock->block_count = lfs_tole32(superblock->block_count); - superblock->name_max = lfs_tole32(superblock->name_max); - superblock->file_max = lfs_tole32(superblock->file_max); - superblock->attr_max = lfs_tole32(superblock->attr_max); -} -#endif - -#ifndef LFS_NO_ASSERT -static bool lfs_mlist_isopen(struct lfs_mlist *head, - struct lfs_mlist *node) { - for (struct lfs_mlist **p = &head; *p; p = &(*p)->next) { - if (*p == (struct lfs_mlist*)node) { - return true; - } - } - - return false; -} -#endif - -static void lfs_mlist_remove(lfs_t *lfs, struct lfs_mlist *mlist) { - for (struct lfs_mlist **p = &lfs->mlist; *p; p = &(*p)->next) { - if (*p == mlist) { - *p = (*p)->next; - break; - } - } -} - -static void lfs_mlist_append(lfs_t *lfs, struct lfs_mlist *mlist) { - mlist->next = lfs->mlist; - lfs->mlist = mlist; -} - - -/// Internal operations predeclared here /// -#ifndef LFS_READONLY -static int lfs_dir_commit(lfs_t *lfs, lfs_mdir_t *dir, - const struct lfs_mattr *attrs, int attrcount); -static int lfs_dir_compact(lfs_t *lfs, - lfs_mdir_t *dir, const struct lfs_mattr *attrs, int attrcount, - lfs_mdir_t *source, uint16_t begin, uint16_t end); -static lfs_ssize_t lfs_file_flushedwrite(lfs_t *lfs, lfs_file_t *file, - const void *buffer, lfs_size_t size); -static lfs_ssize_t lfs_file_rawwrite(lfs_t *lfs, lfs_file_t *file, - const void *buffer, lfs_size_t size); -static int lfs_file_rawsync(lfs_t *lfs, lfs_file_t *file); -static int lfs_file_outline(lfs_t *lfs, lfs_file_t *file); -static int lfs_file_flush(lfs_t *lfs, lfs_file_t *file); - -static int lfs_fs_deorphan(lfs_t *lfs, bool powerloss); -static int lfs_fs_preporphans(lfs_t *lfs, int8_t orphans); -static void lfs_fs_prepmove(lfs_t *lfs, - uint16_t id, const lfs_block_t pair[2]); -static int lfs_fs_pred(lfs_t *lfs, const lfs_block_t dir[2], - lfs_mdir_t *pdir); -static lfs_stag_t lfs_fs_parent(lfs_t *lfs, const lfs_block_t dir[2], - lfs_mdir_t *parent); -static int lfs_fs_forceconsistency(lfs_t *lfs); -#endif - -#ifdef LFS_MIGRATE -static int lfs1_traverse(lfs_t *lfs, - int (*cb)(void*, lfs_block_t), void *data); -#endif - -static int lfs_dir_rawrewind(lfs_t *lfs, lfs_dir_t *dir); - -static lfs_ssize_t lfs_file_flushedread(lfs_t *lfs, lfs_file_t *file, - void *buffer, lfs_size_t size); -static lfs_ssize_t lfs_file_rawread(lfs_t *lfs, lfs_file_t *file, - void *buffer, lfs_size_t size); -static int lfs_file_rawclose(lfs_t *lfs, lfs_file_t *file); -static lfs_soff_t lfs_file_rawsize(lfs_t *lfs, lfs_file_t *file); - -static lfs_ssize_t lfs_fs_rawsize(lfs_t *lfs); -static int lfs_fs_rawtraverse(lfs_t *lfs, - int (*cb)(void *data, lfs_block_t block), void *data, - bool includeorphans); - -static int lfs_deinit(lfs_t *lfs); -static int lfs_rawunmount(lfs_t *lfs); - - -/// Block allocator /// -#ifndef LFS_READONLY -static int lfs_alloc_lookahead(void *p, lfs_block_t block) { - lfs_t *lfs = (lfs_t*)p; - lfs_block_t off = ((block - lfs->free.off) - + lfs->cfg->block_count) % lfs->cfg->block_count; - - if (off < lfs->free.size) { - lfs->free.buffer[off / 32] |= 1U << (off % 32); - } - - return 0; -} -#endif - -// indicate allocated blocks have been committed into the filesystem, this -// is to prevent blocks from being garbage collected in the middle of a -// commit operation -static void lfs_alloc_ack(lfs_t *lfs) { - lfs->free.ack = lfs->cfg->block_count; -} - -// drop the lookahead buffer, this is done during mounting and failed -// traversals in order to avoid invalid lookahead state -static void lfs_alloc_drop(lfs_t *lfs) { - lfs->free.size = 0; - lfs->free.i = 0; - lfs_alloc_ack(lfs); -} - -#ifndef LFS_READONLY -static int lfs_alloc(lfs_t *lfs, lfs_block_t *block) { - while (true) { - while (lfs->free.i != lfs->free.size) { - lfs_block_t off = lfs->free.i; - lfs->free.i += 1; - lfs->free.ack -= 1; - - if (!(lfs->free.buffer[off / 32] & (1U << (off % 32)))) { - // found a free block - *block = (lfs->free.off + off) % lfs->cfg->block_count; - - // eagerly find next off so an alloc ack can - // discredit old lookahead blocks - while (lfs->free.i != lfs->free.size && - (lfs->free.buffer[lfs->free.i / 32] - & (1U << (lfs->free.i % 32)))) { - lfs->free.i += 1; - lfs->free.ack -= 1; - } - - return 0; - } - } - - // check if we have looked at all blocks since last ack - if (lfs->free.ack == 0) { - LFS_ERROR("No more free space %"PRIu32, - lfs->free.i + lfs->free.off); - return LFS_ERR_NOSPC; - } - - lfs->free.off = (lfs->free.off + lfs->free.size) - % lfs->cfg->block_count; - lfs->free.size = lfs_min(8*lfs->cfg->lookahead_size, lfs->free.ack); - lfs->free.i = 0; - - // find mask of free blocks from tree - memset(lfs->free.buffer, 0, lfs->cfg->lookahead_size); - int err = lfs_fs_rawtraverse(lfs, lfs_alloc_lookahead, lfs, true); - if (err) { - lfs_alloc_drop(lfs); - return err; - } - } -} -#endif - -/// Metadata pair and directory operations /// -static lfs_stag_t lfs_dir_getslice(lfs_t *lfs, const lfs_mdir_t *dir, - lfs_tag_t gmask, lfs_tag_t gtag, - lfs_off_t goff, void *gbuffer, lfs_size_t gsize) { - lfs_off_t off = dir->off; - lfs_tag_t ntag = dir->etag; - lfs_stag_t gdiff = 0; - - if (lfs_gstate_hasmovehere(&lfs->gdisk, dir->pair) && - lfs_tag_id(gmask) != 0 && - lfs_tag_id(lfs->gdisk.tag) <= lfs_tag_id(gtag)) { - // synthetic moves - gdiff -= LFS_MKTAG(0, 1, 0); - } - - // iterate over dir block backwards (for faster lookups) - while (off >= sizeof(lfs_tag_t) + lfs_tag_dsize(ntag)) { - off -= lfs_tag_dsize(ntag); - lfs_tag_t tag = ntag; - int err = lfs_bd_read(lfs, - NULL, &lfs->rcache, sizeof(ntag), - dir->pair[0], off, &ntag, sizeof(ntag)); - if (err) { - return err; - } - - ntag = (lfs_frombe32(ntag) ^ tag) & 0x7fffffff; - - if (lfs_tag_id(gmask) != 0 && - lfs_tag_type1(tag) == LFS_TYPE_SPLICE && - lfs_tag_id(tag) <= lfs_tag_id(gtag - gdiff)) { - if (tag == (LFS_MKTAG(LFS_TYPE_CREATE, 0, 0) | - (LFS_MKTAG(0, 0x3ff, 0) & (gtag - gdiff)))) { - // found where we were created - return LFS_ERR_NOENT; - } - - // move around splices - gdiff += LFS_MKTAG(0, lfs_tag_splice(tag), 0); - } - - if ((gmask & tag) == (gmask & (gtag - gdiff))) { - if (lfs_tag_isdelete(tag)) { - return LFS_ERR_NOENT; - } - - lfs_size_t diff = lfs_min(lfs_tag_size(tag), gsize); - err = lfs_bd_read(lfs, - NULL, &lfs->rcache, diff, - dir->pair[0], off+sizeof(tag)+goff, gbuffer, diff); - if (err) { - return err; - } - - memset((uint8_t*)gbuffer + diff, 0, gsize - diff); - - return tag + gdiff; - } - } - - return LFS_ERR_NOENT; -} - -static lfs_stag_t lfs_dir_get(lfs_t *lfs, const lfs_mdir_t *dir, - lfs_tag_t gmask, lfs_tag_t gtag, void *buffer) { - return lfs_dir_getslice(lfs, dir, - gmask, gtag, - 0, buffer, lfs_tag_size(gtag)); -} - -static int lfs_dir_getread(lfs_t *lfs, const lfs_mdir_t *dir, - const lfs_cache_t *pcache, lfs_cache_t *rcache, lfs_size_t hint, - lfs_tag_t gmask, lfs_tag_t gtag, - lfs_off_t off, void *buffer, lfs_size_t size) { - uint8_t *data = buffer; - if (off+size > lfs->cfg->block_size) { - return LFS_ERR_CORRUPT; - } - - while (size > 0) { - lfs_size_t diff = size; - - if (pcache && pcache->block == LFS_BLOCK_INLINE && - off < pcache->off + pcache->size) { - if (off >= pcache->off) { - // is already in pcache? - diff = lfs_min(diff, pcache->size - (off-pcache->off)); - memcpy(data, &pcache->buffer[off-pcache->off], diff); - - data += diff; - off += diff; - size -= diff; - continue; - } - - // pcache takes priority - diff = lfs_min(diff, pcache->off-off); - } - - if (rcache->block == LFS_BLOCK_INLINE && - off < rcache->off + rcache->size) { - if (off >= rcache->off) { - // is already in rcache? - diff = lfs_min(diff, rcache->size - (off-rcache->off)); - memcpy(data, &rcache->buffer[off-rcache->off], diff); - - data += diff; - off += diff; - size -= diff; - continue; - } - - // rcache takes priority - diff = lfs_min(diff, rcache->off-off); - } - - // load to cache, first condition can no longer fail - rcache->block = LFS_BLOCK_INLINE; - rcache->off = lfs_aligndown(off, lfs->cfg->read_size); - rcache->size = lfs_min(lfs_alignup(off+hint, lfs->cfg->read_size), - lfs->cfg->cache_size); - int err = lfs_dir_getslice(lfs, dir, gmask, gtag, - rcache->off, rcache->buffer, rcache->size); - if (err < 0) { - return err; - } - } - - return 0; -} - -#ifndef LFS_READONLY -static int lfs_dir_traverse_filter(void *p, - lfs_tag_t tag, const void *buffer) { - lfs_tag_t *filtertag = p; - (void)buffer; - - // which mask depends on unique bit in tag structure - uint32_t mask = (tag & LFS_MKTAG(0x100, 0, 0)) - ? LFS_MKTAG(0x7ff, 0x3ff, 0) - : LFS_MKTAG(0x700, 0x3ff, 0); - - // check for redundancy - if ((mask & tag) == (mask & *filtertag) || - lfs_tag_isdelete(*filtertag) || - (LFS_MKTAG(0x7ff, 0x3ff, 0) & tag) == ( - LFS_MKTAG(LFS_TYPE_DELETE, 0, 0) | - (LFS_MKTAG(0, 0x3ff, 0) & *filtertag))) { - *filtertag = LFS_MKTAG(LFS_FROM_NOOP, 0, 0); - return true; - } - - // check if we need to adjust for created/deleted tags - if (lfs_tag_type1(tag) == LFS_TYPE_SPLICE && - lfs_tag_id(tag) <= lfs_tag_id(*filtertag)) { - *filtertag += LFS_MKTAG(0, lfs_tag_splice(tag), 0); - } - - return false; -} -#endif - -#ifndef LFS_READONLY -// maximum recursive depth of lfs_dir_traverse, the deepest call: -// -// traverse with commit -// '-> traverse with move -// '-> traverse with filter -// -#define LFS_DIR_TRAVERSE_DEPTH 3 - -struct lfs_dir_traverse { - const lfs_mdir_t *dir; - lfs_off_t off; - lfs_tag_t ptag; - const struct lfs_mattr *attrs; - int attrcount; - - lfs_tag_t tmask; - lfs_tag_t ttag; - uint16_t begin; - uint16_t end; - int16_t diff; - - int (*cb)(void *data, lfs_tag_t tag, const void *buffer); - void *data; - - lfs_tag_t tag; - const void *buffer; - struct lfs_diskoff disk; -}; - -static int lfs_dir_traverse(lfs_t *lfs, - const lfs_mdir_t *dir, lfs_off_t off, lfs_tag_t ptag, - const struct lfs_mattr *attrs, int attrcount, - lfs_tag_t tmask, lfs_tag_t ttag, - uint16_t begin, uint16_t end, int16_t diff, - int (*cb)(void *data, lfs_tag_t tag, const void *buffer), void *data) { - // This function in inherently recursive, but bounded. To allow tool-based - // analysis without unnecessary code-cost we use an explicit stack - struct lfs_dir_traverse stack[LFS_DIR_TRAVERSE_DEPTH-1]; - unsigned sp = 0; - int res; - - // iterate over directory and attrs - lfs_tag_t tag; - const void *buffer; - struct lfs_diskoff disk; - while (true) { - { - if (off+lfs_tag_dsize(ptag) < dir->off) { - off += lfs_tag_dsize(ptag); - int err = lfs_bd_read(lfs, - NULL, &lfs->rcache, sizeof(tag), - dir->pair[0], off, &tag, sizeof(tag)); - if (err) { - return err; - } - - tag = (lfs_frombe32(tag) ^ ptag) | 0x80000000; - disk.block = dir->pair[0]; - disk.off = off+sizeof(lfs_tag_t); - buffer = &disk; - ptag = tag; - } else if (attrcount > 0) { - tag = attrs[0].tag; - buffer = attrs[0].buffer; - attrs += 1; - attrcount -= 1; - } else { - // finished traversal, pop from stack? - res = 0; - break; - } - - // do we need to filter? - lfs_tag_t mask = LFS_MKTAG(0x7ff, 0, 0); - if ((mask & tmask & tag) != (mask & tmask & ttag)) { - continue; - } - - if (lfs_tag_id(tmask) != 0) { - LFS_ASSERT(sp < LFS_DIR_TRAVERSE_DEPTH); - // recurse, scan for duplicates, and update tag based on - // creates/deletes - stack[sp] = (struct lfs_dir_traverse){ - .dir = dir, - .off = off, - .ptag = ptag, - .attrs = attrs, - .attrcount = attrcount, - .tmask = tmask, - .ttag = ttag, - .begin = begin, - .end = end, - .diff = diff, - .cb = cb, - .data = data, - .tag = tag, - .buffer = buffer, - .disk = disk, - }; - sp += 1; - - tmask = 0; - ttag = 0; - begin = 0; - end = 0; - diff = 0; - cb = lfs_dir_traverse_filter; - data = &stack[sp-1].tag; - continue; - } - } - -popped: - // in filter range? - if (lfs_tag_id(tmask) != 0 && - !(lfs_tag_id(tag) >= begin && lfs_tag_id(tag) < end)) { - continue; - } - - // handle special cases for mcu-side operations - if (lfs_tag_type3(tag) == LFS_FROM_NOOP) { - // do nothing - } else if (lfs_tag_type3(tag) == LFS_FROM_MOVE) { - // Without this condition, lfs_dir_traverse can exhibit an - // extremely expensive O(n^3) of nested loops when renaming. - // This happens because lfs_dir_traverse tries to filter tags by - // the tags in the source directory, triggering a second - // lfs_dir_traverse with its own filter operation. - // - // traverse with commit - // '-> traverse with filter - // '-> traverse with move - // '-> traverse with filter - // - // However we don't actually care about filtering the second set of - // tags, since duplicate tags have no effect when filtering. - // - // This check skips this unnecessary recursive filtering explicitly, - // reducing this runtime from O(n^3) to O(n^2). - if (cb == lfs_dir_traverse_filter) { - continue; - } - - // recurse into move - stack[sp] = (struct lfs_dir_traverse){ - .dir = dir, - .off = off, - .ptag = ptag, - .attrs = attrs, - .attrcount = attrcount, - .tmask = tmask, - .ttag = ttag, - .begin = begin, - .end = end, - .diff = diff, - .cb = cb, - .data = data, - .tag = LFS_MKTAG(LFS_FROM_NOOP, 0, 0), - }; - sp += 1; - - uint16_t fromid = lfs_tag_size(tag); - uint16_t toid = lfs_tag_id(tag); - dir = buffer; - off = 0; - ptag = 0xffffffff; - attrs = NULL; - attrcount = 0; - tmask = LFS_MKTAG(0x600, 0x3ff, 0); - ttag = LFS_MKTAG(LFS_TYPE_STRUCT, 0, 0); - begin = fromid; - end = fromid+1; - diff = toid-fromid+diff; - } else if (lfs_tag_type3(tag) == LFS_FROM_USERATTRS) { - for (unsigned i = 0; i < lfs_tag_size(tag); i++) { - const struct lfs_attr *a = buffer; - res = cb(data, LFS_MKTAG(LFS_TYPE_USERATTR + a[i].type, - lfs_tag_id(tag) + diff, a[i].size), a[i].buffer); - if (res < 0) { - return res; - } - - if (res) { - break; - } - } - } else { - res = cb(data, tag + LFS_MKTAG(0, diff, 0), buffer); - if (res < 0) { - return res; - } - - if (res) { - break; - } - } - } - - if (sp > 0) { - // pop from the stack and return, fortunately all pops share - // a destination - dir = stack[sp-1].dir; - off = stack[sp-1].off; - ptag = stack[sp-1].ptag; - attrs = stack[sp-1].attrs; - attrcount = stack[sp-1].attrcount; - tmask = stack[sp-1].tmask; - ttag = stack[sp-1].ttag; - begin = stack[sp-1].begin; - end = stack[sp-1].end; - diff = stack[sp-1].diff; - cb = stack[sp-1].cb; - data = stack[sp-1].data; - tag = stack[sp-1].tag; - buffer = stack[sp-1].buffer; - disk = stack[sp-1].disk; - sp -= 1; - goto popped; - } else { - return res; - } -} -#endif - -static lfs_stag_t lfs_dir_fetchmatch(lfs_t *lfs, - lfs_mdir_t *dir, const lfs_block_t pair[2], - lfs_tag_t fmask, lfs_tag_t ftag, uint16_t *id, - int (*cb)(void *data, lfs_tag_t tag, const void *buffer), void *data) { - // we can find tag very efficiently during a fetch, since we're already - // scanning the entire directory - lfs_stag_t besttag = -1; - - // if either block address is invalid we return LFS_ERR_CORRUPT here, - // otherwise later writes to the pair could fail - if (pair[0] >= lfs->cfg->block_count || pair[1] >= lfs->cfg->block_count) { - return LFS_ERR_CORRUPT; - } - - // find the block with the most recent revision - uint32_t revs[2] = {0, 0}; - int r = 0; - for (int i = 0; i < 2; i++) { - int err = lfs_bd_read(lfs, - NULL, &lfs->rcache, sizeof(revs[i]), - pair[i], 0, &revs[i], sizeof(revs[i])); - revs[i] = lfs_fromle32(revs[i]); - if (err && err != LFS_ERR_CORRUPT) { - return err; - } - - if (err != LFS_ERR_CORRUPT && - lfs_scmp(revs[i], revs[(i+1)%2]) > 0) { - r = i; - } - } - - dir->pair[0] = pair[(r+0)%2]; - dir->pair[1] = pair[(r+1)%2]; - dir->rev = revs[(r+0)%2]; - dir->off = 0; // nonzero = found some commits - - // now scan tags to fetch the actual dir and find possible match - for (int i = 0; i < 2; i++) { - lfs_off_t off = 0; - lfs_tag_t ptag = 0xffffffff; - - uint16_t tempcount = 0; - lfs_block_t temptail[2] = {LFS_BLOCK_NULL, LFS_BLOCK_NULL}; - bool tempsplit = false; - lfs_stag_t tempbesttag = besttag; - - // assume not erased until proven otherwise - bool maybeerased = false; - bool hasfcrc = false; - struct lfs_fcrc fcrc; - - dir->rev = lfs_tole32(dir->rev); - uint32_t crc = lfs_crc(0xffffffff, &dir->rev, sizeof(dir->rev)); - dir->rev = lfs_fromle32(dir->rev); - - while (true) { - // extract next tag - lfs_tag_t tag; - off += lfs_tag_dsize(ptag); - int err = lfs_bd_read(lfs, - NULL, &lfs->rcache, lfs->cfg->block_size, - dir->pair[0], off, &tag, sizeof(tag)); - if (err) { - if (err == LFS_ERR_CORRUPT) { - // can't continue? - break; - } - return err; - } - - crc = lfs_crc(crc, &tag, sizeof(tag)); - tag = lfs_frombe32(tag) ^ ptag; - - // next commit not yet programmed? - if (!lfs_tag_isvalid(tag)) { - maybeerased = true; - break; - // out of range? - } else if (off + lfs_tag_dsize(tag) > lfs->cfg->block_size) { - break; - } - - ptag = tag; - - if (lfs_tag_type2(tag) == LFS_TYPE_CCRC) { - // check the crc attr - uint32_t dcrc; - err = lfs_bd_read(lfs, - NULL, &lfs->rcache, lfs->cfg->block_size, - dir->pair[0], off+sizeof(tag), &dcrc, sizeof(dcrc)); - if (err) { - if (err == LFS_ERR_CORRUPT) { - break; - } - return err; - } - dcrc = lfs_fromle32(dcrc); - - if (crc != dcrc) { - break; - } - - // reset the next bit if we need to - ptag ^= (lfs_tag_t)(lfs_tag_chunk(tag) & 1U) << 31; - - // toss our crc into the filesystem seed for - // pseudorandom numbers, note we use another crc here - // as a collection function because it is sufficiently - // random and convenient - lfs->seed = lfs_crc(lfs->seed, &crc, sizeof(crc)); - - // update with what's found so far - besttag = tempbesttag; - dir->off = off + lfs_tag_dsize(tag); - dir->etag = ptag; - dir->count = tempcount; - dir->tail[0] = temptail[0]; - dir->tail[1] = temptail[1]; - dir->split = tempsplit; - - // reset crc - crc = 0xffffffff; - continue; - } - - // fcrc is only valid when last tag was a crc - hasfcrc = false; - - // crc the entry first, hopefully leaving it in the cache - err = lfs_bd_crc(lfs, - NULL, &lfs->rcache, lfs->cfg->block_size, - dir->pair[0], off+sizeof(tag), - lfs_tag_dsize(tag)-sizeof(tag), &crc); - if (err) { - if (err == LFS_ERR_CORRUPT) { - break; - } - return err; - } - - // directory modification tags? - if (lfs_tag_type1(tag) == LFS_TYPE_NAME) { - // increase count of files if necessary - if (lfs_tag_id(tag) >= tempcount) { - tempcount = lfs_tag_id(tag) + 1; - } - } else if (lfs_tag_type1(tag) == LFS_TYPE_SPLICE) { - tempcount += lfs_tag_splice(tag); - - if (tag == (LFS_MKTAG(LFS_TYPE_DELETE, 0, 0) | - (LFS_MKTAG(0, 0x3ff, 0) & tempbesttag))) { - tempbesttag |= 0x80000000; - } else if (tempbesttag != -1 && - lfs_tag_id(tag) <= lfs_tag_id(tempbesttag)) { - tempbesttag += LFS_MKTAG(0, lfs_tag_splice(tag), 0); - } - } else if (lfs_tag_type1(tag) == LFS_TYPE_TAIL) { - tempsplit = (lfs_tag_chunk(tag) & 1); - - err = lfs_bd_read(lfs, - NULL, &lfs->rcache, lfs->cfg->block_size, - dir->pair[0], off+sizeof(tag), &temptail, 8); - if (err) { - if (err == LFS_ERR_CORRUPT) { - break; - } - return err; - } - lfs_pair_fromle32(temptail); - } else if (lfs_tag_type3(tag) == LFS_TYPE_FCRC) { - err = lfs_bd_read(lfs, - NULL, &lfs->rcache, lfs->cfg->block_size, - dir->pair[0], off+sizeof(tag), - &fcrc, sizeof(fcrc)); - if (err) { - if (err == LFS_ERR_CORRUPT) { - break; - } - } - - lfs_fcrc_fromle32(&fcrc); - hasfcrc = true; - } - - // found a match for our fetcher? - if ((fmask & tag) == (fmask & ftag)) { - int res = cb(data, tag, &(struct lfs_diskoff){ - dir->pair[0], off+sizeof(tag)}); - if (res < 0) { - if (res == LFS_ERR_CORRUPT) { - break; - } - return res; - } - - if (res == LFS_CMP_EQ) { - // found a match - tempbesttag = tag; - } else if ((LFS_MKTAG(0x7ff, 0x3ff, 0) & tag) == - (LFS_MKTAG(0x7ff, 0x3ff, 0) & tempbesttag)) { - // found an identical tag, but contents didn't match - // this must mean that our besttag has been overwritten - tempbesttag = -1; - } else if (res == LFS_CMP_GT && - lfs_tag_id(tag) <= lfs_tag_id(tempbesttag)) { - // found a greater match, keep track to keep things sorted - tempbesttag = tag | 0x80000000; - } - } - } - - // found no valid commits? - if (dir->off == 0) { - // try the other block? - lfs_pair_swap(dir->pair); - dir->rev = revs[(r+1)%2]; - continue; - } - - // did we end on a valid commit? we may have an erased block - dir->erased = false; - if (maybeerased && hasfcrc && dir->off % lfs->cfg->prog_size == 0) { - // check for an fcrc matching the next prog's erased state, if - // this failed most likely a previous prog was interrupted, we - // need a new erase - uint32_t fcrc_ = 0xffffffff; - int err = lfs_bd_crc(lfs, - NULL, &lfs->rcache, lfs->cfg->block_size, - dir->pair[0], dir->off, fcrc.size, &fcrc_); - if (err && err != LFS_ERR_CORRUPT) { - return err; - } - - // found beginning of erased part? - dir->erased = (fcrc_ == fcrc.crc); - } - - // synthetic move - if (lfs_gstate_hasmovehere(&lfs->gdisk, dir->pair)) { - if (lfs_tag_id(lfs->gdisk.tag) == lfs_tag_id(besttag)) { - besttag |= 0x80000000; - } else if (besttag != -1 && - lfs_tag_id(lfs->gdisk.tag) < lfs_tag_id(besttag)) { - besttag -= LFS_MKTAG(0, 1, 0); - } - } - - // found tag? or found best id? - if (id) { - *id = lfs_min(lfs_tag_id(besttag), dir->count); - } - - if (lfs_tag_isvalid(besttag)) { - return besttag; - } else if (lfs_tag_id(besttag) < dir->count) { - return LFS_ERR_NOENT; - } else { - return 0; - } - } - - LFS_ERROR("Corrupted dir pair at {0x%"PRIx32", 0x%"PRIx32"}", - dir->pair[0], dir->pair[1]); - return LFS_ERR_CORRUPT; -} - -static int lfs_dir_fetch(lfs_t *lfs, - lfs_mdir_t *dir, const lfs_block_t pair[2]) { - // note, mask=-1, tag=-1 can never match a tag since this - // pattern has the invalid bit set - return (int)lfs_dir_fetchmatch(lfs, dir, pair, - (lfs_tag_t)-1, (lfs_tag_t)-1, NULL, NULL, NULL); -} - -static int lfs_dir_getgstate(lfs_t *lfs, const lfs_mdir_t *dir, - lfs_gstate_t *gstate) { - lfs_gstate_t temp; - lfs_stag_t res = lfs_dir_get(lfs, dir, LFS_MKTAG(0x7ff, 0, 0), - LFS_MKTAG(LFS_TYPE_MOVESTATE, 0, sizeof(temp)), &temp); - if (res < 0 && res != LFS_ERR_NOENT) { - return res; - } - - if (res != LFS_ERR_NOENT) { - // xor together to find resulting gstate - lfs_gstate_fromle32(&temp); - lfs_gstate_xor(gstate, &temp); - } - - return 0; -} - -static int lfs_dir_getinfo(lfs_t *lfs, lfs_mdir_t *dir, - uint16_t id, struct lfs_info *info) { - if (id == 0x3ff) { - // special case for root - strcpy(info->name, "/"); - info->type = LFS_TYPE_DIR; - return 0; - } - - lfs_stag_t tag = lfs_dir_get(lfs, dir, LFS_MKTAG(0x780, 0x3ff, 0), - LFS_MKTAG(LFS_TYPE_NAME, id, lfs->name_max+1), info->name); - if (tag < 0) { - return (int)tag; - } - - info->type = lfs_tag_type3(tag); - - struct lfs_ctz ctz; - tag = lfs_dir_get(lfs, dir, LFS_MKTAG(0x700, 0x3ff, 0), - LFS_MKTAG(LFS_TYPE_STRUCT, id, sizeof(ctz)), &ctz); - if (tag < 0) { - return (int)tag; - } - lfs_ctz_fromle32(&ctz); - - if (lfs_tag_type3(tag) == LFS_TYPE_CTZSTRUCT) { - info->size = ctz.size; - } else if (lfs_tag_type3(tag) == LFS_TYPE_INLINESTRUCT) { - info->size = lfs_tag_size(tag); - } - - return 0; -} - -struct lfs_dir_find_match { - lfs_t *lfs; - const void *name; - lfs_size_t size; -}; - -static int lfs_dir_find_match(void *data, - lfs_tag_t tag, const void *buffer) { - struct lfs_dir_find_match *name = data; - lfs_t *lfs = name->lfs; - const struct lfs_diskoff *disk = buffer; - - // compare with disk - lfs_size_t diff = lfs_min(name->size, lfs_tag_size(tag)); - int res = lfs_bd_cmp(lfs, - NULL, &lfs->rcache, diff, - disk->block, disk->off, name->name, diff); - if (res != LFS_CMP_EQ) { - return res; - } - - // only equal if our size is still the same - if (name->size != lfs_tag_size(tag)) { - return (name->size < lfs_tag_size(tag)) ? LFS_CMP_LT : LFS_CMP_GT; - } - - // found a match! - return LFS_CMP_EQ; -} - -static lfs_stag_t lfs_dir_find(lfs_t *lfs, lfs_mdir_t *dir, - const char **path, uint16_t *id) { - // we reduce path to a single name if we can find it - const char *name = *path; - if (id) { - *id = 0x3ff; - } - - // default to root dir - lfs_stag_t tag = LFS_MKTAG(LFS_TYPE_DIR, 0x3ff, 0); - dir->tail[0] = lfs->root[0]; - dir->tail[1] = lfs->root[1]; - - while (true) { -nextname: - // skip slashes - name += strspn(name, "/"); - lfs_size_t namelen = strcspn(name, "/"); - - // skip '.' and root '..' - if ((namelen == 1 && memcmp(name, ".", 1) == 0) || - (namelen == 2 && memcmp(name, "..", 2) == 0)) { - name += namelen; - goto nextname; - } - - // skip if matched by '..' in name - const char *suffix = name + namelen; - lfs_size_t sufflen; - int depth = 1; - while (true) { - suffix += strspn(suffix, "/"); - sufflen = strcspn(suffix, "/"); - if (sufflen == 0) { - break; - } - - if (sufflen == 2 && memcmp(suffix, "..", 2) == 0) { - depth -= 1; - if (depth == 0) { - name = suffix + sufflen; - goto nextname; - } - } else { - depth += 1; - } - - suffix += sufflen; - } - - // found path - if (name[0] == '\0') { - return tag; - } - - // update what we've found so far - *path = name; - - // only continue if we hit a directory - if (lfs_tag_type3(tag) != LFS_TYPE_DIR) { - return LFS_ERR_NOTDIR; - } - - // grab the entry data - if (lfs_tag_id(tag) != 0x3ff) { - lfs_stag_t res = lfs_dir_get(lfs, dir, LFS_MKTAG(0x700, 0x3ff, 0), - LFS_MKTAG(LFS_TYPE_STRUCT, lfs_tag_id(tag), 8), dir->tail); - if (res < 0) { - return res; - } - lfs_pair_fromle32(dir->tail); - } - - // find entry matching name - while (true) { - tag = lfs_dir_fetchmatch(lfs, dir, dir->tail, - LFS_MKTAG(0x780, 0, 0), - LFS_MKTAG(LFS_TYPE_NAME, 0, namelen), - // are we last name? - (strchr(name, '/') == NULL) ? id : NULL, - lfs_dir_find_match, &(struct lfs_dir_find_match){ - lfs, name, namelen}); - if (tag < 0) { - return tag; - } - - if (tag) { - break; - } - - if (!dir->split) { - return LFS_ERR_NOENT; - } - } - - // to next name - name += namelen; - } -} - -// commit logic -struct lfs_commit { - lfs_block_t block; - lfs_off_t off; - lfs_tag_t ptag; - uint32_t crc; - - lfs_off_t begin; - lfs_off_t end; -}; - -#ifndef LFS_READONLY -static int lfs_dir_commitprog(lfs_t *lfs, struct lfs_commit *commit, - const void *buffer, lfs_size_t size) { - int err = lfs_bd_prog(lfs, - &lfs->pcache, &lfs->rcache, false, - commit->block, commit->off , - (const uint8_t*)buffer, size); - if (err) { - return err; - } - - commit->crc = lfs_crc(commit->crc, buffer, size); - commit->off += size; - return 0; -} -#endif - -#ifndef LFS_READONLY -static int lfs_dir_commitattr(lfs_t *lfs, struct lfs_commit *commit, - lfs_tag_t tag, const void *buffer) { - // check if we fit - lfs_size_t dsize = lfs_tag_dsize(tag); - if (commit->off + dsize > commit->end) { - return LFS_ERR_NOSPC; - } - - // write out tag - lfs_tag_t ntag = lfs_tobe32((tag & 0x7fffffff) ^ commit->ptag); - int err = lfs_dir_commitprog(lfs, commit, &ntag, sizeof(ntag)); - if (err) { - return err; - } - - if (!(tag & 0x80000000)) { - // from memory - err = lfs_dir_commitprog(lfs, commit, buffer, dsize-sizeof(tag)); - if (err) { - return err; - } - } else { - // from disk - const struct lfs_diskoff *disk = buffer; - for (lfs_off_t i = 0; i < dsize-sizeof(tag); i++) { - // rely on caching to make this efficient - uint8_t dat; - err = lfs_bd_read(lfs, - NULL, &lfs->rcache, dsize-sizeof(tag)-i, - disk->block, disk->off+i, &dat, 1); - if (err) { - return err; - } - - err = lfs_dir_commitprog(lfs, commit, &dat, 1); - if (err) { - return err; - } - } - } - - commit->ptag = tag & 0x7fffffff; - return 0; -} -#endif - -#ifndef LFS_READONLY - -static int lfs_dir_commitcrc(lfs_t *lfs, struct lfs_commit *commit) { - // align to program units - // - // this gets a bit complex as we have two types of crcs: - // - 5-word crc with fcrc to check following prog (middle of block) - // - 2-word crc with no following prog (end of block) - const lfs_off_t end = lfs_alignup( - lfs_min(commit->off + 5*sizeof(uint32_t), lfs->cfg->block_size), - lfs->cfg->prog_size); - - lfs_off_t off1 = 0; - uint32_t crc1 = 0; - - // create crc tags to fill up remainder of commit, note that - // padding is not crced, which lets fetches skip padding but - // makes committing a bit more complicated - while (commit->off < end) { - lfs_off_t noff = ( - lfs_min(end - (commit->off+sizeof(lfs_tag_t)), 0x3fe) - + (commit->off+sizeof(lfs_tag_t))); - // too large for crc tag? need padding commits - if (noff < end) { - noff = lfs_min(noff, end - 5*sizeof(uint32_t)); - } - - // space for fcrc? - uint8_t eperturb = -1; - if (noff >= end && noff <= lfs->cfg->block_size - lfs->cfg->prog_size) { - // first read the leading byte, this always contains a bit - // we can perturb to avoid writes that don't change the fcrc - int err = lfs_bd_read(lfs, - NULL, &lfs->rcache, lfs->cfg->prog_size, - commit->block, noff, &eperturb, 1); - if (err && err != LFS_ERR_CORRUPT) { - return err; - } - - // find the expected fcrc, don't bother avoiding a reread - // of the eperturb, it should still be in our cache - struct lfs_fcrc fcrc = {.size=lfs->cfg->prog_size, .crc=0xffffffff}; - err = lfs_bd_crc(lfs, - NULL, &lfs->rcache, lfs->cfg->prog_size, - commit->block, noff, fcrc.size, &fcrc.crc); - if (err && err != LFS_ERR_CORRUPT) { - return err; - } - - lfs_fcrc_tole32(&fcrc); - err = lfs_dir_commitattr(lfs, commit, - LFS_MKTAG(LFS_TYPE_FCRC, 0x3ff, sizeof(struct lfs_fcrc)), - &fcrc); - if (err) { - return err; - } - } - - // build commit crc - struct { - lfs_tag_t tag; - uint32_t crc; - } ccrc; - lfs_tag_t ntag = LFS_MKTAG( - LFS_TYPE_CCRC + (((uint8_t)~eperturb) >> 7), 0x3ff, - noff - (commit->off+sizeof(lfs_tag_t))); - ccrc.tag = lfs_tobe32(ntag ^ commit->ptag); - commit->crc = lfs_crc(commit->crc, &ccrc.tag, sizeof(lfs_tag_t)); - ccrc.crc = lfs_tole32(commit->crc); - - int err = lfs_bd_prog(lfs, - &lfs->pcache, &lfs->rcache, false, - commit->block, commit->off, &ccrc, sizeof(ccrc)); - if (err) { - return err; - } - - // keep track of non-padding checksum to verify - if (off1 == 0) { - off1 = commit->off + sizeof(lfs_tag_t); - crc1 = commit->crc; - } - - commit->off = noff; - // perturb valid bit? - commit->ptag = ntag ^ ((0x80 & ~eperturb) << 24); - // reset crc for next commit - commit->crc = 0xffffffff; - - // manually flush here since we don't prog the padding, this confuses - // the caching layer - if (noff >= end || noff >= lfs->pcache.off + lfs->cfg->cache_size) { - // flush buffers - int err = lfs_bd_sync(lfs, &lfs->pcache, &lfs->rcache, false); - if (err) { - return err; - } - } - } - - // successful commit, check checksums to make sure - // - // note that we don't need to check padding commits, worst - // case if they are corrupted we would have had to compact anyways - lfs_off_t off = commit->begin; - uint32_t crc = 0xffffffff; - int err = lfs_bd_crc(lfs, - NULL, &lfs->rcache, off1+sizeof(uint32_t), - commit->block, off, off1-off, &crc); - if (err) { - return err; - } - - // check non-padding commits against known crc - if (crc != crc1) { - return LFS_ERR_CORRUPT; - } - - // make sure to check crc in case we happen to pick - // up an unrelated crc (frozen block?) - err = lfs_bd_crc(lfs, - NULL, &lfs->rcache, sizeof(uint32_t), - commit->block, off1, sizeof(uint32_t), &crc); - if (err) { - return err; - } - - if (crc != 0) { - return LFS_ERR_CORRUPT; - } - - return 0; -} -#endif - -#ifndef LFS_READONLY -static int lfs_dir_alloc(lfs_t *lfs, lfs_mdir_t *dir) { - // allocate pair of dir blocks (backwards, so we write block 1 first) - for (int i = 0; i < 2; i++) { - int err = lfs_alloc(lfs, &dir->pair[(i+1)%2]); - if (err) { - return err; - } - } - - // zero for reproducibility in case initial block is unreadable - dir->rev = 0; - - // rather than clobbering one of the blocks we just pretend - // the revision may be valid - int err = lfs_bd_read(lfs, - NULL, &lfs->rcache, sizeof(dir->rev), - dir->pair[0], 0, &dir->rev, sizeof(dir->rev)); - dir->rev = lfs_fromle32(dir->rev); - if (err && err != LFS_ERR_CORRUPT) { - return err; - } - - // to make sure we don't immediately evict, align the new revision count - // to our block_cycles modulus, see lfs_dir_compact for why our modulus - // is tweaked this way - if (lfs->cfg->block_cycles > 0) { - dir->rev = lfs_alignup(dir->rev, ((lfs->cfg->block_cycles+1)|1)); - } - - // set defaults - dir->off = sizeof(dir->rev); - dir->etag = 0xffffffff; - dir->count = 0; - dir->tail[0] = LFS_BLOCK_NULL; - dir->tail[1] = LFS_BLOCK_NULL; - dir->erased = false; - dir->split = false; - - // don't write out yet, let caller take care of that - return 0; -} -#endif - -#ifndef LFS_READONLY -static int lfs_dir_drop(lfs_t *lfs, lfs_mdir_t *dir, lfs_mdir_t *tail) { - // steal state - int err = lfs_dir_getgstate(lfs, tail, &lfs->gdelta); - if (err) { - return err; - } - - // steal tail - lfs_pair_tole32(tail->tail); - err = lfs_dir_commit(lfs, dir, LFS_MKATTRS( - {LFS_MKTAG(LFS_TYPE_TAIL + tail->split, 0x3ff, 8), tail->tail})); - lfs_pair_fromle32(tail->tail); - if (err) { - return err; - } - - return 0; -} -#endif - -#ifndef LFS_READONLY -static int lfs_dir_split(lfs_t *lfs, - lfs_mdir_t *dir, const struct lfs_mattr *attrs, int attrcount, - lfs_mdir_t *source, uint16_t split, uint16_t end) { - // create tail metadata pair - lfs_mdir_t tail; - int err = lfs_dir_alloc(lfs, &tail); - if (err) { - return err; - } - - tail.split = dir->split; - tail.tail[0] = dir->tail[0]; - tail.tail[1] = dir->tail[1]; - - // note we don't care about LFS_OK_RELOCATED - int res = lfs_dir_compact(lfs, &tail, attrs, attrcount, source, split, end); - if (res < 0) { - return res; - } - - dir->tail[0] = tail.pair[0]; - dir->tail[1] = tail.pair[1]; - dir->split = true; - - // update root if needed - if (lfs_pair_cmp(dir->pair, lfs->root) == 0 && split == 0) { - lfs->root[0] = tail.pair[0]; - lfs->root[1] = tail.pair[1]; - } - - return 0; -} -#endif - -#ifndef LFS_READONLY -static int lfs_dir_commit_size(void *p, lfs_tag_t tag, const void *buffer) { - lfs_size_t *size = p; - (void)buffer; - - *size += lfs_tag_dsize(tag); - return 0; -} -#endif - -#ifndef LFS_READONLY -struct lfs_dir_commit_commit { - lfs_t *lfs; - struct lfs_commit *commit; -}; -#endif - -#ifndef LFS_READONLY -static int lfs_dir_commit_commit(void *p, lfs_tag_t tag, const void *buffer) { - struct lfs_dir_commit_commit *commit = p; - return lfs_dir_commitattr(commit->lfs, commit->commit, tag, buffer); -} -#endif - -#ifndef LFS_READONLY -static bool lfs_dir_needsrelocation(lfs_t *lfs, lfs_mdir_t *dir) { - // If our revision count == n * block_cycles, we should force a relocation, - // this is how littlefs wear-levels at the metadata-pair level. Note that we - // actually use (block_cycles+1)|1, this is to avoid two corner cases: - // 1. block_cycles = 1, which would prevent relocations from terminating - // 2. block_cycles = 2n, which, due to aliasing, would only ever relocate - // one metadata block in the pair, effectively making this useless - return (lfs->cfg->block_cycles > 0 - && ((dir->rev + 1) % ((lfs->cfg->block_cycles+1)|1) == 0)); -} -#endif - -#ifndef LFS_READONLY -static int lfs_dir_compact(lfs_t *lfs, - lfs_mdir_t *dir, const struct lfs_mattr *attrs, int attrcount, - lfs_mdir_t *source, uint16_t begin, uint16_t end) { - // save some state in case block is bad - bool relocated = false; - bool tired = lfs_dir_needsrelocation(lfs, dir); - - // increment revision count - dir->rev += 1; - - // do not proactively relocate blocks during migrations, this - // can cause a number of failure states such: clobbering the - // v1 superblock if we relocate root, and invalidating directory - // pointers if we relocate the head of a directory. On top of - // this, relocations increase the overall complexity of - // lfs_migration, which is already a delicate operation. -#ifdef LFS_MIGRATE - if (lfs->lfs1) { - tired = false; - } -#endif - - if (tired && lfs_pair_cmp(dir->pair, (const lfs_block_t[2]){0, 1}) != 0) { - // we're writing too much, time to relocate - goto relocate; - } - - // begin loop to commit compaction to blocks until a compact sticks - while (true) { - { - // setup commit state - struct lfs_commit commit = { - .block = dir->pair[1], - .off = 0, - .ptag = 0xffffffff, - .crc = 0xffffffff, - - .begin = 0, - .end = (lfs->cfg->metadata_max ? - lfs->cfg->metadata_max : lfs->cfg->block_size) - 8, - }; - - // erase block to write to - int err = lfs_bd_erase(lfs, dir->pair[1]); - if (err) { - if (err == LFS_ERR_CORRUPT) { - goto relocate; - } - return err; - } - - // write out header - dir->rev = lfs_tole32(dir->rev); - err = lfs_dir_commitprog(lfs, &commit, - &dir->rev, sizeof(dir->rev)); - dir->rev = lfs_fromle32(dir->rev); - if (err) { - if (err == LFS_ERR_CORRUPT) { - goto relocate; - } - return err; - } - - // traverse the directory, this time writing out all unique tags - err = lfs_dir_traverse(lfs, - source, 0, 0xffffffff, attrs, attrcount, - LFS_MKTAG(0x400, 0x3ff, 0), - LFS_MKTAG(LFS_TYPE_NAME, 0, 0), - begin, end, -begin, - lfs_dir_commit_commit, &(struct lfs_dir_commit_commit){ - lfs, &commit}); - if (err) { - if (err == LFS_ERR_CORRUPT) { - goto relocate; - } - return err; - } - - // commit tail, which may be new after last size check - if (!lfs_pair_isnull(dir->tail)) { - lfs_pair_tole32(dir->tail); - err = lfs_dir_commitattr(lfs, &commit, - LFS_MKTAG(LFS_TYPE_TAIL + dir->split, 0x3ff, 8), - dir->tail); - lfs_pair_fromle32(dir->tail); - if (err) { - if (err == LFS_ERR_CORRUPT) { - goto relocate; - } - return err; - } - } - - // bring over gstate? - lfs_gstate_t delta = {0}; - if (!relocated) { - lfs_gstate_xor(&delta, &lfs->gdisk); - lfs_gstate_xor(&delta, &lfs->gstate); - } - lfs_gstate_xor(&delta, &lfs->gdelta); - delta.tag &= ~LFS_MKTAG(0, 0, 0x3ff); - - err = lfs_dir_getgstate(lfs, dir, &delta); - if (err) { - return err; - } - - if (!lfs_gstate_iszero(&delta)) { - lfs_gstate_tole32(&delta); - err = lfs_dir_commitattr(lfs, &commit, - LFS_MKTAG(LFS_TYPE_MOVESTATE, 0x3ff, - sizeof(delta)), &delta); - if (err) { - if (err == LFS_ERR_CORRUPT) { - goto relocate; - } - return err; - } - } - - // complete commit with crc - err = lfs_dir_commitcrc(lfs, &commit); - if (err) { - if (err == LFS_ERR_CORRUPT) { - goto relocate; - } - return err; - } - - // successful compaction, swap dir pair to indicate most recent - LFS_ASSERT(commit.off % lfs->cfg->prog_size == 0); - lfs_pair_swap(dir->pair); - dir->count = end - begin; - dir->off = commit.off; - dir->etag = commit.ptag; - // update gstate - lfs->gdelta = (lfs_gstate_t){0}; - if (!relocated) { - lfs->gdisk = lfs->gstate; - } - } - break; - -relocate: - // commit was corrupted, drop caches and prepare to relocate block - relocated = true; - lfs_cache_drop(lfs, &lfs->pcache); - if (!tired) { - LFS_DEBUG("Bad block at 0x%"PRIx32, dir->pair[1]); - } - - // can't relocate superblock, filesystem is now frozen - if (lfs_pair_cmp(dir->pair, (const lfs_block_t[2]){0, 1}) == 0) { - LFS_WARN("Superblock 0x%"PRIx32" has become unwritable", - dir->pair[1]); - return LFS_ERR_NOSPC; - } - - // relocate half of pair - int err = lfs_alloc(lfs, &dir->pair[1]); - if (err && (err != LFS_ERR_NOSPC || !tired)) { - return err; - } - - tired = false; - continue; - } - - return relocated ? LFS_OK_RELOCATED : 0; -} -#endif - -#ifndef LFS_READONLY -static int lfs_dir_splittingcompact(lfs_t *lfs, lfs_mdir_t *dir, - const struct lfs_mattr *attrs, int attrcount, - lfs_mdir_t *source, uint16_t begin, uint16_t end) { - while (true) { - // find size of first split, we do this by halving the split until - // the metadata is guaranteed to fit - // - // Note that this isn't a true binary search, we never increase the - // split size. This may result in poorly distributed metadata but isn't - // worth the extra code size or performance hit to fix. - lfs_size_t split = begin; - while (end - split > 1) { - lfs_size_t size = 0; - int err = lfs_dir_traverse(lfs, - source, 0, 0xffffffff, attrs, attrcount, - LFS_MKTAG(0x400, 0x3ff, 0), - LFS_MKTAG(LFS_TYPE_NAME, 0, 0), - split, end, -split, - lfs_dir_commit_size, &size); - if (err) { - return err; - } - - // space is complicated, we need room for: - // - // - tail: 4+2*4 = 12 bytes - // - gstate: 4+3*4 = 16 bytes - // - move delete: 4 = 4 bytes - // - crc: 4+4 = 8 bytes - // total = 40 bytes - // - // And we cap at half a block to avoid degenerate cases with - // nearly-full metadata blocks. - // - if (end - split < 0xff - && size <= lfs_min( - lfs->cfg->block_size - 40, - lfs_alignup( - (lfs->cfg->metadata_max - ? lfs->cfg->metadata_max - : lfs->cfg->block_size)/2, - lfs->cfg->prog_size))) { - break; - } - - split = split + ((end - split) / 2); - } - - if (split == begin) { - // no split needed - break; - } - - // split into two metadata pairs and continue - int err = lfs_dir_split(lfs, dir, attrs, attrcount, - source, split, end); - if (err && err != LFS_ERR_NOSPC) { - return err; - } - - if (err) { - // we can't allocate a new block, try to compact with degraded - // performance - LFS_WARN("Unable to split {0x%"PRIx32", 0x%"PRIx32"}", - dir->pair[0], dir->pair[1]); - break; - } else { - end = split; - } - } - - if (lfs_dir_needsrelocation(lfs, dir) - && lfs_pair_cmp(dir->pair, (const lfs_block_t[2]){0, 1}) == 0) { - // oh no! we're writing too much to the superblock, - // should we expand? - lfs_ssize_t size = lfs_fs_rawsize(lfs); - if (size < 0) { - return size; - } - - // do we have extra space? littlefs can't reclaim this space - // by itself, so expand cautiously - if ((lfs_size_t)size < lfs->cfg->block_count/2) { - LFS_DEBUG("Expanding superblock at rev %"PRIu32, dir->rev); - int err = lfs_dir_split(lfs, dir, attrs, attrcount, - source, begin, end); - if (err && err != LFS_ERR_NOSPC) { - return err; - } - - if (err) { - // welp, we tried, if we ran out of space there's not much - // we can do, we'll error later if we've become frozen - LFS_WARN("Unable to expand superblock"); - } else { - end = begin; - } - } - } - - return lfs_dir_compact(lfs, dir, attrs, attrcount, source, begin, end); -} -#endif - -#ifndef LFS_READONLY -static int lfs_dir_relocatingcommit(lfs_t *lfs, lfs_mdir_t *dir, - const lfs_block_t pair[2], - const struct lfs_mattr *attrs, int attrcount, - lfs_mdir_t *pdir) { - int state = 0; - - // calculate changes to the directory - bool hasdelete = false; - for (int i = 0; i < attrcount; i++) { - if (lfs_tag_type3(attrs[i].tag) == LFS_TYPE_CREATE) { - dir->count += 1; - } else if (lfs_tag_type3(attrs[i].tag) == LFS_TYPE_DELETE) { - LFS_ASSERT(dir->count > 0); - dir->count -= 1; - hasdelete = true; - } else if (lfs_tag_type1(attrs[i].tag) == LFS_TYPE_TAIL) { - dir->tail[0] = ((lfs_block_t*)attrs[i].buffer)[0]; - dir->tail[1] = ((lfs_block_t*)attrs[i].buffer)[1]; - dir->split = (lfs_tag_chunk(attrs[i].tag) & 1); - lfs_pair_fromle32(dir->tail); - } - } - - // should we actually drop the directory block? - if (hasdelete && dir->count == 0) { - LFS_ASSERT(pdir); - int err = lfs_fs_pred(lfs, dir->pair, pdir); - if (err && err != LFS_ERR_NOENT) { - return err; - } - - if (err != LFS_ERR_NOENT && pdir->split) { - state = LFS_OK_DROPPED; - goto fixmlist; - } - } - - if (dir->erased) { - // try to commit - struct lfs_commit commit = { - .block = dir->pair[0], - .off = dir->off, - .ptag = dir->etag, - .crc = 0xffffffff, - - .begin = dir->off, - .end = (lfs->cfg->metadata_max ? - lfs->cfg->metadata_max : lfs->cfg->block_size) - 8, - }; - - // traverse attrs that need to be written out - lfs_pair_tole32(dir->tail); - int err = lfs_dir_traverse(lfs, - dir, dir->off, dir->etag, attrs, attrcount, - 0, 0, 0, 0, 0, - lfs_dir_commit_commit, &(struct lfs_dir_commit_commit){ - lfs, &commit}); - lfs_pair_fromle32(dir->tail); - if (err) { - if (err == LFS_ERR_NOSPC || err == LFS_ERR_CORRUPT) { - goto compact; - } - return err; - } - - // commit any global diffs if we have any - lfs_gstate_t delta = {0}; - lfs_gstate_xor(&delta, &lfs->gstate); - lfs_gstate_xor(&delta, &lfs->gdisk); - lfs_gstate_xor(&delta, &lfs->gdelta); - delta.tag &= ~LFS_MKTAG(0, 0, 0x3ff); - if (!lfs_gstate_iszero(&delta)) { - err = lfs_dir_getgstate(lfs, dir, &delta); - if (err) { - return err; - } - - lfs_gstate_tole32(&delta); - err = lfs_dir_commitattr(lfs, &commit, - LFS_MKTAG(LFS_TYPE_MOVESTATE, 0x3ff, - sizeof(delta)), &delta); - if (err) { - if (err == LFS_ERR_NOSPC || err == LFS_ERR_CORRUPT) { - goto compact; - } - return err; - } - } - - // finalize commit with the crc - err = lfs_dir_commitcrc(lfs, &commit); - if (err) { - if (err == LFS_ERR_NOSPC || err == LFS_ERR_CORRUPT) { - goto compact; - } - return err; - } - - // successful commit, update dir - LFS_ASSERT(commit.off % lfs->cfg->prog_size == 0); - dir->off = commit.off; - dir->etag = commit.ptag; - // and update gstate - lfs->gdisk = lfs->gstate; - lfs->gdelta = (lfs_gstate_t){0}; - - goto fixmlist; - } - -compact: - // fall back to compaction - lfs_cache_drop(lfs, &lfs->pcache); - - state = lfs_dir_splittingcompact(lfs, dir, attrs, attrcount, - dir, 0, dir->count); - if (state < 0) { - return state; - } - - goto fixmlist; - -fixmlist:; - // this complicated bit of logic is for fixing up any active - // metadata-pairs that we may have affected - // - // note we have to make two passes since the mdir passed to - // lfs_dir_commit could also be in this list, and even then - // we need to copy the pair so they don't get clobbered if we refetch - // our mdir. - lfs_block_t oldpair[2] = {pair[0], pair[1]}; - for (struct lfs_mlist *d = lfs->mlist; d; d = d->next) { - if (lfs_pair_cmp(d->m.pair, oldpair) == 0) { - d->m = *dir; - if (d->m.pair != pair) { - for (int i = 0; i < attrcount; i++) { - if (lfs_tag_type3(attrs[i].tag) == LFS_TYPE_DELETE && - d->id == lfs_tag_id(attrs[i].tag)) { - d->m.pair[0] = LFS_BLOCK_NULL; - d->m.pair[1] = LFS_BLOCK_NULL; - } else if (lfs_tag_type3(attrs[i].tag) == LFS_TYPE_DELETE && - d->id > lfs_tag_id(attrs[i].tag)) { - d->id -= 1; - if (d->type == LFS_TYPE_DIR) { - ((lfs_dir_t*)d)->pos -= 1; - } - } else if (lfs_tag_type3(attrs[i].tag) == LFS_TYPE_CREATE && - d->id >= lfs_tag_id(attrs[i].tag)) { - d->id += 1; - if (d->type == LFS_TYPE_DIR) { - ((lfs_dir_t*)d)->pos += 1; - } - } - } - } - - while (d->id >= d->m.count && d->m.split) { - // we split and id is on tail now - d->id -= d->m.count; - int err = lfs_dir_fetch(lfs, &d->m, d->m.tail); - if (err) { - return err; - } - } - } - } - - return state; -} -#endif - -#ifndef LFS_READONLY -static int lfs_dir_orphaningcommit(lfs_t *lfs, lfs_mdir_t *dir, - const struct lfs_mattr *attrs, int attrcount) { - // check for any inline files that aren't RAM backed and - // forcefully evict them, needed for filesystem consistency - for (lfs_file_t *f = (lfs_file_t*)lfs->mlist; f; f = f->next) { - if (dir != &f->m && lfs_pair_cmp(f->m.pair, dir->pair) == 0 && - f->type == LFS_TYPE_REG && (f->flags & LFS_F_INLINE) && - f->ctz.size > lfs->cfg->cache_size) { - int err = lfs_file_outline(lfs, f); - if (err) { - return err; - } - - err = lfs_file_flush(lfs, f); - if (err) { - return err; - } - } - } - - lfs_block_t lpair[2] = {dir->pair[0], dir->pair[1]}; - lfs_mdir_t ldir = *dir; - lfs_mdir_t pdir; - int state = lfs_dir_relocatingcommit(lfs, &ldir, dir->pair, - attrs, attrcount, &pdir); - if (state < 0) { - return state; - } - - // update if we're not in mlist, note we may have already been - // updated if we are in mlist - if (lfs_pair_cmp(dir->pair, lpair) == 0) { - *dir = ldir; - } - - // commit was successful, but may require other changes in the - // filesystem, these would normally be tail recursive, but we have - // flattened them here avoid unbounded stack usage - - // need to drop? - if (state == LFS_OK_DROPPED) { - // steal state - int err = lfs_dir_getgstate(lfs, dir, &lfs->gdelta); - if (err) { - return err; - } - - // steal tail, note that this can't create a recursive drop - lpair[0] = pdir.pair[0]; - lpair[1] = pdir.pair[1]; - lfs_pair_tole32(dir->tail); - state = lfs_dir_relocatingcommit(lfs, &pdir, lpair, LFS_MKATTRS( - {LFS_MKTAG(LFS_TYPE_TAIL + dir->split, 0x3ff, 8), - dir->tail}), - NULL); - lfs_pair_fromle32(dir->tail); - if (state < 0) { - return state; - } - - ldir = pdir; - } - - // need to relocate? - bool orphans = false; - while (state == LFS_OK_RELOCATED) { - LFS_DEBUG("Relocating {0x%"PRIx32", 0x%"PRIx32"} " - "-> {0x%"PRIx32", 0x%"PRIx32"}", - lpair[0], lpair[1], ldir.pair[0], ldir.pair[1]); - state = 0; - - // update internal root - if (lfs_pair_cmp(lpair, lfs->root) == 0) { - lfs->root[0] = ldir.pair[0]; - lfs->root[1] = ldir.pair[1]; - } - - // update internally tracked dirs - for (struct lfs_mlist *d = lfs->mlist; d; d = d->next) { - if (lfs_pair_cmp(lpair, d->m.pair) == 0) { - d->m.pair[0] = ldir.pair[0]; - d->m.pair[1] = ldir.pair[1]; - } - - if (d->type == LFS_TYPE_DIR && - lfs_pair_cmp(lpair, ((lfs_dir_t*)d)->head) == 0) { - ((lfs_dir_t*)d)->head[0] = ldir.pair[0]; - ((lfs_dir_t*)d)->head[1] = ldir.pair[1]; - } - } - - // find parent - lfs_stag_t tag = lfs_fs_parent(lfs, lpair, &pdir); - if (tag < 0 && tag != LFS_ERR_NOENT) { - return tag; - } - - bool hasparent = (tag != LFS_ERR_NOENT); - if (tag != LFS_ERR_NOENT) { - // note that if we have a parent, we must have a pred, so this will - // always create an orphan - int err = lfs_fs_preporphans(lfs, +1); - if (err) { - return err; - } - - // fix pending move in this pair? this looks like an optimization but - // is in fact _required_ since relocating may outdate the move. - uint16_t moveid = 0x3ff; - if (lfs_gstate_hasmovehere(&lfs->gstate, pdir.pair)) { - moveid = lfs_tag_id(lfs->gstate.tag); - LFS_DEBUG("Fixing move while relocating " - "{0x%"PRIx32", 0x%"PRIx32"} 0x%"PRIx16"\n", - pdir.pair[0], pdir.pair[1], moveid); - lfs_fs_prepmove(lfs, 0x3ff, NULL); - if (moveid < lfs_tag_id(tag)) { - tag -= LFS_MKTAG(0, 1, 0); - } - } - - lfs_block_t ppair[2] = {pdir.pair[0], pdir.pair[1]}; - lfs_pair_tole32(ldir.pair); - state = lfs_dir_relocatingcommit(lfs, &pdir, ppair, LFS_MKATTRS( - {LFS_MKTAG_IF(moveid != 0x3ff, - LFS_TYPE_DELETE, moveid, 0), NULL}, - {tag, ldir.pair}), - NULL); - lfs_pair_fromle32(ldir.pair); - if (state < 0) { - return state; - } - - if (state == LFS_OK_RELOCATED) { - lpair[0] = ppair[0]; - lpair[1] = ppair[1]; - ldir = pdir; - orphans = true; - continue; - } - } - - // find pred - int err = lfs_fs_pred(lfs, lpair, &pdir); - if (err && err != LFS_ERR_NOENT) { - return err; - } - LFS_ASSERT(!(hasparent && err == LFS_ERR_NOENT)); - - // if we can't find dir, it must be new - if (err != LFS_ERR_NOENT) { - if (lfs_gstate_hasorphans(&lfs->gstate)) { - // next step, clean up orphans - err = lfs_fs_preporphans(lfs, -hasparent); - if (err) { - return err; - } - } - - // fix pending move in this pair? this looks like an optimization - // but is in fact _required_ since relocating may outdate the move. - uint16_t moveid = 0x3ff; - if (lfs_gstate_hasmovehere(&lfs->gstate, pdir.pair)) { - moveid = lfs_tag_id(lfs->gstate.tag); - LFS_DEBUG("Fixing move while relocating " - "{0x%"PRIx32", 0x%"PRIx32"} 0x%"PRIx16"\n", - pdir.pair[0], pdir.pair[1], moveid); - lfs_fs_prepmove(lfs, 0x3ff, NULL); - } - - // replace bad pair, either we clean up desync, or no desync occured - lpair[0] = pdir.pair[0]; - lpair[1] = pdir.pair[1]; - lfs_pair_tole32(ldir.pair); - state = lfs_dir_relocatingcommit(lfs, &pdir, lpair, LFS_MKATTRS( - {LFS_MKTAG_IF(moveid != 0x3ff, - LFS_TYPE_DELETE, moveid, 0), NULL}, - {LFS_MKTAG(LFS_TYPE_TAIL + pdir.split, 0x3ff, 8), - ldir.pair}), - NULL); - lfs_pair_fromle32(ldir.pair); - if (state < 0) { - return state; - } - - ldir = pdir; - } - } - - return orphans ? LFS_OK_ORPHANED : 0; -} -#endif - -#ifndef LFS_READONLY -static int lfs_dir_commit(lfs_t *lfs, lfs_mdir_t *dir, - const struct lfs_mattr *attrs, int attrcount) { - int orphans = lfs_dir_orphaningcommit(lfs, dir, attrs, attrcount); - if (orphans < 0) { - return orphans; - } - - if (orphans) { - // make sure we've removed all orphans, this is a noop if there - // are none, but if we had nested blocks failures we may have - // created some - int err = lfs_fs_deorphan(lfs, false); - if (err) { - return err; - } - } - - return 0; -} -#endif - - -/// Top level directory operations /// -#ifndef LFS_READONLY -static int lfs_rawmkdir(lfs_t *lfs, const char *path) { - // deorphan if we haven't yet, needed at most once after poweron - int err = lfs_fs_forceconsistency(lfs); - if (err) { - return err; - } - - struct lfs_mlist cwd; - cwd.next = lfs->mlist; - uint16_t id; - err = lfs_dir_find(lfs, &cwd.m, &path, &id); - if (!(err == LFS_ERR_NOENT && id != 0x3ff)) { - return (err < 0) ? err : LFS_ERR_EXIST; - } - - // check that name fits - lfs_size_t nlen = strlen(path); - if (nlen > lfs->name_max) { - return LFS_ERR_NAMETOOLONG; - } - - // build up new directory - lfs_alloc_ack(lfs); - lfs_mdir_t dir; - err = lfs_dir_alloc(lfs, &dir); - if (err) { - return err; - } - - // find end of list - lfs_mdir_t pred = cwd.m; - while (pred.split) { - err = lfs_dir_fetch(lfs, &pred, pred.tail); - if (err) { - return err; - } - } - - // setup dir - lfs_pair_tole32(pred.tail); - err = lfs_dir_commit(lfs, &dir, LFS_MKATTRS( - {LFS_MKTAG(LFS_TYPE_SOFTTAIL, 0x3ff, 8), pred.tail})); - lfs_pair_fromle32(pred.tail); - if (err) { - return err; - } - - // current block not end of list? - if (cwd.m.split) { - // update tails, this creates a desync - err = lfs_fs_preporphans(lfs, +1); - if (err) { - return err; - } - - // it's possible our predecessor has to be relocated, and if - // our parent is our predecessor's predecessor, this could have - // caused our parent to go out of date, fortunately we can hook - // ourselves into littlefs to catch this - cwd.type = 0; - cwd.id = 0; - lfs->mlist = &cwd; - - lfs_pair_tole32(dir.pair); - err = lfs_dir_commit(lfs, &pred, LFS_MKATTRS( - {LFS_MKTAG(LFS_TYPE_SOFTTAIL, 0x3ff, 8), dir.pair})); - lfs_pair_fromle32(dir.pair); - if (err) { - lfs->mlist = cwd.next; - return err; - } - - lfs->mlist = cwd.next; - err = lfs_fs_preporphans(lfs, -1); - if (err) { - return err; - } - } - - // now insert into our parent block - lfs_pair_tole32(dir.pair); - err = lfs_dir_commit(lfs, &cwd.m, LFS_MKATTRS( - {LFS_MKTAG(LFS_TYPE_CREATE, id, 0), NULL}, - {LFS_MKTAG(LFS_TYPE_DIR, id, nlen), path}, - {LFS_MKTAG(LFS_TYPE_DIRSTRUCT, id, 8), dir.pair}, - {LFS_MKTAG_IF(!cwd.m.split, - LFS_TYPE_SOFTTAIL, 0x3ff, 8), dir.pair})); - lfs_pair_fromle32(dir.pair); - if (err) { - return err; - } - - return 0; -} -#endif - -static int lfs_dir_rawopen(lfs_t *lfs, lfs_dir_t *dir, const char *path) { - lfs_stag_t tag = lfs_dir_find(lfs, &dir->m, &path, NULL); - if (tag < 0) { - return tag; - } - - if (lfs_tag_type3(tag) != LFS_TYPE_DIR) { - return LFS_ERR_NOTDIR; - } - - lfs_block_t pair[2]; - if (lfs_tag_id(tag) == 0x3ff) { - // handle root dir separately - pair[0] = lfs->root[0]; - pair[1] = lfs->root[1]; - } else { - // get dir pair from parent - lfs_stag_t res = lfs_dir_get(lfs, &dir->m, LFS_MKTAG(0x700, 0x3ff, 0), - LFS_MKTAG(LFS_TYPE_STRUCT, lfs_tag_id(tag), 8), pair); - if (res < 0) { - return res; - } - lfs_pair_fromle32(pair); - } - - // fetch first pair - int err = lfs_dir_fetch(lfs, &dir->m, pair); - if (err) { - return err; - } - - // setup entry - dir->head[0] = dir->m.pair[0]; - dir->head[1] = dir->m.pair[1]; - dir->id = 0; - dir->pos = 0; - - // add to list of mdirs - dir->type = LFS_TYPE_DIR; - lfs_mlist_append(lfs, (struct lfs_mlist *)dir); - - return 0; -} - -static int lfs_dir_rawclose(lfs_t *lfs, lfs_dir_t *dir) { - // remove from list of mdirs - lfs_mlist_remove(lfs, (struct lfs_mlist *)dir); - - return 0; -} - -static int lfs_dir_rawread(lfs_t *lfs, lfs_dir_t *dir, struct lfs_info *info) { - memset(info, 0, sizeof(*info)); - - // special offset for '.' and '..' - if (dir->pos == 0) { - info->type = LFS_TYPE_DIR; - strcpy(info->name, "."); - dir->pos += 1; - return true; - } else if (dir->pos == 1) { - info->type = LFS_TYPE_DIR; - strcpy(info->name, ".."); - dir->pos += 1; - return true; - } - - while (true) { - if (dir->id == dir->m.count) { - if (!dir->m.split) { - return false; - } - - int err = lfs_dir_fetch(lfs, &dir->m, dir->m.tail); - if (err) { - return err; - } - - dir->id = 0; - } - - int err = lfs_dir_getinfo(lfs, &dir->m, dir->id, info); - if (err && err != LFS_ERR_NOENT) { - return err; - } - - dir->id += 1; - if (err != LFS_ERR_NOENT) { - break; - } - } - - dir->pos += 1; - return true; -} - -static int lfs_dir_rawseek(lfs_t *lfs, lfs_dir_t *dir, lfs_off_t off) { - // simply walk from head dir - int err = lfs_dir_rawrewind(lfs, dir); - if (err) { - return err; - } - - // first two for ./.. - dir->pos = lfs_min(2, off); - off -= dir->pos; - - // skip superblock entry - dir->id = (off > 0 && lfs_pair_cmp(dir->head, lfs->root) == 0); - - while (off > 0) { - int diff = lfs_min(dir->m.count - dir->id, off); - dir->id += diff; - dir->pos += diff; - off -= diff; - - if (dir->id == dir->m.count) { - if (!dir->m.split) { - return LFS_ERR_INVAL; - } - - err = lfs_dir_fetch(lfs, &dir->m, dir->m.tail); - if (err) { - return err; - } - - dir->id = 0; - } - } - - return 0; -} - -static lfs_soff_t lfs_dir_rawtell(lfs_t *lfs, lfs_dir_t *dir) { - (void)lfs; - return dir->pos; -} - -static int lfs_dir_rawrewind(lfs_t *lfs, lfs_dir_t *dir) { - // reload the head dir - int err = lfs_dir_fetch(lfs, &dir->m, dir->head); - if (err) { - return err; - } - - dir->id = 0; - dir->pos = 0; - return 0; -} - - -/// File index list operations /// -static int lfs_ctz_index(lfs_t *lfs, lfs_off_t *off) { - lfs_off_t size = *off; - lfs_off_t b = lfs->cfg->block_size - 2*4; - lfs_off_t i = size / b; - if (i == 0) { - return 0; - } - - i = (size - 4*(lfs_popc(i-1)+2)) / b; - *off = size - b*i - 4*lfs_popc(i); - return i; -} - -static int lfs_ctz_find(lfs_t *lfs, - const lfs_cache_t *pcache, lfs_cache_t *rcache, - lfs_block_t head, lfs_size_t size, - lfs_size_t pos, lfs_block_t *block, lfs_off_t *off) { - if (size == 0) { - *block = LFS_BLOCK_NULL; - *off = 0; - return 0; - } - - lfs_off_t current = lfs_ctz_index(lfs, &(lfs_off_t){size-1}); - lfs_off_t target = lfs_ctz_index(lfs, &pos); - - while (current > target) { - lfs_size_t skip = lfs_min( - lfs_npw2(current-target+1) - 1, - lfs_ctz(current)); - - int err = lfs_bd_read(lfs, - pcache, rcache, sizeof(head), - head, 4*skip, &head, sizeof(head)); - head = lfs_fromle32(head); - if (err) { - return err; - } - - current -= 1 << skip; - } - - *block = head; - *off = pos; - return 0; -} - -#ifndef LFS_READONLY -static int lfs_ctz_extend(lfs_t *lfs, - lfs_cache_t *pcache, lfs_cache_t *rcache, - lfs_block_t head, lfs_size_t size, - lfs_block_t *block, lfs_off_t *off) { - while (true) { - // go ahead and grab a block - lfs_block_t nblock; - int err = lfs_alloc(lfs, &nblock); - if (err) { - return err; - } - - { - err = lfs_bd_erase(lfs, nblock); - if (err) { - if (err == LFS_ERR_CORRUPT) { - goto relocate; - } - return err; - } - - if (size == 0) { - *block = nblock; - *off = 0; - return 0; - } - - lfs_size_t noff = size - 1; - lfs_off_t index = lfs_ctz_index(lfs, &noff); - noff = noff + 1; - - // just copy out the last block if it is incomplete - if (noff != lfs->cfg->block_size) { - for (lfs_off_t i = 0; i < noff; i++) { - uint8_t data; - err = lfs_bd_read(lfs, - NULL, rcache, noff-i, - head, i, &data, 1); - if (err) { - return err; - } - - err = lfs_bd_prog(lfs, - pcache, rcache, true, - nblock, i, &data, 1); - if (err) { - if (err == LFS_ERR_CORRUPT) { - goto relocate; - } - return err; - } - } - - *block = nblock; - *off = noff; - return 0; - } - - // append block - index += 1; - lfs_size_t skips = lfs_ctz(index) + 1; - lfs_block_t nhead = head; - for (lfs_off_t i = 0; i < skips; i++) { - nhead = lfs_tole32(nhead); - err = lfs_bd_prog(lfs, pcache, rcache, true, - nblock, 4*i, &nhead, 4); - nhead = lfs_fromle32(nhead); - if (err) { - if (err == LFS_ERR_CORRUPT) { - goto relocate; - } - return err; - } - - if (i != skips-1) { - err = lfs_bd_read(lfs, - NULL, rcache, sizeof(nhead), - nhead, 4*i, &nhead, sizeof(nhead)); - nhead = lfs_fromle32(nhead); - if (err) { - return err; - } - } - } - - *block = nblock; - *off = 4*skips; - return 0; - } - -relocate: - LFS_DEBUG("Bad block at 0x%"PRIx32, nblock); - - // just clear cache and try a new block - lfs_cache_drop(lfs, pcache); - } -} -#endif - -static int lfs_ctz_traverse(lfs_t *lfs, - const lfs_cache_t *pcache, lfs_cache_t *rcache, - lfs_block_t head, lfs_size_t size, - int (*cb)(void*, lfs_block_t), void *data) { - if (size == 0) { - return 0; - } - - lfs_off_t index = lfs_ctz_index(lfs, &(lfs_off_t){size-1}); - - while (true) { - int err = cb(data, head); - if (err) { - return err; - } - - if (index == 0) { - return 0; - } - - lfs_block_t heads[2]; - int count = 2 - (index & 1); - err = lfs_bd_read(lfs, - pcache, rcache, count*sizeof(head), - head, 0, &heads, count*sizeof(head)); - heads[0] = lfs_fromle32(heads[0]); - heads[1] = lfs_fromle32(heads[1]); - if (err) { - return err; - } - - for (int i = 0; i < count-1; i++) { - err = cb(data, heads[i]); - if (err) { - return err; - } - } - - head = heads[count-1]; - index -= count; - } -} - - -/// Top level file operations /// -static int lfs_file_rawopencfg(lfs_t *lfs, lfs_file_t *file, - const char *path, int flags, - const struct lfs_file_config *cfg) { -#ifndef LFS_READONLY - // deorphan if we haven't yet, needed at most once after poweron - if ((flags & LFS_O_WRONLY) == LFS_O_WRONLY) { - int err = lfs_fs_forceconsistency(lfs); - if (err) { - return err; - } - } -#else - LFS_ASSERT((flags & LFS_O_RDONLY) == LFS_O_RDONLY); -#endif - - // setup simple file details - int err; - file->cfg = cfg; - file->flags = flags; - file->pos = 0; - file->off = 0; - file->cache.buffer = NULL; - - // allocate entry for file if it doesn't exist - lfs_stag_t tag = lfs_dir_find(lfs, &file->m, &path, &file->id); - if (tag < 0 && !(tag == LFS_ERR_NOENT && file->id != 0x3ff)) { - err = tag; - goto cleanup; - } - - // get id, add to list of mdirs to catch update changes - file->type = LFS_TYPE_REG; - lfs_mlist_append(lfs, (struct lfs_mlist *)file); - -#ifdef LFS_READONLY - if (tag == LFS_ERR_NOENT) { - err = LFS_ERR_NOENT; - goto cleanup; -#else - if (tag == LFS_ERR_NOENT) { - if (!(flags & LFS_O_CREAT)) { - err = LFS_ERR_NOENT; - goto cleanup; - } - - // check that name fits - lfs_size_t nlen = strlen(path); - if (nlen > lfs->name_max) { - err = LFS_ERR_NAMETOOLONG; - goto cleanup; - } - - // get next slot and create entry to remember name - err = lfs_dir_commit(lfs, &file->m, LFS_MKATTRS( - {LFS_MKTAG(LFS_TYPE_CREATE, file->id, 0), NULL}, - {LFS_MKTAG(LFS_TYPE_REG, file->id, nlen), path}, - {LFS_MKTAG(LFS_TYPE_INLINESTRUCT, file->id, 0), NULL})); - - // it may happen that the file name doesn't fit in the metadata blocks, e.g., a 256 byte file name will - // not fit in a 128 byte block. - err = (err == LFS_ERR_NOSPC) ? LFS_ERR_NAMETOOLONG : err; - if (err) { - goto cleanup; - } - - tag = LFS_MKTAG(LFS_TYPE_INLINESTRUCT, 0, 0); - } else if (flags & LFS_O_EXCL) { - err = LFS_ERR_EXIST; - goto cleanup; -#endif - } else if (lfs_tag_type3(tag) != LFS_TYPE_REG) { - err = LFS_ERR_ISDIR; - goto cleanup; -#ifndef LFS_READONLY - } else if (flags & LFS_O_TRUNC) { - // truncate if requested - tag = LFS_MKTAG(LFS_TYPE_INLINESTRUCT, file->id, 0); - file->flags |= LFS_F_DIRTY; -#endif - } else { - // try to load what's on disk, if it's inlined we'll fix it later - tag = lfs_dir_get(lfs, &file->m, LFS_MKTAG(0x700, 0x3ff, 0), - LFS_MKTAG(LFS_TYPE_STRUCT, file->id, 8), &file->ctz); - if (tag < 0) { - err = tag; - goto cleanup; - } - lfs_ctz_fromle32(&file->ctz); - } - - // fetch attrs - for (unsigned i = 0; i < file->cfg->attr_count; i++) { - // if opened for read / read-write operations - if ((file->flags & LFS_O_RDONLY) == LFS_O_RDONLY) { - lfs_stag_t res = lfs_dir_get(lfs, &file->m, - LFS_MKTAG(0x7ff, 0x3ff, 0), - LFS_MKTAG(LFS_TYPE_USERATTR + file->cfg->attrs[i].type, - file->id, file->cfg->attrs[i].size), - file->cfg->attrs[i].buffer); - if (res < 0 && res != LFS_ERR_NOENT) { - err = res; - goto cleanup; - } - } - -#ifndef LFS_READONLY - // if opened for write / read-write operations - if ((file->flags & LFS_O_WRONLY) == LFS_O_WRONLY) { - if (file->cfg->attrs[i].size > lfs->attr_max) { - err = LFS_ERR_NOSPC; - goto cleanup; - } - - file->flags |= LFS_F_DIRTY; - } -#endif - } - - // allocate buffer if needed - if (file->cfg->buffer) { - file->cache.buffer = file->cfg->buffer; - } else { - file->cache.buffer = lfs_malloc(lfs->cfg->cache_size); - if (!file->cache.buffer) { - err = LFS_ERR_NOMEM; - goto cleanup; - } - } - - // zero to avoid information leak - lfs_cache_zero(lfs, &file->cache); - - if (lfs_tag_type3(tag) == LFS_TYPE_INLINESTRUCT) { - // load inline files - file->ctz.head = LFS_BLOCK_INLINE; - file->ctz.size = lfs_tag_size(tag); - file->flags |= LFS_F_INLINE; - file->cache.block = file->ctz.head; - file->cache.off = 0; - file->cache.size = lfs->cfg->cache_size; - - // don't always read (may be new/trunc file) - if (file->ctz.size > 0) { - lfs_stag_t res = lfs_dir_get(lfs, &file->m, - LFS_MKTAG(0x700, 0x3ff, 0), - LFS_MKTAG(LFS_TYPE_STRUCT, file->id, - lfs_min(file->cache.size, 0x3fe)), - file->cache.buffer); - if (res < 0) { - err = res; - goto cleanup; - } - } - } - - return 0; - -cleanup: - // clean up lingering resources -#ifndef LFS_READONLY - file->flags |= LFS_F_ERRED; -#endif - lfs_file_rawclose(lfs, file); - return err; -} - -#ifndef LFS_NO_MALLOC -static int lfs_file_rawopen(lfs_t *lfs, lfs_file_t *file, - const char *path, int flags) { - static const struct lfs_file_config defaults = {0}; - int err = lfs_file_rawopencfg(lfs, file, path, flags, &defaults); - return err; -} -#endif - -static int lfs_file_rawclose(lfs_t *lfs, lfs_file_t *file) { -#ifndef LFS_READONLY - int err = lfs_file_rawsync(lfs, file); -#else - int err = 0; -#endif - - // remove from list of mdirs - lfs_mlist_remove(lfs, (struct lfs_mlist*)file); - - // clean up memory - if (!file->cfg->buffer) { - lfs_free(file->cache.buffer); - } - - return err; -} - - -#ifndef LFS_READONLY -static int lfs_file_relocate(lfs_t *lfs, lfs_file_t *file) { - while (true) { - // just relocate what exists into new block - lfs_block_t nblock; - int err = lfs_alloc(lfs, &nblock); - if (err) { - return err; - } - - err = lfs_bd_erase(lfs, nblock); - if (err) { - if (err == LFS_ERR_CORRUPT) { - goto relocate; - } - return err; - } - - // either read from dirty cache or disk - for (lfs_off_t i = 0; i < file->off; i++) { - uint8_t data; - if (file->flags & LFS_F_INLINE) { - err = lfs_dir_getread(lfs, &file->m, - // note we evict inline files before they can be dirty - NULL, &file->cache, file->off-i, - LFS_MKTAG(0xfff, 0x1ff, 0), - LFS_MKTAG(LFS_TYPE_INLINESTRUCT, file->id, 0), - i, &data, 1); - if (err) { - return err; - } - } else { - err = lfs_bd_read(lfs, - &file->cache, &lfs->rcache, file->off-i, - file->block, i, &data, 1); - if (err) { - return err; - } - } - - err = lfs_bd_prog(lfs, - &lfs->pcache, &lfs->rcache, true, - nblock, i, &data, 1); - if (err) { - if (err == LFS_ERR_CORRUPT) { - goto relocate; - } - return err; - } - } - - // copy over new state of file - memcpy(file->cache.buffer, lfs->pcache.buffer, lfs->cfg->cache_size); - file->cache.block = lfs->pcache.block; - file->cache.off = lfs->pcache.off; - file->cache.size = lfs->pcache.size; - lfs_cache_zero(lfs, &lfs->pcache); - - file->block = nblock; - file->flags |= LFS_F_WRITING; - return 0; - -relocate: - LFS_DEBUG("Bad block at 0x%"PRIx32, nblock); - - // just clear cache and try a new block - lfs_cache_drop(lfs, &lfs->pcache); - } -} -#endif - -#ifndef LFS_READONLY -static int lfs_file_outline(lfs_t *lfs, lfs_file_t *file) { - file->off = file->pos; - lfs_alloc_ack(lfs); - int err = lfs_file_relocate(lfs, file); - if (err) { - return err; - } - - file->flags &= ~LFS_F_INLINE; - return 0; -} -#endif - -static int lfs_file_flush(lfs_t *lfs, lfs_file_t *file) { - if (file->flags & LFS_F_READING) { - if (!(file->flags & LFS_F_INLINE)) { - lfs_cache_drop(lfs, &file->cache); - } - file->flags &= ~LFS_F_READING; - } - -#ifndef LFS_READONLY - if (file->flags & LFS_F_WRITING) { - lfs_off_t pos = file->pos; - - if (!(file->flags & LFS_F_INLINE)) { - // copy over anything after current branch - lfs_file_t orig = { - .ctz.head = file->ctz.head, - .ctz.size = file->ctz.size, - .flags = LFS_O_RDONLY, - .pos = file->pos, - .cache = lfs->rcache, - }; - lfs_cache_drop(lfs, &lfs->rcache); - - while (file->pos < file->ctz.size) { - // copy over a byte at a time, leave it up to caching - // to make this efficient - uint8_t data; - lfs_ssize_t res = lfs_file_flushedread(lfs, &orig, &data, 1); - if (res < 0) { - return res; - } - - res = lfs_file_flushedwrite(lfs, file, &data, 1); - if (res < 0) { - return res; - } - - // keep our reference to the rcache in sync - if (lfs->rcache.block != LFS_BLOCK_NULL) { - lfs_cache_drop(lfs, &orig.cache); - lfs_cache_drop(lfs, &lfs->rcache); - } - } - - // write out what we have - while (true) { - int err = lfs_bd_flush(lfs, &file->cache, &lfs->rcache, true); - if (err) { - if (err == LFS_ERR_CORRUPT) { - goto relocate; - } - return err; - } - - break; - -relocate: - LFS_DEBUG("Bad block at 0x%"PRIx32, file->block); - err = lfs_file_relocate(lfs, file); - if (err) { - return err; - } - } - } else { - file->pos = lfs_max(file->pos, file->ctz.size); - } - - // actual file updates - file->ctz.head = file->block; - file->ctz.size = file->pos; - file->flags &= ~LFS_F_WRITING; - file->flags |= LFS_F_DIRTY; - - file->pos = pos; - } -#endif - - return 0; -} - -#ifndef LFS_READONLY -static int lfs_file_rawsync(lfs_t *lfs, lfs_file_t *file) { - if (file->flags & LFS_F_ERRED) { - // it's not safe to do anything if our file errored - return 0; - } - - int err = lfs_file_flush(lfs, file); - if (err) { - file->flags |= LFS_F_ERRED; - return err; - } - - - if ((file->flags & LFS_F_DIRTY) && - !lfs_pair_isnull(file->m.pair)) { - // update dir entry - uint16_t type; - const void *buffer; - lfs_size_t size; - struct lfs_ctz ctz; - if (file->flags & LFS_F_INLINE) { - // inline the whole file - type = LFS_TYPE_INLINESTRUCT; - buffer = file->cache.buffer; - size = file->ctz.size; - } else { - // update the ctz reference - type = LFS_TYPE_CTZSTRUCT; - // copy ctz so alloc will work during a relocate - ctz = file->ctz; - lfs_ctz_tole32(&ctz); - buffer = &ctz; - size = sizeof(ctz); - } - - // commit file data and attributes - err = lfs_dir_commit(lfs, &file->m, LFS_MKATTRS( - {LFS_MKTAG(type, file->id, size), buffer}, - {LFS_MKTAG(LFS_FROM_USERATTRS, file->id, - file->cfg->attr_count), file->cfg->attrs})); - if (err) { - file->flags |= LFS_F_ERRED; - return err; - } - - file->flags &= ~LFS_F_DIRTY; - } - - return 0; -} -#endif - -static lfs_ssize_t lfs_file_flushedread(lfs_t *lfs, lfs_file_t *file, - void *buffer, lfs_size_t size) { - uint8_t *data = buffer; - lfs_size_t nsize = size; - - if (file->pos >= file->ctz.size) { - // eof if past end - return 0; - } - - size = lfs_min(size, file->ctz.size - file->pos); - nsize = size; - - while (nsize > 0) { - // check if we need a new block - if (!(file->flags & LFS_F_READING) || - file->off == lfs->cfg->block_size) { - if (!(file->flags & LFS_F_INLINE)) { - int err = lfs_ctz_find(lfs, NULL, &file->cache, - file->ctz.head, file->ctz.size, - file->pos, &file->block, &file->off); - if (err) { - return err; - } - } else { - file->block = LFS_BLOCK_INLINE; - file->off = file->pos; - } - - file->flags |= LFS_F_READING; - } - - // read as much as we can in current block - lfs_size_t diff = lfs_min(nsize, lfs->cfg->block_size - file->off); - if (file->flags & LFS_F_INLINE) { - int err = lfs_dir_getread(lfs, &file->m, - NULL, &file->cache, lfs->cfg->block_size, - LFS_MKTAG(0xfff, 0x1ff, 0), - LFS_MKTAG(LFS_TYPE_INLINESTRUCT, file->id, 0), - file->off, data, diff); - if (err) { - return err; - } - } else { - int err = lfs_bd_read(lfs, - NULL, &file->cache, lfs->cfg->block_size, - file->block, file->off, data, diff); - if (err) { - return err; - } - } - - file->pos += diff; - file->off += diff; - data += diff; - nsize -= diff; - } - - return size; -} - -static lfs_ssize_t lfs_file_rawread(lfs_t *lfs, lfs_file_t *file, - void *buffer, lfs_size_t size) { - LFS_ASSERT((file->flags & LFS_O_RDONLY) == LFS_O_RDONLY); - -#ifndef LFS_READONLY - if (file->flags & LFS_F_WRITING) { - // flush out any writes - int err = lfs_file_flush(lfs, file); - if (err) { - return err; - } - } -#endif - - return lfs_file_flushedread(lfs, file, buffer, size); -} - - -#ifndef LFS_READONLY -static lfs_ssize_t lfs_file_flushedwrite(lfs_t *lfs, lfs_file_t *file, - const void *buffer, lfs_size_t size) { - const uint8_t *data = buffer; - lfs_size_t nsize = size; - - if ((file->flags & LFS_F_INLINE) && - lfs_max(file->pos+nsize, file->ctz.size) > - lfs_min(0x3fe, lfs_min( - lfs->cfg->cache_size, - (lfs->cfg->metadata_max ? - lfs->cfg->metadata_max : lfs->cfg->block_size) / 8))) { - // inline file doesn't fit anymore - int err = lfs_file_outline(lfs, file); - if (err) { - file->flags |= LFS_F_ERRED; - return err; - } - } - - while (nsize > 0) { - // check if we need a new block - if (!(file->flags & LFS_F_WRITING) || - file->off == lfs->cfg->block_size) { - if (!(file->flags & LFS_F_INLINE)) { - if (!(file->flags & LFS_F_WRITING) && file->pos > 0) { - // find out which block we're extending from - int err = lfs_ctz_find(lfs, NULL, &file->cache, - file->ctz.head, file->ctz.size, - file->pos-1, &file->block, &file->off); - if (err) { - file->flags |= LFS_F_ERRED; - return err; - } - - // mark cache as dirty since we may have read data into it - lfs_cache_zero(lfs, &file->cache); - } - - // extend file with new blocks - lfs_alloc_ack(lfs); - int err = lfs_ctz_extend(lfs, &file->cache, &lfs->rcache, - file->block, file->pos, - &file->block, &file->off); - if (err) { - file->flags |= LFS_F_ERRED; - return err; - } - } else { - file->block = LFS_BLOCK_INLINE; - file->off = file->pos; - } - - file->flags |= LFS_F_WRITING; - } - - // program as much as we can in current block - lfs_size_t diff = lfs_min(nsize, lfs->cfg->block_size - file->off); - while (true) { - int err = lfs_bd_prog(lfs, &file->cache, &lfs->rcache, true, - file->block, file->off, data, diff); - if (err) { - if (err == LFS_ERR_CORRUPT) { - goto relocate; - } - file->flags |= LFS_F_ERRED; - return err; - } - - break; -relocate: - err = lfs_file_relocate(lfs, file); - if (err) { - file->flags |= LFS_F_ERRED; - return err; - } - } - - file->pos += diff; - file->off += diff; - data += diff; - nsize -= diff; - - lfs_alloc_ack(lfs); - } - - return size; -} - -static lfs_ssize_t lfs_file_rawwrite(lfs_t *lfs, lfs_file_t *file, - const void *buffer, lfs_size_t size) { - LFS_ASSERT((file->flags & LFS_O_WRONLY) == LFS_O_WRONLY); - - if (file->flags & LFS_F_READING) { - // drop any reads - int err = lfs_file_flush(lfs, file); - if (err) { - return err; - } - } - - if ((file->flags & LFS_O_APPEND) && file->pos < file->ctz.size) { - file->pos = file->ctz.size; - } - - if (file->pos + size > lfs->file_max) { - // Larger than file limit? - return LFS_ERR_FBIG; - } - - if (!(file->flags & LFS_F_WRITING) && file->pos > file->ctz.size) { - // fill with zeros - lfs_off_t pos = file->pos; - file->pos = file->ctz.size; - - while (file->pos < pos) { - lfs_ssize_t res = lfs_file_flushedwrite(lfs, file, &(uint8_t){0}, 1); - if (res < 0) { - return res; - } - } - } - - lfs_ssize_t nsize = lfs_file_flushedwrite(lfs, file, buffer, size); - if (nsize < 0) { - return nsize; - } - - file->flags &= ~LFS_F_ERRED; - return nsize; -} -#endif - -static lfs_soff_t lfs_file_rawseek(lfs_t *lfs, lfs_file_t *file, - lfs_soff_t off, int whence) { - // find new pos - lfs_off_t npos = file->pos; - if (whence == LFS_SEEK_SET) { - npos = off; - } else if (whence == LFS_SEEK_CUR) { - if ((lfs_soff_t)file->pos + off < 0) { - return LFS_ERR_INVAL; - } else { - npos = file->pos + off; - } - } else if (whence == LFS_SEEK_END) { - lfs_soff_t res = lfs_file_rawsize(lfs, file) + off; - if (res < 0) { - return LFS_ERR_INVAL; - } else { - npos = res; - } - } - - if (npos > lfs->file_max) { - // file position out of range - return LFS_ERR_INVAL; - } - - if (file->pos == npos) { - // noop - position has not changed - return npos; - } - - // if we're only reading and our new offset is still in the file's cache - // we can avoid flushing and needing to reread the data - if ( -#ifndef LFS_READONLY - !(file->flags & LFS_F_WRITING) -#else - true -#endif - ) { - int oindex = lfs_ctz_index(lfs, &(lfs_off_t){file->pos}); - lfs_off_t noff = npos; - int nindex = lfs_ctz_index(lfs, &noff); - if (oindex == nindex - && noff >= file->cache.off - && noff < file->cache.off + file->cache.size) { - file->pos = npos; - file->off = noff; - return npos; - } - } - - // write out everything beforehand, may be noop if rdonly - int err = lfs_file_flush(lfs, file); - if (err) { - return err; - } - - // update pos - file->pos = npos; - return npos; -} - -#ifndef LFS_READONLY -static int lfs_file_rawtruncate(lfs_t *lfs, lfs_file_t *file, lfs_off_t size) { - LFS_ASSERT((file->flags & LFS_O_WRONLY) == LFS_O_WRONLY); - - if (size > LFS_FILE_MAX) { - return LFS_ERR_INVAL; - } - - lfs_off_t pos = file->pos; - lfs_off_t oldsize = lfs_file_rawsize(lfs, file); - if (size < oldsize) { - // need to flush since directly changing metadata - int err = lfs_file_flush(lfs, file); - if (err) { - return err; - } - - // lookup new head in ctz skip list - err = lfs_ctz_find(lfs, NULL, &file->cache, - file->ctz.head, file->ctz.size, - size, &file->block, &file->off); - if (err) { - return err; - } - - // need to set pos/block/off consistently so seeking back to - // the old position does not get confused - file->pos = size; - file->ctz.head = file->block; - file->ctz.size = size; - file->flags |= LFS_F_DIRTY | LFS_F_READING; - } else if (size > oldsize) { - // flush+seek if not already at end - lfs_soff_t res = lfs_file_rawseek(lfs, file, 0, LFS_SEEK_END); - if (res < 0) { - return (int)res; - } - - // fill with zeros - while (file->pos < size) { - res = lfs_file_rawwrite(lfs, file, &(uint8_t){0}, 1); - if (res < 0) { - return (int)res; - } - } - } - - // restore pos - lfs_soff_t res = lfs_file_rawseek(lfs, file, pos, LFS_SEEK_SET); - if (res < 0) { - return (int)res; - } - - return 0; -} -#endif - -static lfs_soff_t lfs_file_rawtell(lfs_t *lfs, lfs_file_t *file) { - (void)lfs; - return file->pos; -} - -static int lfs_file_rawrewind(lfs_t *lfs, lfs_file_t *file) { - lfs_soff_t res = lfs_file_rawseek(lfs, file, 0, LFS_SEEK_SET); - if (res < 0) { - return (int)res; - } - - return 0; -} - -static lfs_soff_t lfs_file_rawsize(lfs_t *lfs, lfs_file_t *file) { - (void)lfs; - -#ifndef LFS_READONLY - if (file->flags & LFS_F_WRITING) { - return lfs_max(file->pos, file->ctz.size); - } -#endif - - return file->ctz.size; -} - - -/// General fs operations /// -static int lfs_rawstat(lfs_t *lfs, const char *path, struct lfs_info *info) { - lfs_mdir_t cwd; - lfs_stag_t tag = lfs_dir_find(lfs, &cwd, &path, NULL); - if (tag < 0) { - return (int)tag; - } - - return lfs_dir_getinfo(lfs, &cwd, lfs_tag_id(tag), info); -} - -#ifndef LFS_READONLY -static int lfs_rawremove(lfs_t *lfs, const char *path) { - // deorphan if we haven't yet, needed at most once after poweron - int err = lfs_fs_forceconsistency(lfs); - if (err) { - return err; - } - - lfs_mdir_t cwd; - lfs_stag_t tag = lfs_dir_find(lfs, &cwd, &path, NULL); - if (tag < 0 || lfs_tag_id(tag) == 0x3ff) { - return (tag < 0) ? (int)tag : LFS_ERR_INVAL; - } - - struct lfs_mlist dir; - dir.next = lfs->mlist; - if (lfs_tag_type3(tag) == LFS_TYPE_DIR) { - // must be empty before removal - lfs_block_t pair[2]; - lfs_stag_t res = lfs_dir_get(lfs, &cwd, LFS_MKTAG(0x700, 0x3ff, 0), - LFS_MKTAG(LFS_TYPE_STRUCT, lfs_tag_id(tag), 8), pair); - if (res < 0) { - return (int)res; - } - lfs_pair_fromle32(pair); - - err = lfs_dir_fetch(lfs, &dir.m, pair); - if (err) { - return err; - } - - if (dir.m.count > 0 || dir.m.split) { - return LFS_ERR_NOTEMPTY; - } - - // mark fs as orphaned - err = lfs_fs_preporphans(lfs, +1); - if (err) { - return err; - } - - // I know it's crazy but yes, dir can be changed by our parent's - // commit (if predecessor is child) - dir.type = 0; - dir.id = 0; - lfs->mlist = &dir; - } - - // delete the entry - err = lfs_dir_commit(lfs, &cwd, LFS_MKATTRS( - {LFS_MKTAG(LFS_TYPE_DELETE, lfs_tag_id(tag), 0), NULL})); - if (err) { - lfs->mlist = dir.next; - return err; - } - - lfs->mlist = dir.next; - if (lfs_tag_type3(tag) == LFS_TYPE_DIR) { - // fix orphan - err = lfs_fs_preporphans(lfs, -1); - if (err) { - return err; - } - - err = lfs_fs_pred(lfs, dir.m.pair, &cwd); - if (err) { - return err; - } - - err = lfs_dir_drop(lfs, &cwd, &dir.m); - if (err) { - return err; - } - } - - return 0; -} -#endif - -#ifndef LFS_READONLY -static int lfs_rawrename(lfs_t *lfs, const char *oldpath, const char *newpath) { - // deorphan if we haven't yet, needed at most once after poweron - int err = lfs_fs_forceconsistency(lfs); - if (err) { - return err; - } - - // find old entry - lfs_mdir_t oldcwd; - lfs_stag_t oldtag = lfs_dir_find(lfs, &oldcwd, &oldpath, NULL); - if (oldtag < 0 || lfs_tag_id(oldtag) == 0x3ff) { - return (oldtag < 0) ? (int)oldtag : LFS_ERR_INVAL; - } - - // find new entry - lfs_mdir_t newcwd; - uint16_t newid; - lfs_stag_t prevtag = lfs_dir_find(lfs, &newcwd, &newpath, &newid); - if ((prevtag < 0 || lfs_tag_id(prevtag) == 0x3ff) && - !(prevtag == LFS_ERR_NOENT && newid != 0x3ff)) { - return (prevtag < 0) ? (int)prevtag : LFS_ERR_INVAL; - } - - // if we're in the same pair there's a few special cases... - bool samepair = (lfs_pair_cmp(oldcwd.pair, newcwd.pair) == 0); - uint16_t newoldid = lfs_tag_id(oldtag); - - struct lfs_mlist prevdir; - prevdir.next = lfs->mlist; - if (prevtag == LFS_ERR_NOENT) { - // check that name fits - lfs_size_t nlen = strlen(newpath); - if (nlen > lfs->name_max) { - return LFS_ERR_NAMETOOLONG; - } - - // there is a small chance we are being renamed in the same - // directory/ to an id less than our old id, the global update - // to handle this is a bit messy - if (samepair && newid <= newoldid) { - newoldid += 1; - } - } else if (lfs_tag_type3(prevtag) != lfs_tag_type3(oldtag)) { - return LFS_ERR_ISDIR; - } else if (samepair && newid == newoldid) { - // we're renaming to ourselves?? - return 0; - } else if (lfs_tag_type3(prevtag) == LFS_TYPE_DIR) { - // must be empty before removal - lfs_block_t prevpair[2]; - lfs_stag_t res = lfs_dir_get(lfs, &newcwd, LFS_MKTAG(0x700, 0x3ff, 0), - LFS_MKTAG(LFS_TYPE_STRUCT, newid, 8), prevpair); - if (res < 0) { - return (int)res; - } - lfs_pair_fromle32(prevpair); - - // must be empty before removal - err = lfs_dir_fetch(lfs, &prevdir.m, prevpair); - if (err) { - return err; - } - - if (prevdir.m.count > 0 || prevdir.m.split) { - return LFS_ERR_NOTEMPTY; - } - - // mark fs as orphaned - err = lfs_fs_preporphans(lfs, +1); - if (err) { - return err; - } - - // I know it's crazy but yes, dir can be changed by our parent's - // commit (if predecessor is child) - prevdir.type = 0; - prevdir.id = 0; - lfs->mlist = &prevdir; - } - - if (!samepair) { - lfs_fs_prepmove(lfs, newoldid, oldcwd.pair); - } - - // move over all attributes - err = lfs_dir_commit(lfs, &newcwd, LFS_MKATTRS( - {LFS_MKTAG_IF(prevtag != LFS_ERR_NOENT, - LFS_TYPE_DELETE, newid, 0), NULL}, - {LFS_MKTAG(LFS_TYPE_CREATE, newid, 0), NULL}, - {LFS_MKTAG(lfs_tag_type3(oldtag), newid, strlen(newpath)), newpath}, - {LFS_MKTAG(LFS_FROM_MOVE, newid, lfs_tag_id(oldtag)), &oldcwd}, - {LFS_MKTAG_IF(samepair, - LFS_TYPE_DELETE, newoldid, 0), NULL})); - if (err) { - lfs->mlist = prevdir.next; - return err; - } - - // let commit clean up after move (if we're different! otherwise move - // logic already fixed it for us) - if (!samepair && lfs_gstate_hasmove(&lfs->gstate)) { - // prep gstate and delete move id - lfs_fs_prepmove(lfs, 0x3ff, NULL); - err = lfs_dir_commit(lfs, &oldcwd, LFS_MKATTRS( - {LFS_MKTAG(LFS_TYPE_DELETE, lfs_tag_id(oldtag), 0), NULL})); - if (err) { - lfs->mlist = prevdir.next; - return err; - } - } - - lfs->mlist = prevdir.next; - if (prevtag != LFS_ERR_NOENT - && lfs_tag_type3(prevtag) == LFS_TYPE_DIR) { - // fix orphan - err = lfs_fs_preporphans(lfs, -1); - if (err) { - return err; - } - - err = lfs_fs_pred(lfs, prevdir.m.pair, &newcwd); - if (err) { - return err; - } - - err = lfs_dir_drop(lfs, &newcwd, &prevdir.m); - if (err) { - return err; - } - } - - return 0; -} -#endif - -static lfs_ssize_t lfs_rawgetattr(lfs_t *lfs, const char *path, - uint8_t type, void *buffer, lfs_size_t size) { - lfs_mdir_t cwd; - lfs_stag_t tag = lfs_dir_find(lfs, &cwd, &path, NULL); - if (tag < 0) { - return tag; - } - - uint16_t id = lfs_tag_id(tag); - if (id == 0x3ff) { - // special case for root - id = 0; - int err = lfs_dir_fetch(lfs, &cwd, lfs->root); - if (err) { - return err; - } - } - - tag = lfs_dir_get(lfs, &cwd, LFS_MKTAG(0x7ff, 0x3ff, 0), - LFS_MKTAG(LFS_TYPE_USERATTR + type, - id, lfs_min(size, lfs->attr_max)), - buffer); - if (tag < 0) { - if (tag == LFS_ERR_NOENT) { - return LFS_ERR_NOATTR; - } - - return tag; - } - - return lfs_tag_size(tag); -} - -#ifndef LFS_READONLY -static int lfs_commitattr(lfs_t *lfs, const char *path, - uint8_t type, const void *buffer, lfs_size_t size) { - lfs_mdir_t cwd; - lfs_stag_t tag = lfs_dir_find(lfs, &cwd, &path, NULL); - if (tag < 0) { - return tag; - } - - uint16_t id = lfs_tag_id(tag); - if (id == 0x3ff) { - // special case for root - id = 0; - int err = lfs_dir_fetch(lfs, &cwd, lfs->root); - if (err) { - return err; - } - } - - return lfs_dir_commit(lfs, &cwd, LFS_MKATTRS( - {LFS_MKTAG(LFS_TYPE_USERATTR + type, id, size), buffer})); -} -#endif - -#ifndef LFS_READONLY -static int lfs_rawsetattr(lfs_t *lfs, const char *path, - uint8_t type, const void *buffer, lfs_size_t size) { - if (size > lfs->attr_max) { - return LFS_ERR_NOSPC; - } - - return lfs_commitattr(lfs, path, type, buffer, size); -} -#endif - -#ifndef LFS_READONLY -static int lfs_rawremoveattr(lfs_t *lfs, const char *path, uint8_t type) { - return lfs_commitattr(lfs, path, type, NULL, 0x3ff); -} -#endif - - -/// Filesystem operations /// -static int lfs_init(lfs_t *lfs, const struct lfs_config *cfg) { - lfs->cfg = cfg; - int err = 0; - - // validate that the lfs-cfg sizes were initiated properly before - // performing any arithmetic logics with them - LFS_ASSERT(lfs->cfg->read_size != 0); - LFS_ASSERT(lfs->cfg->prog_size != 0); - LFS_ASSERT(lfs->cfg->cache_size != 0); - - // check that block size is a multiple of cache size is a multiple - // of prog and read sizes - LFS_ASSERT(lfs->cfg->cache_size % lfs->cfg->read_size == 0); - LFS_ASSERT(lfs->cfg->cache_size % lfs->cfg->prog_size == 0); - LFS_ASSERT(lfs->cfg->block_size % lfs->cfg->cache_size == 0); - - // check that the block size is large enough to fit ctz pointers - LFS_ASSERT(4*lfs_npw2(0xffffffff / (lfs->cfg->block_size-2*4)) - <= lfs->cfg->block_size); - - // block_cycles = 0 is no longer supported. - // - // block_cycles is the number of erase cycles before littlefs evicts - // metadata logs as a part of wear leveling. Suggested values are in the - // range of 100-1000, or set block_cycles to -1 to disable block-level - // wear-leveling. - LFS_ASSERT(lfs->cfg->block_cycles != 0); - - - // setup read cache - if (lfs->cfg->read_buffer) { - lfs->rcache.buffer = lfs->cfg->read_buffer; - } else { - lfs->rcache.buffer = lfs_malloc(lfs->cfg->cache_size); - if (!lfs->rcache.buffer) { - err = LFS_ERR_NOMEM; - goto cleanup; - } - } - - // setup program cache - if (lfs->cfg->prog_buffer) { - lfs->pcache.buffer = lfs->cfg->prog_buffer; - } else { - lfs->pcache.buffer = lfs_malloc(lfs->cfg->cache_size); - if (!lfs->pcache.buffer) { - err = LFS_ERR_NOMEM; - goto cleanup; - } - } - - // zero to avoid information leaks - lfs_cache_zero(lfs, &lfs->rcache); - lfs_cache_zero(lfs, &lfs->pcache); - - // setup lookahead, must be multiple of 64-bits, 32-bit aligned - LFS_ASSERT(lfs->cfg->lookahead_size > 0); - LFS_ASSERT(lfs->cfg->lookahead_size % 8 == 0 && - (uintptr_t)lfs->cfg->lookahead_buffer % 4 == 0); - if (lfs->cfg->lookahead_buffer) { - lfs->free.buffer = lfs->cfg->lookahead_buffer; - } else { - lfs->free.buffer = lfs_malloc(lfs->cfg->lookahead_size); - if (!lfs->free.buffer) { - err = LFS_ERR_NOMEM; - goto cleanup; - } - } - - // check that the size limits are sane - LFS_ASSERT(lfs->cfg->name_max <= LFS_NAME_MAX); - lfs->name_max = lfs->cfg->name_max; - if (!lfs->name_max) { - lfs->name_max = LFS_NAME_MAX; - } - - LFS_ASSERT(lfs->cfg->file_max <= LFS_FILE_MAX); - lfs->file_max = lfs->cfg->file_max; - if (!lfs->file_max) { - lfs->file_max = LFS_FILE_MAX; - } - - LFS_ASSERT(lfs->cfg->attr_max <= LFS_ATTR_MAX); - lfs->attr_max = lfs->cfg->attr_max; - if (!lfs->attr_max) { - lfs->attr_max = LFS_ATTR_MAX; - } - - LFS_ASSERT(lfs->cfg->metadata_max <= lfs->cfg->block_size); - - // setup default state - lfs->root[0] = LFS_BLOCK_NULL; - lfs->root[1] = LFS_BLOCK_NULL; - lfs->mlist = NULL; - lfs->seed = 0; - lfs->gdisk = (lfs_gstate_t){0}; - lfs->gstate = (lfs_gstate_t){0}; - lfs->gdelta = (lfs_gstate_t){0}; -#ifdef LFS_MIGRATE - lfs->lfs1 = NULL; -#endif - - return 0; - -cleanup: - lfs_deinit(lfs); - return err; -} - -static int lfs_deinit(lfs_t *lfs) { - // free allocated memory - if (!lfs->cfg->read_buffer) { - lfs_free(lfs->rcache.buffer); - } - - if (!lfs->cfg->prog_buffer) { - lfs_free(lfs->pcache.buffer); - } - - if (!lfs->cfg->lookahead_buffer) { - lfs_free(lfs->free.buffer); - } - - return 0; -} - -#ifndef LFS_READONLY -static int lfs_rawformat(lfs_t *lfs, const struct lfs_config *cfg) { - int err = 0; - { - err = lfs_init(lfs, cfg); - if (err) { - return err; - } - - // create free lookahead - memset(lfs->free.buffer, 0, lfs->cfg->lookahead_size); - lfs->free.off = 0; - lfs->free.size = lfs_min(8*lfs->cfg->lookahead_size, - lfs->cfg->block_count); - lfs->free.i = 0; - lfs_alloc_ack(lfs); - - // create root dir - lfs_mdir_t root; - err = lfs_dir_alloc(lfs, &root); - if (err) { - goto cleanup; - } - - // write one superblock - lfs_superblock_t superblock = { - .version = LFS_DISK_VERSION, - .block_size = lfs->cfg->block_size, - .block_count = lfs->cfg->block_count, - .name_max = lfs->name_max, - .file_max = lfs->file_max, - .attr_max = lfs->attr_max, - }; - - lfs_superblock_tole32(&superblock); - err = lfs_dir_commit(lfs, &root, LFS_MKATTRS( - {LFS_MKTAG(LFS_TYPE_CREATE, 0, 0), NULL}, - {LFS_MKTAG(LFS_TYPE_SUPERBLOCK, 0, 8), "littlefs"}, - {LFS_MKTAG(LFS_TYPE_INLINESTRUCT, 0, sizeof(superblock)), - &superblock})); - if (err) { - goto cleanup; - } - - // force compaction to prevent accidentally mounting any - // older version of littlefs that may live on disk - root.erased = false; - err = lfs_dir_commit(lfs, &root, NULL, 0); - if (err) { - goto cleanup; - } - - // sanity check that fetch works - err = lfs_dir_fetch(lfs, &root, (const lfs_block_t[2]){0, 1}); - if (err) { - goto cleanup; - } - } - -cleanup: - lfs_deinit(lfs); - return err; - -} -#endif - -static int lfs_rawmount(lfs_t *lfs, const struct lfs_config *cfg) { - int err = lfs_init(lfs, cfg); - if (err) { - return err; - } - - // scan directory blocks for superblock and any global updates - lfs_mdir_t dir = {.tail = {0, 1}}; - lfs_block_t cycle = 0; - while (!lfs_pair_isnull(dir.tail)) { - if (cycle >= lfs->cfg->block_count/2) { - // loop detected - err = LFS_ERR_CORRUPT; - goto cleanup; - } - cycle += 1; - - // fetch next block in tail list - lfs_stag_t tag = lfs_dir_fetchmatch(lfs, &dir, dir.tail, - LFS_MKTAG(0x7ff, 0x3ff, 0), - LFS_MKTAG(LFS_TYPE_SUPERBLOCK, 0, 8), - NULL, - lfs_dir_find_match, &(struct lfs_dir_find_match){ - lfs, "littlefs", 8}); - if (tag < 0) { - err = tag; - goto cleanup; - } - - // has superblock? - if (tag && !lfs_tag_isdelete(tag)) { - // update root - lfs->root[0] = dir.pair[0]; - lfs->root[1] = dir.pair[1]; - - // grab superblock - lfs_superblock_t superblock; - tag = lfs_dir_get(lfs, &dir, LFS_MKTAG(0x7ff, 0x3ff, 0), - LFS_MKTAG(LFS_TYPE_INLINESTRUCT, 0, sizeof(superblock)), - &superblock); - if (tag < 0) { - err = tag; - goto cleanup; - } - lfs_superblock_fromle32(&superblock); - - // check version - uint16_t major_version = (0xffff & (superblock.version >> 16)); - uint16_t minor_version = (0xffff & (superblock.version >> 0)); - if ((major_version != LFS_DISK_VERSION_MAJOR || - minor_version > LFS_DISK_VERSION_MINOR)) { - LFS_ERROR("Invalid version v%"PRIu16".%"PRIu16, - major_version, minor_version); - err = LFS_ERR_INVAL; - goto cleanup; - } - - // check superblock configuration - if (superblock.name_max) { - if (superblock.name_max > lfs->name_max) { - LFS_ERROR("Unsupported name_max (%"PRIu32" > %"PRIu32")", - superblock.name_max, lfs->name_max); - err = LFS_ERR_INVAL; - goto cleanup; - } - - lfs->name_max = superblock.name_max; - } - - if (superblock.file_max) { - if (superblock.file_max > lfs->file_max) { - LFS_ERROR("Unsupported file_max (%"PRIu32" > %"PRIu32")", - superblock.file_max, lfs->file_max); - err = LFS_ERR_INVAL; - goto cleanup; - } - - lfs->file_max = superblock.file_max; - } - - if (superblock.attr_max) { - if (superblock.attr_max > lfs->attr_max) { - LFS_ERROR("Unsupported attr_max (%"PRIu32" > %"PRIu32")", - superblock.attr_max, lfs->attr_max); - err = LFS_ERR_INVAL; - goto cleanup; - } - - lfs->attr_max = superblock.attr_max; - } - - if (superblock.block_count != lfs->cfg->block_count) { - LFS_ERROR("Invalid block count (%"PRIu32" != %"PRIu32")", - superblock.block_count, lfs->cfg->block_count); - err = LFS_ERR_INVAL; - goto cleanup; - } - - if (superblock.block_size != lfs->cfg->block_size) { - LFS_ERROR("Invalid block size (%"PRIu32" != %"PRIu32")", - superblock.block_size, lfs->cfg->block_size); - err = LFS_ERR_INVAL; - goto cleanup; - } - } - - // has gstate? - err = lfs_dir_getgstate(lfs, &dir, &lfs->gstate); - if (err) { - goto cleanup; - } - } - - // found superblock? - if (lfs_pair_isnull(lfs->root)) { - err = LFS_ERR_INVAL; - goto cleanup; - } - - // update littlefs with gstate - if (!lfs_gstate_iszero(&lfs->gstate)) { - LFS_DEBUG("Found pending gstate 0x%08"PRIx32"%08"PRIx32"%08"PRIx32, - lfs->gstate.tag, - lfs->gstate.pair[0], - lfs->gstate.pair[1]); - } - lfs->gstate.tag += !lfs_tag_isvalid(lfs->gstate.tag); - lfs->gdisk = lfs->gstate; - - // setup free lookahead, to distribute allocations uniformly across - // boots, we start the allocator at a random location - lfs->free.off = lfs->seed % lfs->cfg->block_count; - lfs_alloc_drop(lfs); - - return 0; - -cleanup: - lfs_rawunmount(lfs); - return err; -} - -static int lfs_rawunmount(lfs_t *lfs) { - return lfs_deinit(lfs); -} - - -/// Filesystem filesystem operations /// -int lfs_fs_rawtraverse(lfs_t *lfs, - int (*cb)(void *data, lfs_block_t block), void *data, - bool includeorphans) { - // iterate over metadata pairs - lfs_mdir_t dir = {.tail = {0, 1}}; - -#ifdef LFS_MIGRATE - // also consider v1 blocks during migration - if (lfs->lfs1) { - int err = lfs1_traverse(lfs, cb, data); - if (err) { - return err; - } - - dir.tail[0] = lfs->root[0]; - dir.tail[1] = lfs->root[1]; - } -#endif - - lfs_block_t cycle = 0; - while (!lfs_pair_isnull(dir.tail)) { - if (cycle >= lfs->cfg->block_count/2) { - // loop detected - return LFS_ERR_CORRUPT; - } - cycle += 1; - - for (int i = 0; i < 2; i++) { - int err = cb(data, dir.tail[i]); - if (err) { - return err; - } - } - - // iterate through ids in directory - int err = lfs_dir_fetch(lfs, &dir, dir.tail); - if (err) { - return err; - } - - for (uint16_t id = 0; id < dir.count; id++) { - struct lfs_ctz ctz; - lfs_stag_t tag = lfs_dir_get(lfs, &dir, LFS_MKTAG(0x700, 0x3ff, 0), - LFS_MKTAG(LFS_TYPE_STRUCT, id, sizeof(ctz)), &ctz); - if (tag < 0) { - if (tag == LFS_ERR_NOENT) { - continue; - } - return tag; - } - lfs_ctz_fromle32(&ctz); - - if (lfs_tag_type3(tag) == LFS_TYPE_CTZSTRUCT) { - err = lfs_ctz_traverse(lfs, NULL, &lfs->rcache, - ctz.head, ctz.size, cb, data); - if (err) { - return err; - } - } else if (includeorphans && - lfs_tag_type3(tag) == LFS_TYPE_DIRSTRUCT) { - for (int i = 0; i < 2; i++) { - err = cb(data, (&ctz.head)[i]); - if (err) { - return err; - } - } - } - } - } - -#ifndef LFS_READONLY - // iterate over any open files - for (lfs_file_t *f = (lfs_file_t*)lfs->mlist; f; f = f->next) { - if (f->type != LFS_TYPE_REG) { - continue; - } - - if ((f->flags & LFS_F_DIRTY) && !(f->flags & LFS_F_INLINE)) { - int err = lfs_ctz_traverse(lfs, &f->cache, &lfs->rcache, - f->ctz.head, f->ctz.size, cb, data); - if (err) { - return err; - } - } - - if ((f->flags & LFS_F_WRITING) && !(f->flags & LFS_F_INLINE)) { - int err = lfs_ctz_traverse(lfs, &f->cache, &lfs->rcache, - f->block, f->pos, cb, data); - if (err) { - return err; - } - } - } -#endif - - return 0; -} - -#ifndef LFS_READONLY -static int lfs_fs_pred(lfs_t *lfs, - const lfs_block_t pair[2], lfs_mdir_t *pdir) { - // iterate over all directory directory entries - pdir->tail[0] = 0; - pdir->tail[1] = 1; - lfs_block_t cycle = 0; - while (!lfs_pair_isnull(pdir->tail)) { - if (cycle >= lfs->cfg->block_count/2) { - // loop detected - return LFS_ERR_CORRUPT; - } - cycle += 1; - - if (lfs_pair_cmp(pdir->tail, pair) == 0) { - return 0; - } - - int err = lfs_dir_fetch(lfs, pdir, pdir->tail); - if (err) { - return err; - } - } - - return LFS_ERR_NOENT; -} -#endif - -#ifndef LFS_READONLY -struct lfs_fs_parent_match { - lfs_t *lfs; - const lfs_block_t pair[2]; -}; -#endif - -#ifndef LFS_READONLY -static int lfs_fs_parent_match(void *data, - lfs_tag_t tag, const void *buffer) { - struct lfs_fs_parent_match *find = data; - lfs_t *lfs = find->lfs; - const struct lfs_diskoff *disk = buffer; - (void)tag; - - lfs_block_t child[2]; - int err = lfs_bd_read(lfs, - &lfs->pcache, &lfs->rcache, lfs->cfg->block_size, - disk->block, disk->off, &child, sizeof(child)); - if (err) { - return err; - } - - lfs_pair_fromle32(child); - return (lfs_pair_cmp(child, find->pair) == 0) ? LFS_CMP_EQ : LFS_CMP_LT; -} -#endif - -#ifndef LFS_READONLY -static lfs_stag_t lfs_fs_parent(lfs_t *lfs, const lfs_block_t pair[2], - lfs_mdir_t *parent) { - // use fetchmatch with callback to find pairs - parent->tail[0] = 0; - parent->tail[1] = 1; - lfs_block_t cycle = 0; - while (!lfs_pair_isnull(parent->tail)) { - if (cycle >= lfs->cfg->block_count/2) { - // loop detected - return LFS_ERR_CORRUPT; - } - cycle += 1; - - lfs_stag_t tag = lfs_dir_fetchmatch(lfs, parent, parent->tail, - LFS_MKTAG(0x7ff, 0, 0x3ff), - LFS_MKTAG(LFS_TYPE_DIRSTRUCT, 0, 8), - NULL, - lfs_fs_parent_match, &(struct lfs_fs_parent_match){ - lfs, {pair[0], pair[1]}}); - if (tag && tag != LFS_ERR_NOENT) { - return tag; - } - } - - return LFS_ERR_NOENT; -} -#endif - -#ifndef LFS_READONLY -static int lfs_fs_preporphans(lfs_t *lfs, int8_t orphans) { - LFS_ASSERT(lfs_tag_size(lfs->gstate.tag) > 0x000 || orphans >= 0); - LFS_ASSERT(lfs_tag_size(lfs->gstate.tag) < 0x3ff || orphans <= 0); - lfs->gstate.tag += orphans; - lfs->gstate.tag = ((lfs->gstate.tag & ~LFS_MKTAG(0x800, 0, 0)) | - ((uint32_t)lfs_gstate_hasorphans(&lfs->gstate) << 31)); - - return 0; -} -#endif - -#ifndef LFS_READONLY -static void lfs_fs_prepmove(lfs_t *lfs, - uint16_t id, const lfs_block_t pair[2]) { - lfs->gstate.tag = ((lfs->gstate.tag & ~LFS_MKTAG(0x7ff, 0x3ff, 0)) | - ((id != 0x3ff) ? LFS_MKTAG(LFS_TYPE_DELETE, id, 0) : 0)); - lfs->gstate.pair[0] = (id != 0x3ff) ? pair[0] : 0; - lfs->gstate.pair[1] = (id != 0x3ff) ? pair[1] : 0; -} -#endif - -#ifndef LFS_READONLY -static int lfs_fs_demove(lfs_t *lfs) { - if (!lfs_gstate_hasmove(&lfs->gdisk)) { - return 0; - } - - // Fix bad moves - LFS_DEBUG("Fixing move {0x%"PRIx32", 0x%"PRIx32"} 0x%"PRIx16, - lfs->gdisk.pair[0], - lfs->gdisk.pair[1], - lfs_tag_id(lfs->gdisk.tag)); - - // no other gstate is supported at this time, so if we found something else - // something most likely went wrong in gstate calculation - LFS_ASSERT(lfs_tag_type3(lfs->gdisk.tag) == LFS_TYPE_DELETE); - - // fetch and delete the moved entry - lfs_mdir_t movedir; - int err = lfs_dir_fetch(lfs, &movedir, lfs->gdisk.pair); - if (err) { - return err; - } - - // prep gstate and delete move id - uint16_t moveid = lfs_tag_id(lfs->gdisk.tag); - lfs_fs_prepmove(lfs, 0x3ff, NULL); - err = lfs_dir_commit(lfs, &movedir, LFS_MKATTRS( - {LFS_MKTAG(LFS_TYPE_DELETE, moveid, 0), NULL})); - if (err) { - return err; - } - - return 0; -} -#endif - -#ifndef LFS_READONLY -static int lfs_fs_deorphan(lfs_t *lfs, bool powerloss) { - if (!lfs_gstate_hasorphans(&lfs->gstate)) { - return 0; - } - - int8_t found = 0; - - // Check for orphans in two separate passes: - // - 1 for half-orphans (relocations) - // - 2 for full-orphans (removes/renames) - // - // Two separate passes are needed as half-orphans can contain outdated - // references to full-orphans, effectively hiding them from the deorphan - // search. - // - int pass = 0; - while (pass < 2) { - // Fix any orphans - lfs_mdir_t pdir = {.split = true, .tail = {0, 1}}; - lfs_mdir_t dir; - bool moreorphans = false; - - // iterate over all directory directory entries - while (!lfs_pair_isnull(pdir.tail)) { - int err = lfs_dir_fetch(lfs, &dir, pdir.tail); - if (err) { - return err; - } - - // check head blocks for orphans - if (!pdir.split) { - // check if we have a parent - lfs_mdir_t parent; - lfs_stag_t tag = lfs_fs_parent(lfs, pdir.tail, &parent); - if (tag < 0 && tag != LFS_ERR_NOENT) { - return tag; - } - - if (pass == 0 && tag != LFS_ERR_NOENT) { - lfs_block_t pair[2]; - lfs_stag_t state = lfs_dir_get(lfs, &parent, - LFS_MKTAG(0x7ff, 0x3ff, 0), tag, pair); - if (state < 0) { - return state; - } - lfs_pair_fromle32(pair); - - if (!lfs_pair_sync(pair, pdir.tail)) { - // we have desynced - LFS_DEBUG("Fixing half-orphan " - "{0x%"PRIx32", 0x%"PRIx32"} " - "-> {0x%"PRIx32", 0x%"PRIx32"}", - pdir.tail[0], pdir.tail[1], pair[0], pair[1]); - - // fix pending move in this pair? this looks like an - // optimization but is in fact _required_ since - // relocating may outdate the move. - uint16_t moveid = 0x3ff; - if (lfs_gstate_hasmovehere(&lfs->gstate, pdir.pair)) { - moveid = lfs_tag_id(lfs->gstate.tag); - LFS_DEBUG("Fixing move while fixing orphans " - "{0x%"PRIx32", 0x%"PRIx32"} 0x%"PRIx16"\n", - pdir.pair[0], pdir.pair[1], moveid); - lfs_fs_prepmove(lfs, 0x3ff, NULL); - } - - lfs_pair_tole32(pair); - state = lfs_dir_orphaningcommit(lfs, &pdir, LFS_MKATTRS( - {LFS_MKTAG_IF(moveid != 0x3ff, - LFS_TYPE_DELETE, moveid, 0), NULL}, - {LFS_MKTAG(LFS_TYPE_SOFTTAIL, 0x3ff, 8), - pair})); - lfs_pair_fromle32(pair); - if (state < 0) { - return state; - } - - found += 1; - - // did our commit create more orphans? - if (state == LFS_OK_ORPHANED) { - moreorphans = true; - } - - // refetch tail - continue; - } - } - - // note we only check for full orphans if we may have had a - // power-loss, otherwise orphans are created intentionally - // during operations such as lfs_mkdir - if (pass == 1 && tag == LFS_ERR_NOENT && powerloss) { - // we are an orphan - LFS_DEBUG("Fixing orphan {0x%"PRIx32", 0x%"PRIx32"}", - pdir.tail[0], pdir.tail[1]); - - // steal state - err = lfs_dir_getgstate(lfs, &dir, &lfs->gdelta); - if (err) { - return err; - } - - // steal tail - lfs_pair_tole32(dir.tail); - int state = lfs_dir_orphaningcommit(lfs, &pdir, LFS_MKATTRS( - {LFS_MKTAG(LFS_TYPE_TAIL + dir.split, 0x3ff, 8), - dir.tail})); - lfs_pair_fromle32(dir.tail); - if (state < 0) { - return state; - } - - found += 1; - - // did our commit create more orphans? - if (state == LFS_OK_ORPHANED) { - moreorphans = true; - } - - // refetch tail - continue; - } - } - - pdir = dir; - } - - pass = moreorphans ? 0 : pass+1; - } - - // mark orphans as fixed - return lfs_fs_preporphans(lfs, -lfs_min( - lfs_gstate_getorphans(&lfs->gstate), - found)); -} -#endif - -#ifndef LFS_READONLY -static int lfs_fs_forceconsistency(lfs_t *lfs) { - int err = lfs_fs_demove(lfs); - if (err) { - return err; - } - - err = lfs_fs_deorphan(lfs, true); - if (err) { - return err; - } - - return 0; -} -#endif - -static int lfs_fs_size_count(void *p, lfs_block_t block) { - (void)block; - lfs_size_t *size = p; - *size += 1; - return 0; -} - -static lfs_ssize_t lfs_fs_rawsize(lfs_t *lfs) { - lfs_size_t size = 0; - int err = lfs_fs_rawtraverse(lfs, lfs_fs_size_count, &size, false); - if (err) { - return err; - } - - return size; -} - -#ifdef LFS_MIGRATE -////// Migration from littelfs v1 below this ////// - -/// Version info /// - -// Software library version -// Major (top-nibble), incremented on backwards incompatible changes -// Minor (bottom-nibble), incremented on feature additions -#define LFS1_VERSION 0x00010007 -#define LFS1_VERSION_MAJOR (0xffff & (LFS1_VERSION >> 16)) -#define LFS1_VERSION_MINOR (0xffff & (LFS1_VERSION >> 0)) - -// Version of On-disk data structures -// Major (top-nibble), incremented on backwards incompatible changes -// Minor (bottom-nibble), incremented on feature additions -#define LFS1_DISK_VERSION 0x00010001 -#define LFS1_DISK_VERSION_MAJOR (0xffff & (LFS1_DISK_VERSION >> 16)) -#define LFS1_DISK_VERSION_MINOR (0xffff & (LFS1_DISK_VERSION >> 0)) - - -/// v1 Definitions /// - -// File types -enum lfs1_type { - LFS1_TYPE_REG = 0x11, - LFS1_TYPE_DIR = 0x22, - LFS1_TYPE_SUPERBLOCK = 0x2e, -}; - -typedef struct lfs1 { - lfs_block_t root[2]; -} lfs1_t; - -typedef struct lfs1_entry { - lfs_off_t off; - - struct lfs1_disk_entry { - uint8_t type; - uint8_t elen; - uint8_t alen; - uint8_t nlen; - union { - struct { - lfs_block_t head; - lfs_size_t size; - } file; - lfs_block_t dir[2]; - } u; - } d; -} lfs1_entry_t; - -typedef struct lfs1_dir { - struct lfs1_dir *next; - lfs_block_t pair[2]; - lfs_off_t off; - - lfs_block_t head[2]; - lfs_off_t pos; - - struct lfs1_disk_dir { - uint32_t rev; - lfs_size_t size; - lfs_block_t tail[2]; - } d; -} lfs1_dir_t; - -typedef struct lfs1_superblock { - lfs_off_t off; - - struct lfs1_disk_superblock { - uint8_t type; - uint8_t elen; - uint8_t alen; - uint8_t nlen; - lfs_block_t root[2]; - uint32_t block_size; - uint32_t block_count; - uint32_t version; - char magic[8]; - } d; -} lfs1_superblock_t; - - -/// Low-level wrappers v1->v2 /// -static void lfs1_crc(uint32_t *crc, const void *buffer, size_t size) { - *crc = lfs_crc(*crc, buffer, size); -} - -static int lfs1_bd_read(lfs_t *lfs, lfs_block_t block, - lfs_off_t off, void *buffer, lfs_size_t size) { - // if we ever do more than writes to alternating pairs, - // this may need to consider pcache - return lfs_bd_read(lfs, &lfs->pcache, &lfs->rcache, size, - block, off, buffer, size); -} - -static int lfs1_bd_crc(lfs_t *lfs, lfs_block_t block, - lfs_off_t off, lfs_size_t size, uint32_t *crc) { - for (lfs_off_t i = 0; i < size; i++) { - uint8_t c; - int err = lfs1_bd_read(lfs, block, off+i, &c, 1); - if (err) { - return err; - } - - lfs1_crc(crc, &c, 1); - } - - return 0; -} - - -/// Endian swapping functions /// -static void lfs1_dir_fromle32(struct lfs1_disk_dir *d) { - d->rev = lfs_fromle32(d->rev); - d->size = lfs_fromle32(d->size); - d->tail[0] = lfs_fromle32(d->tail[0]); - d->tail[1] = lfs_fromle32(d->tail[1]); -} - -static void lfs1_dir_tole32(struct lfs1_disk_dir *d) { - d->rev = lfs_tole32(d->rev); - d->size = lfs_tole32(d->size); - d->tail[0] = lfs_tole32(d->tail[0]); - d->tail[1] = lfs_tole32(d->tail[1]); -} - -static void lfs1_entry_fromle32(struct lfs1_disk_entry *d) { - d->u.dir[0] = lfs_fromle32(d->u.dir[0]); - d->u.dir[1] = lfs_fromle32(d->u.dir[1]); -} - -static void lfs1_entry_tole32(struct lfs1_disk_entry *d) { - d->u.dir[0] = lfs_tole32(d->u.dir[0]); - d->u.dir[1] = lfs_tole32(d->u.dir[1]); -} - -static void lfs1_superblock_fromle32(struct lfs1_disk_superblock *d) { - d->root[0] = lfs_fromle32(d->root[0]); - d->root[1] = lfs_fromle32(d->root[1]); - d->block_size = lfs_fromle32(d->block_size); - d->block_count = lfs_fromle32(d->block_count); - d->version = lfs_fromle32(d->version); -} - - -///// Metadata pair and directory operations /// -static inline lfs_size_t lfs1_entry_size(const lfs1_entry_t *entry) { - return 4 + entry->d.elen + entry->d.alen + entry->d.nlen; -} - -static int lfs1_dir_fetch(lfs_t *lfs, - lfs1_dir_t *dir, const lfs_block_t pair[2]) { - // copy out pair, otherwise may be aliasing dir - const lfs_block_t tpair[2] = {pair[0], pair[1]}; - bool valid = false; - - // check both blocks for the most recent revision - for (int i = 0; i < 2; i++) { - struct lfs1_disk_dir test; - int err = lfs1_bd_read(lfs, tpair[i], 0, &test, sizeof(test)); - lfs1_dir_fromle32(&test); - if (err) { - if (err == LFS_ERR_CORRUPT) { - continue; - } - return err; - } - - if (valid && lfs_scmp(test.rev, dir->d.rev) < 0) { - continue; - } - - if ((0x7fffffff & test.size) < sizeof(test)+4 || - (0x7fffffff & test.size) > lfs->cfg->block_size) { - continue; - } - - uint32_t crc = 0xffffffff; - lfs1_dir_tole32(&test); - lfs1_crc(&crc, &test, sizeof(test)); - lfs1_dir_fromle32(&test); - err = lfs1_bd_crc(lfs, tpair[i], sizeof(test), - (0x7fffffff & test.size) - sizeof(test), &crc); - if (err) { - if (err == LFS_ERR_CORRUPT) { - continue; - } - return err; - } - - if (crc != 0) { - continue; - } - - valid = true; - - // setup dir in case it's valid - dir->pair[0] = tpair[(i+0) % 2]; - dir->pair[1] = tpair[(i+1) % 2]; - dir->off = sizeof(dir->d); - dir->d = test; - } - - if (!valid) { - LFS_ERROR("Corrupted dir pair at {0x%"PRIx32", 0x%"PRIx32"}", - tpair[0], tpair[1]); - return LFS_ERR_CORRUPT; - } - - return 0; -} - -static int lfs1_dir_next(lfs_t *lfs, lfs1_dir_t *dir, lfs1_entry_t *entry) { - while (dir->off + sizeof(entry->d) > (0x7fffffff & dir->d.size)-4) { - if (!(0x80000000 & dir->d.size)) { - entry->off = dir->off; - return LFS_ERR_NOENT; - } - - int err = lfs1_dir_fetch(lfs, dir, dir->d.tail); - if (err) { - return err; - } - - dir->off = sizeof(dir->d); - dir->pos += sizeof(dir->d) + 4; - } - - int err = lfs1_bd_read(lfs, dir->pair[0], dir->off, - &entry->d, sizeof(entry->d)); - lfs1_entry_fromle32(&entry->d); - if (err) { - return err; - } - - entry->off = dir->off; - dir->off += lfs1_entry_size(entry); - dir->pos += lfs1_entry_size(entry); - return 0; -} - -/// littlefs v1 specific operations /// -int lfs1_traverse(lfs_t *lfs, int (*cb)(void*, lfs_block_t), void *data) { - if (lfs_pair_isnull(lfs->lfs1->root)) { - return 0; - } - - // iterate over metadata pairs - lfs1_dir_t dir; - lfs1_entry_t entry; - lfs_block_t cwd[2] = {0, 1}; - - while (true) { - for (int i = 0; i < 2; i++) { - int err = cb(data, cwd[i]); - if (err) { - return err; - } - } - - int err = lfs1_dir_fetch(lfs, &dir, cwd); - if (err) { - return err; - } - - // iterate over contents - while (dir.off + sizeof(entry.d) <= (0x7fffffff & dir.d.size)-4) { - err = lfs1_bd_read(lfs, dir.pair[0], dir.off, - &entry.d, sizeof(entry.d)); - lfs1_entry_fromle32(&entry.d); - if (err) { - return err; - } - - dir.off += lfs1_entry_size(&entry); - if ((0x70 & entry.d.type) == (0x70 & LFS1_TYPE_REG)) { - err = lfs_ctz_traverse(lfs, NULL, &lfs->rcache, - entry.d.u.file.head, entry.d.u.file.size, cb, data); - if (err) { - return err; - } - } - } - - // we also need to check if we contain a threaded v2 directory - lfs_mdir_t dir2 = {.split=true, .tail={cwd[0], cwd[1]}}; - while (dir2.split) { - err = lfs_dir_fetch(lfs, &dir2, dir2.tail); - if (err) { - break; - } - - for (int i = 0; i < 2; i++) { - err = cb(data, dir2.pair[i]); - if (err) { - return err; - } - } - } - - cwd[0] = dir.d.tail[0]; - cwd[1] = dir.d.tail[1]; - - if (lfs_pair_isnull(cwd)) { - break; - } - } - - return 0; -} - -static int lfs1_moved(lfs_t *lfs, const void *e) { - if (lfs_pair_isnull(lfs->lfs1->root)) { - return 0; - } - - // skip superblock - lfs1_dir_t cwd; - int err = lfs1_dir_fetch(lfs, &cwd, (const lfs_block_t[2]){0, 1}); - if (err) { - return err; - } - - // iterate over all directory directory entries - lfs1_entry_t entry; - while (!lfs_pair_isnull(cwd.d.tail)) { - err = lfs1_dir_fetch(lfs, &cwd, cwd.d.tail); - if (err) { - return err; - } - - while (true) { - err = lfs1_dir_next(lfs, &cwd, &entry); - if (err && err != LFS_ERR_NOENT) { - return err; - } - - if (err == LFS_ERR_NOENT) { - break; - } - - if (!(0x80 & entry.d.type) && - memcmp(&entry.d.u, e, sizeof(entry.d.u)) == 0) { - return true; - } - } - } - - return false; -} - -/// Filesystem operations /// -static int lfs1_mount(lfs_t *lfs, struct lfs1 *lfs1, - const struct lfs_config *cfg) { - int err = 0; - { - err = lfs_init(lfs, cfg); - if (err) { - return err; - } - - lfs->lfs1 = lfs1; - lfs->lfs1->root[0] = LFS_BLOCK_NULL; - lfs->lfs1->root[1] = LFS_BLOCK_NULL; - - // setup free lookahead - lfs->free.off = 0; - lfs->free.size = 0; - lfs->free.i = 0; - lfs_alloc_ack(lfs); - - // load superblock - lfs1_dir_t dir; - lfs1_superblock_t superblock; - err = lfs1_dir_fetch(lfs, &dir, (const lfs_block_t[2]){0, 1}); - if (err && err != LFS_ERR_CORRUPT) { - goto cleanup; - } - - if (!err) { - err = lfs1_bd_read(lfs, dir.pair[0], sizeof(dir.d), - &superblock.d, sizeof(superblock.d)); - lfs1_superblock_fromle32(&superblock.d); - if (err) { - goto cleanup; - } - - lfs->lfs1->root[0] = superblock.d.root[0]; - lfs->lfs1->root[1] = superblock.d.root[1]; - } - - if (err || memcmp(superblock.d.magic, "littlefs", 8) != 0) { - LFS_ERROR("Invalid superblock at {0x%"PRIx32", 0x%"PRIx32"}", - 0, 1); - err = LFS_ERR_CORRUPT; - goto cleanup; - } - - uint16_t major_version = (0xffff & (superblock.d.version >> 16)); - uint16_t minor_version = (0xffff & (superblock.d.version >> 0)); - if ((major_version != LFS1_DISK_VERSION_MAJOR || - minor_version > LFS1_DISK_VERSION_MINOR)) { - LFS_ERROR("Invalid version v%d.%d", major_version, minor_version); - err = LFS_ERR_INVAL; - goto cleanup; - } - - return 0; - } - -cleanup: - lfs_deinit(lfs); - return err; -} - -static int lfs1_unmount(lfs_t *lfs) { - return lfs_deinit(lfs); -} - -/// v1 migration /// -static int lfs_rawmigrate(lfs_t *lfs, const struct lfs_config *cfg) { - struct lfs1 lfs1; - int err = lfs1_mount(lfs, &lfs1, cfg); - if (err) { - return err; - } - - { - // iterate through each directory, copying over entries - // into new directory - lfs1_dir_t dir1; - lfs_mdir_t dir2; - dir1.d.tail[0] = lfs->lfs1->root[0]; - dir1.d.tail[1] = lfs->lfs1->root[1]; - while (!lfs_pair_isnull(dir1.d.tail)) { - // iterate old dir - err = lfs1_dir_fetch(lfs, &dir1, dir1.d.tail); - if (err) { - goto cleanup; - } - - // create new dir and bind as temporary pretend root - err = lfs_dir_alloc(lfs, &dir2); - if (err) { - goto cleanup; - } - - dir2.rev = dir1.d.rev; - dir1.head[0] = dir1.pair[0]; - dir1.head[1] = dir1.pair[1]; - lfs->root[0] = dir2.pair[0]; - lfs->root[1] = dir2.pair[1]; - - err = lfs_dir_commit(lfs, &dir2, NULL, 0); - if (err) { - goto cleanup; - } - - while (true) { - lfs1_entry_t entry1; - err = lfs1_dir_next(lfs, &dir1, &entry1); - if (err && err != LFS_ERR_NOENT) { - goto cleanup; - } - - if (err == LFS_ERR_NOENT) { - break; - } - - // check that entry has not been moved - if (entry1.d.type & 0x80) { - int moved = lfs1_moved(lfs, &entry1.d.u); - if (moved < 0) { - err = moved; - goto cleanup; - } - - if (moved) { - continue; - } - - entry1.d.type &= ~0x80; - } - - // also fetch name - char name[LFS_NAME_MAX+1]; - memset(name, 0, sizeof(name)); - err = lfs1_bd_read(lfs, dir1.pair[0], - entry1.off + 4+entry1.d.elen+entry1.d.alen, - name, entry1.d.nlen); - if (err) { - goto cleanup; - } - - bool isdir = (entry1.d.type == LFS1_TYPE_DIR); - - // create entry in new dir - err = lfs_dir_fetch(lfs, &dir2, lfs->root); - if (err) { - goto cleanup; - } - - uint16_t id; - err = lfs_dir_find(lfs, &dir2, &(const char*){name}, &id); - if (!(err == LFS_ERR_NOENT && id != 0x3ff)) { - err = (err < 0) ? err : LFS_ERR_EXIST; - goto cleanup; - } - - lfs1_entry_tole32(&entry1.d); - err = lfs_dir_commit(lfs, &dir2, LFS_MKATTRS( - {LFS_MKTAG(LFS_TYPE_CREATE, id, 0), NULL}, - {LFS_MKTAG_IF_ELSE(isdir, - LFS_TYPE_DIR, id, entry1.d.nlen, - LFS_TYPE_REG, id, entry1.d.nlen), - name}, - {LFS_MKTAG_IF_ELSE(isdir, - LFS_TYPE_DIRSTRUCT, id, sizeof(entry1.d.u), - LFS_TYPE_CTZSTRUCT, id, sizeof(entry1.d.u)), - &entry1.d.u})); - lfs1_entry_fromle32(&entry1.d); - if (err) { - goto cleanup; - } - } - - if (!lfs_pair_isnull(dir1.d.tail)) { - // find last block and update tail to thread into fs - err = lfs_dir_fetch(lfs, &dir2, lfs->root); - if (err) { - goto cleanup; - } - - while (dir2.split) { - err = lfs_dir_fetch(lfs, &dir2, dir2.tail); - if (err) { - goto cleanup; - } - } - - lfs_pair_tole32(dir2.pair); - err = lfs_dir_commit(lfs, &dir2, LFS_MKATTRS( - {LFS_MKTAG(LFS_TYPE_SOFTTAIL, 0x3ff, 8), dir1.d.tail})); - lfs_pair_fromle32(dir2.pair); - if (err) { - goto cleanup; - } - } - - // Copy over first block to thread into fs. Unfortunately - // if this fails there is not much we can do. - LFS_DEBUG("Migrating {0x%"PRIx32", 0x%"PRIx32"} " - "-> {0x%"PRIx32", 0x%"PRIx32"}", - lfs->root[0], lfs->root[1], dir1.head[0], dir1.head[1]); - - err = lfs_bd_erase(lfs, dir1.head[1]); - if (err) { - goto cleanup; - } - - err = lfs_dir_fetch(lfs, &dir2, lfs->root); - if (err) { - goto cleanup; - } - - for (lfs_off_t i = 0; i < dir2.off; i++) { - uint8_t dat; - err = lfs_bd_read(lfs, - NULL, &lfs->rcache, dir2.off, - dir2.pair[0], i, &dat, 1); - if (err) { - goto cleanup; - } - - err = lfs_bd_prog(lfs, - &lfs->pcache, &lfs->rcache, true, - dir1.head[1], i, &dat, 1); - if (err) { - goto cleanup; - } - } - - err = lfs_bd_flush(lfs, &lfs->pcache, &lfs->rcache, true); - if (err) { - goto cleanup; - } - } - - // Create new superblock. This marks a successful migration! - err = lfs1_dir_fetch(lfs, &dir1, (const lfs_block_t[2]){0, 1}); - if (err) { - goto cleanup; - } - - dir2.pair[0] = dir1.pair[0]; - dir2.pair[1] = dir1.pair[1]; - dir2.rev = dir1.d.rev; - dir2.off = sizeof(dir2.rev); - dir2.etag = 0xffffffff; - dir2.count = 0; - dir2.tail[0] = lfs->lfs1->root[0]; - dir2.tail[1] = lfs->lfs1->root[1]; - dir2.erased = false; - dir2.split = true; - - lfs_superblock_t superblock = { - .version = LFS_DISK_VERSION, - .block_size = lfs->cfg->block_size, - .block_count = lfs->cfg->block_count, - .name_max = lfs->name_max, - .file_max = lfs->file_max, - .attr_max = lfs->attr_max, - }; - - lfs_superblock_tole32(&superblock); - err = lfs_dir_commit(lfs, &dir2, LFS_MKATTRS( - {LFS_MKTAG(LFS_TYPE_CREATE, 0, 0), NULL}, - {LFS_MKTAG(LFS_TYPE_SUPERBLOCK, 0, 8), "littlefs"}, - {LFS_MKTAG(LFS_TYPE_INLINESTRUCT, 0, sizeof(superblock)), - &superblock})); - if (err) { - goto cleanup; - } - - // sanity check that fetch works - err = lfs_dir_fetch(lfs, &dir2, (const lfs_block_t[2]){0, 1}); - if (err) { - goto cleanup; - } - - // force compaction to prevent accidentally mounting v1 - dir2.erased = false; - err = lfs_dir_commit(lfs, &dir2, NULL, 0); - if (err) { - goto cleanup; - } - } - -cleanup: - lfs1_unmount(lfs); - return err; -} - -#endif - - -/// Public API wrappers /// - -// Here we can add tracing/thread safety easily - -// Thread-safe wrappers if enabled -#ifdef LFS_THREADSAFE -#define LFS_LOCK(cfg) cfg->lock(cfg) -#define LFS_UNLOCK(cfg) cfg->unlock(cfg) -#else -#define LFS_LOCK(cfg) ((void)cfg, 0) -#define LFS_UNLOCK(cfg) ((void)cfg) -#endif - -// Public API -#ifndef LFS_READONLY -int lfs_format(lfs_t *lfs, const struct lfs_config *cfg) { - int err = LFS_LOCK(cfg); - if (err) { - return err; - } - LFS_TRACE("lfs_format(%p, %p {.context=%p, " - ".read=%p, .prog=%p, .erase=%p, .sync=%p, " - ".read_size=%"PRIu32", .prog_size=%"PRIu32", " - ".block_size=%"PRIu32", .block_count=%"PRIu32", " - ".block_cycles=%"PRIu32", .cache_size=%"PRIu32", " - ".lookahead_size=%"PRIu32", .read_buffer=%p, " - ".prog_buffer=%p, .lookahead_buffer=%p, " - ".name_max=%"PRIu32", .file_max=%"PRIu32", " - ".attr_max=%"PRIu32"})", - (void*)lfs, (void*)cfg, cfg->context, - (void*)(uintptr_t)cfg->read, (void*)(uintptr_t)cfg->prog, - (void*)(uintptr_t)cfg->erase, (void*)(uintptr_t)cfg->sync, - cfg->read_size, cfg->prog_size, cfg->block_size, cfg->block_count, - cfg->block_cycles, cfg->cache_size, cfg->lookahead_size, - cfg->read_buffer, cfg->prog_buffer, cfg->lookahead_buffer, - cfg->name_max, cfg->file_max, cfg->attr_max); - - err = lfs_rawformat(lfs, cfg); - - LFS_TRACE("lfs_format -> %d", err); - LFS_UNLOCK(cfg); - return err; -} -#endif - -int lfs_mount(lfs_t *lfs, const struct lfs_config *cfg) { - int err = LFS_LOCK(cfg); - if (err) { - return err; - } - LFS_TRACE("lfs_mount(%p, %p {.context=%p, " - ".read=%p, .prog=%p, .erase=%p, .sync=%p, " - ".read_size=%"PRIu32", .prog_size=%"PRIu32", " - ".block_size=%"PRIu32", .block_count=%"PRIu32", " - ".block_cycles=%"PRIu32", .cache_size=%"PRIu32", " - ".lookahead_size=%"PRIu32", .read_buffer=%p, " - ".prog_buffer=%p, .lookahead_buffer=%p, " - ".name_max=%"PRIu32", .file_max=%"PRIu32", " - ".attr_max=%"PRIu32"})", - (void*)lfs, (void*)cfg, cfg->context, - (void*)(uintptr_t)cfg->read, (void*)(uintptr_t)cfg->prog, - (void*)(uintptr_t)cfg->erase, (void*)(uintptr_t)cfg->sync, - cfg->read_size, cfg->prog_size, cfg->block_size, cfg->block_count, - cfg->block_cycles, cfg->cache_size, cfg->lookahead_size, - cfg->read_buffer, cfg->prog_buffer, cfg->lookahead_buffer, - cfg->name_max, cfg->file_max, cfg->attr_max); - - err = lfs_rawmount(lfs, cfg); - - LFS_TRACE("lfs_mount -> %d", err); - LFS_UNLOCK(cfg); - return err; -} - -int lfs_unmount(lfs_t *lfs) { - int err = LFS_LOCK(lfs->cfg); - if (err) { - return err; - } - LFS_TRACE("lfs_unmount(%p)", (void*)lfs); - - err = lfs_rawunmount(lfs); - - LFS_TRACE("lfs_unmount -> %d", err); - LFS_UNLOCK(lfs->cfg); - return err; -} - -#ifndef LFS_READONLY -int lfs_remove(lfs_t *lfs, const char *path) { - int err = LFS_LOCK(lfs->cfg); - if (err) { - return err; - } - LFS_TRACE("lfs_remove(%p, \"%s\")", (void*)lfs, path); - - err = lfs_rawremove(lfs, path); - - LFS_TRACE("lfs_remove -> %d", err); - LFS_UNLOCK(lfs->cfg); - return err; -} -#endif - -#ifndef LFS_READONLY -int lfs_rename(lfs_t *lfs, const char *oldpath, const char *newpath) { - int err = LFS_LOCK(lfs->cfg); - if (err) { - return err; - } - LFS_TRACE("lfs_rename(%p, \"%s\", \"%s\")", (void*)lfs, oldpath, newpath); - - err = lfs_rawrename(lfs, oldpath, newpath); - - LFS_TRACE("lfs_rename -> %d", err); - LFS_UNLOCK(lfs->cfg); - return err; -} -#endif - -int lfs_stat(lfs_t *lfs, const char *path, struct lfs_info *info) { - int err = LFS_LOCK(lfs->cfg); - if (err) { - return err; - } - LFS_TRACE("lfs_stat(%p, \"%s\", %p)", (void*)lfs, path, (void*)info); - - err = lfs_rawstat(lfs, path, info); - - LFS_TRACE("lfs_stat -> %d", err); - LFS_UNLOCK(lfs->cfg); - return err; -} - -lfs_ssize_t lfs_getattr(lfs_t *lfs, const char *path, - uint8_t type, void *buffer, lfs_size_t size) { - int err = LFS_LOCK(lfs->cfg); - if (err) { - return err; - } - LFS_TRACE("lfs_getattr(%p, \"%s\", %"PRIu8", %p, %"PRIu32")", - (void*)lfs, path, type, buffer, size); - - lfs_ssize_t res = lfs_rawgetattr(lfs, path, type, buffer, size); - - LFS_TRACE("lfs_getattr -> %"PRId32, res); - LFS_UNLOCK(lfs->cfg); - return res; -} - -#ifndef LFS_READONLY -int lfs_setattr(lfs_t *lfs, const char *path, - uint8_t type, const void *buffer, lfs_size_t size) { - int err = LFS_LOCK(lfs->cfg); - if (err) { - return err; - } - LFS_TRACE("lfs_setattr(%p, \"%s\", %"PRIu8", %p, %"PRIu32")", - (void*)lfs, path, type, buffer, size); - - err = lfs_rawsetattr(lfs, path, type, buffer, size); - - LFS_TRACE("lfs_setattr -> %d", err); - LFS_UNLOCK(lfs->cfg); - return err; -} -#endif - -#ifndef LFS_READONLY -int lfs_removeattr(lfs_t *lfs, const char *path, uint8_t type) { - int err = LFS_LOCK(lfs->cfg); - if (err) { - return err; - } - LFS_TRACE("lfs_removeattr(%p, \"%s\", %"PRIu8")", (void*)lfs, path, type); - - err = lfs_rawremoveattr(lfs, path, type); - - LFS_TRACE("lfs_removeattr -> %d", err); - LFS_UNLOCK(lfs->cfg); - return err; -} -#endif - -#ifndef LFS_NO_MALLOC -int lfs_file_open(lfs_t *lfs, lfs_file_t *file, const char *path, int flags) { - int err = LFS_LOCK(lfs->cfg); - if (err) { - return err; - } - LFS_TRACE("lfs_file_open(%p, %p, \"%s\", %x)", - (void*)lfs, (void*)file, path, flags); - LFS_ASSERT(!lfs_mlist_isopen(lfs->mlist, (struct lfs_mlist*)file)); - - err = lfs_file_rawopen(lfs, file, path, flags); - - LFS_TRACE("lfs_file_open -> %d", err); - LFS_UNLOCK(lfs->cfg); - return err; -} -#endif - -int lfs_file_opencfg(lfs_t *lfs, lfs_file_t *file, - const char *path, int flags, - const struct lfs_file_config *cfg) { - int err = LFS_LOCK(lfs->cfg); - if (err) { - return err; - } - LFS_TRACE("lfs_file_opencfg(%p, %p, \"%s\", %x, %p {" - ".buffer=%p, .attrs=%p, .attr_count=%"PRIu32"})", - (void*)lfs, (void*)file, path, flags, - (void*)cfg, cfg->buffer, (void*)cfg->attrs, cfg->attr_count); - LFS_ASSERT(!lfs_mlist_isopen(lfs->mlist, (struct lfs_mlist*)file)); - - err = lfs_file_rawopencfg(lfs, file, path, flags, cfg); - - LFS_TRACE("lfs_file_opencfg -> %d", err); - LFS_UNLOCK(lfs->cfg); - return err; -} - -int lfs_file_close(lfs_t *lfs, lfs_file_t *file) { - int err = LFS_LOCK(lfs->cfg); - if (err) { - return err; - } - LFS_TRACE("lfs_file_close(%p, %p)", (void*)lfs, (void*)file); - LFS_ASSERT(lfs_mlist_isopen(lfs->mlist, (struct lfs_mlist*)file)); - - err = lfs_file_rawclose(lfs, file); - - LFS_TRACE("lfs_file_close -> %d", err); - LFS_UNLOCK(lfs->cfg); - return err; -} - -#ifndef LFS_READONLY -int lfs_file_sync(lfs_t *lfs, lfs_file_t *file) { - int err = LFS_LOCK(lfs->cfg); - if (err) { - return err; - } - LFS_TRACE("lfs_file_sync(%p, %p)", (void*)lfs, (void*)file); - LFS_ASSERT(lfs_mlist_isopen(lfs->mlist, (struct lfs_mlist*)file)); - - err = lfs_file_rawsync(lfs, file); - - LFS_TRACE("lfs_file_sync -> %d", err); - LFS_UNLOCK(lfs->cfg); - return err; -} -#endif - -lfs_ssize_t lfs_file_read(lfs_t *lfs, lfs_file_t *file, - void *buffer, lfs_size_t size) { - int err = LFS_LOCK(lfs->cfg); - if (err) { - return err; - } - LFS_TRACE("lfs_file_read(%p, %p, %p, %"PRIu32")", - (void*)lfs, (void*)file, buffer, size); - LFS_ASSERT(lfs_mlist_isopen(lfs->mlist, (struct lfs_mlist*)file)); - - lfs_ssize_t res = lfs_file_rawread(lfs, file, buffer, size); - - LFS_TRACE("lfs_file_read -> %"PRId32, res); - LFS_UNLOCK(lfs->cfg); - return res; -} - -#ifndef LFS_READONLY -lfs_ssize_t lfs_file_write(lfs_t *lfs, lfs_file_t *file, - const void *buffer, lfs_size_t size) { - int err = LFS_LOCK(lfs->cfg); - if (err) { - return err; - } - LFS_TRACE("lfs_file_write(%p, %p, %p, %"PRIu32")", - (void*)lfs, (void*)file, buffer, size); - LFS_ASSERT(lfs_mlist_isopen(lfs->mlist, (struct lfs_mlist*)file)); - - lfs_ssize_t res = lfs_file_rawwrite(lfs, file, buffer, size); - - LFS_TRACE("lfs_file_write -> %"PRId32, res); - LFS_UNLOCK(lfs->cfg); - return res; -} -#endif - -lfs_soff_t lfs_file_seek(lfs_t *lfs, lfs_file_t *file, - lfs_soff_t off, int whence) { - int err = LFS_LOCK(lfs->cfg); - if (err) { - return err; - } - LFS_TRACE("lfs_file_seek(%p, %p, %"PRId32", %d)", - (void*)lfs, (void*)file, off, whence); - LFS_ASSERT(lfs_mlist_isopen(lfs->mlist, (struct lfs_mlist*)file)); - - lfs_soff_t res = lfs_file_rawseek(lfs, file, off, whence); - - LFS_TRACE("lfs_file_seek -> %"PRId32, res); - LFS_UNLOCK(lfs->cfg); - return res; -} - -#ifndef LFS_READONLY -int lfs_file_truncate(lfs_t *lfs, lfs_file_t *file, lfs_off_t size) { - int err = LFS_LOCK(lfs->cfg); - if (err) { - return err; - } - LFS_TRACE("lfs_file_truncate(%p, %p, %"PRIu32")", - (void*)lfs, (void*)file, size); - LFS_ASSERT(lfs_mlist_isopen(lfs->mlist, (struct lfs_mlist*)file)); - - err = lfs_file_rawtruncate(lfs, file, size); - - LFS_TRACE("lfs_file_truncate -> %d", err); - LFS_UNLOCK(lfs->cfg); - return err; -} -#endif - -lfs_soff_t lfs_file_tell(lfs_t *lfs, lfs_file_t *file) { - int err = LFS_LOCK(lfs->cfg); - if (err) { - return err; - } - LFS_TRACE("lfs_file_tell(%p, %p)", (void*)lfs, (void*)file); - LFS_ASSERT(lfs_mlist_isopen(lfs->mlist, (struct lfs_mlist*)file)); - - lfs_soff_t res = lfs_file_rawtell(lfs, file); - - LFS_TRACE("lfs_file_tell -> %"PRId32, res); - LFS_UNLOCK(lfs->cfg); - return res; -} - -int lfs_file_rewind(lfs_t *lfs, lfs_file_t *file) { - int err = LFS_LOCK(lfs->cfg); - if (err) { - return err; - } - LFS_TRACE("lfs_file_rewind(%p, %p)", (void*)lfs, (void*)file); - - err = lfs_file_rawrewind(lfs, file); - - LFS_TRACE("lfs_file_rewind -> %d", err); - LFS_UNLOCK(lfs->cfg); - return err; -} - -lfs_soff_t lfs_file_size(lfs_t *lfs, lfs_file_t *file) { - int err = LFS_LOCK(lfs->cfg); - if (err) { - return err; - } - LFS_TRACE("lfs_file_size(%p, %p)", (void*)lfs, (void*)file); - LFS_ASSERT(lfs_mlist_isopen(lfs->mlist, (struct lfs_mlist*)file)); - - lfs_soff_t res = lfs_file_rawsize(lfs, file); - - LFS_TRACE("lfs_file_size -> %"PRId32, res); - LFS_UNLOCK(lfs->cfg); - return res; -} - -#ifndef LFS_READONLY -int lfs_mkdir(lfs_t *lfs, const char *path) { - int err = LFS_LOCK(lfs->cfg); - if (err) { - return err; - } - LFS_TRACE("lfs_mkdir(%p, \"%s\")", (void*)lfs, path); - - err = lfs_rawmkdir(lfs, path); - - LFS_TRACE("lfs_mkdir -> %d", err); - LFS_UNLOCK(lfs->cfg); - return err; -} -#endif - -int lfs_dir_open(lfs_t *lfs, lfs_dir_t *dir, const char *path) { - int err = LFS_LOCK(lfs->cfg); - if (err) { - return err; - } - LFS_TRACE("lfs_dir_open(%p, %p, \"%s\")", (void*)lfs, (void*)dir, path); - LFS_ASSERT(!lfs_mlist_isopen(lfs->mlist, (struct lfs_mlist*)dir)); - - err = lfs_dir_rawopen(lfs, dir, path); - - LFS_TRACE("lfs_dir_open -> %d", err); - LFS_UNLOCK(lfs->cfg); - return err; -} - -int lfs_dir_close(lfs_t *lfs, lfs_dir_t *dir) { - int err = LFS_LOCK(lfs->cfg); - if (err) { - return err; - } - LFS_TRACE("lfs_dir_close(%p, %p)", (void*)lfs, (void*)dir); - - err = lfs_dir_rawclose(lfs, dir); - - LFS_TRACE("lfs_dir_close -> %d", err); - LFS_UNLOCK(lfs->cfg); - return err; -} - -int lfs_dir_read(lfs_t *lfs, lfs_dir_t *dir, struct lfs_info *info) { - int err = LFS_LOCK(lfs->cfg); - if (err) { - return err; - } - LFS_TRACE("lfs_dir_read(%p, %p, %p)", - (void*)lfs, (void*)dir, (void*)info); - - err = lfs_dir_rawread(lfs, dir, info); - - LFS_TRACE("lfs_dir_read -> %d", err); - LFS_UNLOCK(lfs->cfg); - return err; -} - -int lfs_dir_seek(lfs_t *lfs, lfs_dir_t *dir, lfs_off_t off) { - int err = LFS_LOCK(lfs->cfg); - if (err) { - return err; - } - LFS_TRACE("lfs_dir_seek(%p, %p, %"PRIu32")", - (void*)lfs, (void*)dir, off); - - err = lfs_dir_rawseek(lfs, dir, off); - - LFS_TRACE("lfs_dir_seek -> %d", err); - LFS_UNLOCK(lfs->cfg); - return err; -} - -lfs_soff_t lfs_dir_tell(lfs_t *lfs, lfs_dir_t *dir) { - int err = LFS_LOCK(lfs->cfg); - if (err) { - return err; - } - LFS_TRACE("lfs_dir_tell(%p, %p)", (void*)lfs, (void*)dir); - - lfs_soff_t res = lfs_dir_rawtell(lfs, dir); - - LFS_TRACE("lfs_dir_tell -> %"PRId32, res); - LFS_UNLOCK(lfs->cfg); - return res; -} - -int lfs_dir_rewind(lfs_t *lfs, lfs_dir_t *dir) { - int err = LFS_LOCK(lfs->cfg); - if (err) { - return err; - } - LFS_TRACE("lfs_dir_rewind(%p, %p)", (void*)lfs, (void*)dir); - - err = lfs_dir_rawrewind(lfs, dir); - - LFS_TRACE("lfs_dir_rewind -> %d", err); - LFS_UNLOCK(lfs->cfg); - return err; -} - -lfs_ssize_t lfs_fs_size(lfs_t *lfs) { - int err = LFS_LOCK(lfs->cfg); - if (err) { - return err; - } - LFS_TRACE("lfs_fs_size(%p)", (void*)lfs); - - lfs_ssize_t res = lfs_fs_rawsize(lfs); - - LFS_TRACE("lfs_fs_size -> %"PRId32, res); - LFS_UNLOCK(lfs->cfg); - return res; -} - -int lfs_fs_traverse(lfs_t *lfs, int (*cb)(void *, lfs_block_t), void *data) { - int err = LFS_LOCK(lfs->cfg); - if (err) { - return err; - } - LFS_TRACE("lfs_fs_traverse(%p, %p, %p)", - (void*)lfs, (void*)(uintptr_t)cb, data); - - err = lfs_fs_rawtraverse(lfs, cb, data, true); - - LFS_TRACE("lfs_fs_traverse -> %d", err); - LFS_UNLOCK(lfs->cfg); - return err; -} - -#ifdef LFS_MIGRATE -int lfs_migrate(lfs_t *lfs, const struct lfs_config *cfg) { - int err = LFS_LOCK(cfg); - if (err) { - return err; - } - LFS_TRACE("lfs_migrate(%p, %p {.context=%p, " - ".read=%p, .prog=%p, .erase=%p, .sync=%p, " - ".read_size=%"PRIu32", .prog_size=%"PRIu32", " - ".block_size=%"PRIu32", .block_count=%"PRIu32", " - ".block_cycles=%"PRIu32", .cache_size=%"PRIu32", " - ".lookahead_size=%"PRIu32", .read_buffer=%p, " - ".prog_buffer=%p, .lookahead_buffer=%p, " - ".name_max=%"PRIu32", .file_max=%"PRIu32", " - ".attr_max=%"PRIu32"})", - (void*)lfs, (void*)cfg, cfg->context, - (void*)(uintptr_t)cfg->read, (void*)(uintptr_t)cfg->prog, - (void*)(uintptr_t)cfg->erase, (void*)(uintptr_t)cfg->sync, - cfg->read_size, cfg->prog_size, cfg->block_size, cfg->block_count, - cfg->block_cycles, cfg->cache_size, cfg->lookahead_size, - cfg->read_buffer, cfg->prog_buffer, cfg->lookahead_buffer, - cfg->name_max, cfg->file_max, cfg->attr_max); - - err = lfs_rawmigrate(lfs, cfg); - - LFS_TRACE("lfs_migrate -> %d", err); - LFS_UNLOCK(cfg); - return err; -} -#endif - diff --git a/lfs.h b/lfs.h deleted file mode 100644 index a1de75e8d..000000000 --- a/lfs.h +++ /dev/null @@ -1,701 +0,0 @@ -/* - * The little filesystem - * - * Copyright (c) 2022, The littlefs authors. - * Copyright (c) 2017, Arm Limited. All rights reserved. - * SPDX-License-Identifier: BSD-3-Clause - */ -#ifndef LFS_H -#define LFS_H - -#include "lfs_util.h" - -#ifdef __cplusplus -extern "C" -{ -#endif - - -/// Version info /// - -// Software library version -// Major (top-nibble), incremented on backwards incompatible changes -// Minor (bottom-nibble), incremented on feature additions -#define LFS_VERSION 0x00020005 -#define LFS_VERSION_MAJOR (0xffff & (LFS_VERSION >> 16)) -#define LFS_VERSION_MINOR (0xffff & (LFS_VERSION >> 0)) - -// Version of On-disk data structures -// Major (top-nibble), incremented on backwards incompatible changes -// Minor (bottom-nibble), incremented on feature additions -#define LFS_DISK_VERSION 0x00020000 -#define LFS_DISK_VERSION_MAJOR (0xffff & (LFS_DISK_VERSION >> 16)) -#define LFS_DISK_VERSION_MINOR (0xffff & (LFS_DISK_VERSION >> 0)) - - -/// Definitions /// - -// Type definitions -typedef uint32_t lfs_size_t; -typedef uint32_t lfs_off_t; - -typedef int32_t lfs_ssize_t; -typedef int32_t lfs_soff_t; - -typedef uint32_t lfs_block_t; - -// Maximum name size in bytes, may be redefined to reduce the size of the -// info struct. Limited to <= 1022. Stored in superblock and must be -// respected by other littlefs drivers. -#ifndef LFS_NAME_MAX -#define LFS_NAME_MAX 255 -#endif - -// Maximum size of a file in bytes, may be redefined to limit to support other -// drivers. Limited on disk to <= 4294967296. However, above 2147483647 the -// functions lfs_file_seek, lfs_file_size, and lfs_file_tell will return -// incorrect values due to using signed integers. Stored in superblock and -// must be respected by other littlefs drivers. -#ifndef LFS_FILE_MAX -#define LFS_FILE_MAX 2147483647 -#endif - -// Maximum size of custom attributes in bytes, may be redefined, but there is -// no real benefit to using a smaller LFS_ATTR_MAX. Limited to <= 1022. -#ifndef LFS_ATTR_MAX -#define LFS_ATTR_MAX 1022 -#endif - -// Possible error codes, these are negative to allow -// valid positive return values -enum lfs_error { - LFS_ERR_OK = 0, // No error - LFS_ERR_IO = -5, // Error during device operation - LFS_ERR_CORRUPT = -84, // Corrupted - LFS_ERR_NOENT = -2, // No directory entry - LFS_ERR_EXIST = -17, // Entry already exists - LFS_ERR_NOTDIR = -20, // Entry is not a dir - LFS_ERR_ISDIR = -21, // Entry is a dir - LFS_ERR_NOTEMPTY = -39, // Dir is not empty - LFS_ERR_BADF = -9, // Bad file number - LFS_ERR_FBIG = -27, // File too large - LFS_ERR_INVAL = -22, // Invalid parameter - LFS_ERR_NOSPC = -28, // No space left on device - LFS_ERR_NOMEM = -12, // No more memory available - LFS_ERR_NOATTR = -61, // No data/attr available - LFS_ERR_NAMETOOLONG = -36, // File name too long -}; - -// File types -enum lfs_type { - // file types - LFS_TYPE_REG = 0x001, - LFS_TYPE_DIR = 0x002, - - // internally used types - LFS_TYPE_SPLICE = 0x400, - LFS_TYPE_NAME = 0x000, - LFS_TYPE_STRUCT = 0x200, - LFS_TYPE_USERATTR = 0x300, - LFS_TYPE_FROM = 0x100, - LFS_TYPE_TAIL = 0x600, - LFS_TYPE_GLOBALS = 0x700, - LFS_TYPE_CRC = 0x500, - - // internally used type specializations - LFS_TYPE_CREATE = 0x401, - LFS_TYPE_DELETE = 0x4ff, - LFS_TYPE_SUPERBLOCK = 0x0ff, - LFS_TYPE_DIRSTRUCT = 0x200, - LFS_TYPE_CTZSTRUCT = 0x202, - LFS_TYPE_INLINESTRUCT = 0x201, - LFS_TYPE_SOFTTAIL = 0x600, - LFS_TYPE_HARDTAIL = 0x601, - LFS_TYPE_MOVESTATE = 0x7ff, - LFS_TYPE_CCRC = 0x500, - LFS_TYPE_FCRC = 0x5ff, - - // internal chip sources - LFS_FROM_NOOP = 0x000, - LFS_FROM_MOVE = 0x101, - LFS_FROM_USERATTRS = 0x102, -}; - -// File open flags -enum lfs_open_flags { - // open flags - LFS_O_RDONLY = 1, // Open a file as read only -#ifndef LFS_READONLY - LFS_O_WRONLY = 2, // Open a file as write only - LFS_O_RDWR = 3, // Open a file as read and write - LFS_O_CREAT = 0x0100, // Create a file if it does not exist - LFS_O_EXCL = 0x0200, // Fail if a file already exists - LFS_O_TRUNC = 0x0400, // Truncate the existing file to zero size - LFS_O_APPEND = 0x0800, // Move to end of file on every write -#endif - - // internally used flags -#ifndef LFS_READONLY - LFS_F_DIRTY = 0x010000, // File does not match storage - LFS_F_WRITING = 0x020000, // File has been written since last flush -#endif - LFS_F_READING = 0x040000, // File has been read since last flush -#ifndef LFS_READONLY - LFS_F_ERRED = 0x080000, // An error occurred during write -#endif - LFS_F_INLINE = 0x100000, // Currently inlined in directory entry -}; - -// File seek flags -enum lfs_whence_flags { - LFS_SEEK_SET = 0, // Seek relative to an absolute position - LFS_SEEK_CUR = 1, // Seek relative to the current file position - LFS_SEEK_END = 2, // Seek relative to the end of the file -}; - - -// Configuration provided during initialization of the littlefs -struct lfs_config { - // Opaque user provided context that can be used to pass - // information to the block device operations - void *context; - - // Read a region in a block. Negative error codes are propagated - // to the user. - int (*read)(const struct lfs_config *c, lfs_block_t block, - lfs_off_t off, void *buffer, lfs_size_t size); - - // Program a region in a block. The block must have previously - // been erased. Negative error codes are propagated to the user. - // May return LFS_ERR_CORRUPT if the block should be considered bad. - int (*prog)(const struct lfs_config *c, lfs_block_t block, - lfs_off_t off, const void *buffer, lfs_size_t size); - - // Erase a block. A block must be erased before being programmed. - // The state of an erased block is undefined. Negative error codes - // are propagated to the user. - // May return LFS_ERR_CORRUPT if the block should be considered bad. - int (*erase)(const struct lfs_config *c, lfs_block_t block); - - // Sync the state of the underlying block device. Negative error codes - // are propagated to the user. - int (*sync)(const struct lfs_config *c); - -#ifdef LFS_THREADSAFE - // Lock the underlying block device. Negative error codes - // are propagated to the user. - int (*lock)(const struct lfs_config *c); - - // Unlock the underlying block device. Negative error codes - // are propagated to the user. - int (*unlock)(const struct lfs_config *c); -#endif - - // Minimum size of a block read in bytes. All read operations will be a - // multiple of this value. - lfs_size_t read_size; - - // Minimum size of a block program in bytes. All program operations will be - // a multiple of this value. - lfs_size_t prog_size; - - // Size of an erasable block in bytes. This does not impact ram consumption - // and may be larger than the physical erase size. However, non-inlined - // files take up at minimum one block. Must be a multiple of the read and - // program sizes. - lfs_size_t block_size; - - // Number of erasable blocks on the device. - lfs_size_t block_count; - - // Number of erase cycles before littlefs evicts metadata logs and moves - // the metadata to another block. Suggested values are in the - // range 100-1000, with large values having better performance at the cost - // of less consistent wear distribution. - // - // Set to -1 to disable block-level wear-leveling. - int32_t block_cycles; - - // Size of block caches in bytes. Each cache buffers a portion of a block in - // RAM. The littlefs needs a read cache, a program cache, and one additional - // cache per file. Larger caches can improve performance by storing more - // data and reducing the number of disk accesses. Must be a multiple of the - // read and program sizes, and a factor of the block size. - lfs_size_t cache_size; - - // Size of the lookahead buffer in bytes. A larger lookahead buffer - // increases the number of blocks found during an allocation pass. The - // lookahead buffer is stored as a compact bitmap, so each byte of RAM - // can track 8 blocks. Must be a multiple of 8. - lfs_size_t lookahead_size; - - // Optional statically allocated read buffer. Must be cache_size. - // By default lfs_malloc is used to allocate this buffer. - void *read_buffer; - - // Optional statically allocated program buffer. Must be cache_size. - // By default lfs_malloc is used to allocate this buffer. - void *prog_buffer; - - // Optional statically allocated lookahead buffer. Must be lookahead_size - // and aligned to a 32-bit boundary. By default lfs_malloc is used to - // allocate this buffer. - void *lookahead_buffer; - - // Optional upper limit on length of file names in bytes. No downside for - // larger names except the size of the info struct which is controlled by - // the LFS_NAME_MAX define. Defaults to LFS_NAME_MAX when zero. Stored in - // superblock and must be respected by other littlefs drivers. - lfs_size_t name_max; - - // Optional upper limit on files in bytes. No downside for larger files - // but must be <= LFS_FILE_MAX. Defaults to LFS_FILE_MAX when zero. Stored - // in superblock and must be respected by other littlefs drivers. - lfs_size_t file_max; - - // Optional upper limit on custom attributes in bytes. No downside for - // larger attributes size but must be <= LFS_ATTR_MAX. Defaults to - // LFS_ATTR_MAX when zero. - lfs_size_t attr_max; - - // Optional upper limit on total space given to metadata pairs in bytes. On - // devices with large blocks (e.g. 128kB) setting this to a low size (2-8kB) - // can help bound the metadata compaction time. Must be <= block_size. - // Defaults to block_size when zero. - lfs_size_t metadata_max; -}; - -// File info structure -struct lfs_info { - // Type of the file, either LFS_TYPE_REG or LFS_TYPE_DIR - uint8_t type; - - // Size of the file, only valid for REG files. Limited to 32-bits. - lfs_size_t size; - - // Name of the file stored as a null-terminated string. Limited to - // LFS_NAME_MAX+1, which can be changed by redefining LFS_NAME_MAX to - // reduce RAM. LFS_NAME_MAX is stored in superblock and must be - // respected by other littlefs drivers. - char name[LFS_NAME_MAX+1]; -}; - -// Custom attribute structure, used to describe custom attributes -// committed atomically during file writes. -struct lfs_attr { - // 8-bit type of attribute, provided by user and used to - // identify the attribute - uint8_t type; - - // Pointer to buffer containing the attribute - void *buffer; - - // Size of attribute in bytes, limited to LFS_ATTR_MAX - lfs_size_t size; -}; - -// Optional configuration provided during lfs_file_opencfg -struct lfs_file_config { - // Optional statically allocated file buffer. Must be cache_size. - // By default lfs_malloc is used to allocate this buffer. - void *buffer; - - // Optional list of custom attributes related to the file. If the file - // is opened with read access, these attributes will be read from disk - // during the open call. If the file is opened with write access, the - // attributes will be written to disk every file sync or close. This - // write occurs atomically with update to the file's contents. - // - // Custom attributes are uniquely identified by an 8-bit type and limited - // to LFS_ATTR_MAX bytes. When read, if the stored attribute is smaller - // than the buffer, it will be padded with zeros. If the stored attribute - // is larger, then it will be silently truncated. If the attribute is not - // found, it will be created implicitly. - struct lfs_attr *attrs; - - // Number of custom attributes in the list - lfs_size_t attr_count; -}; - - -/// internal littlefs data structures /// -typedef struct lfs_cache { - lfs_block_t block; - lfs_off_t off; - lfs_size_t size; - uint8_t *buffer; -} lfs_cache_t; - -typedef struct lfs_mdir { - lfs_block_t pair[2]; - uint32_t rev; - lfs_off_t off; - uint32_t etag; - uint16_t count; - bool erased; - bool split; - lfs_block_t tail[2]; -} lfs_mdir_t; - -// littlefs directory type -typedef struct lfs_dir { - struct lfs_dir *next; - uint16_t id; - uint8_t type; - lfs_mdir_t m; - - lfs_off_t pos; - lfs_block_t head[2]; -} lfs_dir_t; - -// littlefs file type -typedef struct lfs_file { - struct lfs_file *next; - uint16_t id; - uint8_t type; - lfs_mdir_t m; - - struct lfs_ctz { - lfs_block_t head; - lfs_size_t size; - } ctz; - - uint32_t flags; - lfs_off_t pos; - lfs_block_t block; - lfs_off_t off; - lfs_cache_t cache; - - const struct lfs_file_config *cfg; -} lfs_file_t; - -typedef struct lfs_superblock { - uint32_t version; - lfs_size_t block_size; - lfs_size_t block_count; - lfs_size_t name_max; - lfs_size_t file_max; - lfs_size_t attr_max; -} lfs_superblock_t; - -typedef struct lfs_gstate { - uint32_t tag; - lfs_block_t pair[2]; -} lfs_gstate_t; - -// The littlefs filesystem type -typedef struct lfs { - lfs_cache_t rcache; - lfs_cache_t pcache; - - lfs_block_t root[2]; - struct lfs_mlist { - struct lfs_mlist *next; - uint16_t id; - uint8_t type; - lfs_mdir_t m; - } *mlist; - uint32_t seed; - - lfs_gstate_t gstate; - lfs_gstate_t gdisk; - lfs_gstate_t gdelta; - - struct lfs_free { - lfs_block_t off; - lfs_block_t size; - lfs_block_t i; - lfs_block_t ack; - uint32_t *buffer; - } free; - - const struct lfs_config *cfg; - lfs_size_t name_max; - lfs_size_t file_max; - lfs_size_t attr_max; - -#ifdef LFS_MIGRATE - struct lfs1 *lfs1; -#endif -} lfs_t; - - -/// Filesystem functions /// - -#ifndef LFS_READONLY -// Format a block device with the littlefs -// -// Requires a littlefs object and config struct. This clobbers the littlefs -// object, and does not leave the filesystem mounted. The config struct must -// be zeroed for defaults and backwards compatibility. -// -// Returns a negative error code on failure. -int lfs_format(lfs_t *lfs, const struct lfs_config *config); -#endif - -// Mounts a littlefs -// -// Requires a littlefs object and config struct. Multiple filesystems -// may be mounted simultaneously with multiple littlefs objects. Both -// lfs and config must be allocated while mounted. The config struct must -// be zeroed for defaults and backwards compatibility. -// -// Returns a negative error code on failure. -int lfs_mount(lfs_t *lfs, const struct lfs_config *config); - -// Unmounts a littlefs -// -// Does nothing besides releasing any allocated resources. -// Returns a negative error code on failure. -int lfs_unmount(lfs_t *lfs); - -/// General operations /// - -#ifndef LFS_READONLY -// Removes a file or directory -// -// If removing a directory, the directory must be empty. -// Returns a negative error code on failure. -int lfs_remove(lfs_t *lfs, const char *path); -#endif - -#ifndef LFS_READONLY -// Rename or move a file or directory -// -// If the destination exists, it must match the source in type. -// If the destination is a directory, the directory must be empty. -// -// Returns a negative error code on failure. -int lfs_rename(lfs_t *lfs, const char *oldpath, const char *newpath); -#endif - -// Find info about a file or directory -// -// Fills out the info structure, based on the specified file or directory. -// Returns a negative error code on failure. -int lfs_stat(lfs_t *lfs, const char *path, struct lfs_info *info); - -// Get a custom attribute -// -// Custom attributes are uniquely identified by an 8-bit type and limited -// to LFS_ATTR_MAX bytes. When read, if the stored attribute is smaller than -// the buffer, it will be padded with zeros. If the stored attribute is larger, -// then it will be silently truncated. If no attribute is found, the error -// LFS_ERR_NOATTR is returned and the buffer is filled with zeros. -// -// Returns the size of the attribute, or a negative error code on failure. -// Note, the returned size is the size of the attribute on disk, irrespective -// of the size of the buffer. This can be used to dynamically allocate a buffer -// or check for existence. -lfs_ssize_t lfs_getattr(lfs_t *lfs, const char *path, - uint8_t type, void *buffer, lfs_size_t size); - -#ifndef LFS_READONLY -// Set custom attributes -// -// Custom attributes are uniquely identified by an 8-bit type and limited -// to LFS_ATTR_MAX bytes. If an attribute is not found, it will be -// implicitly created. -// -// Returns a negative error code on failure. -int lfs_setattr(lfs_t *lfs, const char *path, - uint8_t type, const void *buffer, lfs_size_t size); -#endif - -#ifndef LFS_READONLY -// Removes a custom attribute -// -// If an attribute is not found, nothing happens. -// -// Returns a negative error code on failure. -int lfs_removeattr(lfs_t *lfs, const char *path, uint8_t type); -#endif - - -/// File operations /// - -#ifndef LFS_NO_MALLOC -// Open a file -// -// The mode that the file is opened in is determined by the flags, which -// are values from the enum lfs_open_flags that are bitwise-ored together. -// -// Returns a negative error code on failure. -int lfs_file_open(lfs_t *lfs, lfs_file_t *file, - const char *path, int flags); - -// if LFS_NO_MALLOC is defined, lfs_file_open() will fail with LFS_ERR_NOMEM -// thus use lfs_file_opencfg() with config.buffer set. -#endif - -// Open a file with extra configuration -// -// The mode that the file is opened in is determined by the flags, which -// are values from the enum lfs_open_flags that are bitwise-ored together. -// -// The config struct provides additional config options per file as described -// above. The config struct must remain allocated while the file is open, and -// the config struct must be zeroed for defaults and backwards compatibility. -// -// Returns a negative error code on failure. -int lfs_file_opencfg(lfs_t *lfs, lfs_file_t *file, - const char *path, int flags, - const struct lfs_file_config *config); - -// Close a file -// -// Any pending writes are written out to storage as though -// sync had been called and releases any allocated resources. -// -// Returns a negative error code on failure. -int lfs_file_close(lfs_t *lfs, lfs_file_t *file); - -// Synchronize a file on storage -// -// Any pending writes are written out to storage. -// Returns a negative error code on failure. -int lfs_file_sync(lfs_t *lfs, lfs_file_t *file); - -// Read data from file -// -// Takes a buffer and size indicating where to store the read data. -// Returns the number of bytes read, or a negative error code on failure. -lfs_ssize_t lfs_file_read(lfs_t *lfs, lfs_file_t *file, - void *buffer, lfs_size_t size); - -#ifndef LFS_READONLY -// Write data to file -// -// Takes a buffer and size indicating the data to write. The file will not -// actually be updated on the storage until either sync or close is called. -// -// Returns the number of bytes written, or a negative error code on failure. -lfs_ssize_t lfs_file_write(lfs_t *lfs, lfs_file_t *file, - const void *buffer, lfs_size_t size); -#endif - -// Change the position of the file -// -// The change in position is determined by the offset and whence flag. -// Returns the new position of the file, or a negative error code on failure. -lfs_soff_t lfs_file_seek(lfs_t *lfs, lfs_file_t *file, - lfs_soff_t off, int whence); - -#ifndef LFS_READONLY -// Truncates the size of the file to the specified size -// -// Returns a negative error code on failure. -int lfs_file_truncate(lfs_t *lfs, lfs_file_t *file, lfs_off_t size); -#endif - -// Return the position of the file -// -// Equivalent to lfs_file_seek(lfs, file, 0, LFS_SEEK_CUR) -// Returns the position of the file, or a negative error code on failure. -lfs_soff_t lfs_file_tell(lfs_t *lfs, lfs_file_t *file); - -// Change the position of the file to the beginning of the file -// -// Equivalent to lfs_file_seek(lfs, file, 0, LFS_SEEK_SET) -// Returns a negative error code on failure. -int lfs_file_rewind(lfs_t *lfs, lfs_file_t *file); - -// Return the size of the file -// -// Similar to lfs_file_seek(lfs, file, 0, LFS_SEEK_END) -// Returns the size of the file, or a negative error code on failure. -lfs_soff_t lfs_file_size(lfs_t *lfs, lfs_file_t *file); - - -/// Directory operations /// - -#ifndef LFS_READONLY -// Create a directory -// -// Returns a negative error code on failure. -int lfs_mkdir(lfs_t *lfs, const char *path); -#endif - -// Open a directory -// -// Once open a directory can be used with read to iterate over files. -// Returns a negative error code on failure. -int lfs_dir_open(lfs_t *lfs, lfs_dir_t *dir, const char *path); - -// Close a directory -// -// Releases any allocated resources. -// Returns a negative error code on failure. -int lfs_dir_close(lfs_t *lfs, lfs_dir_t *dir); - -// Read an entry in the directory -// -// Fills out the info structure, based on the specified file or directory. -// Returns a positive value on success, 0 at the end of directory, -// or a negative error code on failure. -int lfs_dir_read(lfs_t *lfs, lfs_dir_t *dir, struct lfs_info *info); - -// Change the position of the directory -// -// The new off must be a value previous returned from tell and specifies -// an absolute offset in the directory seek. -// -// Returns a negative error code on failure. -int lfs_dir_seek(lfs_t *lfs, lfs_dir_t *dir, lfs_off_t off); - -// Return the position of the directory -// -// The returned offset is only meant to be consumed by seek and may not make -// sense, but does indicate the current position in the directory iteration. -// -// Returns the position of the directory, or a negative error code on failure. -lfs_soff_t lfs_dir_tell(lfs_t *lfs, lfs_dir_t *dir); - -// Change the position of the directory to the beginning of the directory -// -// Returns a negative error code on failure. -int lfs_dir_rewind(lfs_t *lfs, lfs_dir_t *dir); - - -/// Filesystem-level filesystem operations - -// Finds the current size of the filesystem -// -// Note: Result is best effort. If files share COW structures, the returned -// size may be larger than the filesystem actually is. -// -// Returns the number of allocated blocks, or a negative error code on failure. -lfs_ssize_t lfs_fs_size(lfs_t *lfs); - -// Traverse through all blocks in use by the filesystem -// -// The provided callback will be called with each block address that is -// currently in use by the filesystem. This can be used to determine which -// blocks are in use or how much of the storage is available. -// -// Returns a negative error code on failure. -int lfs_fs_traverse(lfs_t *lfs, int (*cb)(void*, lfs_block_t), void *data); - -#ifndef LFS_READONLY -#ifdef LFS_MIGRATE -// Attempts to migrate a previous version of littlefs -// -// Behaves similarly to the lfs_format function. Attempts to mount -// the previous version of littlefs and update the filesystem so it can be -// mounted with the current version of littlefs. -// -// Requires a littlefs object and config struct. This clobbers the littlefs -// object, and does not leave the filesystem mounted. The config struct must -// be zeroed for defaults and backwards compatibility. -// -// Returns a negative error code on failure. -int lfs_migrate(lfs_t *lfs, const struct lfs_config *cfg); -#endif -#endif - - -#ifdef __cplusplus -} /* extern "C" */ -#endif - -#endif diff --git a/lfs3.c b/lfs3.c new file mode 100644 index 000000000..15c5c40f2 --- /dev/null +++ b/lfs3.c @@ -0,0 +1,16723 @@ +/* + * The little filesystem + * + * Copyright (c) 2022, The littlefs authors. + * Copyright (c) 2017, Arm Limited. All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + */ +#include "lfs3.h" +#include "lfs3_util.h" + + +// internally used disk-comparison enum +// +// note LT < EQ < GT +enum lfs3_cmp { + LFS3_CMP_LT = 0, // disk < query + LFS3_CMP_EQ = 1, // disk = query + LFS3_CMP_GT = 2, // disk > query +}; + +typedef int lfs3_scmp_t; + +// this is just a hint that the function returns a bool + err union +typedef int lfs3_sbool_t; + + +/// Simple bd wrappers (asserts go here) /// + +static int lfs3_bd_read__(lfs3_t *lfs3, lfs3_block_t block, lfs3_size_t off, + void *buffer, lfs3_size_t size) { + // must be in-bounds + LFS3_ASSERT(block < lfs3->block_count); + LFS3_ASSERT(off+size <= lfs3->cfg->block_size); + // must be aligned + LFS3_ASSERT(off % lfs3->cfg->read_size == 0); + LFS3_ASSERT(size % lfs3->cfg->read_size == 0); + + // bd read + int err = lfs3->cfg->read(lfs3->cfg, block, off, buffer, size); + LFS3_ASSERT(err <= 0); + if (err) { + LFS3_INFO("Bad read 0x%"PRIx32".%"PRIx32" %"PRIu32" (%d)", + block, off, size, err); + return err; + } + + return 0; +} + +#ifndef LFS3_RDONLY +static int lfs3_bd_prog__(lfs3_t *lfs3, lfs3_block_t block, lfs3_size_t off, + const void *buffer, lfs3_size_t size) { + // must be in-bounds + LFS3_ASSERT(block < lfs3->block_count); + LFS3_ASSERT(off+size <= lfs3->cfg->block_size); + // must be aligned + LFS3_ASSERT(off % lfs3->cfg->prog_size == 0); + LFS3_ASSERT(size % lfs3->cfg->prog_size == 0); + + // bd prog + int err = lfs3->cfg->prog(lfs3->cfg, block, off, buffer, size); + LFS3_ASSERT(err <= 0); + if (err) { + LFS3_INFO("Bad prog 0x%"PRIx32".%"PRIx32" %"PRIu32" (%d)", + block, off, size, err); + return err; + } + + return 0; +} +#endif + +#ifndef LFS3_RDONLY +static int lfs3_bd_erase__(lfs3_t *lfs3, lfs3_block_t block) { + // must be in-bounds + LFS3_ASSERT(block < lfs3->block_count); + + // bd erase + int err = lfs3->cfg->erase(lfs3->cfg, block); + LFS3_ASSERT(err <= 0); + if (err) { + LFS3_INFO("Bad erase 0x%"PRIx32" (%d)", + block, err); + return err; + } + + return 0; +} +#endif + +#ifndef LFS3_RDONLY +static int lfs3_bd_sync__(lfs3_t *lfs3) { + // bd sync + int err = lfs3->cfg->sync(lfs3->cfg); + LFS3_ASSERT(err <= 0); + if (err) { + LFS3_INFO("Bad sync (%d)", err); + return err; + } + + return 0; +} +#endif + + +/// Caching block device operations /// + +static inline void lfs3_bd_droprcache(lfs3_t *lfs3) { + lfs3->rcache.size = 0; +} + +#ifndef LFS3_RDONLY +static inline void lfs3_bd_droppcache(lfs3_t *lfs3) { + lfs3->pcache.size = 0; +} +#endif + +// caching read that lends you a buffer +// +// note hint has two conveniences: +// 0 => minimal caching +// -1 => maximal caching +static int lfs3_bd_readnext(lfs3_t *lfs3, + lfs3_block_t block, lfs3_size_t off, lfs3_size_t hint, + lfs3_size_t size, + const uint8_t **buffer_, lfs3_size_t *size_) { + // must be in-bounds + LFS3_ASSERT(block < lfs3->block_count); + LFS3_ASSERT(off+size <= lfs3->cfg->block_size); + + lfs3_size_t hint_ = lfs3_max(hint, size); // make sure hint >= size + while (true) { + lfs3_size_t d = hint_; + + // already in pcache? + #ifndef LFS3_RDONLY + if (block == lfs3->pcache.block + && off < lfs3->pcache.off + lfs3->pcache.size) { + if (off >= lfs3->pcache.off) { + *buffer_ = &lfs3->pcache.buffer[off-lfs3->pcache.off]; + *size_ = lfs3_min( + lfs3_min(d, size), + lfs3->pcache.size - (off-lfs3->pcache.off)); + return 0; + } + + // pcache takes priority + d = lfs3_min(d, lfs3->pcache.off - off); + } + #endif + + // already in rcache? + if (block == lfs3->rcache.block + && off < lfs3->rcache.off + lfs3->rcache.size + && off >= lfs3->rcache.off) { + *buffer_ = &lfs3->rcache.buffer[off-lfs3->rcache.off]; + *size_ = lfs3_min( + lfs3_min(d, size), + lfs3->rcache.size - (off-lfs3->rcache.off)); + return 0; + } + + // drop rcache in case read fails + lfs3_bd_droprcache(lfs3); + + // load into rcache, above conditions can no longer fail + // + // note it's ok if we overlap the pcache a bit, pcache always + // takes priority until flush, which updates the rcache + lfs3_size_t off__ = lfs3_aligndown(off, lfs3->cfg->read_size); + lfs3_size_t size__ = lfs3_alignup( + lfs3_min( + // watch out for overflow when hint_=-1! + (off-off__) + lfs3_min( + d, + lfs3->cfg->block_size - off), + lfs3->cfg->rcache_size), + lfs3->cfg->read_size); + int err = lfs3_bd_read__(lfs3, block, off__, + lfs3->rcache.buffer, size__); + if (err) { + return err; + } + + lfs3->rcache.block = block; + lfs3->rcache.off = off__; + lfs3->rcache.size = size__; + } +} + +// caching read +// +// note hint has two conveniences: +// 0 => minimal caching +// -1 => maximal caching +static int lfs3_bd_read(lfs3_t *lfs3, + lfs3_block_t block, lfs3_size_t off, lfs3_size_t hint, + void *buffer, lfs3_size_t size) { + // must be in-bounds + LFS3_ASSERT(block < lfs3->block_count); + LFS3_ASSERT(off+size <= lfs3->cfg->block_size); + + lfs3_size_t off_ = off; + lfs3_size_t hint_ = lfs3_max(hint, size); // make sure hint >= size + uint8_t *buffer_ = buffer; + lfs3_size_t size_ = size; + while (size_ > 0) { + lfs3_size_t d = hint_; + + // already in pcache? + #ifndef LFS3_RDONLY + if (block == lfs3->pcache.block + && off_ < lfs3->pcache.off + lfs3->pcache.size) { + if (off_ >= lfs3->pcache.off) { + d = lfs3_min( + lfs3_min(d, size_), + lfs3->pcache.size - (off_-lfs3->pcache.off)); + lfs3_memcpy(buffer_, + &lfs3->pcache.buffer[off_-lfs3->pcache.off], + d); + + off_ += d; + hint_ -= d; + buffer_ += d; + size_ -= d; + continue; + } + + // pcache takes priority + d = lfs3_min(d, lfs3->pcache.off - off_); + } + #endif + + // already in rcache? + if (block == lfs3->rcache.block + && off_ < lfs3->rcache.off + lfs3->rcache.size) { + if (off_ >= lfs3->rcache.off) { + d = lfs3_min( + lfs3_min(d, size_), + lfs3->rcache.size - (off_-lfs3->rcache.off)); + lfs3_memcpy(buffer_, + &lfs3->rcache.buffer[off_-lfs3->rcache.off], + d); + + off_ += d; + hint_ -= d; + buffer_ += d; + size_ -= d; + continue; + } + + // rcache takes priority + d = lfs3_min(d, lfs3->rcache.off - off_); + } + + // bypass rcache? + if (off_ % lfs3->cfg->read_size == 0 + && lfs3_min(d, size_) >= lfs3_min(hint_, lfs3->cfg->rcache_size) + && lfs3_min(d, size_) >= lfs3->cfg->read_size) { + d = lfs3_aligndown(size_, lfs3->cfg->read_size); + int err = lfs3_bd_read__(lfs3, block, off_, buffer_, d); + if (err) { + return err; + } + + off_ += d; + hint_ -= d; + buffer_ += d; + size_ -= d; + continue; + } + + // drop rcache in case read fails + lfs3_bd_droprcache(lfs3); + + // load into rcache, above conditions can no longer fail + // + // note it's ok if we overlap the pcache a bit, pcache always + // takes priority until flush, which updates the rcache + lfs3_size_t off__ = lfs3_aligndown(off_, lfs3->cfg->read_size); + lfs3_size_t size__ = lfs3_alignup( + lfs3_min( + // watch out for overflow when hint_=-1! + (off_-off__) + lfs3_min( + lfs3_min(hint_, d), + lfs3->cfg->block_size - off_), + lfs3->cfg->rcache_size), + lfs3->cfg->read_size); + int err = lfs3_bd_read__(lfs3, block, off__, + lfs3->rcache.buffer, size__); + if (err) { + return err; + } + + lfs3->rcache.block = block; + lfs3->rcache.off = off__; + lfs3->rcache.size = size__; + } + + return 0; +} + +// needed in lfs3_bd_prog_ for prog validation +#ifdef LFS3_CKPROGS +static inline bool lfs3_m_isckprogs(uint32_t flags); +#endif +static lfs3_scmp_t lfs3_bd_cmp(lfs3_t *lfs3, + lfs3_block_t block, lfs3_size_t off, lfs3_size_t hint, + const void *buffer, lfs3_size_t size); + +// low-level prog stuff +#ifndef LFS3_RDONLY +static int lfs3_bd_prog_(lfs3_t *lfs3, lfs3_block_t block, lfs3_size_t off, + const void *buffer, lfs3_size_t size, + uint32_t *cksum) { + // must be in-bounds + LFS3_ASSERT(block < lfs3->block_count); + LFS3_ASSERT(off+size <= lfs3->cfg->block_size); + + // prog to disk + int err = lfs3_bd_prog__(lfs3, block, off, buffer, size); + if (err) { + return err; + } + + // checking progs? + #ifdef LFS3_CKPROGS + if (lfs3_m_isckprogs(lfs3->flags)) { + // pcache should have been dropped at this point + LFS3_ASSERT(lfs3->pcache.size == 0); + + // invalidate rcache, we're going to clobber it anyways + lfs3_bd_droprcache(lfs3); + + lfs3_scmp_t cmp = lfs3_bd_cmp(lfs3, block, off, 0, + buffer, size); + if (cmp < 0) { + return cmp; + } + + if (cmp != LFS3_CMP_EQ) { + LFS3_WARN("Found ckprog mismatch 0x%"PRIx32".%"PRIx32" %"PRId32, + block, off, size); + return LFS3_ERR_CORRUPT; + } + } + #endif + + // update rcache if we can + if (block == lfs3->rcache.block + && off <= lfs3->rcache.off + lfs3->rcache.size) { + lfs3->rcache.off = lfs3_min(off, lfs3->rcache.off); + lfs3->rcache.size = lfs3_min( + (off-lfs3->rcache.off) + size, + lfs3->cfg->rcache_size); + lfs3_memcpy(&lfs3->rcache.buffer[off-lfs3->rcache.off], + buffer, + lfs3->rcache.size - (off-lfs3->rcache.off)); + } + + // optional prog-aligned checksum + if (cksum && cksum == &lfs3->pcksum) { + lfs3->pcksum = lfs3_crc32c(lfs3->pcksum, buffer, size); + } + + return 0; +} +#endif + +// flush the pcache +#ifndef LFS3_RDONLY +static int lfs3_bd_flush(lfs3_t *lfs3, uint32_t *cksum) { + if (lfs3->pcache.size != 0) { + // must be in-bounds + LFS3_ASSERT(lfs3->pcache.block < lfs3->block_count); + // must be aligned + LFS3_ASSERT(lfs3->pcache.off % lfs3->cfg->prog_size == 0); + lfs3_size_t size = lfs3_alignup( + lfs3->pcache.size, + lfs3->cfg->prog_size); + + // make this cache available, if we error anything in this cache + // would be useless anyways + lfs3_bd_droppcache(lfs3); + + // flush + int err = lfs3_bd_prog_(lfs3, lfs3->pcache.block, + lfs3->pcache.off, lfs3->pcache.buffer, size, + cksum); + if (err) { + return err; + } + } + + return 0; +} +#endif + +// caching prog that lends you a buffer +// +// with optional checksum +#ifndef LFS3_RDONLY +static int lfs3_bd_prognext(lfs3_t *lfs3, lfs3_block_t block, lfs3_size_t off, + lfs3_size_t size, + uint8_t **buffer_, lfs3_size_t *size_, + uint32_t *cksum) { + // must be in-bounds + LFS3_ASSERT(block < lfs3->block_count); + LFS3_ASSERT(off+size <= lfs3->cfg->block_size); + + while (true) { + // active pcache? + if (lfs3->pcache.size != 0) { + // wait, wrong block? this must be a leftover pcache due to + // an error, discard + if (lfs3->pcache.block != block) { + lfs3_bd_droppcache(lfs3); + continue; + } + + // fits in pcache? + if (off < lfs3->pcache.off + lfs3->cfg->pcache_size) { + // you can't prog backwards silly + LFS3_ASSERT(off >= lfs3->pcache.off); + + // expand the pcache? + lfs3->pcache.size = lfs3_min( + (off-lfs3->pcache.off) + size, + lfs3->cfg->pcache_size); + + *buffer_ = &lfs3->pcache.buffer[off-lfs3->pcache.off]; + *size_ = lfs3_min( + size, + lfs3->pcache.size - (off-lfs3->pcache.off)); + return 0; + } + + // flush pcache? + int err = lfs3_bd_flush(lfs3, cksum); + if (err) { + return err; + } + } + + // move the pcache, above conditions can no longer fail + lfs3->pcache.block = block; + lfs3->pcache.off = lfs3_aligndown(off, lfs3->cfg->prog_size); + lfs3->pcache.size = lfs3_min( + (off-lfs3->pcache.off) + size, + lfs3->cfg->pcache_size); + + // zero to avoid any information leaks + lfs3_memset(lfs3->pcache.buffer, 0xff, lfs3->cfg->pcache_size); + } +} +#endif + +// caching prog +// +// with optional checksum +#ifndef LFS3_RDONLY +static int lfs3_bd_prog(lfs3_t *lfs3, lfs3_block_t block, lfs3_size_t off, + const void *buffer, lfs3_size_t size, + uint32_t *cksum) { + // must be in-bounds + LFS3_ASSERT(block < lfs3->block_count); + LFS3_ASSERT(off+size <= lfs3->cfg->block_size); + + lfs3_size_t off_ = off; + const uint8_t *buffer_ = buffer; + lfs3_size_t size_ = size; + while (size_ > 0) { + // active pcache? + if (lfs3->pcache.size != 0) { + // wait, wrong block? this must be a leftover pcache due to + // an error, discard + if (lfs3->pcache.block != block) { + lfs3_bd_droppcache(lfs3); + continue; + } + + // fits in pcache? + if (off_ < lfs3->pcache.off + lfs3->cfg->pcache_size) { + // you can't prog backwards silly + LFS3_ASSERT(off_ >= lfs3->pcache.off); + + // expand the pcache? + lfs3->pcache.size = lfs3_min( + (off_-lfs3->pcache.off) + size_, + lfs3->cfg->pcache_size); + + lfs3_size_t d = lfs3_min( + size_, + lfs3->pcache.size - (off_-lfs3->pcache.off)); + lfs3_memcpy(&lfs3->pcache.buffer[off_-lfs3->pcache.off], + buffer_, + d); + + off_ += d; + buffer_ += d; + size_ -= d; + continue; + } + + // flush pcache? + // + // flush even if we're bypassing pcache, some devices don't + // support out-of-order progs in a block + int err = lfs3_bd_flush(lfs3, cksum); + if (err) { + return err; + } + } + + // bypass pcache? + if (off_ % lfs3->cfg->prog_size == 0 + && size_ >= lfs3->cfg->pcache_size) { + lfs3_size_t d = lfs3_aligndown(size_, lfs3->cfg->prog_size); + int err = lfs3_bd_prog_(lfs3, block, off_, buffer_, d, + cksum); + if (err) { + return err; + } + + off_ += d; + buffer_ += d; + size_ -= d; + continue; + } + + // move the pcache, above conditions can no longer fail + lfs3->pcache.block = block; + lfs3->pcache.off = lfs3_aligndown(off_, lfs3->cfg->prog_size); + lfs3->pcache.size = lfs3_min( + (off_-lfs3->pcache.off) + size_, + lfs3->cfg->pcache_size); + + // zero to avoid any information leaks + lfs3_memset(lfs3->pcache.buffer, 0xff, lfs3->cfg->pcache_size); + } + + // optional checksum + if (cksum && cksum != &lfs3->pcksum) { + *cksum = lfs3_crc32c(*cksum, buffer, size); + } + + return 0; +} +#endif + +#ifndef LFS3_RDONLY +static int lfs3_bd_sync(lfs3_t *lfs3) { + // make sure we flush any caches + int err = lfs3_bd_flush(lfs3, NULL); + if (err) { + return err; + } + + return lfs3_bd_sync__(lfs3); +} +#endif + +#ifndef LFS3_RDONLY +static int lfs3_bd_erase(lfs3_t *lfs3, lfs3_block_t block) { + // must be in-bounds + LFS3_ASSERT(block < lfs3->block_count); + + // invalidate any relevant caches + if (lfs3->pcache.block == block) { + lfs3_bd_droppcache(lfs3); + } + if (lfs3->rcache.block == block) { + lfs3_bd_droprcache(lfs3); + } + + return lfs3_bd_erase__(lfs3, block); +} +#endif + + +// other block device utils + +static int lfs3_bd_cksum(lfs3_t *lfs3, + lfs3_block_t block, lfs3_size_t off, lfs3_size_t hint, + lfs3_size_t size, + uint32_t *cksum) { + // must be in-bounds + LFS3_ASSERT(block < lfs3->block_count); + LFS3_ASSERT(off+size <= lfs3->cfg->block_size); + + lfs3_size_t off_ = off; + lfs3_size_t hint_ = lfs3_max(hint, size); // make sure hint >= size + lfs3_size_t size_ = size; + while (size_ > 0) { + const uint8_t *buffer__; + lfs3_size_t size__; + int err = lfs3_bd_readnext(lfs3, block, off_, hint_, size_, + &buffer__, &size__); + if (err) { + return err; + } + + *cksum = lfs3_crc32c(*cksum, buffer__, size__); + + off_ += size__; + hint_ -= size__; + size_ -= size__; + } + + return 0; +} + +static lfs3_scmp_t lfs3_bd_cmp(lfs3_t *lfs3, + lfs3_block_t block, lfs3_size_t off, lfs3_size_t hint, + const void *buffer, lfs3_size_t size) { + // must be in-bounds + LFS3_ASSERT(block < lfs3->block_count); + LFS3_ASSERT(off+size <= lfs3->cfg->block_size); + + lfs3_size_t off_ = off; + lfs3_size_t hint_ = lfs3_max(hint, size); // make sure hint >= size + const uint8_t *buffer_ = buffer; + lfs3_size_t size_ = size; + while (size_ > 0) { + const uint8_t *buffer__; + lfs3_size_t size__; + int err = lfs3_bd_readnext(lfs3, block, off_, hint_, size_, + &buffer__, &size__); + if (err) { + return err; + } + + int cmp = lfs3_memcmp(buffer__, buffer_, size__); + if (cmp != 0) { + return (cmp < 0) ? LFS3_CMP_LT : LFS3_CMP_GT; + } + + off_ += size__; + hint_ -= size__; + buffer_ += size__; + size_ -= size__; + } + + return LFS3_CMP_EQ; +} + +#ifndef LFS3_RDONLY +static int lfs3_bd_cpy(lfs3_t *lfs3, + lfs3_block_t dst_block, lfs3_size_t dst_off, + lfs3_block_t src_block, lfs3_size_t src_off, lfs3_size_t hint, + lfs3_size_t size, + uint32_t *cksum) { + // must be in-bounds + LFS3_ASSERT(dst_block < lfs3->block_count); + LFS3_ASSERT(dst_off+size <= lfs3->cfg->block_size); + LFS3_ASSERT(src_block < lfs3->block_count); + LFS3_ASSERT(src_off+size <= lfs3->cfg->block_size); + + lfs3_size_t dst_off_ = dst_off; + lfs3_size_t src_off_ = src_off; + lfs3_size_t hint_ = lfs3_max(hint, size); // make sure hint >= size + lfs3_size_t size_ = size; + while (size_ > 0) { + // prefer the pcache here to avoid rcache conflicts with prog + // validation, if we're lucky we might even be able to avoid + // clobbering the rcache at all + uint8_t *buffer__; + lfs3_size_t size__; + int err = lfs3_bd_prognext(lfs3, dst_block, dst_off_, size_, + &buffer__, &size__, + cksum); + if (err) { + return err; + } + + err = lfs3_bd_read(lfs3, src_block, src_off_, hint_, + buffer__, size__); + if (err) { + return err; + } + + // optional checksum + if (cksum && cksum != &lfs3->pcksum) { + *cksum = lfs3_crc32c(*cksum, buffer__, size__); + } + + dst_off_ += size__; + src_off_ += size__; + hint_ -= size__; + size_ -= size__; + } + + return 0; +} +#endif + +#ifndef LFS3_RDONLY +static int lfs3_bd_set(lfs3_t *lfs3, lfs3_block_t block, lfs3_size_t off, + uint8_t c, lfs3_size_t size, + uint32_t *cksum) { + // must be in-bounds + LFS3_ASSERT(block < lfs3->block_count); + LFS3_ASSERT(off+size <= lfs3->cfg->block_size); + + lfs3_size_t off_ = off; + lfs3_size_t size_ = size; + while (size_ > 0) { + uint8_t *buffer__; + lfs3_size_t size__; + int err = lfs3_bd_prognext(lfs3, block, off_, size_, + &buffer__, &size__, + cksum); + if (err) { + return err; + } + + lfs3_memset(buffer__, c, size__); + + // optional checksum + if (cksum && cksum != &lfs3->pcksum) { + *cksum = lfs3_crc32c(*cksum, buffer__, size__); + } + + off_ += size__; + size_ -= size__; + } + + return 0; +} +#endif + + +// lfs3_ptail_t stuff +// +// ptail tracks the most recent trunk's parity so we can parity-check +// if it hasn't been written to disk yet + +#if !defined(LFS3_RDONLY) && defined(LFS3_CKMETAPARITY) +#define LFS3_PTAIL_PARITY 0x80000000 +#endif + +#if !defined(LFS3_RDONLY) && defined(LFS3_CKMETAPARITY) +static inline bool lfs3_ptail_parity(const lfs3_t *lfs3) { + return lfs3->ptail.off & LFS3_PTAIL_PARITY; +} +#endif + +#if !defined(LFS3_RDONLY) && defined(LFS3_CKMETAPARITY) +static inline lfs3_size_t lfs3_ptail_off(const lfs3_t *lfs3) { + return lfs3->ptail.off & ~LFS3_PTAIL_PARITY; +} +#endif + + +// checked read helpers + +#ifdef LFS3_CKDATACKSUMS +static int lfs3_bd_ckprefix(lfs3_t *lfs3, + lfs3_block_t block, lfs3_size_t off, lfs3_size_t hint, + lfs3_size_t cksize, uint32_t cksum, + lfs3_size_t *hint_, + uint32_t *cksum__) { + (void)cksum; + // must be in-bounds + LFS3_ASSERT(block < lfs3->block_count); + LFS3_ASSERT(cksize <= lfs3->cfg->block_size); + + // make sure hint includes our prefix/suffix + lfs3_size_t hint__ = lfs3_max( + // watch out for overflow when hint=-1! + off + lfs3_min( + hint, + lfs3->cfg->block_size - off), + cksize); + + // checksum any prefixed data + int err = lfs3_bd_cksum(lfs3, + block, 0, hint__, + off, + cksum__); + if (err) { + return err; + } + + // return adjusted hint, note we clamped this to a positive range + // earlier, otherwise we'd have real problems with hint=-1! + *hint_ = hint__ - off; + return 0; +} +#endif + +#ifdef LFS3_CKDATACKSUMS +static int lfs3_bd_cksuffix(lfs3_t *lfs3, + lfs3_block_t block, lfs3_size_t off, lfs3_size_t hint, + lfs3_size_t cksize, uint32_t cksum, + uint32_t cksum__) { + // must be in-bounds + LFS3_ASSERT(block < lfs3->block_count); + LFS3_ASSERT(cksize <= lfs3->cfg->block_size); + + // checksum any suffixed data + int err = lfs3_bd_cksum(lfs3, + block, off, hint, + cksize - off, + &cksum__); + if (err) { + return err; + } + + // do checksums match? + if (cksum__ != cksum) { + LFS3_ERROR("Found ckdatacksums mismatch " + "0x%"PRIx32".%"PRIx32" %"PRId32", " + "cksum %08"PRIx32" (!= %08"PRIx32")", + block, 0, cksize, + cksum__, cksum); + return LFS3_ERR_CORRUPT; + } + + return 0; +} +#endif + + +// checked read functions + +// caching read with parity/checksum checks +// +// the main downside of checking reads is we need to read all data that +// contributes to the relevant parity/checksum, this may be +// significantly more than the data we actually end up using +// +#ifdef LFS3_CKDATACKSUMS +static int lfs3_bd_readck(lfs3_t *lfs3, + lfs3_block_t block, lfs3_size_t off, lfs3_size_t hint, + void *buffer, lfs3_size_t size, + lfs3_size_t cksize, uint32_t cksum) { + // must be in-bounds + LFS3_ASSERT(block < lfs3->block_count); + LFS3_ASSERT(cksize <= lfs3->cfg->block_size); + // read should fit in ck info + LFS3_ASSERT(off+size <= cksize); + + // checksum any prefixed data + uint32_t cksum__ = 0; + lfs3_size_t hint_; + int err = lfs3_bd_ckprefix(lfs3, block, off, hint, + cksize, cksum, + &hint_, + &cksum__); + if (err) { + return err; + } + + // read and checksum the data we're interested in + err = lfs3_bd_read(lfs3, + block, off, hint_, + buffer, size); + if (err) { + return err; + } + + cksum__ = lfs3_crc32c(cksum__, buffer, size); + + // checksum any suffixed data and validate + err = lfs3_bd_cksuffix(lfs3, block, off+size, hint_-size, + cksize, cksum, + cksum__); + if (err) { + return err; + } + + return 0; +} +#endif + +// these could probably be a bit better deduplicated with their +// unchecked counterparts, but we don't generally use both at the same +// time +// +// we'd also need to worry about early termination in lfs3_bd_cmp/cmpck + +#ifdef LFS3_CKDATACKSUMS +static lfs3_scmp_t lfs3_bd_cmpck(lfs3_t *lfs3, + lfs3_block_t block, lfs3_size_t off, lfs3_size_t hint, + const void *buffer, lfs3_size_t size, + lfs3_size_t cksize, uint32_t cksum) { + // must be in-bounds + LFS3_ASSERT(block < lfs3->block_count); + LFS3_ASSERT(cksize <= lfs3->cfg->block_size); + // read should fit in ck info + LFS3_ASSERT(off+size <= cksize); + + // checksum any prefixed data + uint32_t cksum__ = 0; + lfs3_size_t hint_; + int err = lfs3_bd_ckprefix(lfs3, block, off, hint, + cksize, cksum, + &hint_, + &cksum__); + if (err) { + return err; + } + + // compare the data while simultaneously updating the checksum + lfs3_size_t off_ = off; + lfs3_size_t hint__ = hint_ - off; + const uint8_t *buffer_ = buffer; + lfs3_size_t size_ = size; + int cmp = LFS3_CMP_EQ; + while (size_ > 0) { + const uint8_t *buffer__; + lfs3_size_t size__; + err = lfs3_bd_readnext(lfs3, block, off_, hint__, size_, + &buffer__, &size__); + if (err) { + return err; + } + + cksum__ = lfs3_crc32c(cksum__, buffer__, size__); + + if (cmp == LFS3_CMP_EQ) { + int cmp_ = lfs3_memcmp(buffer__, buffer_, size__); + if (cmp_ != 0) { + cmp = (cmp_ < 0) ? LFS3_CMP_LT : LFS3_CMP_GT; + } + } + + off_ += size__; + hint__ -= size__; + buffer_ += size__; + size_ -= size__; + } + + // checksum any suffixed data and validate + err = lfs3_bd_cksuffix(lfs3, block, off+size, hint_-size, + cksize, cksum, + cksum__); + if (err) { + return err; + } + + return cmp; +} +#endif + +#if !defined(LFS3_RDONLY) && defined(LFS3_CKDATACKSUMS) +static int lfs3_bd_cpyck(lfs3_t *lfs3, + lfs3_block_t dst_block, lfs3_size_t dst_off, + lfs3_block_t src_block, lfs3_size_t src_off, lfs3_size_t hint, + lfs3_size_t size, + lfs3_size_t src_cksize, uint32_t src_cksum, + uint32_t *cksum) { + // must be in-bounds + LFS3_ASSERT(dst_block < lfs3->block_count); + LFS3_ASSERT(dst_off+size <= lfs3->cfg->block_size); + LFS3_ASSERT(src_block < lfs3->block_count); + LFS3_ASSERT(src_cksize <= lfs3->cfg->block_size); + // read should fit in ck info + LFS3_ASSERT(src_off+size <= src_cksize); + + // checksum any prefixed data + uint32_t cksum__ = 0; + lfs3_size_t hint_; + int err = lfs3_bd_ckprefix(lfs3, src_block, src_off, hint, + src_cksize, src_cksum, + &hint_, + &cksum__); + if (err) { + return err; + } + + // copy the data while simultaneously updating our checksum + lfs3_size_t dst_off_ = dst_off; + lfs3_size_t src_off_ = src_off; + lfs3_size_t hint__ = hint_; + lfs3_size_t size_ = size; + while (size_ > 0) { + // prefer the pcache here to avoid rcache conflicts with prog + // validation, if we're lucky we might even be able to avoid + // clobbering the rcache at all + uint8_t *buffer__; + lfs3_size_t size__; + err = lfs3_bd_prognext(lfs3, dst_block, dst_off_, size_, + &buffer__, &size__, + cksum); + if (err) { + return err; + } + + err = lfs3_bd_read(lfs3, src_block, src_off_, hint__, + buffer__, size__); + if (err) { + return err; + } + + // validating checksum + cksum__ = lfs3_crc32c(cksum__, buffer__, size__); + + // optional prog checksum + if (cksum && cksum != &lfs3->pcksum) { + *cksum = lfs3_crc32c(*cksum, buffer__, size__); + } + + dst_off_ += size__; + src_off_ += size__; + hint__ -= size__; + size_ -= size__; + } + + // checksum any suffixed data and validate + err = lfs3_bd_cksuffix(lfs3, src_block, src_off+size, hint_-size, + src_cksize, src_cksum, + cksum__); + if (err) { + return err; + } + + return 0; +} +#endif + + + + +/// Tags - lfs3_tag_t stuff /// + +// tag type operations +static inline lfs3_tag_t lfs3_tag_mode(lfs3_tag_t tag) { + return tag & 0xf000; +} + +static inline lfs3_tag_t lfs3_tag_suptype(lfs3_tag_t tag) { + return tag & 0xff00; +} + +static inline uint8_t lfs3_tag_subtype(lfs3_tag_t tag) { + return tag & 0x00ff; +} + +static inline lfs3_tag_t lfs3_tag_key(lfs3_tag_t tag) { + return tag & 0x0fff; +} + +static inline lfs3_tag_t lfs3_tag_supkey(lfs3_tag_t tag) { + return tag & 0x0f00; +} + +static inline lfs3_tag_t lfs3_tag_subkey(lfs3_tag_t tag) { + return tag & 0x00ff; +} + +static inline uint8_t lfs3_tag_redund(lfs3_tag_t tag) { + return tag & 0x0003; +} + +static inline bool lfs3_tag_isalt(lfs3_tag_t tag) { + return tag & LFS3_TAG_ALT; +} + +static inline bool lfs3_tag_isshrub(lfs3_tag_t tag) { + return tag & LFS3_TAG_SHRUB; +} + +static inline bool lfs3_tag_istrunk(lfs3_tag_t tag) { + return lfs3_tag_mode(tag) != LFS3_TAG_CKSUM; +} + +static inline uint8_t lfs3_tag_phase(lfs3_tag_t tag) { + return tag & LFS3_TAG_PHASE; +} + +static inline bool lfs3_tag_perturb(lfs3_tag_t tag) { + return tag & LFS3_TAG_PERTURB; +} + +static inline bool lfs3_tag_isinternal(lfs3_tag_t tag) { + return tag & LFS3_tag_INTERNAL; +} + +static inline bool lfs3_tag_isrm(lfs3_tag_t tag) { + return tag & LFS3_tag_RM; +} + +static inline bool lfs3_tag_isgrow(lfs3_tag_t tag) { + return tag & LFS3_tag_GROW; +} + +static inline bool lfs3_tag_ismask0(lfs3_tag_t tag) { + return ((tag >> 12) & 0x3) == 0; +} + +static inline bool lfs3_tag_ismask2(lfs3_tag_t tag) { + return ((tag >> 12) & 0x3) == 1; +} + +static inline bool lfs3_tag_ismask8(lfs3_tag_t tag) { + return ((tag >> 12) & 0x3) == 2; +} + +static inline bool lfs3_tag_ismask12(lfs3_tag_t tag) { + return ((tag >> 12) & 0x3) == 3; +} + +static inline lfs3_tag_t lfs3_tag_mask(lfs3_tag_t tag) { + return 0x0fff & (-1U << ((0xc820 >> (4*((tag >> 12) & 0x3))) & 0xf)); +} + +// alt operations +static inline bool lfs3_tag_isblack(lfs3_tag_t tag) { + return !(tag & LFS3_TAG_R); +} + +static inline bool lfs3_tag_isred(lfs3_tag_t tag) { + return tag & LFS3_TAG_R; +} + +static inline bool lfs3_tag_isle(lfs3_tag_t tag) { + return !(tag & LFS3_TAG_GT); +} + +static inline bool lfs3_tag_isgt(lfs3_tag_t tag) { + return tag & LFS3_TAG_GT; +} + +static inline lfs3_tag_t lfs3_tag_isparallel(lfs3_tag_t a, lfs3_tag_t b) { + return (a & LFS3_TAG_GT) == (b & LFS3_TAG_GT); +} + +static inline bool lfs3_tag_follow( + lfs3_tag_t alt, lfs3_rid_t weight, + lfs3_srid_t lower_rid, lfs3_srid_t upper_rid, + lfs3_srid_t rid, lfs3_tag_t tag) { + // null tags break the following logic for unreachable alts + LFS3_ASSERT(lfs3_tag_key(tag) != 0); + + if (lfs3_tag_isgt(alt)) { + return rid > upper_rid - (lfs3_srid_t)weight - 1 + || (rid == upper_rid - (lfs3_srid_t)weight - 1 + && lfs3_tag_key(tag) > lfs3_tag_key(alt)); + } else { + return rid < lower_rid + (lfs3_srid_t)weight - 1 + || (rid == lower_rid + (lfs3_srid_t)weight - 1 + && lfs3_tag_key(tag) <= lfs3_tag_key(alt)); + } +} + +static inline bool lfs3_tag_follow2( + lfs3_tag_t alt, lfs3_rid_t weight, + lfs3_tag_t alt2, lfs3_rid_t weight2, + lfs3_srid_t lower_rid, lfs3_srid_t upper_rid, + lfs3_srid_t rid, lfs3_tag_t tag) { + if (lfs3_tag_isred(alt2) && lfs3_tag_isparallel(alt, alt2)) { + weight += weight2; + } + + return lfs3_tag_follow(alt, weight, lower_rid, upper_rid, rid, tag); +} + +static inline void lfs3_tag_flip( + lfs3_tag_t *alt, lfs3_rid_t *weight, + lfs3_srid_t lower_rid, lfs3_srid_t upper_rid) { + *alt = *alt ^ LFS3_TAG_GT; + *weight = (upper_rid - lower_rid) - *weight; +} + +static inline void lfs3_tag_flip2( + lfs3_tag_t *alt, lfs3_rid_t *weight, + lfs3_tag_t alt2, lfs3_rid_t weight2, + lfs3_srid_t lower_rid, lfs3_srid_t upper_rid) { + if (lfs3_tag_isred(alt2)) { + *weight += weight2; + } + + lfs3_tag_flip(alt, weight, lower_rid, upper_rid); +} + +static inline void lfs3_tag_trim( + lfs3_tag_t alt, lfs3_rid_t weight, + lfs3_srid_t *lower_rid, lfs3_srid_t *upper_rid, + lfs3_tag_t *lower_tag, lfs3_tag_t *upper_tag) { + LFS3_ASSERT((lfs3_srid_t)weight >= 0); + if (lfs3_tag_isgt(alt)) { + *upper_rid -= weight; + if (upper_tag) { + *upper_tag = alt + 1; + } + } else { + *lower_rid += weight; + if (lower_tag) { + *lower_tag = alt; + } + } +} + +static inline void lfs3_tag_trim2( + lfs3_tag_t alt, lfs3_rid_t weight, + lfs3_tag_t alt2, lfs3_rid_t weight2, + lfs3_srid_t *lower_rid, lfs3_srid_t *upper_rid, + lfs3_tag_t *lower_tag, lfs3_tag_t *upper_tag) { + if (lfs3_tag_isred(alt2)) { + lfs3_tag_trim( + alt2, weight2, + lower_rid, upper_rid, + lower_tag, upper_tag); + } + + lfs3_tag_trim( + alt, weight, + lower_rid, upper_rid, + lower_tag, upper_tag); +} + +static inline bool lfs3_tag_unreachable( + lfs3_tag_t alt, lfs3_rid_t weight, + lfs3_srid_t lower_rid, lfs3_srid_t upper_rid, + lfs3_tag_t lower_tag, lfs3_tag_t upper_tag) { + if (lfs3_tag_isgt(alt)) { + return !lfs3_tag_follow( + alt, weight, + lower_rid, upper_rid, + upper_rid-1, upper_tag-1); + } else { + return !lfs3_tag_follow( + alt, weight, + lower_rid, upper_rid, + lower_rid-1, lower_tag+1); + } +} + +static inline bool lfs3_tag_unreachable2( + lfs3_tag_t alt, lfs3_rid_t weight, + lfs3_tag_t alt2, lfs3_rid_t weight2, + lfs3_srid_t lower_rid, lfs3_srid_t upper_rid, + lfs3_tag_t lower_tag, lfs3_tag_t upper_tag) { + if (lfs3_tag_isred(alt2)) { + lfs3_tag_trim( + alt2, weight2, + &lower_rid, &upper_rid, + &lower_tag, &upper_tag); + } + + return lfs3_tag_unreachable( + alt, weight, + lower_rid, upper_rid, + lower_tag, upper_tag); +} + +static inline bool lfs3_tag_diverging( + lfs3_tag_t alt, lfs3_rid_t weight, + lfs3_srid_t lower_rid, lfs3_srid_t upper_rid, + lfs3_srid_t a_rid, lfs3_tag_t a_tag, + lfs3_srid_t b_rid, lfs3_tag_t b_tag) { + return lfs3_tag_follow( + alt, weight, + lower_rid, upper_rid, + a_rid, a_tag) + != lfs3_tag_follow( + alt, weight, + lower_rid, upper_rid, + b_rid, b_tag); +} + +static inline bool lfs3_tag_diverging2( + lfs3_tag_t alt, lfs3_rid_t weight, + lfs3_tag_t alt2, lfs3_rid_t weight2, + lfs3_srid_t lower_rid, lfs3_srid_t upper_rid, + lfs3_srid_t a_rid, lfs3_tag_t a_tag, + lfs3_srid_t b_rid, lfs3_tag_t b_tag) { + return lfs3_tag_follow2( + alt, weight, + alt2, weight2, + lower_rid, upper_rid, + a_rid, a_tag) + != lfs3_tag_follow2( + alt, weight, + alt2, weight2, + lower_rid, upper_rid, + b_rid, b_tag); +} + + +// support for encoding/decoding tags on disk + +// needed in lfs3_bd_readtag +#ifdef LFS3_CKMETAPARITY +static inline bool lfs3_m_isckparity(uint32_t flags); +#endif + +static lfs3_ssize_t lfs3_bd_readtag(lfs3_t *lfs3, + lfs3_block_t block, lfs3_size_t off, lfs3_size_t hint, + lfs3_tag_t *tag_, lfs3_rid_t *weight_, lfs3_size_t *size_, + uint32_t *cksum) { + // read the largest possible tag size + uint8_t tag_buf[LFS3_TAG_DSIZE]; + lfs3_size_t tag_dsize = lfs3_min(LFS3_TAG_DSIZE, lfs3->cfg->block_size-off); + if (tag_dsize < 4) { + return LFS3_ERR_CORRUPT; + } + + int err = lfs3_bd_read(lfs3, block, off, hint, + tag_buf, tag_dsize); + if (err) { + return err; + } + + // check the valid bit? + if (cksum) { + // on-disk, the tag's valid bit must reflect the parity of the + // preceding data + // + // fortunately crc32cs are parity-preserving, so this is the + // same as the parity of the checksum + if ((tag_buf[0] >> 7) != lfs3_parity(*cksum)) { + return LFS3_ERR_CORRUPT; + } + } + + lfs3_tag_t tag + = ((lfs3_tag_t)tag_buf[0] << 8) + | ((lfs3_tag_t)tag_buf[1] << 0); + lfs3_ssize_t d = 2; + + lfs3_rid_t weight; + lfs3_ssize_t d_ = lfs3_fromleb128(&weight, &tag_buf[d], tag_dsize-d); + if (d_ < 0) { + return d_; + } + // weights should be limited to 31-bits + if (weight > 0x7fffffff) { + return LFS3_ERR_CORRUPT; + } + d += d_; + + lfs3_size_t size; + d_ = lfs3_fromleb128(&size, &tag_buf[d], tag_dsize-d); + if (d_ < 0) { + return d_; + } + // sizes should be limited to 28-bits + if (size > 0x0fffffff) { + return LFS3_ERR_CORRUPT; + } + d += d_; + + // check our tag does not go out of bounds + if (!lfs3_tag_isalt(tag) && off+d + size > lfs3->cfg->block_size) { + return LFS3_ERR_CORRUPT; + } + + // check the parity if we're checking parity + // + // this requires reading all of the data as well, but with any luck + // the data will stick around in the cache + #ifdef LFS3_CKMETAPARITY + if (lfs3_m_isckparity(lfs3->flags) + // don't bother checking parity if we're already calculating + // a checksum + && !cksum) { + // checksum the tag, including our valid bit + uint32_t cksum_ = lfs3_crc32c(0, tag_buf, d); + + // checksum the data, if we have any + lfs3_size_t hint_ = hint - lfs3_min(d, hint); + lfs3_size_t d_ = d; + if (!lfs3_tag_isalt(tag)) { + err = lfs3_bd_cksum(lfs3, + // make sure hint includes our pesky parity byte + block, off+d_, lfs3_max(hint_, size+1), + size, + &cksum_); + if (err) { + return err; + } + + hint_ -= lfs3_min(size, hint_); + d_ += size; + } + + // pesky parity byte + if (off+d_ > lfs3->cfg->block_size-1) { + return LFS3_ERR_CORRUPT; + } + + // read the pesky parity byte + // + // _usually_, the byte following a tag contains the tag's parity + // + // unless we're in the middle of building a commit, where things get + // tricky... to avoid problems with not-yet-written parity bits + // ptail tracks the most recent trunk's parity + // + + // parity in in ptail? + bool parity; + if (LFS3_IFDEF_RDONLY( + false, + block == lfs3->ptail.block + && off+d_ == lfs3_ptail_off(lfs3))) { + #ifndef LFS3_RDONLY + parity = lfs3_ptail_parity(lfs3); + #endif + + // parity on disk? + } else { + uint8_t p; + err = lfs3_bd_read(lfs3, block, off+d_, hint_, + &p, 1); + if (err) { + return err; + } + + parity = p >> 7; + } + + // does parity match? + if (lfs3_parity(cksum_) != parity) { + LFS3_ERROR("Found ckparity mismatch " + "0x%"PRIx32".%"PRIx32" %"PRId32", " + "parity %01"PRIx32" (!= %01"PRIx32")", + block, off, d_, + lfs3_parity(cksum_), parity); + return LFS3_ERR_CORRUPT; + } + } + #endif + + // optional checksum + if (cksum) { + // exclude valid bit from checksum + *cksum ^= tag_buf[0] & 0x00000080; + // calculate checksum + *cksum = lfs3_crc32c(*cksum, tag_buf, d); + } + + // save what we found, clearing the valid bit, we don't need it + // anymore + *tag_ = tag & 0x7fff; + *weight_ = weight; + *size_ = size; + return d; +} + +#ifndef LFS3_RDONLY +static lfs3_ssize_t lfs3_bd_progtag(lfs3_t *lfs3, + lfs3_block_t block, lfs3_size_t off, bool perturb, + lfs3_tag_t tag, lfs3_rid_t weight, lfs3_size_t size, + uint32_t *cksum) { + // we set the valid bit here + LFS3_ASSERT(!(tag & 0x8000)); + // bit 7 is reserved for future subtype extensions + LFS3_ASSERT(!(tag & 0x80)); + // weight should not exceed 31-bits + LFS3_ASSERT(weight <= 0x7fffffff); + // size should not exceed 28-bits + LFS3_ASSERT(size <= 0x0fffffff); + + // set the valid bit to the parity of the current checksum, inverted + // if the perturb bit is set, and exclude from the next checksum + LFS3_ASSERT(cksum); + bool v = lfs3_parity(*cksum) ^ perturb; + tag |= (lfs3_tag_t)v << 15; + *cksum ^= (uint32_t)v << 7; + + // encode into a be16 and pair of leb128s + uint8_t tag_buf[LFS3_TAG_DSIZE]; + tag_buf[0] = (uint8_t)(tag >> 8); + tag_buf[1] = (uint8_t)(tag >> 0); + lfs3_ssize_t d = 2; + + lfs3_ssize_t d_ = lfs3_toleb128(weight, &tag_buf[d], 5); + if (d_ < 0) { + return d_; + } + d += d_; + + d_ = lfs3_toleb128(size, &tag_buf[d], 4); + if (d_ < 0) { + return d_; + } + d += d_; + + int err = lfs3_bd_prog(lfs3, block, off, tag_buf, d, + cksum); + if (err) { + return err; + } + + return d; +} +#endif + + +/// Data - lfs3_data_t stuff /// + +#define LFS3_DATA_ONDISK 0x80000000 +#define LFS3_DATA_ISBPTR 0x40000000 + +#ifdef LFS3_CKDATACKSUMS +#define LFS3_DATA_ISERASED 0x80000000 +#endif + +#define LFS3_DATA_NULL() \ + ((lfs3_data_t){ \ + .size=0, \ + .u.buffer=NULL}) + +#define LFS3_DATA_BUF(_buffer, _size) \ + ((lfs3_data_t){ \ + .size=_size, \ + .u.buffer=(const void*)(_buffer)}) + +#define LFS3_DATA_DISK(_block, _off, _size) \ + ((lfs3_data_t){ \ + .size=LFS3_DATA_ONDISK | (_size), \ + .u.disk.block=_block, \ + .u.disk.off=_off}) + +// data helpers +static inline bool lfs3_data_ondisk(lfs3_data_t data) { + return data.size & LFS3_DATA_ONDISK; +} + +static inline bool lfs3_data_isbuf(lfs3_data_t data) { + return !(data.size & LFS3_DATA_ONDISK); +} + +static inline bool lfs3_data_isbptr(lfs3_data_t data) { + return data.size & LFS3_DATA_ISBPTR; +} + +static inline lfs3_size_t lfs3_data_size(lfs3_data_t data) { + return data.size & ~LFS3_DATA_ONDISK & ~LFS3_DATA_ISBPTR; +} + +#ifdef LFS3_CKDATACKSUMS +static inline lfs3_size_t lfs3_data_cksize(lfs3_data_t data) { + return data.u.disk.cksize & ~LFS3_DATA_ISERASED; +} +#endif + +#ifdef LFS3_CKDATACKSUMS +static inline uint32_t lfs3_data_cksum(lfs3_data_t data) { + return data.u.disk.cksum; +} +#endif + +// data slicing +static inline lfs3_data_t lfs3_data_slice(lfs3_data_t data, + lfs3_ssize_t off, lfs3_ssize_t size) { + // limit our off/size to data range, note the use of unsigned casts + // here to treat -1 as unbounded + lfs3_size_t off_ = lfs3_min( + lfs3_smax(off, 0), + lfs3_data_size(data)); + lfs3_size_t size_ = lfs3_min( + (lfs3_size_t)size, + lfs3_data_size(data) - off_); + + // on-disk? + if (lfs3_data_ondisk(data)) { + data.u.disk.off += off_; + data.size -= lfs3_data_size(data) - size_; + + // buffer? + } else { + data.u.buffer += off_; + data.size -= lfs3_data_size(data) - size_; + } + + return data; +} + +// this macro provides an lvalue for use in other macros, but compound +// literals currently optimize poorly, so measure before use and consider +// just using lfs3_data_slice instead +#define LFS3_DATA_SLICE(_data, _off, _size) \ + ((struct {lfs3_data_t d;}){lfs3_data_slice(_data, _off, _size)}.d) + + +// data <-> bd interactions + +// lfs3_data_read* operations update the lfs3_data_t, effectively +// consuming the data + +// needed in lfs3_data_read and friends +#ifdef LFS3_CKDATACKSUMS +static inline bool lfs3_m_isckdatacksums(uint32_t flags); +#endif + +static lfs3_ssize_t lfs3_data_read(lfs3_t *lfs3, lfs3_data_t *data, + void *buffer, lfs3_size_t size) { + // limit our size to data range + lfs3_size_t d = lfs3_min(size, lfs3_data_size(*data)); + + // on-disk? + if (lfs3_data_ondisk(*data)) { + // validating data cksums? + if (LFS3_IFDEF_CKDATACKSUMS( + lfs3_m_isckdatacksums(lfs3->flags) + && lfs3_data_isbptr(*data), + false)) { + #ifdef LFS3_CKDATACKSUMS + int err = lfs3_bd_readck(lfs3, + data->u.disk.block, data->u.disk.off, + // note our hint includes the full data range + lfs3_data_size(*data), + buffer, d, + lfs3_data_cksize(*data), lfs3_data_cksum(*data)); + if (err) { + return err; + } + #endif + + } else { + int err = lfs3_bd_read(lfs3, + data->u.disk.block, data->u.disk.off, + // note our hint includes the full data range + lfs3_data_size(*data), + buffer, d); + if (err) { + return err; + } + } + + // buffer? + } else { + lfs3_memcpy(buffer, data->u.buffer, d); + } + + *data = lfs3_data_slice(*data, d, -1); + return d; +} + +static int lfs3_data_readle32(lfs3_t *lfs3, lfs3_data_t *data, + uint32_t *word) { + uint8_t buf[4]; + lfs3_ssize_t d = lfs3_data_read(lfs3, data, buf, 4); + if (d < 0) { + return d; + } + + // truncated? + if (d < 4) { + return LFS3_ERR_CORRUPT; + } + + *word = lfs3_fromle32(buf); + return 0; +} + +// note all leb128s in our system reserve the sign bit +static int lfs3_data_readleb128(lfs3_t *lfs3, lfs3_data_t *data, + uint32_t *word_) { + // note we make sure not to update our data offset until after leb128 + // decoding + lfs3_data_t data_ = *data; + + // for 32-bits we can assume worst-case leb128 size is 5-bytes + uint8_t buf[5]; + lfs3_ssize_t d = lfs3_data_read(lfs3, &data_, buf, 5); + if (d < 0) { + return d; + } + + d = lfs3_fromleb128(word_, buf, d); + if (d < 0) { + return d; + } + // all leb128s in our system reserve the sign bit + if (*word_ > 0x7fffffff) { + return LFS3_ERR_CORRUPT; + } + + *data = lfs3_data_slice(*data, d, -1); + return 0; +} + +// a little-leb128 in our system is truncated to align nicely +// +// for 32-bit words, little-leb128s are truncated to 28-bits, so the +// resulting leb128 encoding fits nicely in 4-bytes +static inline int lfs3_data_readlleb128(lfs3_t *lfs3, lfs3_data_t *data, + uint32_t *word_) { + // just call readleb128 here + int err = lfs3_data_readleb128(lfs3, data, word_); + if (err) { + return err; + } + // little-leb128s should be limited to 28-bits + if (*word_ > 0x0fffffff) { + return LFS3_ERR_CORRUPT; + } + + return 0; +} + +static lfs3_scmp_t lfs3_data_cmp(lfs3_t *lfs3, lfs3_data_t data, + const void *buffer, lfs3_size_t size) { + // compare common prefix + lfs3_size_t d = lfs3_min(size, lfs3_data_size(data)); + + // on-disk? + if (lfs3_data_ondisk(data)) { + // validating data cksums? + if (LFS3_IFDEF_CKDATACKSUMS( + lfs3_m_isckdatacksums(lfs3->flags) + && lfs3_data_isbptr(data), + false)) { + #ifdef LFS3_CKDATACKSUMS + int cmp = lfs3_bd_cmpck(lfs3, + // note the 0 hint, we don't usually use any + // following data + data.u.disk.block, data.u.disk.off, 0, + buffer, d, + lfs3_data_cksize(data), lfs3_data_cksum(data)); + if (cmp != LFS3_CMP_EQ) { + return cmp; + } + #endif + + } else { + int cmp = lfs3_bd_cmp(lfs3, + // note the 0 hint, we don't usually use any + // following data + data.u.disk.block, data.u.disk.off, 0, + buffer, d); + if (cmp != LFS3_CMP_EQ) { + return cmp; + } + } + + // buffer? + } else { + int cmp = lfs3_memcmp(data.u.buffer, buffer, d); + if (cmp < 0) { + return LFS3_CMP_LT; + } else if (cmp > 0) { + return LFS3_CMP_GT; + } + } + + // if data is equal, check for size mismatch + if (lfs3_data_size(data) < size) { + return LFS3_CMP_LT; + } else if (lfs3_data_size(data) > size) { + return LFS3_CMP_GT; + } else { + return LFS3_CMP_EQ; + } +} + +static lfs3_scmp_t lfs3_data_namecmp(lfs3_t *lfs3, lfs3_data_t data, + lfs3_did_t did, const char *name, lfs3_size_t name_len) { + // first compare the did + lfs3_did_t did_; + int err = lfs3_data_readleb128(lfs3, &data, &did_); + if (err) { + return err; + } + + if (did_ < did) { + return LFS3_CMP_LT; + } else if (did_ > did) { + return LFS3_CMP_GT; + } + + // then compare the actual name + return lfs3_data_cmp(lfs3, data, name, name_len); +} + +#ifndef LFS3_RDONLY +static int lfs3_bd_progdata(lfs3_t *lfs3, + lfs3_block_t block, lfs3_size_t off, lfs3_data_t data, + uint32_t *cksum) { + // on-disk? + if (lfs3_data_ondisk(data)) { + // validating data cksums? + if (LFS3_IFDEF_CKDATACKSUMS( + lfs3_m_isckdatacksums(lfs3->flags) + && lfs3_data_isbptr(data), + false)) { + #ifdef LFS3_CKDATACKSUMS + int err = lfs3_bd_cpyck(lfs3, block, off, + data.u.disk.block, data.u.disk.off, lfs3_data_size(data), + lfs3_data_size(data), + lfs3_data_cksize(data), lfs3_data_cksum(data), + cksum); + if (err) { + return err; + } + #endif + + } else { + int err = lfs3_bd_cpy(lfs3, block, off, + data.u.disk.block, data.u.disk.off, lfs3_data_size(data), + lfs3_data_size(data), + cksum); + if (err) { + return err; + } + } + + // buffer? + } else { + int err = lfs3_bd_prog(lfs3, block, off, + data.u.buffer, data.size, + cksum); + if (err) { + return err; + } + } + + return 0; +} +#endif + + +// macros for le32/leb128/lleb128 encoding, these are useful for +// building rattrs + +#ifndef LFS3_RDONLY +static inline lfs3_data_t lfs3_data_fromle32(uint32_t word, + uint8_t buffer[static LFS3_LE32_DSIZE]) { + lfs3_tole32(word, buffer); + return LFS3_DATA_BUF(buffer, LFS3_LE32_DSIZE); +} +#endif + +#ifndef LFS3_RDONLY +static inline lfs3_data_t lfs3_data_fromleb128(uint32_t word, + uint8_t buffer[static LFS3_LEB128_DSIZE]) { + // leb128s should not exceed 31-bits + LFS3_ASSERT(word <= 0x7fffffff); + + lfs3_ssize_t d = lfs3_toleb128(word, buffer, LFS3_LEB128_DSIZE); + if (d < 0) { + LFS3_UNREACHABLE(); + } + + return LFS3_DATA_BUF(buffer, d); +} +#endif + +#ifndef LFS3_RDONLY +static inline lfs3_data_t lfs3_data_fromlleb128(uint32_t word, + uint8_t buffer[static LFS3_LLEB128_DSIZE]) { + // little-leb128s should not exceed 28-bits + LFS3_ASSERT(word <= 0x0fffffff); + + lfs3_ssize_t d = lfs3_toleb128(word, buffer, LFS3_LLEB128_DSIZE); + if (d < 0) { + LFS3_UNREACHABLE(); + } + + return LFS3_DATA_BUF(buffer, d); +} +#endif + + +/// Rattrs - lfs3_rattr_t stuff /// + +// rattr layouts/lazy encoders +enum lfs3_from { + LFS3_FROM_BUF = 0, + LFS3_FROM_DATA = 1, + + LFS3_FROM_LE32 = 2, + LFS3_FROM_LEB128 = 3, + LFS3_FROM_NAME = 4, + + LFS3_FROM_ECKSUM = 5, + LFS3_FROM_BPTR = 6, + LFS3_FROM_BTREE = 7, + LFS3_FROM_SHRUB = 8, + LFS3_FROM_MPTR = 9, + LFS3_FROM_GEOMETRY = 10, +}; + +// operations on attribute lists + +// our core attribute type +#ifndef LFS3_RDONLY +typedef struct lfs3_rattr { + lfs3_tag_t tag; + uint8_t from; + uint8_t count; + lfs3_srid_t weight; + union { + const uint8_t *buffer; + const lfs3_data_t *datas; + uint32_t le32; + uint32_t leb128; + uint32_t lleb128; + const void *etc; + } u; +} lfs3_rattr_t; +#endif + +// low-level attr macro +#define LFS3_RATTR_(_tag, _weight, _rattr) \ + ((lfs3_rattr_t){ \ + .tag=_tag, \ + .from=(_rattr).from, \ + .count=(_rattr).count, \ + .weight=_weight, \ + .u=(_rattr).u}) + +// high-level attr macros +#define LFS3_RATTR(_tag, _weight) \ + ((lfs3_rattr_t){ \ + .tag=_tag, \ + .from=LFS3_FROM_BUF, \ + .count=0, \ + .weight=_weight, \ + .u.datas=NULL}) + +#define LFS3_RATTR_BUF(_tag, _weight, _buffer, _size) \ + ((lfs3_rattr_t){ \ + .tag=_tag, \ + .from=LFS3_FROM_BUF, \ + .count=_size, \ + .weight=_weight, \ + .u.buffer=(const void*)(_buffer)}) + +#define LFS3_RATTR_DATA(_tag, _weight, _data) \ + ((lfs3_rattr_t){ \ + .tag=_tag, \ + .from=LFS3_FROM_DATA, \ + .count=1, \ + .weight=_weight, \ + .u.datas=_data}) + +#define LFS3_RATTR_CAT_(_tag, _weight, _datas, _data_count) \ + ((lfs3_rattr_t){ \ + .tag=_tag, \ + .from=LFS3_FROM_DATA, \ + .count=_data_count, \ + .weight=_weight, \ + .u.datas=_datas}) + +#define LFS3_RATTR_CAT(_tag, _weight, ...) \ + LFS3_RATTR_CAT_( \ + _tag, \ + _weight, \ + ((const lfs3_data_t[]){__VA_ARGS__}), \ + sizeof((const lfs3_data_t[]){__VA_ARGS__}) / sizeof(lfs3_data_t)) + +#define LFS3_RATTR_NOOP() \ + ((lfs3_rattr_t){ \ + .tag=LFS3_TAG_NULL, \ + .from=LFS3_FROM_BUF, \ + .count=0, \ + .weight=0, \ + .u.buffer=NULL}) + +// as convenience we lazily encode single le32/leb128/lleb128 attrs +// +// this also avoids needing a stack allocation for these attrs +#define LFS3_RATTR_LE32(_tag, _weight, _le32) \ + ((lfs3_rattr_t){ \ + .tag=_tag, \ + .from=LFS3_FROM_LE32, \ + .count=0, \ + .weight=_weight, \ + .u.le32=_le32}) + +#define LFS3_RATTR_LEB128(_tag, _weight, _leb128) \ + ((lfs3_rattr_t){ \ + .tag=_tag, \ + .from=LFS3_FROM_LEB128, \ + .count=0, \ + .weight=_weight, \ + .u.leb128=_leb128}) + +#define LFS3_RATTR_LLEB128(_tag, _weight, _lleb128) \ + ((lfs3_rattr_t){ \ + .tag=_tag, \ + .from=LFS3_FROM_LEB128, \ + .count=0, \ + .weight=_weight, \ + .u.lleb128=_lleb128}) + +// helper macro for did + name pairs +#ifndef LFS3_RDONLY +typedef struct lfs3_name { + uint32_t did; + const char *name; + lfs3_size_t name_len; +} lfs3_name_t; +#endif + +#define LFS3_RATTR_NAME_(_tag, _weight, _name) \ + ((lfs3_rattr_t){ \ + .tag=_tag, \ + .from=LFS3_FROM_NAME, \ + .count=0, \ + .weight=_weight, \ + .u.etc=(const lfs3_name_t*){_name}}) + +#define LFS3_RATTR_NAME(_tag, _weight, _did, _name, _name_len) \ + LFS3_RATTR_NAME_( \ + _tag, \ + _weight, \ + (&(const lfs3_name_t){ \ + .did=_did, \ + .name=_name, \ + .name_len=_name_len})) + +// macros for other lazily encoded attrs +#define LFS3_RATTR_ECKSUM(_tag, _weight, _ecksum) \ + ((lfs3_rattr_t){ \ + .tag=_tag, \ + .from=LFS3_FROM_ECKSUM, \ + .count=0, \ + .weight=_weight, \ + .u.etc=(const lfs3_ecksum_t*){_ecksum}}) + +// note the LFS3_BPTR_DSIZE hint so shrub estimates work +#define LFS3_RATTR_BPTR(_tag, _weight, _bptr) \ + ((lfs3_rattr_t){ \ + .tag=_tag, \ + .from=LFS3_FROM_BPTR, \ + .count=LFS3_BPTR_DSIZE, \ + .weight=_weight, \ + .u.etc=(const lfs3_bptr_t*){_bptr}}) + +#define LFS3_RATTR_BTREE(_tag, _weight, _btree) \ + ((lfs3_rattr_t){ \ + .tag=_tag, \ + .from=LFS3_FROM_BTREE, \ + .count=0, \ + .weight=_weight, \ + .u.etc=(const lfs3_btree_t*){_btree}}) + +#define LFS3_RATTR_SHRUB(_tag, _weight, _shrub) \ + ((lfs3_rattr_t){ \ + .tag=_tag, \ + .from=LFS3_FROM_SHRUB, \ + .count=0, \ + .weight=_weight, \ + .u.etc=(const lfs3_shrub_t*){_shrub}}) + +#define LFS3_RATTR_MPTR(_tag, _weight, _mptr) \ + ((lfs3_rattr_t){ \ + .tag=_tag, \ + .from=LFS3_FROM_MPTR, \ + .count=0, \ + .weight=_weight, \ + .u.etc=(const lfs3_block_t*){_mptr}}) + +#define LFS3_RATTR_GEOMETRY(_tag, _weight, _geometry) \ + ((lfs3_rattr_t){ \ + .tag=_tag, \ + .from=LFS3_FROM_GEOMETRY, \ + .count=0, \ + .weight=_weight, \ + .u.etc=(const lfs3_geometry_t*){_geometry}}) + +// these are special attrs that trigger unique behavior in +// lfs3_mdir_commit___ +#define LFS3_RATTR_RATTRS(_rattrs, _rattr_count) \ + ((lfs3_rattr_t){ \ + .tag=LFS3_tag_RATTRS, \ + .from=LFS3_FROM_BUF, \ + .count=_rattr_count, \ + .weight=0, \ + .u.etc=(const lfs3_rattr_t*){_rattrs}}) + +#define LFS3_RATTR_SHRUBCOMMIT(_shrubcommit) \ + ((lfs3_rattr_t){ \ + .tag=LFS3_tag_SHRUBCOMMIT, \ + .from=LFS3_FROM_BUF, \ + .count=0, \ + .weight=0, \ + .u.etc=(const lfs3_shrubcommit_t*){_shrubcommit}}) + +#define LFS3_RATTR_MOVE(_move) \ + ((lfs3_rattr_t){ \ + .tag=LFS3_tag_MOVE, \ + .from=LFS3_FROM_BUF, \ + .count=0, \ + .weight=0, \ + .u.etc=(const lfs3_mdir_t*){_move}}) + +#define LFS3_RATTR_ATTRS(_attrs, _attr_count) \ + ((lfs3_rattr_t){ \ + .tag=LFS3_tag_ATTRS, \ + .from=LFS3_FROM_BUF, \ + .count=_attr_count, \ + .weight=0, \ + .u.etc=(const struct lfs3_attr*){_attrs}}) + +// create an attribute list +#define LFS3_RATTRS(...) \ + (const lfs3_rattr_t[]){__VA_ARGS__}, \ + sizeof((const lfs3_rattr_t[]){__VA_ARGS__}) / sizeof(lfs3_rattr_t) + +// rattr helpers +#ifndef LFS3_RDONLY +static inline bool lfs3_rattr_isnoop(lfs3_rattr_t rattr) { + // noop rattrs must have zero weight + LFS3_ASSERT(rattr.tag || rattr.weight == 0); + return !rattr.tag; +} +#endif + +#ifndef LFS3_RDONLY +static inline bool lfs3_rattr_isinsert(lfs3_rattr_t rattr) { + return !lfs3_tag_isgrow(rattr.tag) && rattr.weight > 0; +} +#endif + +#ifndef LFS3_RDONLY +static inline lfs3_srid_t lfs3_rattr_nextrid(lfs3_rattr_t rattr, + lfs3_srid_t rid) { + if (lfs3_rattr_isinsert(rattr)) { + return rid + rattr.weight-1; + } else { + return rid + rattr.weight; + } +} +#endif + + +// operations on custom attribute lists +// +// a slightly different struct because it's user facing + +static inline lfs3_ssize_t lfs3_attr_size(const struct lfs3_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 lfs3_attr_isnoattr(const struct lfs3_attr *attr) { + return lfs3_attr_size(attr) == LFS3_ERR_NOATTR; +} + +static lfs3_scmp_t lfs3_attr_cmp(lfs3_t *lfs3, const struct lfs3_attr *attr, + const lfs3_data_t *data) { + // note data=NULL => NOATTR + if (!data) { + return (lfs3_attr_isnoattr(attr)) ? LFS3_CMP_EQ : LFS3_CMP_GT; + } else { + if (lfs3_attr_isnoattr(attr)) { + return LFS3_CMP_LT; + } else { + return lfs3_data_cmp(lfs3, *data, + attr->buffer, + lfs3_attr_size(attr)); + } + } +} + + + +/// Block allocator definitions /// + +// the block allocator is humorously cyclic in its definition +// +// to avoid the mess of redeclaring flags and things, just declare +// everything we need here + +// block allocator flags +#define LFS3_ALLOC_ERASE 0x000000001 // Please erase the block + +static inline bool lfs3_alloc_iserase(uint32_t flags) { + return flags & LFS3_ALLOC_ERASE; +} + +// checkpoint the allocator +// +// operations that need to alloc should call this when all in-use blocks +// are tracked, either by the filesystem or an opened mdir +// +// blocks are allocated at most once, and never reallocated, between +// checkpoints +#if !defined(LFS3_RDONLY) +static inline int lfs3_alloc_ckpoint(lfs3_t *lfs3); +#endif + +// discard any lookahead state, this is necessary if block_count changes +#ifndef LFS3_RDONLY +static inline void lfs3_alloc_discard(lfs3_t *lfs3); +#endif + +// allocate a block +#ifndef LFS3_RDONLY +static lfs3_sblock_t lfs3_alloc(lfs3_t *lfs3, uint32_t flags); +#endif + + + +/// Block pointer things /// + +#define LFS3_BPTR_ONDISK LFS3_DATA_ONDISK +#define LFS3_BPTR_ISBPTR LFS3_DATA_ISBPTR + +#ifndef LFS3_RDONLY +#define LFS3_BPTR_ISERASED 0x80000000 +#endif + +static void lfs3_bptr_init(lfs3_bptr_t *bptr, + lfs3_data_t data, lfs3_size_t cksize, uint32_t cksum) { + // make sure the bptr flag is set + LFS3_ASSERT(lfs3_data_ondisk(data)); + bptr->d.size = LFS3_DATA_ONDISK | LFS3_BPTR_ISBPTR | data.size; + bptr->d.u.disk.block = data.u.disk.block; + bptr->d.u.disk.off = data.u.disk.off; + #ifdef LFS3_CKDATACKSUMS + bptr->d.u.disk.cksize = cksize; + bptr->d.u.disk.cksum = cksum; + #else + bptr->cksize = cksize; + bptr->cksum = cksum; + #endif +} + +static inline void lfs3_bptr_discard(lfs3_bptr_t *bptr) { + bptr->d = LFS3_DATA_NULL(); + #ifndef LFS3_CKDATACKSUMS + bptr->cksize = 0; + bptr->cksum = 0; + #endif +} + +#ifndef LFS3_RDONLY +static inline void lfs3_bptr_claim(lfs3_bptr_t *bptr) { + #ifdef LFS3_CKDATACKSUMS + bptr->d.u.disk.cksize &= ~LFS3_BPTR_ISERASED; + #else + bptr->cksize &= ~LFS3_BPTR_ISERASED; + #endif +} +#endif + +static inline bool lfs3_bptr_isbptr(const lfs3_bptr_t *bptr) { + return bptr->d.size & LFS3_BPTR_ISBPTR; +} + +static inline lfs3_block_t lfs3_bptr_block(const lfs3_bptr_t *bptr) { + return bptr->d.u.disk.block; +} + +static inline lfs3_size_t lfs3_bptr_off(const lfs3_bptr_t *bptr) { + return bptr->d.u.disk.off; +} + +static inline lfs3_size_t lfs3_bptr_size(const lfs3_bptr_t *bptr) { + return bptr->d.size & ~LFS3_BPTR_ONDISK & ~LFS3_BPTR_ISBPTR; +} + +// checked reads adds ck info to lfs3_data_t that we don't want to +// unnecessarily duplicate, this makes accessing ck info annoyingly +// messy... +#ifndef LFS3_RDONLY +static inline bool lfs3_bptr_iserased(const lfs3_bptr_t *bptr) { + #ifdef LFS3_CKDATACKSUMS + return bptr->d.u.disk.cksize & LFS3_BPTR_ISERASED; + #else + return bptr->cksize & LFS3_BPTR_ISERASED; + #endif +} +#endif + +static inline lfs3_size_t lfs3_bptr_cksize(const lfs3_bptr_t *bptr) { + #ifdef LFS3_CKDATACKSUMS + return LFS3_IFDEF_RDONLY( + bptr->d.u.disk.cksize, + bptr->d.u.disk.cksize & ~LFS3_BPTR_ISERASED); + #else + return LFS3_IFDEF_RDONLY( + bptr->cksize, + bptr->cksize & ~LFS3_BPTR_ISERASED); + #endif +} + +static inline uint32_t lfs3_bptr_cksum(const lfs3_bptr_t *bptr) { + #ifdef LFS3_CKDATACKSUMS + return bptr->d.u.disk.cksum; + #else + return bptr->cksum; + #endif +} + +// slice a bptr in-place +static inline void lfs3_bptr_slice(lfs3_bptr_t *bptr, + lfs3_ssize_t off, lfs3_ssize_t size) { + bptr->d = lfs3_data_slice(bptr->d, off, size); +} + +// bptr on-disk encoding +#ifndef LFS3_RDONLY +static lfs3_data_t lfs3_data_frombptr(const lfs3_bptr_t *bptr, + uint8_t buffer[static LFS3_BPTR_DSIZE]) { + // size should not exceed 28-bits + LFS3_ASSERT(lfs3_data_size(bptr->d) <= 0x0fffffff); + // block should not exceed 31-bits + LFS3_ASSERT(lfs3_bptr_block(bptr) <= 0x7fffffff); + // off should not exceed 28-bits + LFS3_ASSERT(lfs3_bptr_off(bptr) <= 0x0fffffff); + // cksize should not exceed 28-bits + LFS3_ASSERT(lfs3_bptr_cksize(bptr) <= 0x0fffffff); + lfs3_ssize_t d = 0; + + // write the block, offset, size + lfs3_ssize_t d_ = lfs3_toleb128(lfs3_data_size(bptr->d), &buffer[d], 4); + if (d_ < 0) { + LFS3_UNREACHABLE(); + } + d += d_; + + d_ = lfs3_toleb128(lfs3_bptr_block(bptr), &buffer[d], 5); + if (d_ < 0) { + LFS3_UNREACHABLE(); + } + d += d_; + + d_ = lfs3_toleb128(lfs3_bptr_off(bptr), &buffer[d], 4); + if (d_ < 0) { + LFS3_UNREACHABLE(); + } + d += d_; + + // write the cksize, cksum + d_ = lfs3_toleb128(lfs3_bptr_cksize(bptr), &buffer[d], 4); + if (d_ < 0) { + LFS3_UNREACHABLE(); + } + d += d_; + + lfs3_tole32(lfs3_bptr_cksum(bptr), &buffer[d]); + d += 4; + + return LFS3_DATA_BUF(buffer, d); +} +#endif + +static int lfs3_data_readbptr(lfs3_t *lfs3, lfs3_data_t *data, + lfs3_bptr_t *bptr) { + // read the block, offset, size + int err = lfs3_data_readlleb128(lfs3, data, &bptr->d.size); + if (err) { + return err; + } + + err = lfs3_data_readleb128(lfs3, data, &bptr->d.u.disk.block); + if (err) { + return err; + } + + err = lfs3_data_readlleb128(lfs3, data, &bptr->d.u.disk.off); + if (err) { + return err; + } + + // read the cksize, cksum + err = lfs3_data_readlleb128(lfs3, data, + LFS3_IFDEF_CKDATACKSUMS( + &bptr->d.u.disk.cksize, + &bptr->cksize)); + if (err) { + return err; + } + + err = lfs3_data_readle32(lfs3, data, + LFS3_IFDEF_CKDATACKSUMS( + &bptr->d.u.disk.cksum, + &bptr->cksum)); + if (err) { + return err; + } + + // mark as on-disk + cksum + bptr->d.size |= LFS3_DATA_ONDISK | LFS3_DATA_ISBPTR; + return 0; +} + + +// allocate a bptr +#ifndef LFS3_RDONLY +static int lfs3_bptr_alloc(lfs3_t *lfs3, lfs3_bptr_t *bptr) { + lfs3_sblock_t block = lfs3_alloc(lfs3, LFS3_ALLOC_ERASE); + if (block < 0) { + return block; + } + + lfs3_bptr_init(bptr, + LFS3_DATA_DISK(block, 0, 0), + // mark as erased + LFS3_BPTR_ISERASED | 0, + 0); + return 0; +} +#endif + +// needed in lfs3_bptr_fetch +#ifdef LFS3_CKFETCHES +static inline bool lfs3_m_isckfetches(uint32_t flags); +#endif +static int lfs3_bptr_ck(lfs3_t *lfs3, const lfs3_bptr_t *bptr); + +// fetch a bptr or data fragment +static int lfs3_bptr_fetch(lfs3_t *lfs3, lfs3_bptr_t *bptr, + lfs3_tag_t tag, lfs3_bid_t weight, lfs3_data_t data) { + // fragment? (inlined data) + if (tag == LFS3_TAG_DATA) { + bptr->d = data; + + // bptr? + } else if (tag == LFS3_TAG_BLOCK) { + int err = lfs3_data_readbptr(lfs3, &data, + bptr); + if (err) { + return err; + } + + } else { + LFS3_UNREACHABLE(); + } + + // limit bptrs to btree weights, this may be useful for + // compression in the future + lfs3_bptr_slice(bptr, -1, weight); + + // checking fetches? + #ifdef LFS3_CKFETCHES + if (lfs3_m_isckfetches(lfs3->flags) + && lfs3_bptr_isbptr(bptr)) { + int err = lfs3_bptr_ck(lfs3, bptr); + if (err) { + return err; + } + } + #endif + + return 0; +} + +// check the contents of a bptr +static int lfs3_bptr_ck(lfs3_t *lfs3, const lfs3_bptr_t *bptr) { + uint32_t cksum = 0; + int err = lfs3_bd_cksum(lfs3, + lfs3_bptr_block(bptr), 0, 0, + lfs3_bptr_cksize(bptr), + &cksum); + if (err) { + return err; + } + + // test that our cksum matches what's expected + if (cksum != lfs3_bptr_cksum(bptr)) { + LFS3_ERROR("Found bptr cksum mismatch " + "0x%"PRIx32".%"PRIx32" %"PRId32", " + "cksum %08"PRIx32" (!= %08"PRIx32")", + lfs3_bptr_block(bptr), 0, + lfs3_bptr_cksize(bptr), + cksum, lfs3_bptr_cksum(bptr)); + return LFS3_ERR_CORRUPT; + } + + return 0; +} + + + + +/// Erased-state checksum stuff /// + +// erased-state checksum +#ifndef LFS3_RDONLY +typedef struct lfs3_ecksum { + // cksize=-1 indicates no ecksum + lfs3_ssize_t cksize; + uint32_t cksum; +} lfs3_ecksum_t; +#endif + +// erased-state checksum on-disk encoding +#ifndef LFS3_RDONLY +static lfs3_data_t lfs3_data_fromecksum(const lfs3_ecksum_t *ecksum, + uint8_t buffer[static LFS3_ECKSUM_DSIZE]) { + // you shouldn't try to encode a not-ecksum, that doesn't make sense + LFS3_ASSERT(ecksum->cksize != -1); + // cksize should not exceed 28-bits + LFS3_ASSERT((lfs3_size_t)ecksum->cksize <= 0x0fffffff); + + lfs3_ssize_t d = 0; + lfs3_ssize_t d_ = lfs3_toleb128(ecksum->cksize, &buffer[d], 4); + if (d_ < 0) { + LFS3_UNREACHABLE(); + } + d += d_; + + lfs3_tole32(ecksum->cksum, &buffer[d]); + d += 4; + + return LFS3_DATA_BUF(buffer, d); +} +#endif + +#ifndef LFS3_RDONLY +static int lfs3_data_readecksum(lfs3_t *lfs3, lfs3_data_t *data, + lfs3_ecksum_t *ecksum) { + int err = lfs3_data_readlleb128(lfs3, data, (lfs3_size_t*)&ecksum->cksize); + if (err) { + return err; + } + + err = lfs3_data_readle32(lfs3, data, &ecksum->cksum); + if (err) { + return err; + } + + return 0; +} +#endif + + + +/// Red-black-yellow Dhara tree operations /// + +#define LFS3_RBYD_ISSHRUB 0x80000000 +#define LFS3_RBYD_ISPERTURB 0x80000000 + +// helper functions +static void lfs3_rbyd_init(lfs3_rbyd_t *rbyd, lfs3_block_t block) { + rbyd->blocks[0] = block; + rbyd->trunk = 0; + rbyd->weight = 0; + #ifndef LFS3_RDONLY + rbyd->eoff = 0; + rbyd->cksum = 0; + #endif +} + +#ifndef LFS3_RDONLY +static inline void lfs3_rbyd_claim(lfs3_rbyd_t *rbyd) { + // mark as needing fetch + rbyd->eoff = 0; +} +#endif + +static inline bool lfs3_rbyd_isshrub(const lfs3_rbyd_t *rbyd) { + return rbyd->trunk & LFS3_RBYD_ISSHRUB; +} + +static inline lfs3_size_t lfs3_rbyd_trunk(const lfs3_rbyd_t *rbyd) { + return rbyd->trunk & ~LFS3_RBYD_ISSHRUB; +} + +#ifndef LFS3_RDONLY +static inline bool lfs3_rbyd_isfetched(const lfs3_rbyd_t *rbyd) { + return !lfs3_rbyd_trunk(rbyd) || rbyd->eoff; +} +#endif + +#ifndef LFS3_RDONLY +static inline bool lfs3_rbyd_isperturb(const lfs3_rbyd_t *rbyd) { + return rbyd->eoff & LFS3_RBYD_ISPERTURB; +} +#endif + +#ifndef LFS3_RDONLY +static inline lfs3_size_t lfs3_rbyd_eoff(const lfs3_rbyd_t *rbyd) { + return rbyd->eoff & ~LFS3_RBYD_ISPERTURB; +} +#endif + +static inline int lfs3_rbyd_cmp( + const lfs3_rbyd_t *a, + const lfs3_rbyd_t *b) { + if (a->blocks[0] != b->blocks[0]) { + return a->blocks[0] - b->blocks[0]; + } else { + return a->trunk - b->trunk; + } +} + + +// allocate an rbyd block +#ifndef LFS3_RDONLY +static int lfs3_rbyd_alloc(lfs3_t *lfs3, lfs3_rbyd_t *rbyd) { + lfs3_sblock_t block = lfs3_alloc(lfs3, LFS3_ALLOC_ERASE); + if (block < 0) { + return block; + } + + lfs3_rbyd_init(rbyd, block); + return 0; +} +#endif + +#ifndef LFS3_RDONLY +static int lfs3_rbyd_ckecksum(lfs3_t *lfs3, const lfs3_rbyd_t *rbyd, + const lfs3_ecksum_t *ecksum) { + // check that the ecksum looks right + if (lfs3_rbyd_eoff(rbyd) + ecksum->cksize >= lfs3->cfg->block_size + || lfs3_rbyd_eoff(rbyd) % lfs3->cfg->prog_size != 0) { + return LFS3_ERR_CORRUPT; + } + + // the next valid bit must _not_ match, or a commit was attempted, + // this should hopefully stay in our cache + uint8_t e; + int err = lfs3_bd_read(lfs3, + rbyd->blocks[0], lfs3_rbyd_eoff(rbyd), ecksum->cksize, + &e, 1); + if (err) { + return err; + } + + if (((e >> 7)^lfs3_rbyd_isperturb(rbyd)) == lfs3_parity(rbyd->cksum)) { + return LFS3_ERR_CORRUPT; + } + + // check that erased-state matches our checksum, if this fails + // most likely a write was interrupted + uint32_t ecksum_ = 0; + err = lfs3_bd_cksum(lfs3, + rbyd->blocks[0], lfs3_rbyd_eoff(rbyd), 0, + ecksum->cksize, + &ecksum_); + if (err) { + return err; + } + + // found erased-state? + return (ecksum_ == ecksum->cksum) ? 0 : LFS3_ERR_CORRUPT; +} +#endif + +// rbyd fetch flags +#define LFS3_RBYD_QUICKFETCH 0x80000000 // only fetch one trunk + +static inline bool lfs3_rbyd_isquickfetch(lfs3_size_t trunk) { + return trunk & LFS3_RBYD_QUICKFETCH; +} + +static inline lfs3_size_t lfs3_rbyd_fetchtrunk(lfs3_size_t trunk) { + return trunk & ~LFS3_RBYD_QUICKFETCH; +} + +// optional height calculation for debugging rbyd balance +typedef struct lfs3_rheight { + lfs3_size_t height; + lfs3_size_t bheight; +} lfs3_rheight_t; + +// needed in lfs3_rbyd_fetch_ if debugging rbyd balance +static lfs3_stag_t lfs3_rbyd_lookupnext_(lfs3_t *lfs3, const lfs3_rbyd_t *rbyd, + lfs3_srid_t rid, lfs3_tag_t tag, + lfs3_srid_t *rid_, lfs3_rid_t *weight_, lfs3_data_t *data_, + lfs3_rheight_t *rheight_); + +// fetch an rbyd +static int lfs3_rbyd_fetch_(lfs3_t *lfs3, + lfs3_rbyd_t *rbyd, uint32_t *gcksumdelta, + lfs3_block_t block, lfs3_size_t trunk) { + // set up some initial state + rbyd->blocks[0] = block; + rbyd->trunk = 0; + rbyd->weight = 0; + #ifndef LFS3_RDONLY + rbyd->eoff = 0; + #endif + + // if we're quick fetching, we can start from the trunk, + // otherwise we start from 0 and try to find the trunk + lfs3_size_t off_ = (lfs3_rbyd_isquickfetch(trunk)) + ? lfs3_rbyd_fetchtrunk(trunk) + : sizeof(uint32_t); + + // keep track of last commit off and perturb bit + lfs3_size_t eoff = 0; + bool perturb = false; + + // checksum the revision count to get the cksum started + uint32_t cksum_ = 0; + if (!lfs3_rbyd_isquickfetch(trunk)) { + int err = lfs3_bd_cksum(lfs3, block, 0, -1, sizeof(uint32_t), + &cksum_); + if (err) { + return err; + } + } + + // temporary state until we validate a cksum + uint32_t cksum__ = cksum_; + lfs3_size_t trunk_ = 0; + lfs3_size_t trunk__ = 0; + lfs3_rid_t weight_ = 0; + lfs3_rid_t weight__ = 0; + + // assume unerased until proven otherwise + #ifndef LFS3_RDONLY + lfs3_ecksum_t ecksum = {.cksize=-1}; + lfs3_ecksum_t ecksum_ = {.cksize=-1}; + #endif + + // also find gcksumdelta, though this is only used by mdirs + uint32_t gcksumdelta_ = 0; + + // scan tags, checking valid bits, cksums, etc + while (off_ < lfs3->cfg->block_size + && (!trunk || eoff <= lfs3_rbyd_fetchtrunk(trunk))) { + // read next tag + lfs3_tag_t tag; + lfs3_rid_t weight; + lfs3_size_t size; + lfs3_ssize_t d = lfs3_bd_readtag(lfs3, block, off_, -1, + &tag, &weight, &size, + (lfs3_rbyd_isquickfetch(trunk)) + ? NULL + : &cksum__); + if (d < 0) { + if (d == LFS3_ERR_CORRUPT) { + break; + } + return d; + } + lfs3_size_t off__ = off_ + d; + + // readtag should already check we're in-bounds + LFS3_ASSERT(lfs3_tag_isalt(tag) + || off__ + size <= lfs3->cfg->block_size); + + // take care of cksum + if (!lfs3_tag_isalt(tag)) { + // not an end-of-commit cksum + if (lfs3_tag_suptype(tag) != LFS3_TAG_CKSUM) { + if (!lfs3_rbyd_isquickfetch(trunk)) { + // cksum the entry, hopefully leaving it in the cache + int err = lfs3_bd_cksum(lfs3, block, off__, -1, size, + &cksum__); + if (err) { + if (err == LFS3_ERR_CORRUPT) { + break; + } + return err; + } + } + + // found an ecksum? save for later + if (LFS3_IFDEF_RDONLY( + false, + tag == LFS3_TAG_ECKSUM)) { + #ifndef LFS3_RDONLY + int err = lfs3_data_readecksum(lfs3, + &LFS3_DATA_DISK(block, off__, + // note this size is to make the hint do + // what we want + lfs3->cfg->block_size - off__), + &ecksum_); + if (err) { + if (err == LFS3_ERR_CORRUPT) { + break; + } + return err; + } + #endif + + // found gcksumdelta? save for later + } else if (tag == LFS3_TAG_GCKSUMDELTA) { + int err = lfs3_data_readle32(lfs3, + &LFS3_DATA_DISK(block, off__, + // note this size is to make the hint do + // what we want + lfs3->cfg->block_size - off__), + &gcksumdelta_); + if (err) { + if (err == LFS3_ERR_CORRUPT) { + break; + } + return err; + } + } + + // is an end-of-commit cksum + } else { + // truncated checksum? + if (size < sizeof(uint32_t)) { + break; + } + + // check phase + if (lfs3_tag_phase(tag) != (block & 0x3)) { + // uh oh, phase doesn't match, mounted incorrectly? + break; + } + + // check checksum, unless we're recklessly quick fetching + if (!lfs3_rbyd_isquickfetch(trunk)) { + uint32_t cksum___ = 0; + int err = lfs3_bd_read(lfs3, block, off__, -1, + &cksum___, sizeof(uint32_t)); + if (err) { + if (err == LFS3_ERR_CORRUPT) { + break; + } + return err; + } + cksum___ = lfs3_fromle32(&cksum___); + + if (cksum__ != cksum___) { + // uh oh, checksums don't match + break; + } + } + + // save what we've found so far + eoff = off__ + size; + rbyd->trunk = trunk_; + rbyd->weight = weight_; + if (!lfs3_rbyd_isquickfetch(trunk)) { + rbyd->cksum = cksum_; + } + if (gcksumdelta) { + *gcksumdelta = gcksumdelta_; + } + gcksumdelta_ = 0; + + // update perturb bit + perturb = lfs3_tag_perturb(tag); + + #ifndef LFS3_RDONLY + rbyd->eoff + = ((lfs3_size_t)perturb << (8*sizeof(lfs3_size_t)-1)) + | eoff; + ecksum = ecksum_; + ecksum_.cksize = -1; + #endif + + // revert to canonical checksum and perturb if necessary + cksum__ = cksum_ ^ ((perturb) ? LFS3_CRC32C_ODDZERO : 0); + } + } + + // found a trunk? + if (lfs3_tag_istrunk(tag)) { + if (!(trunk && off_ > lfs3_rbyd_fetchtrunk(trunk) && !trunk__)) { + // start of trunk? + if (!trunk__) { + // keep track of trunk's entry point + trunk__ = off_; + // reset weight + weight__ = 0; + } + + // derive weight of the tree from alt pointers + // + // NOTE we can't check for overflow/underflow here because we + // may be overeagerly parsing an invalid commit, it's ok for + // this to overflow/underflow as long as we throw it out later + // on a bad cksum + weight__ += weight; + + // end of trunk? + if (!lfs3_tag_isalt(tag)) { + // update trunk and weight, unless we are a shrub trunk, + // this prevents fetching shrub trunks, but why would + // you want to fetch a shrub trunk? + if (!lfs3_tag_isshrub(tag)) { + trunk_ = trunk__; + weight_ = weight__; + } + trunk__ = 0; + } + } + + // update canonical checksum, xoring out any perturb + // state, we don't want erased-state affecting our + // canonical checksum + cksum_ = cksum__ ^ ((perturb) ? LFS3_CRC32C_ODDZERO : 0); + } + + // skip data + if (!lfs3_tag_isalt(tag)) { + off__ += size; + } + + off_ = off__; + } + + // no valid commits? + if (!lfs3_rbyd_trunk(rbyd)) { + return LFS3_ERR_CORRUPT; + } + + // did we end on a valid commit? we may have erased-state + #ifndef LFS3_RDONLY + bool erased = false; + if (ecksum.cksize != -1) { + // check the erased-state checksum + int err = lfs3_rbyd_ckecksum(lfs3, rbyd, &ecksum); + if (err && err != LFS3_ERR_CORRUPT) { + return err; + } + + // found valid erased-state? + erased = (err != LFS3_ERR_CORRUPT); + } + + // used eoff=-1 to indicate when there is no erased-state + if (!erased) { + rbyd->eoff = -1; + } + #endif + + #ifdef LFS3_DBGRBYDFETCHES + if (lfs3_rbyd_isquickfetch(trunk)) { + LFS3_DEBUG("Quick-fetched rbyd 0x%"PRIx32".%"PRIx32" w%"PRId32", " + "eoff %"PRId32", cksum %"PRIx32, + rbyd->blocks[0], lfs3_rbyd_trunk(rbyd), + rbyd->weight, + LFS3_IFDEF_RDONLY( + -1, + (lfs3_rbyd_eoff(rbyd) >= lfs3->cfg->block_size) + ? -1 + : (lfs3_ssize_t)lfs3_rbyd_eoff(rbyd)), + rbyd->cksum); + } else { + LFS3_DEBUG("Fetched rbyd 0x%"PRIx32".%"PRIx32" w%"PRId32", " + "eoff %"PRId32", cksum %"PRIx32, + rbyd->blocks[0], lfs3_rbyd_trunk(rbyd), + rbyd->weight, + LFS3_IFDEF_RDONLY( + -1, + (lfs3_rbyd_eoff(rbyd) >= lfs3->cfg->block_size) + ? -1 + : (lfs3_ssize_t)lfs3_rbyd_eoff(rbyd)), + rbyd->cksum); + } + #endif + + // debugging rbyd balance? check that all branches in the rbyd have + // the same height + #ifdef LFS3_DBGRBYDBALANCE + lfs3_srid_t rid = -1; + lfs3_stag_t tag = 0; + lfs3_size_t min_height = -1; + lfs3_size_t max_height = 0; + lfs3_size_t min_bheight = -1; + lfs3_size_t max_bheight = 0; + while (true) { + lfs3_rheight_t rheight; + tag = lfs3_rbyd_lookupnext_(lfs3, rbyd, + rid, tag+1, + &rid, NULL, NULL, + &rheight); + if (tag < 0) { + if (tag == LFS3_ERR_NOENT) { + break; + } + return tag; + } + + // find the min/max height and bheight + min_height = lfs3_min(min_height, rheight.height); + max_height = lfs3_max(max_height, rheight.height); + min_bheight = lfs3_min(min_bheight, rheight.bheight); + max_bheight = lfs3_max(max_bheight, rheight.bheight); + } + min_height = (min_height == (lfs3_size_t)-1) ? 0 : min_height; + min_bheight = (min_bheight == (lfs3_size_t)-1) ? 0 : min_bheight; + LFS3_DEBUG("Fetched rbyd 0x%"PRIx32".%"PRIx32" w%"PRId32", " + "height %"PRId32"-%"PRId32", " + "bheight %"PRId32"-%"PRId32, + rbyd->blocks[0], lfs3_rbyd_trunk(rbyd), + rbyd->weight, + min_height, max_height, + min_bheight, max_bheight); + // all branches in the rbyd should have the same bheight + LFS3_ASSERT(max_bheight == min_bheight); + // this limits alt height to no worse than 2*bheight+2 (2*bheight+1 + // for normal appends, 2*bheight+2 with range removals) + LFS3_ASSERT(max_height <= 2*min_height+2); + #endif + + return 0; +} + +static int lfs3_rbyd_fetch(lfs3_t *lfs3, lfs3_rbyd_t *rbyd, + lfs3_block_t block, lfs3_size_t trunk) { + // why would you try to fetch a shrub? + LFS3_ASSERT(!(trunk & LFS3_RBYD_ISSHRUB)); + + return lfs3_rbyd_fetch_(lfs3, rbyd, NULL, block, trunk); +} + +// a more reckless fetch when checksum is known +// +// this just finds the eoff/perturb/ecksum for the current trunk to +// enable reckless commits +static int lfs3_rbyd_fetchquick(lfs3_t *lfs3, lfs3_rbyd_t *rbyd, + lfs3_block_t block, lfs3_size_t trunk, + uint32_t cksum) { + // why would you try to fetch a shrub? + LFS3_ASSERT(!(trunk & LFS3_RBYD_ISSHRUB)); + + // the only thing quick fetch can't figure out is the checksum + rbyd->cksum = cksum; + + int err = lfs3_rbyd_fetch_(lfs3, rbyd, NULL, + block, LFS3_RBYD_QUICKFETCH | trunk); + if (err) { + return err; + } + + // quick fetch should leave the cksum unaffected + LFS3_ASSERT(rbyd->cksum == cksum); + return 0; +} + +// a more aggressive fetch when checksum is known +static int lfs3_rbyd_fetchck(lfs3_t *lfs3, lfs3_rbyd_t *rbyd, + lfs3_block_t block, lfs3_size_t trunk, + uint32_t cksum) { + // why would you try to fetch a shrub? + LFS3_ASSERT(!(trunk & LFS3_RBYD_ISSHRUB)); + + int err = lfs3_rbyd_fetch(lfs3, rbyd, block, trunk); + if (err) { + if (err == LFS3_ERR_CORRUPT) { + LFS3_ERROR("Found corrupted rbyd 0x%"PRIx32".%"PRIx32", " + "cksum %08"PRIx32, + block, trunk, cksum); + } + return err; + } + + // test that our cksum matches what's expected + // + // it should be noted that this is very unlikely to happen without the + // above fetch failing, since that would require the rbyd to have the + // same trunk and pass its internal cksum + if (rbyd->cksum != cksum) { + LFS3_ERROR("Found rbyd cksum mismatch 0x%"PRIx32".%"PRIx32", " + "cksum %08"PRIx32" (!= %08"PRIx32")", + rbyd->blocks[0], lfs3_rbyd_trunk(rbyd), + rbyd->cksum, cksum); + return LFS3_ERR_CORRUPT; + } + + // if trunk/weight mismatch _after_ cksums match, that's not a storage + // error, that's a programming error + LFS3_ASSERT(lfs3_rbyd_trunk(rbyd) == trunk); + return 0; +} + + +// our core rbyd lookup algorithm +// +// finds the next rid+tag such that rid_+tag_ >= rid+tag +static lfs3_stag_t lfs3_rbyd_lookupnext_(lfs3_t *lfs3, const lfs3_rbyd_t *rbyd, + lfs3_srid_t rid, lfs3_tag_t tag, + lfs3_srid_t *rid_, lfs3_rid_t *weight_, lfs3_data_t *data_, + lfs3_rheight_t *rheight_) { + (void)rheight_; + // these bits should be clear at this point + LFS3_ASSERT(lfs3_tag_mode(tag) == 0); + + // make sure we never look up zero tags, the way we create + // unreachable tags has a hole here + tag = lfs3_max(tag, 0x1); + + // out of bounds? no trunk yet? + if (rid >= (lfs3_srid_t)rbyd->weight || !lfs3_rbyd_trunk(rbyd)) { + return LFS3_ERR_NOENT; + } + + // optionally find height/bheight for debugging rbyd balance + #ifdef LFS3_DBGRBYDBALANCE + if (rheight_) { + rheight_->height = 0; + rheight_->bheight = 0; + } + #endif + + // keep track of bounds as we descend down the tree + lfs3_size_t branch = lfs3_rbyd_trunk(rbyd); + lfs3_srid_t lower_rid = 0; + lfs3_srid_t upper_rid = rbyd->weight; + + // descend down tree + while (true) { + lfs3_tag_t alt; + lfs3_rid_t weight; + lfs3_size_t jump; + lfs3_ssize_t d = lfs3_bd_readtag(lfs3, + rbyd->blocks[0], branch, 0, + &alt, &weight, &jump, + NULL); + if (d < 0) { + return d; + } + + // found an alt? + if (lfs3_tag_isalt(alt)) { + lfs3_size_t branch_ = branch + d; + + // keep track of height for debugging + #ifdef LFS3_DBGRBYDBALANCE + if (rheight_) { + rheight_->height += 1; + + // only count black+followed alts towards bheight + if (lfs3_tag_isblack(alt) + || lfs3_tag_follow( + alt, weight, + lower_rid, upper_rid, + rid, tag)) { + rheight_->bheight += 1; + } + } + #endif + + // take alt? + if (lfs3_tag_follow( + alt, weight, + lower_rid, upper_rid, + rid, tag)) { + lfs3_tag_flip( + &alt, &weight, + lower_rid, upper_rid); + branch_ = branch - jump; + } + + lfs3_tag_trim( + alt, weight, + &lower_rid, &upper_rid, + NULL, NULL); + LFS3_ASSERT(branch_ != branch); + branch = branch_; + + // found end of tree? + } else { + // update the tag rid + lfs3_srid_t rid__ = upper_rid-1; + lfs3_tag_t tag__ = lfs3_tag_key(alt); + + // not what we're looking for? + if (!tag__ + || rid__ < rid + || (rid__ == rid && tag__ < tag)) { + return LFS3_ERR_NOENT; + } + + // save what we found + // TODO how many of these need to be conditional? + if (rid_) { + *rid_ = rid__; + } + if (weight_) { + *weight_ = upper_rid - lower_rid; + } + if (data_) { + *data_ = LFS3_DATA_DISK(rbyd->blocks[0], branch + d, jump); + } + return tag__; + } + } +} + + +// finds the next rid_+tag_ such that rid_+tag_ >= rid+tag +static lfs3_stag_t lfs3_rbyd_lookupnext(lfs3_t *lfs3, const lfs3_rbyd_t *rbyd, + lfs3_srid_t rid, lfs3_tag_t tag, + lfs3_srid_t *rid_, lfs3_rid_t *weight_, lfs3_data_t *data_) { + return lfs3_rbyd_lookupnext_(lfs3, rbyd, rid, tag, + rid_, weight_, data_, + NULL); +} + +// lookup assumes a known rid +static lfs3_stag_t lfs3_rbyd_lookup(lfs3_t *lfs3, const lfs3_rbyd_t *rbyd, + lfs3_srid_t rid, lfs3_tag_t tag, + lfs3_data_t *data_) { + lfs3_srid_t rid__; + lfs3_stag_t tag__ = lfs3_rbyd_lookupnext(lfs3, rbyd, + rid, lfs3_tag_key(tag), + &rid__, NULL, data_); + if (tag__ < 0) { + return tag__; + } + + // lookup finds the next-smallest tag, all we need to do is fail if it + // picks up the wrong tag + if (rid__ != rid + || (tag__ & lfs3_tag_mask(tag)) != (tag & lfs3_tag_mask(tag))) { + return LFS3_ERR_NOENT; + } + + return tag__; +} + + + +// rbyd append operations + + +// append a revision count +// +// this is optional, if not called revision count defaults to 0 (for btrees) +#ifndef LFS3_RDONLY +static int lfs3_rbyd_appendrev(lfs3_t *lfs3, lfs3_rbyd_t *rbyd, uint32_t rev) { + // should only be called before any tags are written + LFS3_ASSERT(rbyd->eoff == 0); + LFS3_ASSERT(rbyd->cksum == 0); + + // revision count stored as le32, we don't use a leb128 encoding as we + // intentionally allow the revision count to overflow + uint8_t rev_buf[sizeof(uint32_t)]; + lfs3_tole32(rev, &rev_buf); + + int err = lfs3_bd_prog(lfs3, + rbyd->blocks[0], lfs3_rbyd_eoff(rbyd), + &rev_buf, sizeof(uint32_t), + &rbyd->cksum); + if (err) { + return err; + } + + rbyd->eoff += sizeof(uint32_t); + return 0; +} +#endif + +// other low-level appends +#ifndef LFS3_RDONLY +static int lfs3_rbyd_appendtag(lfs3_t *lfs3, lfs3_rbyd_t *rbyd, + lfs3_tag_t tag, lfs3_rid_t weight, lfs3_size_t size) { + // tag must not be internal at this point + LFS3_ASSERT(!lfs3_tag_isinternal(tag)); + // bit 7 is reserved for future subtype extensions + LFS3_ASSERT(!(tag & 0x80)); + + // do we fit? + if (lfs3_rbyd_eoff(rbyd) + LFS3_TAG_DSIZE + > lfs3->cfg->block_size) { + return LFS3_ERR_RANGE; + } + + lfs3_ssize_t d = lfs3_bd_progtag(lfs3, + rbyd->blocks[0], lfs3_rbyd_eoff(rbyd), lfs3_rbyd_isperturb(rbyd), + tag, weight, size, + &rbyd->cksum); + if (d < 0) { + return d; + } + + rbyd->eoff += d; + + // keep track of most recent parity + #ifdef LFS3_CKMETAPARITY + lfs3->ptail.block = rbyd->blocks[0]; + lfs3->ptail.off + = ((lfs3_size_t)( + lfs3_parity(rbyd->cksum) ^ lfs3_rbyd_isperturb(rbyd) + ) << (8*sizeof(lfs3_size_t)-1)) + | lfs3_rbyd_eoff(rbyd); + #endif + + return 0; +} +#endif + +// needed in lfs3_rbyd_appendrattr_ +static lfs3_data_t lfs3_data_frombtree(const lfs3_btree_t *btree, + uint8_t buffer[static LFS3_BTREE_DSIZE]); +static lfs3_data_t lfs3_data_fromshrub(const lfs3_shrub_t *shrub, + uint8_t buffer[static LFS3_SHRUB_DSIZE]); +static lfs3_data_t lfs3_data_frommptr(const lfs3_block_t mptr[static 2], + uint8_t buffer[static LFS3_MPTR_DSIZE]); +typedef struct lfs3_geometry lfs3_geometry_t; +static lfs3_data_t lfs3_data_fromgeometry(const lfs3_geometry_t *geometry, + uint8_t buffer[static LFS3_GEOMETRY_DSIZE]); + +// encode rattrs +#ifndef LFS3_RDONLY +static int lfs3_rbyd_appendrattr_(lfs3_t *lfs3, lfs3_rbyd_t *rbyd, + lfs3_rattr_t rattr) { + // tag must not be internal at this point + LFS3_ASSERT(!lfs3_tag_isinternal(rattr.tag)); + // bit 7 is reserved for future subtype extensions + LFS3_ASSERT(!(rattr.tag & 0x80)); + + // encode lazy tags? + // + // we encode most tags lazily as this heavily reduces stack usage, + // though this does make things less gc-able at compile time + // + const lfs3_data_t *datas; + lfs3_size_t data_count; + struct { + union { + lfs3_data_t data; + + struct { + lfs3_data_t data; + uint8_t buf[LFS3_LE32_DSIZE]; + } le32; + struct { + lfs3_data_t data; + uint8_t buf[LFS3_LEB128_DSIZE]; + } leb128; + struct { + lfs3_data_t datas[2]; + uint8_t buf[LFS3_LEB128_DSIZE]; + } name; + + struct { + lfs3_data_t data; + uint8_t buf[LFS3_BPTR_DSIZE]; + } bptr; + struct { + lfs3_data_t data; + uint8_t buf[LFS3_ECKSUM_DSIZE]; + } ecksum; + struct { + lfs3_data_t data; + uint8_t buf[LFS3_BTREE_DSIZE]; + } btree; + struct { + lfs3_data_t data; + uint8_t buf[LFS3_SHRUB_DSIZE]; + } shrub; + struct { + lfs3_data_t data; + uint8_t buf[LFS3_MPTR_DSIZE]; + } mptr; + struct { + lfs3_data_t data; + uint8_t buf[LFS3_GEOMETRY_DSIZE]; + } geometry; + } u; + } ctx; + + // direct buffer? + if (rattr.from == LFS3_FROM_BUF) { + ctx.u.data = LFS3_DATA_BUF(rattr.u.buffer, rattr.count); + datas = &ctx.u.data; + data_count = 1; + + // indirect concatenated data? + } else if (rattr.from == LFS3_FROM_DATA) { + datas = rattr.u.datas; + data_count = rattr.count; + + // le32? + } else if (rattr.from == LFS3_FROM_LE32) { + ctx.u.le32.data = lfs3_data_fromle32(rattr.u.le32, + ctx.u.le32.buf); + datas = &ctx.u.le32.data; + data_count = 1; + + // leb128? + } else if (rattr.from == LFS3_FROM_LEB128) { + // leb128s should not exceed 31-bits + LFS3_ASSERT(rattr.u.leb128 <= 0x7fffffff); + // little-leb128s should not exceed 28-bits + LFS3_ASSERT(rattr.tag != LFS3_TAG_NAMELIMIT + || rattr.u.leb128 <= 0x0fffffff); + ctx.u.leb128.data = lfs3_data_fromleb128(rattr.u.leb128, + ctx.u.leb128.buf); + datas = &ctx.u.leb128.data; + data_count = 1; + + // name? + } else if (rattr.from == LFS3_FROM_NAME) { + const lfs3_name_t *name = rattr.u.etc; + ctx.u.name.datas[0] = lfs3_data_fromleb128(name->did, ctx.u.name.buf); + ctx.u.name.datas[1] = LFS3_DATA_BUF(name->name, name->name_len); + datas = ctx.u.name.datas; + data_count = 2; + + // bptr? + } else if (rattr.from == LFS3_FROM_BPTR) { + ctx.u.bptr.data = lfs3_data_frombptr(rattr.u.etc, + ctx.u.bptr.buf); + datas = &ctx.u.bptr.data; + data_count = 1; + + // ecksum? + } else if (rattr.from == LFS3_FROM_ECKSUM) { + ctx.u.ecksum.data = lfs3_data_fromecksum(rattr.u.etc, + ctx.u.ecksum.buf); + datas = &ctx.u.ecksum.data; + data_count = 1; + + // btree? + } else if (rattr.from == LFS3_FROM_BTREE) { + ctx.u.btree.data = lfs3_data_frombtree(rattr.u.etc, + ctx.u.btree.buf); + datas = &ctx.u.btree.data; + data_count = 1; + + // shrub trunk? + } else if (rattr.from == LFS3_FROM_SHRUB) { + // note unlike the other lazy tags, we _need_ to lazily encode + // shrub trunks, since they change underneath us during mdir + // compactions, relocations, etc + ctx.u.shrub.data = lfs3_data_fromshrub(rattr.u.etc, + ctx.u.shrub.buf); + datas = &ctx.u.shrub.data; + data_count = 1; + + // mptr? + } else if (rattr.from == LFS3_FROM_MPTR) { + ctx.u.mptr.data = lfs3_data_frommptr(rattr.u.etc, + ctx.u.mptr.buf); + datas = &ctx.u.mptr.data; + data_count = 1; + + // geometry? + } else if (rattr.from == LFS3_FROM_GEOMETRY) { + ctx.u.geometry.data = lfs3_data_fromgeometry(rattr.u.etc, + ctx.u.geometry.buf); + datas = &ctx.u.geometry.data; + data_count = 1; + + } else { + LFS3_UNREACHABLE(); + } + + // now everything should be raw data, either in-ram or on-disk + + // find the concatenated size + lfs3_size_t size = 0; + for (lfs3_size_t i = 0; i < data_count; i++) { + size += lfs3_data_size(datas[i]); + } + + // do we fit? + if (lfs3_rbyd_eoff(rbyd) + LFS3_TAG_DSIZE + size + > lfs3->cfg->block_size) { + return LFS3_ERR_RANGE; + } + + // append tag + int err = lfs3_rbyd_appendtag(lfs3, rbyd, + rattr.tag, rattr.weight, size); + if (err) { + return err; + } + + // append data + for (lfs3_size_t i = 0; i < data_count; i++) { + err = lfs3_bd_progdata(lfs3, + rbyd->blocks[0], lfs3_rbyd_eoff(rbyd), datas[i], + &rbyd->cksum); + if (err) { + return err; + } + + rbyd->eoff += lfs3_data_size(datas[i]); + } + + // keep track of most recent parity + #ifdef LFS3_CKMETAPARITY + lfs3->ptail.block = rbyd->blocks[0]; + lfs3->ptail.off + = ((lfs3_size_t)( + lfs3_parity(rbyd->cksum) ^ lfs3_rbyd_isperturb(rbyd) + ) << (8*sizeof(lfs3_size_t)-1)) + | lfs3_rbyd_eoff(rbyd); + #endif + + return 0; +} +#endif + +// checks before we append +#ifndef LFS3_RDONLY +static int lfs3_rbyd_appendinit(lfs3_t *lfs3, lfs3_rbyd_t *rbyd) { + // must fetch before mutating! + LFS3_ASSERT(lfs3_rbyd_isfetched(rbyd)); + + // we can't do anything if we're not erased + if (lfs3_rbyd_eoff(rbyd) >= lfs3->cfg->block_size) { + return LFS3_ERR_RANGE; + } + + // make sure every rbyd starts with a revision count + if (rbyd->eoff == 0) { + int err = lfs3_rbyd_appendrev(lfs3, rbyd, 0); + if (err) { + return err; + } + } + + return 0; +} +#endif + +// helper functions for managing the 3-element fifo used in +// lfs3_rbyd_appendrattr +#ifndef LFS3_RDONLY +typedef struct lfs3_alt { + lfs3_tag_t alt; + lfs3_rid_t weight; + lfs3_size_t jump; +} lfs3_alt_t; +#endif + +#ifndef LFS3_RDONLY +static int lfs3_rbyd_p_flush(lfs3_t *lfs3, lfs3_rbyd_t *rbyd, + lfs3_alt_t p[static 3], + int count) { + // write out some number of alt pointers in our queue + for (int i = 0; i < count; i++) { + if (p[3-1-i].alt) { + // jump=0 represents an unreachable alt, we do write out + // unreachable alts sometimes in order to maintain the + // balance of the tree + LFS3_ASSERT(p[3-1-i].jump || lfs3_tag_isblack(p[3-1-i].alt)); + lfs3_tag_t alt = p[3-1-i].alt; + lfs3_rid_t weight = p[3-1-i].weight; + // change to a relative jump at the last minute + lfs3_size_t jump = (p[3-1-i].jump) + ? lfs3_rbyd_eoff(rbyd) - p[3-1-i].jump + : 0; + + int err = lfs3_rbyd_appendtag(lfs3, rbyd, alt, weight, jump); + if (err) { + return err; + } + } + } + + return 0; +} +#endif + +#ifndef LFS3_RDONLY +static inline int lfs3_rbyd_p_push(lfs3_t *lfs3, lfs3_rbyd_t *rbyd, + lfs3_alt_t p[static 3], + lfs3_tag_t alt, lfs3_rid_t weight, lfs3_size_t jump) { + // jump should actually be in the rbyd + LFS3_ASSERT(jump < lfs3_rbyd_eoff(rbyd)); + + int err = lfs3_rbyd_p_flush(lfs3, rbyd, p, 1); + if (err) { + return err; + } + + lfs3_memmove(p+1, p, 2*sizeof(lfs3_alt_t)); + p[0].alt = alt; + p[0].weight = weight; + p[0].jump = jump; + return 0; +} +#endif + +#ifndef LFS3_RDONLY +static inline void lfs3_rbyd_p_pop( + lfs3_alt_t p[static 3]) { + lfs3_memmove(p, p+1, 2*sizeof(lfs3_alt_t)); + p[2].alt = 0; +} +#endif + +#ifndef LFS3_RDONLY +static void lfs3_rbyd_p_recolor( + lfs3_alt_t p[static 3]) { + // propagate a red edge upwards + p[0].alt &= ~LFS3_TAG_R; + + if (p[1].alt) { + p[1].alt |= LFS3_TAG_R; + + // unreachable alt? we can prune this now + if (!p[1].jump) { + p[1] = p[2]; + p[2].alt = 0; + + // reorder so that top two edges always go in the same direction + } else if (lfs3_tag_isred(p[2].alt)) { + if (lfs3_tag_isparallel(p[1].alt, p[2].alt)) { + // no reorder needed + } else if (lfs3_tag_isparallel(p[0].alt, p[2].alt)) { + lfs3_tag_t alt_ = p[1].alt; + lfs3_rid_t weight_ = p[1].weight; + lfs3_size_t jump_ = p[1].jump; + p[1].alt = p[0].alt | LFS3_TAG_R; + p[1].weight = p[0].weight; + p[1].jump = p[0].jump; + p[0].alt = alt_ & ~LFS3_TAG_R; + p[0].weight = weight_; + p[0].jump = jump_; + } else if (lfs3_tag_isparallel(p[0].alt, p[1].alt)) { + lfs3_tag_t alt_ = p[2].alt; + lfs3_rid_t weight_ = p[2].weight; + lfs3_size_t jump_ = p[2].jump; + p[2].alt = p[1].alt | LFS3_TAG_R; + p[2].weight = p[1].weight; + p[2].jump = p[1].jump; + p[1].alt = p[0].alt | LFS3_TAG_R; + p[1].weight = p[0].weight; + p[1].jump = p[0].jump; + p[0].alt = alt_ & ~LFS3_TAG_R; + p[0].weight = weight_; + p[0].jump = jump_; + } else { + LFS3_UNREACHABLE(); + } + } + } +} +#endif + +// our core rbyd append algorithm +#ifndef LFS3_RDONLY +static int lfs3_rbyd_appendrattr(lfs3_t *lfs3, lfs3_rbyd_t *rbyd, + lfs3_srid_t rid, lfs3_rattr_t rattr) { + // must fetch before mutating! + LFS3_ASSERT(lfs3_rbyd_isfetched(rbyd)); + // tag must not be internal at this point + LFS3_ASSERT(!lfs3_tag_isinternal(rattr.tag)); + // bit 7 is reserved for future subtype extensions + LFS3_ASSERT(!(rattr.tag & 0x80)); + // you can't delete more than what's in the rbyd + LFS3_ASSERT(rattr.weight >= -(lfs3_srid_t)rbyd->weight); + + // ignore noops + if (lfs3_rattr_isnoop(rattr)) { + return 0; + } + + // begin appending + int err = lfs3_rbyd_appendinit(lfs3, rbyd); + if (err) { + return err; + } + + // figure out what range of tags we're operating on + lfs3_srid_t a_rid; + lfs3_srid_t b_rid; + lfs3_tag_t a_tag; + lfs3_tag_t b_tag; + if (!lfs3_tag_isgrow(rattr.tag) && rattr.weight != 0) { + if (rattr.weight > 0) { + LFS3_ASSERT(rid <= (lfs3_srid_t)rbyd->weight); + + // it's a bit ugly, but adjusting the rid here makes the following + // logic work out more consistently + rid -= 1; + a_rid = rid + 1; + b_rid = rid + 1; + } else { + LFS3_ASSERT(rid < (lfs3_srid_t)rbyd->weight); + + // it's a bit ugly, but adjusting the rid here makes the following + // logic work out more consistently + rid += 1; + a_rid = rid - lfs3_smax(-rattr.weight, 0); + b_rid = rid; + } + + a_tag = 0; + b_tag = 0; + + } else { + LFS3_ASSERT(rid < (lfs3_srid_t)rbyd->weight); + + a_rid = rid - lfs3_smax(-rattr.weight, 0); + b_rid = rid; + + // note both normal and rm wide-tags have the same bounds, really it's + // the normal non-wide-tags that are an outlier here + if (lfs3_tag_ismask12(rattr.tag)) { + a_tag = 0x000; + b_tag = 0xfff; + } else if (lfs3_tag_ismask8(rattr.tag)) { + a_tag = (rattr.tag & 0xf00); + b_tag = (rattr.tag & 0xf00) + 0x100; + } else if (lfs3_tag_ismask2(rattr.tag)) { + a_tag = (rattr.tag & 0xffc); + b_tag = (rattr.tag & 0xffc) + 0x004; + } else if (lfs3_tag_isrm(rattr.tag)) { + a_tag = lfs3_tag_key(rattr.tag); + b_tag = lfs3_tag_key(rattr.tag) + 1; + } else { + a_tag = lfs3_tag_key(rattr.tag); + b_tag = lfs3_tag_key(rattr.tag); + } + } + a_tag = lfs3_max(a_tag, 0x1); + b_tag = lfs3_max(b_tag, 0x1); + + // keep track of diverged state + // + // this is only used if we operate on a range of tags, in which case + // we may need to write two trunks + // + // to pull this off, we make two passes: + // 1. to write the common trunk + diverged-lower trunk + // 2. to write the common trunk + diverged-upper trunk, stitching the + // two diverged trunks together where they diverged + // + bool diverged = false; + lfs3_tag_t d_tag = 0; + lfs3_srid_t d_weight = 0; + + // follow the current trunk + lfs3_size_t branch = lfs3_rbyd_trunk(rbyd); + +trunk:; + // the new trunk starts here + lfs3_size_t trunk_ = lfs3_rbyd_eoff(rbyd); + + // keep track of bounds as we descend down the tree + // + // this gets a bit confusing as we also may need to keep + // track of both the lower and upper bounds of diverging paths + // in the case of range deletions + lfs3_srid_t lower_rid = 0; + lfs3_srid_t upper_rid = rbyd->weight; + lfs3_tag_t lower_tag = 0x000; + lfs3_tag_t upper_tag = 0xfff; + + // no trunk yet? + if (!branch) { + goto leaf; + } + + // queue of pending alts we can emulate rotations with + lfs3_alt_t p[3] = {{0}, {0}, {0}}; + // keep track of the last incoming branch for yellow splits + lfs3_size_t y_branch = 0; + // keep track of the tag we find at the end of the trunk + lfs3_tag_t tag_ = 0; + + // descend down tree, building alt pointers + while (true) { + // keep track of incoming branch + if (lfs3_tag_isblack(p[0].alt)) { + y_branch = branch; + } + + // read the alt pointer + lfs3_tag_t alt; + lfs3_rid_t weight; + lfs3_size_t jump; + lfs3_ssize_t d = lfs3_bd_readtag(lfs3, + rbyd->blocks[0], branch, 0, + &alt, &weight, &jump, + NULL); + if (d < 0) { + return d; + } + + // found an alt? + if (lfs3_tag_isalt(alt)) { + // make jump absolute + jump = branch - jump; + lfs3_size_t branch_ = branch + d; + + // yellow alts should be parallel + LFS3_ASSERT(!(lfs3_tag_isred(alt) && lfs3_tag_isred(p[0].alt)) + || lfs3_tag_isparallel(alt, p[0].alt)); + + // take black alt? needs a flip + // b + // .-'| => .-'| + // 1 2 1 2 1 + if (lfs3_tag_follow2( + alt, weight, + p[0].alt, p[0].weight, + lower_rid, upper_rid, + a_rid, a_tag)) { + lfs3_tag_flip2( + &alt, &weight, + p[0].alt, p[0].weight, + lower_rid, upper_rid); + LFS3_SWAP(lfs3_size_t, &jump, &branch_); + } + + // should've taken red alt? needs a flip + // r + // .----'| .-'| + // | | >b + // | .-'| .--|-'| + // 1 2 3 1 2 3 1 + if (lfs3_tag_isred(p[0].alt) + && lfs3_tag_follow( + p[0].alt, p[0].weight, + lower_rid, upper_rid, + a_rid, a_tag)) { + LFS3_SWAP(lfs3_tag_t, &p[0].alt, &alt); + LFS3_SWAP(lfs3_rid_t, &p[0].weight, &weight); + LFS3_SWAP(lfs3_size_t, &p[0].jump, &jump); + alt = (alt & ~LFS3_TAG_R) | (p[0].alt & LFS3_TAG_R); + p[0].alt |= LFS3_TAG_R; + + lfs3_tag_flip2( + &alt, &weight, + p[0].alt, p[0].weight, + lower_rid, upper_rid); + LFS3_SWAP(lfs3_size_t, &jump, &branch_); + } + + // do bounds want to take different paths? begin diverging + // >b | nb => nb | + // .----'| .--------|--' .-----------' | + // b + // .----'| .-'| + // | | | + // | .-'| .-----|--' + // 1 2 3 1 2 3 x + if (diverging_b && diverging_r) { + LFS3_ASSERT(a_rid < b_rid || a_tag < b_tag); + LFS3_ASSERT(lfs3_tag_isparallel(alt, p[0].alt)); + + weight += p[0].weight; + jump = p[0].jump; + lfs3_rbyd_p_pop(p); + + diverging_r = false; + } + + // diverging? start trimming inner alts + // >b + // .-'| + // | nb + // .----'| .--------|--' + // b nb | + // .--------|--' .-----------' | + // | b_rid || a_tag > b_tag) { + LFS3_ASSERT(!diverging_r); + + alt = LFS3_TAG_ALT( + alt & LFS3_TAG_R, + LFS3_TAG_LE, + d_tag); + weight -= d_weight; + lower_rid += d_weight; + } + } + + } else { + // diverged? trim so alt will be pruned + // nb + // .-'| .--' + // 3 4 3 4 x + if (diverging_b) { + lfs3_tag_trim( + alt, weight, + &lower_rid, &upper_rid, + &lower_tag, &upper_tag); + weight = 0; + } + } + + // note we need to prioritize yellow-split pruning here, + // which unfortunately makes this logic a bit of a mess + + // prune unreachable yellow-split yellow alts + // b + // .-'| .-'| + // | >b + // | .----' | .--------|-'| + // | | branch) { + alt &= ~LFS3_TAG_R; + lfs3_rbyd_p_pop(p); + + // prune unreachable yellow-split red alts + // b + // .-'| .-'| + // | | | + // | .----' | | | | + // | | branch) { + alt = p[0].alt & ~LFS3_TAG_R; + weight = p[0].weight; + jump = p[0].jump; + lfs3_rbyd_p_pop(p); + } + + // prune red alts + if (lfs3_tag_isred(p[0].alt) + && lfs3_tag_unreachable( + p[0].alt, p[0].weight, + lower_rid, upper_rid, + lower_tag, upper_tag)) { + // prune unreachable recolorable alts + // nb + // .-'| .--' + // 3 4 3 4 x + } else if (lfs3_tag_isblack(alt)) { + alt = LFS3_TAG_ALT( + LFS3_TAG_B, + LFS3_TAG_LE, + (diverged && (a_rid > b_rid || a_tag > b_tag)) + ? d_tag + : lower_tag); + LFS3_ASSERT(weight == 0); + // jump=0 also asserts the alt is unreachable (or + // else we loop indefinitely), and uses the minimum + // alt encoding + jump = 0; + } + } + + // two reds makes a yellow, split? + // + // note we've lost the original yellow edge because of flips, but + // we know the red edge is the only branch_ > branch + if (lfs3_tag_isred(alt) && lfs3_tag_isred(p[0].alt)) { + // if we take the red or yellow alt we can just point + // to the black alt + // b + // .-------'| .-'| + // | b + // | .----'| => .-----|-'| + // | | branch) { + LFS3_SWAP(lfs3_tag_t, &p[0].alt, &alt); + LFS3_SWAP(lfs3_rid_t, &p[0].weight, &weight); + LFS3_SWAP(lfs3_size_t, &p[0].jump, &jump); + } + alt &= ~LFS3_TAG_R; + + lfs3_tag_trim( + p[0].alt, p[0].weight, + &lower_rid, &upper_rid, + &lower_tag, &upper_tag); + lfs3_rbyd_p_recolor(p); + + // otherwise we need to point to the yellow alt and + // prune later + // | split if tags mismatch + // - weight>0, !grow => split if tags mismatch or we're inserting a new tag + // - rm-bit set => never split, but emit alt-always tags, making our + // tag effectively unreachable + // + lfs3_tag_t alt = 0; + lfs3_rid_t weight = 0; + if (tag_ + && (upper_rid-1 < rid-lfs3_smax(-rattr.weight, 0) + || (upper_rid-1 == rid-lfs3_smax(-rattr.weight, 0) + && ((!lfs3_tag_isgrow(rattr.tag) && rattr.weight > 0) + || ((tag_ & lfs3_tag_mask(rattr.tag)) + < (rattr.tag & lfs3_tag_mask(rattr.tag))))))) { + if (lfs3_tag_isrm(rattr.tag) || !lfs3_tag_key(rattr.tag)) { + // if removed, make our tag unreachable + alt = LFS3_TAG_ALT(LFS3_TAG_B, LFS3_TAG_GT, lower_tag); + weight = upper_rid - lower_rid + rattr.weight; + upper_rid -= weight; + } else { + // split less than + alt = LFS3_TAG_ALT(LFS3_TAG_B, LFS3_TAG_LE, tag_); + weight = upper_rid - lower_rid; + lower_rid += weight; + } + + } else if (tag_ + && (upper_rid-1 > rid + || (upper_rid-1 == rid + && ((!lfs3_tag_isgrow(rattr.tag) && rattr.weight > 0) + || ((tag_ & lfs3_tag_mask(rattr.tag)) + > (rattr.tag & lfs3_tag_mask(rattr.tag))))))) { + if (lfs3_tag_isrm(rattr.tag) || !lfs3_tag_key(rattr.tag)) { + // if removed, make our tag unreachable + alt = LFS3_TAG_ALT(LFS3_TAG_B, LFS3_TAG_GT, lower_tag); + weight = upper_rid - lower_rid + rattr.weight; + upper_rid -= weight; + } else { + // split greater than + alt = LFS3_TAG_ALT(LFS3_TAG_B, LFS3_TAG_GT, rattr.tag); + weight = upper_rid - (rid+1); + upper_rid -= weight; + } + } + + if (alt) { + err = lfs3_rbyd_p_push(lfs3, rbyd, p, + alt, weight, branch); + if (err) { + return err; + } + + // introduce a red edge + lfs3_rbyd_p_recolor(p); + } + + // flush any pending alts + err = lfs3_rbyd_p_flush(lfs3, rbyd, p, 3); + if (err) { + return err; + } + +leaf:; + // write the actual tag + // + // note we always need a non-alt to terminate the trunk, otherwise we + // can't find trunks during fetch + err = lfs3_rbyd_appendrattr_(lfs3, rbyd, LFS3_RATTR_( + // mark as shrub if we are a shrub + (lfs3_rbyd_isshrub(rbyd) ? LFS3_TAG_SHRUB : 0) + // rm => null, otherwise strip off control bits + | ((lfs3_tag_isrm(rattr.tag)) + ? LFS3_TAG_NULL + : lfs3_tag_key(rattr.tag)), + upper_rid - lower_rid + rattr.weight, + rattr)); + if (err) { + return err; + } + + // update the trunk and weight + rbyd->trunk = (rbyd->trunk & LFS3_RBYD_ISSHRUB) | trunk_; + rbyd->weight += rattr.weight; + return 0; +} +#endif + +#ifndef LFS3_RDONLY +static int lfs3_rbyd_appendcksum_(lfs3_t *lfs3, lfs3_rbyd_t *rbyd, + uint32_t cksum) { + // align to the next prog unit + // + // this gets a bit complicated as we have two types of cksums: + // + // - 9-word cksum with ecksum to check following prog (middle of block): + // .---+---+---+---. ecksum tag: 1 be16 2 bytes + // | tag | 0 |siz| ecksum weight (0): 1 leb128 1 byte + // +---+---+---+---+ ecksum size: 1 leb128 1 byte + // | ecksize | ecksum cksize: 1 leb128 <=4 bytes + // +---+- -+- -+- -+ ecksum cksum: 1 le32 4 bytes + // | ecksum | + // +---+---+---+---+- -+- -+- -. cksum tag: 1 be16 2 bytes + // | tag | 0 | size | cksum weight (0): 1 leb128 1 byte + // +---+---+---+---+- -+- -+- -' cksum size: 1 leb128 <=4 bytes + // | cksum | cksum cksum: 1 le32 4 bytes + // '---+---+---+---' total: <=23 bytes + // + // - 4-word cksum with no following prog (end of block): + // .---+---+---+---+- -+- -+- -. cksum tag: 1 be16 2 bytes + // | tag | 0 | size | cksum weight (0): 1 leb128 1 byte + // +---+---+---+---+- -+- -+- -' cksum size: 1 leb128 <=4 bytes + // | cksum | cksum cksum: 1 le32 4 bytes + // '---+---+---+---' total: <=11 bytes + // + lfs3_size_t off_ = lfs3_alignup( + lfs3_rbyd_eoff(rbyd) + 2+1+1+4+4 + 2+1+4+4, + lfs3->cfg->prog_size); + + // space for ecksum? + bool perturb = false; + if (off_ < lfs3->cfg->block_size) { + // read the leading byte in case we need to perturb the next commit, + // this should hopefully stay in our cache + uint8_t e = 0; + int err = lfs3_bd_read(lfs3, + rbyd->blocks[0], off_, lfs3->cfg->prog_size, + &e, 1); + if (err && err != LFS3_ERR_CORRUPT) { + return err; + } + + // we don't want the next commit to appear as valid, so we + // intentionally perturb the commit if this happens, this is + // roughly equivalent to inverting all tags' valid bits + perturb = ((e >> 7) == lfs3_parity(cksum)); + + // calculate the erased-state checksum + uint32_t ecksum = 0; + err = lfs3_bd_cksum(lfs3, + rbyd->blocks[0], off_, lfs3->cfg->prog_size, + lfs3->cfg->prog_size, + &ecksum); + if (err && err != LFS3_ERR_CORRUPT) { + return err; + } + + err = lfs3_rbyd_appendrattr_(lfs3, rbyd, LFS3_RATTR_ECKSUM( + LFS3_TAG_ECKSUM, 0, + (&(lfs3_ecksum_t){ + .cksize=lfs3->cfg->prog_size, + .cksum=ecksum}))); + if (err) { + return err; + } + + // at least space for a cksum? + } else if (lfs3_rbyd_eoff(rbyd) + 2+1+4+4 <= lfs3->cfg->block_size) { + // note this implicitly marks the rbyd as unerased + off_ = lfs3->cfg->block_size; + + // not even space for a cksum? we can't finish the commit + } else { + return LFS3_ERR_RANGE; + } + + // build the end-of-commit checksum tag + // + // note padding-size depends on leb-encoding depends on padding-size + // depends leb-encoding depends on... to get around this catch-22 we + // just always write a fully-expanded leb128 encoding + // + bool v = lfs3_parity(rbyd->cksum) ^ lfs3_rbyd_isperturb(rbyd); + uint8_t cksum_buf[2+1+4+4]; + cksum_buf[0] = (uint8_t)(LFS3_TAG_CKSUM >> 8) + // set the valid bit to the cksum parity + | ((uint8_t)v << 7); + cksum_buf[1] = (uint8_t)(LFS3_TAG_CKSUM >> 0) + // set the perturb bit so next commit is invalid + | ((uint8_t)perturb << 2) + // include the lower 2 bits of the block address to help + // with resynchronization + | (rbyd->blocks[0] & 0x3); + cksum_buf[2] = 0; + + lfs3_size_t padding = off_ - (lfs3_rbyd_eoff(rbyd) + 2+1+4); + cksum_buf[3] = 0x80 | (0x7f & (padding >> 0)); + cksum_buf[4] = 0x80 | (0x7f & (padding >> 7)); + cksum_buf[5] = 0x80 | (0x7f & (padding >> 14)); + cksum_buf[6] = 0x00 | (0x7f & (padding >> 21)); + + // exclude the valid bit + uint32_t cksum_ = rbyd->cksum ^ ((uint32_t)v << 7); + // calculate the commit checksum + cksum_ = lfs3_crc32c(cksum_, cksum_buf, 2+1+4); + // and perturb, perturbing the commit checksum avoids a perturb hole + // after the last valid bit + // + // note the odd-parity zero preserves our position in the crc32c + // ring while only changing the parity + cksum_ ^= (lfs3_rbyd_isperturb(rbyd)) ? LFS3_CRC32C_ODDZERO : 0; + lfs3_tole32(cksum_, &cksum_buf[2+1+4]); + + // prog, when this lands on disk commit is committed + int err = lfs3_bd_prog(lfs3, rbyd->blocks[0], lfs3_rbyd_eoff(rbyd), + cksum_buf, 2+1+4+4, + NULL); + if (err) { + return err; + } + + // flush any pending progs + err = lfs3_bd_flush(lfs3, NULL); + if (err) { + return err; + } + + // update the eoff and perturb + rbyd->eoff + = ((lfs3_size_t)perturb << (8*sizeof(lfs3_size_t)-1)) + | off_; + // revert to canonical checksum + rbyd->cksum = cksum; + + #ifdef LFS3_DBGRBYDCOMMITS + LFS3_DEBUG("Committed rbyd 0x%"PRIx32".%"PRIx32" w%"PRId32", " + "eoff %"PRId32", cksum %"PRIx32, + rbyd->blocks[0], lfs3_rbyd_trunk(rbyd), + rbyd->weight, + (lfs3_rbyd_eoff(rbyd) >= lfs3->cfg->block_size) + ? -1 + : (lfs3_ssize_t)lfs3_rbyd_eoff(rbyd), + rbyd->cksum); + #endif + return 0; +} +#endif + +#ifndef LFS3_RDONLY +static int lfs3_rbyd_appendcksum(lfs3_t *lfs3, lfs3_rbyd_t *rbyd) { + // begin appending + int err = lfs3_rbyd_appendinit(lfs3, rbyd); + if (err) { + return err; + } + + // append checksum stuff + return lfs3_rbyd_appendcksum_(lfs3, rbyd, rbyd->cksum); +} +#endif + +#ifndef LFS3_RDONLY +static int lfs3_rbyd_appendrattrs(lfs3_t *lfs3, lfs3_rbyd_t *rbyd, + lfs3_srid_t rid, lfs3_srid_t start_rid, lfs3_srid_t end_rid, + const lfs3_rattr_t *rattrs, lfs3_size_t rattr_count) { + // append each tag to the tree + for (lfs3_size_t i = 0; i < rattr_count; i++) { + // treat inserts after the first tag as though they are splits, + // sequential inserts don't really make sense otherwise + if (i > 0 && lfs3_rattr_isinsert(rattrs[i])) { + rid += 1; + } + + // don't write tags outside of the requested range + if (rid >= start_rid + // note the use of rid+1 and unsigned comparison here to + // treat end_rid=-1 as "unbounded" in such a way that rid=-1 + // is still included + && (lfs3_size_t)(rid + 1) <= (lfs3_size_t)end_rid) { + int err = lfs3_rbyd_appendrattr(lfs3, rbyd, + rid - lfs3_smax(start_rid, 0), + rattrs[i]); + if (err) { + return err; + } + } + + // we need to make sure we keep start_rid/end_rid updated with + // weight changes + if (rid < start_rid) { + start_rid += rattrs[i].weight; + } + if (rid < end_rid) { + end_rid += rattrs[i].weight; + } + + // adjust rid + rid = lfs3_rattr_nextrid(rattrs[i], rid); + } + + return 0; +} + +static int lfs3_rbyd_commit(lfs3_t *lfs3, lfs3_rbyd_t *rbyd, + lfs3_srid_t rid, const lfs3_rattr_t *rattrs, lfs3_size_t rattr_count) { + // append each tag to the tree + int err = lfs3_rbyd_appendrattrs(lfs3, rbyd, rid, -1, -1, + rattrs, rattr_count); + if (err) { + return err; + } + + // append a cksum, finalizing the commit + err = lfs3_rbyd_appendcksum(lfs3, rbyd); + if (err) { + return err; + } + + return 0; +} +#endif + + +// Calculate the maximum possible disk usage required by this rbyd after +// compaction. This uses a conservative estimate so the actual on-disk cost +// should be smaller. +// +// This also returns a good split_rid in case the rbyd needs to be split. +// +// TODO do we need to include commit overhead here? +#ifndef LFS3_RDONLY +static lfs3_ssize_t lfs3_rbyd_estimate(lfs3_t *lfs3, const lfs3_rbyd_t *rbyd, + lfs3_srid_t start_rid, lfs3_srid_t end_rid, + lfs3_srid_t *split_rid_) { + // calculate dsize by starting from the outside ids and working inwards, + // this naturally gives us a split rid + // + // TODO adopt this a/b naming scheme in lfs3_rbyd_appendrattr? + lfs3_srid_t a_rid = start_rid; + lfs3_srid_t b_rid = lfs3_min(rbyd->weight, end_rid); + lfs3_size_t a_dsize = 0; + lfs3_size_t b_dsize = 0; + lfs3_size_t rbyd_dsize = 0; + + while (a_rid != b_rid) { + if (a_dsize > b_dsize + // bias so lower dsize >= upper dsize + || (a_dsize == b_dsize && a_rid > b_rid)) { + LFS3_SWAP(lfs3_srid_t, &a_rid, &b_rid); + LFS3_SWAP(lfs3_size_t, &a_dsize, &b_dsize); + } + + if (a_rid > b_rid) { + a_rid -= 1; + } + + lfs3_stag_t tag = 0; + lfs3_rid_t weight = 0; + lfs3_size_t dsize_ = 0; + while (true) { + lfs3_srid_t rid_; + lfs3_rid_t weight_; + lfs3_data_t data; + tag = lfs3_rbyd_lookupnext(lfs3, rbyd, + a_rid, tag+1, + &rid_, &weight_, &data); + if (tag < 0) { + if (tag == LFS3_ERR_NOENT) { + break; + } + return tag; + } + if (rid_ > a_rid+lfs3_smax(weight_-1, 0)) { + break; + } + + // keep track of rid and weight + a_rid = rid_; + weight += weight_; + + // include the cost of this tag + dsize_ += lfs3->rattr_estimate + lfs3_data_size(data); + } + + if (a_rid == -1) { + rbyd_dsize += dsize_; + } else { + a_dsize += dsize_; + } + + if (a_rid < b_rid) { + a_rid += 1; + } else { + a_rid -= lfs3_smax(weight-1, 0); + } + } + + if (split_rid_) { + *split_rid_ = a_rid; + } + + return rbyd_dsize + a_dsize + b_dsize; +} +#endif + +// appends a raw tag as a part of compaction, note these must +// be appended in order! +// +// also note rattr.weight here is total weight not delta weight +#ifndef LFS3_RDONLY +static int lfs3_rbyd_appendcompactrattr(lfs3_t *lfs3, lfs3_rbyd_t *rbyd, + lfs3_rattr_t rattr) { + // begin appending + int err = lfs3_rbyd_appendinit(lfs3, rbyd); + if (err) { + return err; + } + + // write the tag + err = lfs3_rbyd_appendrattr_(lfs3, rbyd, LFS3_RATTR_( + (lfs3_rbyd_isshrub(rbyd) ? LFS3_TAG_SHRUB : 0) | rattr.tag, + rattr.weight, + rattr)); + if (err) { + return err; + } + + return 0; +} +#endif + +#ifndef LFS3_RDONLY +static int lfs3_rbyd_appendcompactrbyd(lfs3_t *lfs3, lfs3_rbyd_t *rbyd_, + const lfs3_rbyd_t *rbyd, lfs3_srid_t start_rid, lfs3_srid_t end_rid) { + // copy over tags in the rbyd in order + lfs3_srid_t rid = start_rid; + lfs3_stag_t tag = 0; + while (true) { + lfs3_rid_t weight; + lfs3_data_t data; + tag = lfs3_rbyd_lookupnext(lfs3, rbyd, + rid, tag+1, + &rid, &weight, &data); + if (tag < 0) { + if (tag == LFS3_ERR_NOENT) { + break; + } + return tag; + } + // end of range? note the use of rid+1 and unsigned comparison here to + // treat end_rid=-1 as "unbounded" in such a way that rid=-1 is still + // included + if ((lfs3_size_t)(rid + 1) > (lfs3_size_t)end_rid) { + break; + } + + // write the tag + int err = lfs3_rbyd_appendcompactrattr(lfs3, rbyd_, LFS3_RATTR_DATA( + tag, weight, &data)); + if (err) { + return err; + } + } + + return 0; +} +#endif + +#ifndef LFS3_RDONLY +static int lfs3_rbyd_appendcompaction(lfs3_t *lfs3, lfs3_rbyd_t *rbyd, + lfs3_size_t off) { + // begin appending + int err = lfs3_rbyd_appendinit(lfs3, rbyd); + if (err) { + return err; + } + + // clamp offset to be after the revision count + off = lfs3_max(off, sizeof(uint32_t)); + + // empty rbyd? write a null tag so our trunk can still point to something + if (lfs3_rbyd_eoff(rbyd) == off) { + err = lfs3_rbyd_appendtag(lfs3, rbyd, + // mark as shrub if we are a shrub + (lfs3_rbyd_isshrub(rbyd) ? LFS3_TAG_SHRUB : 0) + | LFS3_TAG_NULL, + 0, + 0); + if (err) { + return err; + } + + rbyd->trunk = (rbyd->trunk & LFS3_RBYD_ISSHRUB) | off; + rbyd->weight = 0; + return 0; + } + + // connect every other trunk together, building layers of a perfectly + // balanced binary tree upwards until we have a single trunk + lfs3_size_t layer = off; + lfs3_rid_t weight = 0; + lfs3_tag_t tag_ = 0; + while (true) { + lfs3_size_t layer_ = lfs3_rbyd_eoff(rbyd); + off = layer; + while (off < layer_) { + // connect two trunks together with a new binary trunk + for (int i = 0; i < 2 && off < layer_; i++) { + lfs3_size_t trunk = off; + lfs3_tag_t tag = 0; + weight = 0; + while (true) { + lfs3_tag_t tag__; + lfs3_rid_t weight__; + lfs3_size_t size__; + lfs3_ssize_t d = lfs3_bd_readtag(lfs3, + rbyd->blocks[0], off, layer_ - off, + &tag__, &weight__, &size__, + NULL); + if (d < 0) { + return d; + } + off += d; + + // skip any data + if (!lfs3_tag_isalt(tag__)) { + off += size__; + } + + // ignore shrub trunks, unless we are actually compacting + // a shrub tree + if (!lfs3_tag_isalt(tag__) + && lfs3_tag_isshrub(tag__) + && !lfs3_rbyd_isshrub(rbyd)) { + trunk = off; + weight = 0; + continue; + } + + // keep track of trunk's trunk and weight + weight += weight__; + + // keep track of the last non-null tag in our trunk. + // Because of how we construct each layer, the last + // non-null tag is the largest tag in that part of + // the tree + if (tag__ & ~LFS3_TAG_SHRUB) { + tag = tag__; + } + + // did we hit a tag that terminates our trunk? + if (!lfs3_tag_isalt(tag__)) { + break; + } + } + + // do we only have one trunk? we must be done + if (trunk == layer && off >= layer_) { + goto done; + } + + // connect with an altle/altgt + // + // note we need to use altles for all but the last tag + // so we know the largest tag when building the next + // layer, but for that last tag we need an altgt so + // future appends maintain the balance of the tree + err = lfs3_rbyd_appendtag(lfs3, rbyd, + (off < layer_) + ? LFS3_TAG_ALT( + (i == 0) ? LFS3_TAG_R : LFS3_TAG_B, + LFS3_TAG_LE, + tag) + : LFS3_TAG_ALT( + LFS3_TAG_B, + LFS3_TAG_GT, + tag_), + weight, + lfs3_rbyd_eoff(rbyd) - trunk); + if (err) { + return err; + } + + // keep track of the previous tag for altgts + tag_ = tag; + } + + // terminate with a null tag + err = lfs3_rbyd_appendtag(lfs3, rbyd, + // mark as shrub if we are a shrub + (lfs3_rbyd_isshrub(rbyd) ? LFS3_TAG_SHRUB : 0) + | LFS3_TAG_NULL, + 0, + 0); + if (err) { + return err; + } + } + + layer = layer_; + } + +done:; + // done! just need to update our trunk. Note we could have no trunks + // after compaction. Leave this to upper layers to take care of this. + rbyd->trunk = (rbyd->trunk & LFS3_RBYD_ISSHRUB) | layer; + rbyd->weight = weight; + + return 0; +} +#endif + +#ifndef LFS3_RDONLY +static int lfs3_rbyd_compact(lfs3_t *lfs3, lfs3_rbyd_t *rbyd_, + const lfs3_rbyd_t *rbyd, lfs3_srid_t start_rid, lfs3_srid_t end_rid) { + // append rbyd + int err = lfs3_rbyd_appendcompactrbyd(lfs3, rbyd_, + rbyd, start_rid, end_rid); + if (err) { + return err; + } + + // compact + err = lfs3_rbyd_appendcompaction(lfs3, rbyd_, 0); + if (err) { + return err; + } + + return 0; +} +#endif + +// append a secondary "shrub" tree +#ifndef LFS3_RDONLY +static int lfs3_rbyd_appendshrub(lfs3_t *lfs3, lfs3_rbyd_t *rbyd, + const lfs3_shrub_t *shrub) { + // keep track of the start of the new tree + lfs3_size_t off = lfs3_rbyd_eoff(rbyd); + // mark as shrub + rbyd->trunk |= LFS3_RBYD_ISSHRUB; + + // compact our shrub + int err = lfs3_rbyd_appendcompactrbyd(lfs3, rbyd, + shrub, -1, -1); + if (err) { + return err; + } + + err = lfs3_rbyd_appendcompaction(lfs3, rbyd, off); + if (err) { + return err; + } + + return 0; +} +#endif + + +// some low-level name things +// +// names in littlefs are tuples of directory-ids + ascii/utf8 strings + +// binary search an rbyd for a name, leaving the rid_/tag_/weight_/data_ +// with the best matching name if not found +static lfs3_scmp_t lfs3_rbyd_namelookup(lfs3_t *lfs3, const lfs3_rbyd_t *rbyd, + lfs3_did_t did, const char *name, lfs3_size_t name_len, + lfs3_srid_t *rid_, lfs3_tag_t *tag_, lfs3_rid_t *weight_, + lfs3_data_t *data_) { + // empty rbyd? leave it up to upper layers to handle this + if (rbyd->weight == 0) { + return LFS3_ERR_NOENT; + } + + // compiler needs this to be happy about initialization in callers + if (rid_) { + *rid_ = 0; + } + if (tag_) { + *tag_ = 0; + } + if (weight_) { + *weight_ = 0; + } + + // binary search for our name + lfs3_srid_t lower_rid = 0; + lfs3_srid_t upper_rid = rbyd->weight; + lfs3_scmp_t cmp; + while (lower_rid < upper_rid) { + lfs3_srid_t rid__; + lfs3_rid_t weight__; + lfs3_data_t data__; + lfs3_stag_t tag__ = lfs3_rbyd_lookupnext(lfs3, rbyd, + // lookup ~middle rid, note we may end up in the middle + // of a weighted rid with this + lower_rid + (upper_rid-1-lower_rid)/2, 0, + &rid__, &weight__, &data__); + if (tag__ < 0) { + LFS3_ASSERT(tag__ != LFS3_ERR_NOENT); + return tag__; + } + + // if we have no name, treat this rid as always lt + if (lfs3_tag_suptype(tag__) != LFS3_TAG_NAME) { + cmp = LFS3_CMP_LT; + + // compare names + } else { + cmp = lfs3_data_namecmp(lfs3, data__, did, name, name_len); + if (cmp < 0) { + return cmp; + } + } + + // bisect search space + if (cmp > LFS3_CMP_EQ) { + upper_rid = rid__ - (weight__-1); + + // only keep track of best-match rids > our target if we haven't + // seen an rid < our target + if (lower_rid == 0) { + if (rid_) { + *rid_ = rid__; + } + if (tag_) { + *tag_ = tag__; + } + if (weight_) { + *weight_ = weight__; + } + if (data_) { + *data_ = data__; + } + } + + } else if (cmp < LFS3_CMP_EQ) { + lower_rid = rid__ + 1; + + // keep track of best-matching rid < our target + if (rid_) { + *rid_ = rid__; + } + if (tag_) { + *tag_ = tag__; + } + if (weight_) { + *weight_ = weight__; + } + if (data_) { + *data_ = data__; + } + + } else { + // found a match? + if (rid_) { + *rid_ = rid__; + } + if (tag_) { + *tag_ = tag__; + } + if (weight_) { + *weight_ = weight__; + } + if (data_) { + *data_ = data__; + } + return LFS3_CMP_EQ; + } + } + + // no match, return if found name was lt/gt expect + // + // this will always be lt unless all rids are gt + return (lower_rid == 0) ? LFS3_CMP_GT : LFS3_CMP_LT; +} + + + + +/// B-tree operations /// + +// create an empty btree +static void lfs3_btree_init(lfs3_btree_t *btree) { + btree->r.weight = 0; + btree->r.blocks[0] = -1; + btree->r.trunk = 0; + #ifdef LFS3_BLEAFCACHE + // weight=0 indicates no leaf + btree->leaf.r.weight = 0; + #endif +} + +// convenience operations +#ifndef LFS3_RDONLY +static inline void lfs3_btree_claim(lfs3_btree_t *btree) { + // note we don't claim shrubs, as this would clobber shrub estimates + if (!lfs3_rbyd_isshrub(&btree->r)) { + lfs3_rbyd_claim(&btree->r); + } + #ifdef LFS3_BLEAFCACHE + if (!lfs3_rbyd_isshrub(&btree->leaf.r)) { + lfs3_rbyd_claim(&btree->leaf.r); + } + #endif +} +#endif + +#ifdef LFS3_BLEAFCACHE +static inline void lfs3_btree_discardleaf(lfs3_btree_t *btree) { + btree->leaf.r.weight = 0; +} +#endif + +static inline int lfs3_btree_cmp( + const lfs3_btree_t *a, + const lfs3_btree_t *b) { + return lfs3_rbyd_cmp(&a->r, &b->r); +} + +// needed in lfs3_fs_claimbtree +static inline uint8_t lfs3_o_type(uint32_t flags); + +// claim all btrees known to the system +// +// note this doesn't, and can't, include any stack allocated btrees +#ifndef LFS3_RDONLY +static void lfs3_fs_claimbtree(lfs3_t *lfs3, lfs3_btree_t *btree) { + // claim the mtree + if (&lfs3->mtree != btree + && lfs3->mtree.r.blocks[0] == btree->r.blocks[0]) { + lfs3_btree_claim(&lfs3->mtree); + } + + // claim gbmap snapshots + #ifdef LFS3_GBMAP + if (&lfs3->gbmap.b != btree + && lfs3->gbmap.b.r.blocks[0] == btree->r.blocks[0]) { + lfs3_btree_claim(&lfs3->gbmap.b); + } + if (&lfs3->gbmap.b_p != btree + && lfs3->gbmap.b_p.r.blocks[0] == btree->r.blocks[0]) { + lfs3_btree_claim(&lfs3->gbmap.b_p); + } + #endif + + // claim file btrees/bshrubs + for (lfs3_handle_t *h = lfs3->handles; h; h = h->next) { + if (lfs3_o_type(h->flags) == LFS3_TYPE_REG + && &((lfs3_bshrub_t*)h)->b != btree + && ((lfs3_bshrub_t*)h)->b.r.blocks[0] + == btree->r.blocks[0]) { + lfs3_btree_claim(&((lfs3_bshrub_t*)h)->b); + } + } +} +#endif + + +// branch on-disk encoding +#ifndef LFS3_RDONLY +static lfs3_data_t lfs3_data_frombranch(const lfs3_rbyd_t *branch, + uint8_t buffer[static LFS3_BRANCH_DSIZE]) { + // block should not exceed 31-bits + LFS3_ASSERT(branch->blocks[0] <= 0x7fffffff); + // trunk should not exceed 28-bits + LFS3_ASSERT(lfs3_rbyd_trunk(branch) <= 0x0fffffff); + lfs3_ssize_t d = 0; + + lfs3_ssize_t d_ = lfs3_toleb128(branch->blocks[0], &buffer[d], 5); + if (d_ < 0) { + LFS3_UNREACHABLE(); + } + d += d_; + + d_ = lfs3_toleb128(lfs3_rbyd_trunk(branch), &buffer[d], 4); + if (d_ < 0) { + LFS3_UNREACHABLE(); + } + d += d_; + + lfs3_tole32(branch->cksum, &buffer[d]); + d += 4; + + return LFS3_DATA_BUF(buffer, d); +} +#endif + +static int lfs3_data_readbranch(lfs3_t *lfs3, lfs3_data_t *data, + lfs3_bid_t weight, + lfs3_rbyd_t *branch) { + // setting eoff to 0 here will trigger asserts if we try to append + // without fetching first + #ifndef LFS3_RDONLY + branch->eoff = 0; + #endif + + branch->weight = weight; + + int err = lfs3_data_readleb128(lfs3, data, &branch->blocks[0]); + if (err) { + return err; + } + + err = lfs3_data_readlleb128(lfs3, data, &branch->trunk); + if (err) { + return err; + } + + err = lfs3_data_readle32(lfs3, data, &branch->cksum); + if (err) { + return err; + } + + return 0; +} + +static int lfs3_branch_fetch(lfs3_t *lfs3, lfs3_rbyd_t *branch, + lfs3_block_t block, lfs3_size_t trunk, lfs3_bid_t weight, + uint32_t cksum) { + (void)lfs3; + branch->blocks[0] = block; + branch->trunk = trunk; + branch->weight = weight; + #ifndef LFS3_RDONLY + branch->eoff = 0; + #endif + branch->cksum = cksum; + + // checking fetches? + #ifdef LFS3_CKFETCHES + if (lfs3_m_isckfetches(lfs3->flags)) { + int err = lfs3_rbyd_fetchck(lfs3, branch, + branch->blocks[0], lfs3_rbyd_trunk(branch), + branch->cksum); + if (err) { + return err; + } + LFS3_ASSERT(branch->weight == weight); + } + #endif + + return 0; +} + +static int lfs3_data_fetchbranch(lfs3_t *lfs3, + lfs3_data_t *data, lfs3_bid_t weight, + lfs3_rbyd_t *branch) { + // decode branch and fetch + int err = lfs3_data_readbranch(lfs3, data, weight, + branch); + if (err) { + return err; + } + + return lfs3_branch_fetch(lfs3, branch, + branch->blocks[0], branch->trunk, branch->weight, + branch->cksum); +} + + +// btree on-disk encoding +// +// this is the same as the branch on-disk econding, but prefixed with the +// btree's weight +#ifndef LFS3_RDONLY +static lfs3_data_t lfs3_data_frombtree(const lfs3_btree_t *btree, + uint8_t buffer[static LFS3_BTREE_DSIZE]) { + // weight should not exceed 31-bits + LFS3_ASSERT(btree->r.weight <= 0x7fffffff); + lfs3_ssize_t d = 0; + + lfs3_ssize_t d_ = lfs3_toleb128(btree->r.weight, &buffer[d], 5); + if (d_ < 0) { + LFS3_UNREACHABLE(); + } + d += d_; + + lfs3_data_t data = lfs3_data_frombranch(&btree->r, &buffer[d]); + d += lfs3_data_size(data); + + return LFS3_DATA_BUF(buffer, d); +} +#endif + +static int lfs3_data_readbtree(lfs3_t *lfs3, lfs3_data_t *data, + lfs3_btree_t *btree) { + lfs3_bid_t weight; + int err = lfs3_data_readleb128(lfs3, data, &weight); + if (err) { + return err; + } + + err = lfs3_data_readbranch(lfs3, data, weight, &btree->r); + if (err) { + return err; + } + + #ifdef LFS3_BLEAFCACHE + // make sure to zero btree leaf + lfs3_btree_discardleaf(btree); + #endif + return 0; +} + + +// core btree operations + +static int lfs3_btree_fetch(lfs3_t *lfs3, lfs3_btree_t *btree, + lfs3_block_t block, lfs3_size_t trunk, lfs3_bid_t weight, + uint32_t cksum) { + // btree/branch fetch really are the same once we know the weight + int err = lfs3_branch_fetch(lfs3, &btree->r, + block, trunk, weight, + cksum); + if (err) { + return err; + } + + #ifdef LFS3_DBGBTREEFETCHES + LFS3_DEBUG("Fetched btree 0x%"PRIx32".%"PRIx32" w%"PRId32", " + "cksum %"PRIx32, + btree->r.blocks[0], lfs3_rbyd_trunk(&btree->r), + btree->r.weight, + btree->r.cksum); + #endif + return 0; +} + +static int lfs3_data_fetchbtree(lfs3_t *lfs3, lfs3_data_t *data, + lfs3_btree_t *btree) { + // decode btree and fetch + int err = lfs3_data_readbtree(lfs3, data, + btree); + if (err) { + return err; + } + + return lfs3_btree_fetch(lfs3, btree, + btree->r.blocks[0], btree->r.trunk, btree->r.weight, + btree->r.cksum); +} + +// unfortunately C's const becomes a bit useless when we add btree leaf +// caches, but we can still assert constness at compile time otherwise +#ifdef LFS3_BLEAFCACHE +#define LFS3_BCONST +#else +#define LFS3_BCONST const +#endif + +// lookup rbyd/rid containing a given bid +static lfs3_stag_t lfs3_btree_lookupnext_(lfs3_t *lfs3, + LFS3_BCONST lfs3_btree_t *btree, + lfs3_bid_t bid, + lfs3_bid_t *bid_, lfs3_rbyd_t *rbyd_, lfs3_srid_t *rid_, + lfs3_bid_t *weight_, lfs3_data_t *data_) { + // is our bid in the leaf? can we skip the btree walk? + // + // if not we need to restart from the root + lfs3_bid_t bid__; + if (LFS3_IFDEF_BLEAFCACHE( + bid >= btree->leaf.bid-(btree->leaf.r.weight-1) + && bid < btree->leaf.bid+1, + false)) { + #ifdef LFS3_BLEAFCACHE + bid__ = btree->leaf.bid; + *rbyd_ = btree->leaf.r; + #endif + } else { + bid__ = btree->r.weight-1; + *rbyd_ = btree->r; + } + + // descend down the btree looking for our bid + while (true) { + // lookup our bid in the rbyd + lfs3_srid_t rid__; + lfs3_rid_t weight__; + lfs3_data_t data__; + lfs3_stag_t tag__ = lfs3_rbyd_lookupnext(lfs3, rbyd_, + bid - (bid__-(rbyd_->weight-1)), 0, + &rid__, &weight__, &data__); + if (tag__ < 0) { + return tag__; + } + + // if we found a bname, lookup the branch + if (tag__ == LFS3_TAG_BNAME) { + tag__ = lfs3_rbyd_lookup(lfs3, rbyd_, rid__, LFS3_TAG_BRANCH, + &data__); + if (tag__ < 0) { + LFS3_ASSERT(tag__ != LFS3_ERR_NOENT); + return tag__; + } + } + + // found another branch + if (tag__ == LFS3_TAG_BRANCH) { + // adjust bid__ with subtree's weight + bid__ = (bid__-(rbyd_->weight-1)) + rid__; + + // fetch the next branch + int err = lfs3_data_fetchbranch(lfs3, &data__, weight__, + rbyd_); + if (err) { + return err; + } + + // found our bid + } else { + #ifdef LFS3_BLEAFCACHE + // keep track of the most recent leaf + btree->leaf.bid = bid__; + btree->leaf.r = *rbyd_; + #endif + + // TODO how many of these should be conditional? + if (bid_) { + *bid_ = (bid__-(rbyd_->weight-1)) + rid__; + } + if (rid_) { + *rid_ = rid__; + } + if (weight_) { + *weight_ = weight__; + } + if (data_) { + *data_ = data__; + } + return tag__; + } + } +} + +static lfs3_stag_t lfs3_btree_lookupnext(lfs3_t *lfs3, + LFS3_BCONST lfs3_btree_t *btree, + lfs3_bid_t bid, + lfs3_bid_t *bid_, lfs3_bid_t *weight_, lfs3_data_t *data_) { + lfs3_rbyd_t rbyd__; + return lfs3_btree_lookupnext_(lfs3, btree, bid, + bid_, &rbyd__, NULL, weight_, data_); +} + +// lfs3_btree_lookup assumes a known bid, matching lfs3_rbyd_lookup's +// behavior, if you don't care about the exact bid either first call +// lfs3_btree_lookupnext +static lfs3_stag_t lfs3_btree_lookup(lfs3_t *lfs3, + LFS3_BCONST lfs3_btree_t *btree, + lfs3_bid_t bid, lfs3_tag_t tag, + lfs3_data_t *data_) { + // lookup rbyd in btree + lfs3_bid_t bid__; + lfs3_rbyd_t rbyd__; + lfs3_srid_t rid__; + lfs3_stag_t tag__ = lfs3_btree_lookupnext_(lfs3, btree, bid, + &bid__, &rbyd__, &rid__, NULL, NULL); + if (tag__ < 0) { + return tag__; + } + + // lookup finds the next-smallest bid, all we need to do is fail + // if it picks up the wrong bid + if (bid__ != bid) { + return LFS3_ERR_NOENT; + } + + // lookup tag in rbyd + return lfs3_rbyd_lookup(lfs3, &rbyd__, rid__, tag, + data_); +} + +// TODO should lfs3_btree_lookupnext/lfs3_btree_parent be deduplicated? +#ifndef LFS3_RDONLY +static int lfs3_btree_parent(lfs3_t *lfs3, + const lfs3_btree_t *btree, + lfs3_bid_t bid, const lfs3_rbyd_t *child, + lfs3_rbyd_t *rbyd_, lfs3_srid_t *rid_) { + // we should only call this when we actually have parents + LFS3_ASSERT(bid < btree->r.weight); + LFS3_ASSERT(lfs3_rbyd_cmp(&btree->r, child) != 0); + + // descend down the btree looking for our bid + lfs3_bid_t bid__ = btree->r.weight-1; + *rbyd_ = btree->r; + while (true) { + // each branch is a pair of optional name + on-disk structure + lfs3_srid_t rid__; + lfs3_rid_t weight__; + lfs3_data_t data__; + lfs3_stag_t tag__ = lfs3_rbyd_lookupnext(lfs3, rbyd_, + bid - (bid__-(rbyd_->weight-1)), 0, + &rid__, &weight__, &data__); + if (tag__ < 0) { + LFS3_ASSERT(tag__ != LFS3_ERR_NOENT); + return tag__; + } + + // if we found a bname, lookup the branch + if (tag__ == LFS3_TAG_BNAME) { + tag__ = lfs3_rbyd_lookup(lfs3, rbyd_, rid__, LFS3_TAG_BRANCH, + &data__); + if (tag__ < 0) { + LFS3_ASSERT(tag__ != LFS3_ERR_NOENT); + return tag__; + } + } + + // didn't find our child? + if (tag__ != LFS3_TAG_BRANCH) { + return LFS3_ERR_NOENT; + } + + // adjust bid__ with subtree's weight + bid__ = (bid__-(rbyd_->weight-1)) + rid__; + + // fetch the next branch + lfs3_rbyd_t child__; + int err = lfs3_data_readbranch(lfs3, &data__, weight__, &child__); + if (err) { + return err; + } + + // found our child? + if (lfs3_rbyd_cmp(&child__, child) == 0) { + // TODO how many of these should be conditional? + if (rid_) { + *rid_ = rid__; + } + return 0; + } + + err = lfs3_branch_fetch(lfs3, rbyd_, + child__.blocks[0], child__.trunk, child__.weight, + child__.cksum); + if (err) { + return err; + } + } +} +#endif + + +// extra state needed for non-terminating lfs3_btree_commit_ calls +#ifndef LFS3_RDONLY +typedef struct lfs3_bcommit { + // pending commit, this is updates as lfs3_btree_commit_ recurses + lfs3_bid_t bid; + const lfs3_rattr_t *rattrs; + lfs3_size_t rattr_count; + + // internal lfs3_btree_commit_ state that needs to persist until + // the root is committed + struct { + lfs3_rattr_t rattrs[4]; + lfs3_data_t split_name; + uint8_t l_buf[LFS3_BRANCH_DSIZE]; + uint8_t r_buf[LFS3_BRANCH_DSIZE]; + } ctx; +} lfs3_bcommit_t; +#endif + +// needed in lfs3_btree_commit_ +static inline uint32_t lfs3_rev_btree(lfs3_t *lfs3); + +// core btree algorithm +// +// this commits up to the root, but stops if: +// 1. we need a new root => LFS3_ERR_RANGE +// 2. we hit a shrub root => LFS3_ERR_EXIST +// +// --- +// +// note! all non-bid-0 name updates must be via splits! +// +// This is because our btrees contain vestigial names, i.e. our inner +// nodes may contain names no longer in the tree. This simplifies +// lfs3_btree_commit_, but means insert-before-bid+1 is _not_ the same +// as insert-after-bid when named btrees are involved. If you try this +// it _will not_ work and if try to make it work you _will_ cry: +// +// .-----f-----. insert-after-d .-------f-----. +// .-b--. .--j-. => .-b---. .--j-. +// | .-. .-. | | .---. .-. | +// a c d h i k a c d e h i k +// ^ +// insert-before-h +// => .-----f-------. +// .-b--. .---j-. +// | .-. .---. | +// a c d g h i k +// ^ +// +// The problem is that lfs3_btree_commit_ needs to find the same leaf +// rbyd as lfs3_btree_namelookup, and potentially insert-before the +// first rid or insert-after the last rid. +// +// Instead of separate insert-before/after flags, we make the first tag +// in a commit insert-before, and all following non-grow tags +// insert-after (splits). +// +#ifndef LFS3_RDONLY +static int lfs3_btree_commit_(lfs3_t *lfs3, + lfs3_rbyd_t *btree_, lfs3_btree_t *btree, + lfs3_bcommit_t *bcommit) { + LFS3_ASSERT(bcommit->bid <= btree->r.weight); + + // before committing, claim any matching btrees we know about + // + // is this overkill? probably, but hey, better safe than sorry, + // claiming things here reduces the chance of forgetting to claim + // things in above layers + lfs3_fs_claimbtree(lfs3, btree); + + // lookup which leaf our bid resides + lfs3_rbyd_t child = btree->r; + lfs3_srid_t rid = bcommit->bid; + if (btree->r.weight > 0) { + lfs3_srid_t rid_; + lfs3_stag_t tag = lfs3_btree_lookupnext_(lfs3, btree, + // for lfs3_btree_commit_ operations to work out, we + // need to limit our bid to an rid in the tree, which + // is what this min is doing + lfs3_min(bcommit->bid, btree->r.weight-1), + &bcommit->bid, &child, &rid_, NULL, NULL); + if (tag < 0) { + LFS3_ASSERT(tag != LFS3_ERR_NOENT); + return tag; + } + + // adjust rid + rid -= (bcommit->bid - rid_); + } + + // tail-recursively commit to btree + lfs3_rbyd_t *const child_ = btree_; + while (true) { + // we will always need our parent, so go ahead and find it + lfs3_rbyd_t parent = {.trunk=0, .weight=0}; + lfs3_srid_t pid = 0; + // new root? shrub root? yield the final root commit to + // higher-level btree/bshrub logic + if (!lfs3_rbyd_trunk(&child) || lfs3_rbyd_isshrub(&child)) { + bcommit->bid = rid; + return (!lfs3_rbyd_trunk(&child)) + ? LFS3_ERR_RANGE + : LFS3_ERR_EXIST; + + // are we root? + } else if (child.blocks[0] == btree->r.blocks[0]) { + // mark btree as unfetched in case of failure, our btree rbyd and + // root rbyd can diverge if there's a split, but we would have + // marked the old root as unfetched earlier anyways + lfs3_btree_claim(btree); + + // need to lookup child's parent + } else { + int err = lfs3_btree_parent(lfs3, btree, bcommit->bid, &child, + &parent, &pid); + if (err) { + LFS3_ASSERT(err != LFS3_ERR_NOENT); + return err; + } + } + + // fetch our rbyd so we can mutate it + // + // note that some paths lead this to being a newly allocated rbyd, + // these will fail to fetch so we need to check that this rbyd is + // unfetched + // + // a funny benefit is we cache the root of our btree this way + if (!lfs3_rbyd_isfetched(&child)) { + // if we're not checking fetches, we can get away with a + // quick fetch + if (LFS3_IFDEF_CKFETCHES( + !lfs3_m_isckfetches(lfs3->flags), + true)) { + int err = lfs3_rbyd_fetchquick(lfs3, &child, + child.blocks[0], lfs3_rbyd_trunk(&child), + child.cksum); + if (err) { + return err; + } + } else { + int err = lfs3_rbyd_fetchck(lfs3, &child, + child.blocks[0], lfs3_rbyd_trunk(&child), + child.cksum); + if (err) { + return err; + } + } + } + + // is rbyd erased? can we sneak our commit into any remaining + // erased bytes? note that the btree trunk field prevents this from + // interacting with other references to the rbyd + *child_ = child; + int err = lfs3_rbyd_commit(lfs3, child_, rid, + bcommit->rattrs, bcommit->rattr_count); + if (err) { + if (err == LFS3_ERR_RANGE || err == LFS3_ERR_CORRUPT) { + goto compact; + } + return err; + } + + recurse:; + // propagate successful commits + + // done? + if (!lfs3_rbyd_trunk(&parent)) { + // update the root + // (note btree_ == child_) + return 0; + } + + // is our parent the root and is the root degenerate? + if (child.weight == btree->r.weight) { + // collapse the root, decreasing the height of the tree + // (note btree_ == child_) + return 0; + } + + // prepare commit to parent, tail recursing upwards + // + // note that since we defer merges to compaction time, we can + // end up removing an rbyd here + bcommit->bid -= pid - (child.weight-1); + lfs3_size_t rattr_count = 0; + lfs3_data_t branch; + // drop child? + if (child_->weight == 0) { + // drop child + bcommit->ctx.rattrs[rattr_count++] = LFS3_RATTR( + LFS3_tag_RM, -child.weight); + } else { + // update child + branch = lfs3_data_frombranch(child_, bcommit->ctx.l_buf); + bcommit->ctx.rattrs[rattr_count++] = LFS3_RATTR_BUF( + LFS3_TAG_BRANCH, 0, + branch.u.buffer, lfs3_data_size(branch)); + if (child_->weight != child.weight) { + bcommit->ctx.rattrs[rattr_count++] = LFS3_RATTR( + LFS3_tag_GROW, -child.weight + child_->weight); + } + } + LFS3_ASSERT(rattr_count + <= sizeof(bcommit->ctx.rattrs) + / sizeof(lfs3_rattr_t)); + bcommit->rattrs = bcommit->ctx.rattrs; + bcommit->rattr_count = rattr_count; + + // recurse! + child = parent; + rid = pid; + continue; + + compact:; + // estimate our compacted size + lfs3_srid_t split_rid; + lfs3_ssize_t estimate = lfs3_rbyd_estimate(lfs3, &child, -1, -1, + &split_rid); + if (estimate < 0) { + return estimate; + } + + // are we too big? need to split? + if ((lfs3_size_t)estimate > lfs3->cfg->block_size/2) { + // need to split + goto split; + } + + // before we compact, can we merge with our siblings? + lfs3_rbyd_t sibling; + if ((lfs3_size_t)estimate <= lfs3->cfg->block_size/4 + // no parent? can't merge + && lfs3_rbyd_trunk(&parent)) { + // try the right sibling + if (pid+1 < (lfs3_srid_t)parent.weight) { + // try looking up the sibling + lfs3_srid_t sibling_rid; + lfs3_rid_t sibling_weight; + lfs3_data_t sibling_data; + lfs3_stag_t sibling_tag = lfs3_rbyd_lookupnext(lfs3, &parent, + pid+1, 0, + &sibling_rid, &sibling_weight, &sibling_data); + if (sibling_tag < 0) { + LFS3_ASSERT(sibling_tag != LFS3_ERR_NOENT); + return sibling_tag; + } + + // if we found a bname, lookup the branch + if (sibling_tag == LFS3_TAG_BNAME) { + sibling_tag = lfs3_rbyd_lookup(lfs3, &parent, + sibling_rid, LFS3_TAG_BRANCH, + &sibling_data); + if (sibling_tag < 0) { + LFS3_ASSERT(sibling_tag != LFS3_ERR_NOENT); + return sibling_tag; + } + } + + LFS3_ASSERT(sibling_tag == LFS3_TAG_BRANCH); + err = lfs3_data_fetchbranch(lfs3, &sibling_data, sibling_weight, + &sibling); + if (err) { + return err; + } + + // estimate if our sibling will fit + lfs3_ssize_t sibling_estimate = lfs3_rbyd_estimate(lfs3, + &sibling, -1, -1, + NULL); + if (sibling_estimate < 0) { + return sibling_estimate; + } + + // fits? try to merge + if ((lfs3_size_t)(estimate + sibling_estimate) + < lfs3->cfg->block_size/2) { + goto merge; + } + } + + // try the left sibling + if (pid-(lfs3_srid_t)child.weight >= 0) { + // try looking up the sibling + lfs3_srid_t sibling_rid; + lfs3_rid_t sibling_weight; + lfs3_data_t sibling_data; + lfs3_stag_t sibling_tag = lfs3_rbyd_lookupnext(lfs3, &parent, + pid-child.weight, 0, + &sibling_rid, &sibling_weight, &sibling_data); + if (sibling_tag < 0) { + LFS3_ASSERT(sibling_tag != LFS3_ERR_NOENT); + return sibling_tag; + } + + // if we found a bname, lookup the branch + if (sibling_tag == LFS3_TAG_BNAME) { + sibling_tag = lfs3_rbyd_lookup(lfs3, &parent, + sibling_rid, LFS3_TAG_BRANCH, + &sibling_data); + if (sibling_tag < 0) { + LFS3_ASSERT(sibling_tag != LFS3_ERR_NOENT); + return sibling_tag; + } + } + + LFS3_ASSERT(sibling_tag == LFS3_TAG_BRANCH); + err = lfs3_data_fetchbranch(lfs3, &sibling_data, sibling_weight, + &sibling); + if (err) { + return err; + } + + // estimate if our sibling will fit + lfs3_ssize_t sibling_estimate = lfs3_rbyd_estimate(lfs3, + &sibling, -1, -1, + NULL); + if (sibling_estimate < 0) { + return sibling_estimate; + } + + // fits? try to merge + if ((lfs3_size_t)(estimate + sibling_estimate) + < lfs3->cfg->block_size/2) { + // if we're merging our left sibling, swap our rbyds + // so our sibling is on the right + bcommit->bid -= sibling.weight; + rid += sibling.weight; + pid -= child.weight; + + *child_ = sibling; + sibling = child; + child = *child_; + + goto merge; + } + } + } + + relocate:; + // allocate a new rbyd + err = lfs3_rbyd_alloc(lfs3, child_); + if (err) { + return err; + } + + #if defined(LFS3_REVDBG) || defined(LFS3_REVNOISE) + // append a revision count? + err = lfs3_rbyd_appendrev(lfs3, child_, lfs3_rev_btree(lfs3)); + if (err) { + // bad prog? try another block + if (err == LFS3_ERR_CORRUPT) { + goto relocate; + } + return err; + } + #endif + + // try to compact + err = lfs3_rbyd_compact(lfs3, child_, &child, -1, -1); + if (err) { + LFS3_ASSERT(err != LFS3_ERR_RANGE); + // bad prog? try another block + if (err == LFS3_ERR_CORRUPT) { + goto relocate; + } + return err; + } + + // append any pending rattrs, it's up to upper + // layers to make sure these always fit + err = lfs3_rbyd_commit(lfs3, child_, rid, + bcommit->rattrs, bcommit->rattr_count); + if (err) { + LFS3_ASSERT(err != LFS3_ERR_RANGE); + // bad prog? try another block + if (err == LFS3_ERR_CORRUPT) { + goto relocate; + } + return err; + } + + goto recurse; + + split:; + // we should have something to split here + LFS3_ASSERT(split_rid > 0 + && split_rid < (lfs3_srid_t)child.weight); + + split_relocate_l:; + // allocate a new rbyd + err = lfs3_rbyd_alloc(lfs3, child_); + if (err) { + return err; + } + + #if defined(LFS3_REVDBG) || defined(LFS3_REVNOISE) + // append a revision count? + err = lfs3_rbyd_appendrev(lfs3, child_, lfs3_rev_btree(lfs3)); + if (err) { + // bad prog? try another block + if (err == LFS3_ERR_CORRUPT) { + goto split_relocate_l; + } + return err; + } + #endif + + // copy over tags < split_rid + err = lfs3_rbyd_compact(lfs3, child_, &child, -1, split_rid); + if (err) { + LFS3_ASSERT(err != LFS3_ERR_RANGE); + // bad prog? try another block + if (err == LFS3_ERR_CORRUPT) { + goto split_relocate_l; + } + return err; + } + + // append pending rattrs < split_rid + // + // upper layers should make sure this can't fail by limiting the + // maximum commit size + err = lfs3_rbyd_appendrattrs(lfs3, child_, rid, -1, split_rid, + bcommit->rattrs, bcommit->rattr_count); + if (err) { + LFS3_ASSERT(err != LFS3_ERR_RANGE); + // bad prog? try another block + if (err == LFS3_ERR_CORRUPT) { + goto split_relocate_l; + } + return err; + } + + // finalize commit + err = lfs3_rbyd_appendcksum(lfs3, child_); + if (err) { + LFS3_ASSERT(err != LFS3_ERR_RANGE); + // bad prog? try another block + if (err == LFS3_ERR_CORRUPT) { + goto split_relocate_l; + } + return err; + } + + split_relocate_r:; + // allocate a sibling + err = lfs3_rbyd_alloc(lfs3, &sibling); + if (err) { + return err; + } + + #if defined(LFS3_REVDBG) || defined(LFS3_REVNOISE) + // append a revision count? + err = lfs3_rbyd_appendrev(lfs3, &sibling, lfs3_rev_btree(lfs3)); + if (err) { + // bad prog? try another block + if (err == LFS3_ERR_CORRUPT) { + goto split_relocate_r; + } + return err; + } + #endif + + // copy over tags >= split_rid + err = lfs3_rbyd_compact(lfs3, &sibling, &child, split_rid, -1); + if (err) { + LFS3_ASSERT(err != LFS3_ERR_RANGE); + // bad prog? try another block + if (err == LFS3_ERR_CORRUPT) { + goto split_relocate_r; + } + return err; + } + + // append pending rattrs >= split_rid + // + // upper layers should make sure this can't fail by limiting the + // maximum commit size + err = lfs3_rbyd_appendrattrs(lfs3, &sibling, rid, split_rid, -1, + bcommit->rattrs, bcommit->rattr_count); + if (err) { + LFS3_ASSERT(err != LFS3_ERR_RANGE); + // bad prog? try another block + if (err == LFS3_ERR_CORRUPT) { + goto split_relocate_r; + } + return err; + } + + // finalize commit + err = lfs3_rbyd_appendcksum(lfs3, &sibling); + if (err) { + LFS3_ASSERT(err != LFS3_ERR_RANGE); + // bad prog? try another block + if (err == LFS3_ERR_CORRUPT) { + goto split_relocate_r; + } + return err; + } + + // did one of our siblings drop to zero? yes this can happen! revert + // to a normal commit in that case + if (child_->weight == 0 || sibling.weight == 0) { + if (child_->weight == 0) { + *child_ = sibling; + } + goto recurse; + } + + split_recurse:; + // lookup first name in sibling to use as the split name + // + // note we need to do this after playing out pending rattrs in case + // they introduce a new name! + lfs3_stag_t split_tag = lfs3_rbyd_lookupnext(lfs3, &sibling, 0, 0, + NULL, NULL, &bcommit->ctx.split_name); + if (split_tag < 0) { + LFS3_ASSERT(split_tag != LFS3_ERR_NOENT); + return split_tag; + } + + // prepare commit to parent, tail recursing upwards + LFS3_ASSERT(child_->weight > 0); + LFS3_ASSERT(sibling.weight > 0); + // don't worry about bid if new root, we discard it anyways + bcommit->bid -= pid - (child.weight-1); + rattr_count = 0; + branch = lfs3_data_frombranch(child_, bcommit->ctx.l_buf); + // new root? + if (!lfs3_rbyd_trunk(&parent)) { + // new child + bcommit->ctx.rattrs[rattr_count++] = LFS3_RATTR_BUF( + LFS3_TAG_BRANCH, +child_->weight, + branch.u.buffer, lfs3_data_size(branch)); + // split root? + } else { + // update child + bcommit->ctx.rattrs[rattr_count++] = LFS3_RATTR_BUF( + LFS3_TAG_BRANCH, 0, + branch.u.buffer, lfs3_data_size(branch)); + if (child_->weight != child.weight) { + bcommit->ctx.rattrs[rattr_count++] = LFS3_RATTR( + LFS3_tag_GROW, -child.weight + child_->weight); + } + } + // new sibling + branch = lfs3_data_frombranch(&sibling, bcommit->ctx.r_buf); + bcommit->ctx.rattrs[rattr_count++] = LFS3_RATTR_BUF( + LFS3_TAG_BRANCH, +sibling.weight, + branch.u.buffer, lfs3_data_size(branch)); + if (lfs3_tag_suptype(split_tag) == LFS3_TAG_NAME) { + bcommit->ctx.rattrs[rattr_count++] = LFS3_RATTR_DATA( + LFS3_TAG_BNAME, 0, + &bcommit->ctx.split_name); + } + LFS3_ASSERT(rattr_count + <= sizeof(bcommit->ctx.rattrs) + / sizeof(lfs3_rattr_t)); + bcommit->rattrs = bcommit->ctx.rattrs; + bcommit->rattr_count = rattr_count; + + // recurse! + child = parent; + rid = pid; + continue; + + merge:; + merge_relocate:; + // allocate a new rbyd + err = lfs3_rbyd_alloc(lfs3, child_); + if (err) { + return err; + } + + #if defined(LFS3_REVDBG) || defined(LFS3_REVNOISE) + // append a revision count? + err = lfs3_rbyd_appendrev(lfs3, child_, lfs3_rev_btree(lfs3)); + if (err) { + // bad prog? try another block + if (err == LFS3_ERR_CORRUPT) { + goto merge_relocate; + } + return err; + } + #endif + + // merge the siblings together + err = lfs3_rbyd_appendcompactrbyd(lfs3, child_, &child, -1, -1); + if (err) { + LFS3_ASSERT(err != LFS3_ERR_RANGE); + // bad prog? try another block + if (err == LFS3_ERR_CORRUPT) { + goto merge_relocate; + } + return err; + } + + err = lfs3_rbyd_appendcompactrbyd(lfs3, child_, &sibling, -1, -1); + if (err) { + LFS3_ASSERT(err != LFS3_ERR_RANGE); + // bad prog? try another block + if (err == LFS3_ERR_CORRUPT) { + goto merge_relocate; + } + return err; + } + + err = lfs3_rbyd_appendcompaction(lfs3, child_, 0); + if (err) { + LFS3_ASSERT(err != LFS3_ERR_RANGE); + // bad prog? try another block + if (err == LFS3_ERR_CORRUPT) { + goto merge_relocate; + } + return err; + } + + // append any pending rattrs, it's up to upper + // layers to make sure these always fit + err = lfs3_rbyd_commit(lfs3, child_, rid, + bcommit->rattrs, bcommit->rattr_count); + if (err) { + LFS3_ASSERT(err != LFS3_ERR_RANGE); + // bad prog? try another block + if (err == LFS3_ERR_CORRUPT) { + goto merge_relocate; + } + return err; + } + + merge_recurse:; + // we must have a parent at this point, but is our parent the root + // and is the root degenerate? + LFS3_ASSERT(lfs3_rbyd_trunk(&parent)); + if (child.weight+sibling.weight == btree->r.weight) { + // collapse the root, decreasing the height of the tree + // (note btree_ == child_) + return 0; + } + + // prepare commit to parent, tail recursing upwards + LFS3_ASSERT(child_->weight > 0); + bcommit->bid -= pid - (child.weight-1); + rattr_count = 0; + // merge sibling + bcommit->ctx.rattrs[rattr_count++] = LFS3_RATTR( + LFS3_tag_RM, -sibling.weight); + // update child + branch = lfs3_data_frombranch(child_, bcommit->ctx.l_buf); + bcommit->ctx.rattrs[rattr_count++] = LFS3_RATTR_BUF( + LFS3_TAG_BRANCH, 0, + branch.u.buffer, lfs3_data_size(branch)); + if (child_->weight != child.weight) { + bcommit->ctx.rattrs[rattr_count++] = LFS3_RATTR( + LFS3_tag_GROW, -child.weight + child_->weight); + } + LFS3_ASSERT(rattr_count + <= sizeof(bcommit->ctx.rattrs) + / sizeof(lfs3_rattr_t)); + bcommit->rattrs = bcommit->ctx.rattrs; + bcommit->rattr_count = rattr_count; + + // recurse! + child = parent; + rid = pid + sibling.weight; + continue; + } +} +#endif + +// commit/alloc a new btree root +#ifndef LFS3_RDONLY +static int lfs3_btree_commitroot_(lfs3_t *lfs3, + lfs3_rbyd_t *btree_, lfs3_btree_t *btree, + lfs3_bid_t bid, const lfs3_rattr_t *rattrs, lfs3_size_t rattr_count) { +relocate:; + int err = lfs3_rbyd_alloc(lfs3, btree_); + if (err) { + return err; + } + + #if defined(LFS3_REVDBG) || defined(LFS3_REVNOISE) + // append a revision count? + err = lfs3_rbyd_appendrev(lfs3, btree_, lfs3_rev_btree(lfs3)); + if (err) { + // bad prog? try another block + if (err == LFS3_ERR_CORRUPT) { + goto relocate; + } + return err; + } + #endif + + // bshrubs may call this just to migrate rattrs to a btree + if (lfs3_rbyd_isshrub(&btree->r)) { + err = lfs3_rbyd_compact(lfs3, btree_, &btree->r, -1, -1); + if (err) { + LFS3_ASSERT(err != LFS3_ERR_RANGE); + // bad prog? try another block + if (err == LFS3_ERR_CORRUPT) { + goto relocate; + } + return err; + } + } + + err = lfs3_rbyd_commit(lfs3, btree_, bid, rattrs, rattr_count); + if (err) { + LFS3_ASSERT(err != LFS3_ERR_RANGE); + // bad prog? try another block + if (err == LFS3_ERR_CORRUPT) { + goto relocate; + } + return err; + } + + return 0; +} +#endif + +// commit to a btree, this is atomic +#ifndef LFS3_RDONLY +static int lfs3_btree_commit(lfs3_t *lfs3, lfs3_btree_t *btree, + lfs3_bid_t bid, const lfs3_rattr_t *rattrs, lfs3_size_t rattr_count) { + // try to commit to the btree + lfs3_rbyd_t btree_; + lfs3_bcommit_t bcommit; // do _not_ fully init this + bcommit.bid = bid; + bcommit.rattrs = rattrs; + bcommit.rattr_count = rattr_count; + int err = lfs3_btree_commit_(lfs3, &btree_, btree, + &bcommit); + if (err && err != LFS3_ERR_RANGE) { + LFS3_ASSERT(err != LFS3_ERR_EXIST); + return err; + } + + // needs a new root? + if (err == LFS3_ERR_RANGE) { + err = lfs3_btree_commitroot_(lfs3, &btree_, btree, + bcommit.bid, bcommit.rattrs, bcommit.rattr_count); + if (err) { + return err; + } + } + + // update the btree + btree->r = btree_; + #ifdef LFS3_BLEAFCACHE + // discard the leaf + lfs3_btree_discardleaf(btree); + #endif + + LFS3_ASSERT(lfs3_rbyd_trunk(&btree->r)); + #ifdef LFS3_DBGBTREECOMMITS + LFS3_DEBUG("Committed btree 0x%"PRIx32".%"PRIx32" w%"PRId32", " + "cksum %"PRIx32, + btree->r.blocks[0], lfs3_rbyd_trunk(&btree->r), + btree->r.weight, + btree->r.cksum); + #endif + return 0; +} +#endif + +// lookup in a btree by name +static lfs3_scmp_t lfs3_btree_namelookup(lfs3_t *lfs3, + LFS3_BCONST lfs3_btree_t *btree, + lfs3_did_t did, const char *name, lfs3_size_t name_len, + lfs3_bid_t *bid_, lfs3_tag_t *tag_, lfs3_bid_t *weight_, + lfs3_data_t *data_) { + // an empty tree? + if (btree->r.weight == 0) { + return LFS3_ERR_NOENT; + } + + // compiler needs this to be happy about initialization in callers + if (bid_) { + *bid_ = 0; + } + if (tag_) { + *tag_ = 0; + } + if (weight_) { + *weight_ = 0; + } + + // descend down the btree looking for our name + lfs3_bid_t bid__ = btree->r.weight-1; + lfs3_rbyd_t rbyd__ = btree->r; + while (true) { + // lookup our name in the rbyd via binary search + lfs3_srid_t rid__; + lfs3_stag_t tag__; + lfs3_rid_t weight__; + lfs3_data_t data__; + lfs3_scmp_t cmp = lfs3_rbyd_namelookup(lfs3, &rbyd__, + did, name, name_len, + &rid__, (lfs3_tag_t*)&tag__, &weight__, &data__); + if (cmp < 0) { + LFS3_ASSERT(cmp != LFS3_ERR_NOENT); + return cmp; + } + + // if we found a bname, lookup the branch + if (tag__ == LFS3_TAG_BNAME) { + tag__ = lfs3_rbyd_lookup(lfs3, &rbyd__, rid__, + LFS3_tag_MASK8 | LFS3_TAG_STRUCT, + &data__); + if (tag__ < 0) { + LFS3_ASSERT(tag__ != LFS3_ERR_NOENT); + return tag__; + } + } + + // found another branch + if (tag__ == LFS3_TAG_BRANCH) { + // adjust bid__ with subtree's weight + bid__ = (bid__-(rbyd__.weight-1)) + rid__; + + // fetch the next branch + int err = lfs3_data_fetchbranch(lfs3, &data__, weight__, + &rbyd__); + if (err) { + return err; + } + + // found our rid + } else { + #ifdef LFS3_BLEAFCACHE + // keep track of the most recent leaf + btree->leaf.bid = bid__; + btree->leaf.r = rbyd__; + #endif + + // TODO how many of these should be conditional? + if (bid_) { + *bid_ = (bid__-(rbyd__.weight-1)) + rid__; + } + if (tag_) { + *tag_ = tag__; + } + if (weight_) { + *weight_ = weight__; + } + if (data_) { + *data_ = data__; + } + return cmp; + } + } +} + +// incremental btree traversal +// +// unlike lfs3_btree_lookupnext, this includes inner btree nodes + +static void lfs3_btrv_init(lfs3_btrv_t *btrv) { + btrv->bid = -1; +} + +static lfs3_stag_t lfs3_btree_traverse(lfs3_t *lfs3, + const lfs3_btree_t *btree, + lfs3_btrv_t *btrv, + lfs3_sbid_t *bid_, lfs3_bid_t *weight_, lfs3_data_t *data_) { + // restart from the root? + if (btrv->bid == -1 + || btrv->rid >= (lfs3_srid_t)btrv->rbyd.weight + // we do this unconditionally when rbyd == root to avoid + // bshrubs falling out-of-sync + || btrv->rbyd.weight == btree->r.weight) { + // end of traversal? + if (btrv->bid >= (lfs3_sbid_t)btree->r.weight) { + return LFS3_ERR_NOENT; + } + + // restart from the root + btrv->rbyd = btree->r; + btrv->rid = btrv->bid; + + // explicitly traverse the root even if weight=0 + if (btrv->bid == -1) { + btrv->bid += 1; + btrv->rid += 1; + + // unless we don't even have a root yet + if (lfs3_rbyd_trunk(&btree->r) != 0 + // or are a shrub + && !lfs3_rbyd_isshrub(&btree->r)) { + if (bid_) { + *bid_ = btree->r.weight-1; + } + if (weight_) { + *weight_ = btree->r.weight; + } + if (data_) { + // note we point data_ at the actual root here! this + // avoids redundant fetches if the traversal fetches + // btree nodes + data_->u.buffer = (const uint8_t*)&btree->r; + } + return LFS3_TAG_BRANCH; + } + } + } + + // descend down the tree + while (true) { + lfs3_srid_t rid__; + lfs3_rid_t weight__; + lfs3_data_t data__; + lfs3_stag_t tag__ = lfs3_rbyd_lookupnext(lfs3, &btrv->rbyd, + btrv->rid, 0, + &rid__, &weight__, &data__); + if (tag__ < 0) { + return tag__; + } + + // if we found a bname, lookup the branch + if (tag__ == LFS3_TAG_BNAME) { + tag__ = lfs3_rbyd_lookup(lfs3, &btrv->rbyd, + rid__, LFS3_TAG_BRANCH, + &data__); + if (tag__ < 0) { + LFS3_ASSERT(tag__ != LFS3_ERR_NOENT); + return tag__; + } + } + + // found another branch + if (tag__ == LFS3_TAG_BRANCH) { + // adjust rid with subtree's weight + btrv->rid -= (rid__ - (weight__-1)); + + // fetch the next branch + int err = lfs3_data_fetchbranch(lfs3, &data__, weight__, + &btrv->rbyd); + if (err) { + return err; + } + + // return inner btree nodes if this is the first time we've + // seen them + if (btrv->rid == 0) { + if (bid_) { + *bid_ = btrv->bid + (rid__ - btrv->rid); + } + if (weight_) { + *weight_ = weight__; + } + if (data_) { + data_->u.buffer = (const uint8_t*)&btrv->rbyd; + } + return LFS3_TAG_BRANCH; + } + + // found our bid + } else { + // move on to the next rid + // + // note this effectively traverses a full leaf without redoing + // the btree walk + lfs3_bid_t bid__ = btrv->bid + (rid__ - btrv->rid); + btrv->bid = bid__ + 1; + btrv->rid = rid__ + 1; + + if (bid_) { + *bid_ = bid__; + } + if (weight_) { + *weight_ = weight__; + } + if (data_) { + *data_ = data__; + } + return tag__; + } + } +} + + + + +/// B-shrub operations /// + +// shrub things + +// helper functions +static inline bool lfs3_shrub_isshrub(const lfs3_shrub_t *shrub) { + return lfs3_rbyd_isshrub(shrub); +} + +static inline lfs3_size_t lfs3_shrub_trunk(const lfs3_shrub_t *shrub) { + return lfs3_rbyd_trunk(shrub); +} + +static inline int lfs3_shrub_cmp( + const lfs3_shrub_t *a, + const lfs3_shrub_t *b) { + return lfs3_rbyd_cmp(a, b); +} + +// shrub on-disk encoding +#ifndef LFS3_RDONLY +static lfs3_data_t lfs3_data_fromshrub(const lfs3_shrub_t *shrub, + uint8_t buffer[static LFS3_SHRUB_DSIZE]) { + // shrub trunks should never be null + LFS3_ASSERT(lfs3_shrub_trunk(shrub) != 0); + // weight should not exceed 31-bits + LFS3_ASSERT(shrub->weight <= 0x7fffffff); + // trunk should not exceed 28-bits + LFS3_ASSERT(lfs3_shrub_trunk(shrub) <= 0x0fffffff); + lfs3_ssize_t d = 0; + + // just write the trunk and weight, the rest of the rbyd is contextual + lfs3_ssize_t d_ = lfs3_toleb128(shrub->weight, &buffer[d], 5); + if (d_ < 0) { + LFS3_UNREACHABLE(); + } + d += d_; + + d_ = lfs3_toleb128(lfs3_shrub_trunk(shrub), + &buffer[d], 4); + if (d_ < 0) { + LFS3_UNREACHABLE(); + } + d += d_; + + return LFS3_DATA_BUF(buffer, d); +} +#endif + +static int lfs3_data_readshrub(lfs3_t *lfs3, + const lfs3_mdir_t *mdir, lfs3_data_t *data, + lfs3_shrub_t *shrub) { + // copy the mdir block + shrub->blocks[0] = mdir->r.blocks[0]; + // force estimate recalculation if we write to this shrub + #ifndef LFS3_RDONLY + shrub->eoff = -1; + #endif + + int err = lfs3_data_readleb128(lfs3, data, &shrub->weight); + if (err) { + return err; + } + + err = lfs3_data_readlleb128(lfs3, data, &shrub->trunk); + if (err) { + return err; + } + // shrub trunks should never be null + LFS3_ASSERT(lfs3_shrub_trunk(shrub)); + + // set the shrub bit in our trunk + shrub->trunk |= LFS3_RBYD_ISSHRUB; + return 0; +} + +// these are used in mdir commit/compaction +#ifndef LFS3_RDONLY +static lfs3_ssize_t lfs3_shrub_estimate(lfs3_t *lfs3, + const lfs3_shrub_t *shrub) { + // only include the last reference + const lfs3_shrub_t *last = NULL; + for (lfs3_handle_t *h = lfs3->handles; h; h = h->next) { + if (lfs3_o_type(h->flags) == LFS3_TYPE_REG + && lfs3_shrub_cmp( + &((lfs3_bshrub_t*)h)->b.r, + shrub) == 0) { + last = &((lfs3_bshrub_t*)h)->b.r; + } + } + if (last && shrub != last) { + return 0; + } + + return lfs3_rbyd_estimate(lfs3, shrub, -1, -1, + NULL); +} +#endif + +#ifndef LFS3_RDONLY +static int lfs3_shrub_compact(lfs3_t *lfs3, lfs3_rbyd_t *rbyd_, + lfs3_shrub_t *shrub_, const lfs3_shrub_t *shrub) { + // save our current trunk/weight + lfs3_size_t trunk = rbyd_->trunk; + lfs3_srid_t weight = rbyd_->weight; + + // compact our bshrub + int err = lfs3_rbyd_appendshrub(lfs3, rbyd_, shrub); + if (err) { + return err; + } + + // stage any opened shrubs with their new location so we can + // update these later if our commit is a success + // + // this should include our current bshrub + for (lfs3_handle_t *h = lfs3->handles; h; h = h->next) { + if (lfs3_o_type(h->flags) == LFS3_TYPE_REG + && lfs3_shrub_cmp( + &((lfs3_bshrub_t*)h)->b.r, + shrub) == 0) { + ((lfs3_bshrub_t*)h)->b_.blocks[0] = rbyd_->blocks[0]; + ((lfs3_bshrub_t*)h)->b_.trunk = rbyd_->trunk; + ((lfs3_bshrub_t*)h)->b_.weight = rbyd_->weight; + } + } + + // revert rbyd trunk/weight + shrub_->blocks[0] = rbyd_->blocks[0]; + shrub_->trunk = rbyd_->trunk; + shrub_->weight = rbyd_->weight; + rbyd_->trunk = trunk; + rbyd_->weight = weight; + return 0; +} +#endif + +// this is needed to sneak shrub commits into mdir commits +#ifndef LFS3_RDONLY +typedef struct lfs3_shrubcommit { + lfs3_bshrub_t *bshrub; + lfs3_srid_t rid; + const lfs3_rattr_t *rattrs; + lfs3_size_t rattr_count; +} lfs3_shrubcommit_t; +#endif + +#ifndef LFS3_RDONLY +static int lfs3_shrub_commit(lfs3_t *lfs3, lfs3_rbyd_t *rbyd_, + lfs3_shrub_t *shrub, lfs3_srid_t rid, + const lfs3_rattr_t *rattrs, lfs3_size_t rattr_count) { + // swap out our trunk/weight temporarily, note we're + // operating on a copy so if this fails we shouldn't mess + // things up too much + // + // it is important that these rbyds share eoff/cksum/etc + lfs3_size_t trunk = rbyd_->trunk; + lfs3_srid_t weight = rbyd_->weight; + rbyd_->trunk = shrub->trunk; + rbyd_->weight = shrub->weight; + + // append any bshrub attributes + int err = lfs3_rbyd_appendrattrs(lfs3, rbyd_, rid, -1, -1, + rattrs, rattr_count); + if (err) { + return err; + } + + // restore mdir to the main trunk/weight + shrub->trunk = rbyd_->trunk; + shrub->weight = rbyd_->weight; + rbyd_->trunk = trunk; + rbyd_->weight = weight; + return 0; +} +#endif + + +// ok, actual bshrub things + +// create a non-existant bshrub +static void lfs3_bshrub_init(lfs3_bshrub_t *bshrub) { + // set up a null bshrub + bshrub->b.r.weight = 0; + bshrub->b.r.blocks[0] = -1; + bshrub->b.r.trunk = 0; + // force estimate recalculation + #ifndef LFS3_RDONLY + bshrub->b.r.eoff = -1; + #endif + #ifdef LFS3_BLEAFCACHE + // weight=0 indicates no leaf + bshrub->b.leaf.r.weight = 0; + #endif +} + +static inline bool lfs3_bshrub_isbnull(const lfs3_bshrub_t *bshrub) { + return !bshrub->b.r.trunk; +} + +static inline bool lfs3_bshrub_isbshrub(const lfs3_bshrub_t *bshrub) { + return lfs3_shrub_isshrub(&bshrub->b.r); +} + +static inline bool lfs3_bshrub_isbtree(const lfs3_bshrub_t *bshrub) { + return !lfs3_shrub_isshrub(&bshrub->b.r); +} + +#ifdef LFS3_BLEAFCACHE +static inline void lfs3_bshrub_discardleaf(lfs3_bshrub_t *bshrub) { + lfs3_btree_discardleaf(&bshrub->b); +} +#endif + +static inline int lfs3_bshrub_cmp( + const lfs3_bshrub_t *a, + const lfs3_bshrub_t *b) { + return lfs3_btree_cmp(&a->b, &b->b); +} + +// needed in lfs3_bshrub_fetch +static lfs3_stag_t lfs3_mdir_lookup(lfs3_t *lfs3, const lfs3_mdir_t *mdir, + lfs3_tag_t tag, + lfs3_data_t *data_); + +// fetch the bshrub/btree attatched to the current mdir+mid, if there +// is one +// +// note we don't mess with bshrub on error! +static int lfs3_bshrub_fetch_(lfs3_t *lfs3, const lfs3_mdir_t *mdir, + lfs3_btree_t *btree) { + // lookup the file struct, if there is one + lfs3_data_t data; + lfs3_stag_t tag = lfs3_mdir_lookup(lfs3, mdir, + LFS3_tag_MASK8 | LFS3_TAG_STRUCT, + &data); + if (tag < 0) { + return tag; + } + + // these functions leave bshrub undefined if there is an error, so + // first read into a temporary bshrub/btree + lfs3_btree_t btree_; + #ifdef LFS3_BLEAFCACHE + // make sure leaf is discarded + lfs3_btree_discardleaf(&btree_); + #endif + + // found a bshrub? (inlined btree) + if (tag == LFS3_TAG_BSHRUB) { + int err = lfs3_data_readshrub(lfs3, mdir, &data, + &btree_.r); + if (err) { + return err; + } + + // found a btree? + } else if (tag == LFS3_TAG_BTREE) { + int err = lfs3_data_fetchbtree(lfs3, &data, + &btree_); + if (err) { + return err; + } + + // we can run into other structs, dids in lfs3_mtree_traverse for + // example, just ignore these for now + } else { + return LFS3_ERR_NOENT; + } + + // update the bshrub/btree + *btree = btree_; + return 0; +} + +static int lfs3_bshrub_fetch(lfs3_t *lfs3, lfs3_bshrub_t *bshrub) { + return lfs3_bshrub_fetch_(lfs3, &bshrub->h.mdir, &bshrub->b); +} + +// find a tight upper bound on the _full_ bshrub size, this includes +// any on-disk bshrubs, and all pending bshrubs +#ifndef LFS3_RDONLY +static lfs3_ssize_t lfs3_bshrub_estimate(lfs3_t *lfs3, + const lfs3_bshrub_t *bshrub) { + lfs3_size_t estimate = 0; + + // include all unique shrubs related to our file, including the + // on-disk shrub + lfs3_data_t data; + lfs3_stag_t tag = lfs3_mdir_lookup(lfs3, &bshrub->h.mdir, LFS3_TAG_BSHRUB, + &data); + if (tag < 0 && tag != LFS3_ERR_NOENT) { + return tag; + } + + if (tag != LFS3_ERR_NOENT) { + lfs3_shrub_t shrub; + int err = lfs3_data_readshrub(lfs3, &bshrub->h.mdir, &data, + &shrub); + if (err) { + return err; + } + + lfs3_ssize_t dsize = lfs3_shrub_estimate(lfs3, &shrub); + if (dsize < 0) { + return dsize; + } + estimate += dsize; + } + + // this includes our current shrub + for (lfs3_handle_t *h = lfs3->handles; h; h = h->next) { + if (lfs3_o_type(h->flags) == LFS3_TYPE_REG + && h->mdir.mid == bshrub->h.mdir.mid + && lfs3_bshrub_isbshrub((lfs3_bshrub_t*)h)) { + lfs3_ssize_t dsize = lfs3_shrub_estimate(lfs3, + &((lfs3_bshrub_t*)h)->b.r); + if (dsize < 0) { + return dsize; + } + estimate += dsize; + } + } + + return estimate; +} +#endif + +// bshrub lookup functions +static lfs3_stag_t lfs3_bshrub_lookupnext_(lfs3_t *lfs3, + LFS3_BCONST lfs3_bshrub_t *bshrub, + lfs3_bid_t bid, + lfs3_bid_t *bid_, lfs3_rbyd_t *rbyd_, lfs3_srid_t *rid_, + lfs3_bid_t *weight_, lfs3_data_t *data_) { + return lfs3_btree_lookupnext_(lfs3, &bshrub->b, bid, + bid_, rbyd_, rid_, weight_, data_); +} + +static lfs3_stag_t lfs3_bshrub_lookupnext(lfs3_t *lfs3, + LFS3_BCONST lfs3_bshrub_t *bshrub, + lfs3_bid_t bid, + lfs3_bid_t *bid_, lfs3_bid_t *weight_, lfs3_data_t *data_) { + return lfs3_btree_lookupnext(lfs3, &bshrub->b, bid, + bid_, weight_, data_); +} + +static lfs3_stag_t lfs3_bshrub_lookup(lfs3_t *lfs3, + LFS3_BCONST lfs3_bshrub_t *bshrub, + lfs3_bid_t bid, lfs3_tag_t tag, + lfs3_data_t *data_) { + return lfs3_btree_lookup(lfs3, &bshrub->b, bid, tag, + data_); +} + +static lfs3_stag_t lfs3_bshrub_traverse(lfs3_t *lfs3, + const lfs3_bshrub_t *bshrub, + lfs3_btrv_t *btrv, + lfs3_sbid_t *bid_, lfs3_bid_t *weight_, lfs3_data_t *data_) { + return lfs3_btree_traverse(lfs3, &bshrub->b, btrv, + bid_, weight_, data_); +} + +// needed in lfs3_bshrub_commitroot_ +#ifndef LFS3_RDONLY +static int lfs3_mdir_commit_(lfs3_t *lfs3, lfs3_mdir_t *mdir, + const lfs3_rattr_t *rattrs, lfs3_size_t rattr_count); +#endif + +// commit to the bshrub root, i.e. the bshrub's shrub +#ifndef LFS3_RDONLY +static int lfs3_bshrub_commitroot_(lfs3_t *lfs3, lfs3_bshrub_t *bshrub, + lfs3_bid_t bid, const lfs3_rattr_t *rattrs, lfs3_size_t rattr_count) { + // we need to prevent our shrub from overflowing our mdir somehow + // + // maintaining an accurate estimate is tricky and error-prone, + // but recalculating an estimate every commit is expensive + // + // Instead, we keep track of an estimate of how many bytes have + // been progged to the shrub since the last estimate, and recalculate + // the estimate when this overflows our shrub_size. This mirrors how + // block_size and rbyds interact, and amortizes the estimate cost. + + // figure out how much data this commit progs + lfs3_size_t commit_estimate = 0; + for (lfs3_size_t i = 0; i < rattr_count; i++) { + commit_estimate += lfs3->rattr_estimate; + // fortunately the tags we commit to shrubs are actually quite + // limited, if lazily encoded the rattr should set rattr.count + // to the expected dsize + if (rattrs[i].from == LFS3_FROM_DATA) { + for (lfs3_size_t j = 0; j < rattrs[i].count; j++) { + commit_estimate += lfs3_data_size(rattrs[i].u.datas[j]); + } + } else { + commit_estimate += rattrs[i].count; + } + } + + // does our estimate exceed our shrub_size? need to recalculate an + // accurate estimate + lfs3_ssize_t estimate = (lfs3_bshrub_isbshrub(bshrub)) + ? bshrub->b.r.eoff + : (lfs3_size_t)-1; + // this double condition avoids overflow issues + if ((lfs3_size_t)estimate > lfs3->cfg->shrub_size + || estimate + commit_estimate > lfs3->cfg->shrub_size) { + estimate = lfs3_bshrub_estimate(lfs3, bshrub); + if (estimate < 0) { + return estimate; + } + + // two cases where we evict: + // - overflow shrub_size/2 - don't penalize for commits here + // - overflow shrub_size - must include commits or risk overflow + // + // the 1/2 here prevents runaway performance with the shrub is + // near full, but it's a heuristic, so including the commit would + // just be mean + // + if ((lfs3_size_t)estimate > lfs3->cfg->shrub_size/2 + || estimate + commit_estimate > lfs3->cfg->shrub_size) { + return LFS3_ERR_RANGE; + } + } + + // include our pending commit in the new estimate + estimate += commit_estimate; + + // commit to shrub + // + // note we do _not_ checkpoint the allocator here, blocks may be + // in-flight! + int err = lfs3_mdir_commit_(lfs3, &bshrub->h.mdir, LFS3_RATTRS( + LFS3_RATTR_SHRUBCOMMIT( + (&(lfs3_shrubcommit_t){ + .bshrub=bshrub, + .rid=bid, + .rattrs=rattrs, + .rattr_count=rattr_count})))); + if (err) { + return err; + } + LFS3_ASSERT(bshrub->b.r.blocks[0] == bshrub->h.mdir.r.blocks[0]); + + // update _all_ shrubs with the new estimate + for (lfs3_handle_t *h = lfs3->handles; h; h = h->next) { + if (lfs3_o_type(h->flags) == LFS3_TYPE_REG + && h->mdir.mid == bshrub->h.mdir.mid + && lfs3_bshrub_isbshrub((lfs3_bshrub_t*)h)) { + ((lfs3_bshrub_t*)h)->b.r.eoff = estimate; + // TODO bit of a hack, is this the best way to make sure + // estimate is not clobbered on redundant shrub sync? should + // we instead let eoff/estimate survive staging in mdir + // commit? + ((lfs3_bshrub_t*)h)->b_.eoff = estimate; + } + } + LFS3_ASSERT(bshrub->b.r.eoff == (lfs3_size_t)estimate); + // note above layers may redundantly sync shrub_ -> shrub + LFS3_ASSERT(bshrub->b_.eoff == (lfs3_size_t)estimate); + + return 0; +} +#endif + +// commit to bshrub, this is atomic +#ifndef LFS3_RDONLY +static int lfs3_bshrub_commit(lfs3_t *lfs3, lfs3_bshrub_t *bshrub, + lfs3_bid_t bid, const lfs3_rattr_t *rattrs, lfs3_size_t rattr_count) { + // try to commit to the btree + lfs3_bcommit_t bcommit; // do _not_ fully init this + bcommit.bid = bid; + bcommit.rattrs = rattrs; + bcommit.rattr_count = rattr_count; + int err = lfs3_btree_commit_(lfs3, &bshrub->b_, &bshrub->b, + &bcommit); + if (err && err != LFS3_ERR_RANGE + && err != LFS3_ERR_EXIST) { + return err; + } + + // when btree is shrubbed or split, lfs3_btree_commit_ stops at the + // root and returns with pending rattrs + // + // note that bshrubs can't go straight to splitting, bshrubs are + // always converted to btrees first, which can't fail (shrub < 1/2 + // block + commit < 1/2 block) + if (err == LFS3_ERR_RANGE + || err == LFS3_ERR_EXIST) { + // bshrubs can't go straight to splitting + LFS3_ASSERT(!lfs3_bshrub_isbshrub(bshrub) + || err != LFS3_ERR_RANGE); + + // try to commit to shrub root + err = lfs3_bshrub_commitroot_(lfs3, bshrub, + bcommit.bid, bcommit.rattrs, bcommit.rattr_count); + if (err && err != LFS3_ERR_RANGE) { + return err; + } + + // if we don't fit, convert to btree + if (err == LFS3_ERR_RANGE) { + err = lfs3_btree_commitroot_(lfs3, + &bshrub->b_, &bshrub->b, + bcommit.bid, bcommit.rattrs, bcommit.rattr_count); + if (err) { + return err; + } + } + } + + // update the bshrub/btree + bshrub->b.r = bshrub->b_; + #ifdef LFS3_BLEAFCACHE + // discard the leaf + lfs3_bshrub_discardleaf(bshrub); + #endif + + LFS3_ASSERT(lfs3_shrub_trunk(&bshrub->b.r)); + #ifdef LFS3_DBGBTREECOMMITS + if (lfs3_bshrub_isbshrub(bshrub)) { + LFS3_DEBUG("Committed bshrub " + "0x{%"PRIx32",%"PRIx32"}.%"PRIx32" w%"PRId32, + bshrub->h.mdir.r.blocks[0], bshrub->h.mdir.r.blocks[1], + lfs3_shrub_trunk(&bshrub->b), + bshrub->b.weight); + } else { + LFS3_DEBUG("Committed btree 0x%"PRIx32".%"PRIx32" w%"PRId32", " + "cksum %"PRIx32, + bshrub->b.blocks[0], lfs3_shrub_trunk(&bshrub->b), + bshrub->b.weight, + bshrub->b.cksum); + } + #endif + return 0; +} +#endif + + + + +/// Metadata-id things /// + +#define LFS3_MID(_lfs, _bid, _rid) \ + (((_bid) & ~((1 << (_lfs)->mbits)-1)) + (_rid)) + +static inline lfs3_sbid_t lfs3_mbid(const lfs3_t *lfs3, lfs3_smid_t mid) { + return mid | ((1 << lfs3->mbits) - 1); +} + +static inline lfs3_srid_t lfs3_mrid(const lfs3_t *lfs3, lfs3_smid_t mid) { + // bit of a strange mapping, but we want to preserve mid<=-1 => rid=-1 + return (mid >> (8*sizeof(lfs3_smid_t)-1)) + | (mid & ((1 << lfs3->mbits) - 1)); +} + +// these should only be used for logging +static inline lfs3_sbid_t lfs3_dbgmbid(const lfs3_t *lfs3, lfs3_smid_t mid) { + if (lfs3->mtree.r.weight == 0) { + return -1; + } else { + return mid >> lfs3->mbits; + } +} + +static inline lfs3_srid_t lfs3_dbgmrid(const lfs3_t *lfs3, lfs3_smid_t mid) { + return lfs3_mrid(lfs3, mid); +} + + +/// Metadata-pointer things /// + +// the mroot anchor, mdir 0x{0,1} is the entry point into the filesystem +#define LFS3_MPTR_MROOTANCHOR() ((const lfs3_block_t[2]){0, 1}) + +static inline int lfs3_mptr_cmp( + const lfs3_block_t a[static 2], + const lfs3_block_t b[static 2]) { + // note these can be in either order + if (lfs3_max(a[0], a[1]) != lfs3_max(b[0], b[1])) { + return lfs3_max(a[0], a[1]) - lfs3_max(b[0], b[1]); + } else { + return lfs3_min(a[0], a[1]) - lfs3_min(b[0], b[1]); + } +} + +static inline bool lfs3_mptr_ismrootanchor( + const lfs3_block_t mptr[static 2]) { + // mrootanchor is always at 0x{0,1} + // just check that the first block is in mroot anchor range + return mptr[0] <= 1; +} + +// mptr on-disk encoding +#ifndef LFS3_RDONLY +static lfs3_data_t lfs3_data_frommptr(const lfs3_block_t mptr[static 2], + uint8_t buffer[static LFS3_MPTR_DSIZE]) { + // blocks should not exceed 31-bits + LFS3_ASSERT(mptr[0] <= 0x7fffffff); + LFS3_ASSERT(mptr[1] <= 0x7fffffff); + + lfs3_ssize_t d = 0; + for (int i = 0; i < 2; i++) { + lfs3_ssize_t d_ = lfs3_toleb128(mptr[i], &buffer[d], 5); + if (d_ < 0) { + LFS3_UNREACHABLE(); + } + d += d_; + } + + return LFS3_DATA_BUF(buffer, d); +} +#endif + +static int lfs3_data_readmptr(lfs3_t *lfs3, lfs3_data_t *data, + lfs3_block_t mptr[static 2]) { + for (int i = 0; i < 2; i++) { + int err = lfs3_data_readleb128(lfs3, data, &mptr[i]); + if (err) { + return err; + } + } + + return 0; +} + + + +/// Various flag things /// + +// open flags +static inline bool lfs3_o_isrdonly(uint32_t flags) { + (void)flags; + #ifndef LFS3_RDONLY + return (flags & LFS3_O_MODE) == LFS3_O_RDONLY; + #else + return true; + #endif +} + +static inline bool lfs3_o_iswronly(uint32_t flags) { + (void)flags; + #ifndef LFS3_RDONLY + return (flags & LFS3_O_MODE) == LFS3_O_WRONLY; + #else + return false; + #endif +} + +static inline bool lfs3_o_iswrset(uint32_t flags) { + (void)flags; + #ifndef LFS3_RDONLY + return (flags & LFS3_O_MODE) == LFS3_o_WRSET; + #else + return false; + #endif +} + +static inline bool lfs3_o_iscreat(uint32_t flags) { + (void)flags; + #ifndef LFS3_RDONLY + return flags & LFS3_O_CREAT; + #else + return false; + #endif +} + +static inline bool lfs3_o_isexcl(uint32_t flags) { + (void)flags; + #ifndef LFS3_RDONLY + return flags & LFS3_O_EXCL; + #else + return false; + #endif +} + +static inline bool lfs3_o_istrunc(uint32_t flags) { + (void)flags; + #ifndef LFS3_RDONLY + return flags & LFS3_O_TRUNC; + #else + return false; + #endif +} + +static inline bool lfs3_o_isappend(uint32_t flags) { + (void)flags; + #ifndef LFS3_RDONLY + return flags & LFS3_O_APPEND; + #else + return false; + #endif +} + +static inline bool lfs3_o_isflush(uint32_t flags) { + (void)flags; + #ifdef LFS3_YES_FLUSH + return true; + #else + return flags & LFS3_O_FLUSH; + #endif +} + +static inline bool lfs3_o_issync(uint32_t flags) { + (void)flags; + #ifdef LFS3_YES_SYNC + return true; + #else + return flags & LFS3_O_SYNC; + #endif +} + +static inline bool lfs3_o_isdesync(uint32_t flags) { + return flags & LFS3_O_DESYNC; +} + +// internal open flags +static inline uint8_t lfs3_o_type(uint32_t flags) { + return flags >> 28; +} + +static inline uint32_t lfs3_o_typeflags(uint8_t type) { + return (uint32_t)type << 28; +} + +static inline void lfs3_o_settype(uint32_t *flags, uint8_t type) { + *flags = (*flags & ~LFS3_o_TYPE) | lfs3_o_typeflags(type); +} + +// TODO drop for lfs3_o_type(flags) == LFS3_TYPE_REG? +static inline bool lfs3_o_isbshrub(uint32_t flags) { + return lfs3_o_type(flags) == LFS3_TYPE_REG; +} + +static inline bool lfs3_o_iszombie(uint32_t flags) { + return flags & LFS3_o_ZOMBIE; +} + +static inline bool lfs3_o_isuncreat(uint32_t flags) { + return flags & LFS3_o_UNCREAT; +} + +static inline bool lfs3_o_isunsync(uint32_t flags) { + return flags & LFS3_o_UNSYNC; +} + +static inline bool lfs3_o_isuncryst(uint32_t flags) { + return flags & LFS3_o_UNCRYST; +} + +static inline bool lfs3_o_isungraft(uint32_t flags) { + return flags & LFS3_o_UNGRAFT; +} + +static inline bool lfs3_o_isunflush(uint32_t flags) { + return flags & LFS3_o_UNFLUSH; +} + +// custom attr flags +static inline bool lfs3_a_islazy(uint32_t flags) { + return flags & LFS3_A_LAZY; +} + +// traversal flags +static inline bool lfs3_t_isrdonly(uint32_t flags) { + (void)flags; + #ifndef LFS3_RDONLY + return flags & LFS3_T_RDONLY; + #else + return true; + #endif +} + +static inline bool lfs3_t_ismtreeonly(uint32_t flags) { + return flags & LFS3_T_MTREEONLY; +} + +static inline bool lfs3_t_isexcl(uint32_t flags) { + return flags & LFS3_T_EXCL; +} + +static inline bool lfs3_t_ismkconsistent(uint32_t flags) { + (void)flags; + #ifndef LFS3_RDONLY + return flags & LFS3_T_MKCONSISTENT; + #else + return false; + #endif +} + +static inline bool lfs3_t_islookahead(uint32_t flags) { + (void)flags; + #ifndef LFS3_RDONLY + return flags & LFS3_T_LOOKAHEAD; + #else + return false; + #endif +} + +static inline bool lfs3_t_islookgbmap(uint32_t flags) { + (void)flags; + #if !defined(LFS3_RDONLY) && defined(LFS3_GBMAP) + return flags & LFS3_T_LOOKGBMAP; + #else + return false; + #endif +} + +static inline bool lfs3_t_compactmeta(uint32_t flags) { + (void)flags; + #ifndef LFS3_RDONLY + return flags & LFS3_T_COMPACTMETA; + #else + return false; + #endif +} + +static inline bool lfs3_t_isckmeta(uint32_t flags) { + (void)flags; + #ifndef LFS3_RDONLY + return flags & LFS3_T_CKMETA; + #else + return false; + #endif +} + +static inline bool lfs3_t_isckdata(uint32_t flags) { + (void)flags; + #ifndef LFS3_RDONLY + return flags & LFS3_T_CKDATA; + #else + return false; + #endif +} + +// internal traversal flags +static inline uint8_t lfs3_t_btype(uint32_t flags) { + return (flags >> 20) & 0xf; +} + +static inline uint32_t lfs3_t_btypeflags(uint8_t btype) { + return (uint32_t)btype << 20; +} + +static inline void lfs3_t_setbtype(uint32_t *flags, uint8_t btype) { + *flags = (*flags & ~LFS3_t_BTYPE) | lfs3_t_btypeflags(btype); +} + +static inline bool lfs3_t_isckpointed(uint32_t flags) { + return flags & LFS3_t_CKPOINTED; +} + +static inline bool lfs3_t_isdirty(uint32_t flags) { + return flags & LFS3_t_DIRTY; +} + +static inline bool lfs3_t_isstale(uint32_t flags) { + return flags & LFS3_t_STALE; +} + +// mount flags +static inline bool lfs3_m_isrdonly(uint32_t flags) { + (void)flags; + #ifndef LFS3_RDONLY + return flags & LFS3_M_RDONLY; + #else + return true; + #endif +} + +#ifdef LFS3_REVDBG +static inline bool lfs3_m_isrevdbg(uint32_t flags) { + (void)flags; + #ifdef LFS3_YES_REVDBG + return true; + #else + return flags & LFS3_M_REVDBG; + #endif +} +#endif + +#ifdef LFS3_REVNOISE +static inline bool lfs3_m_isrevnoise(uint32_t flags) { + (void)flags; + #ifdef LFS3_YES_REVNOISE + return true; + #else + return flags & LFS3_M_REVNOISE; + #endif +} +#endif + +#ifdef LFS3_CKPROGS +static inline bool lfs3_m_isckprogs(uint32_t flags) { + (void)flags; + #ifdef LFS3_YES_CKPROGS + return true; + #else + return flags & LFS3_M_CKPROGS; + #endif +} +#endif + +#ifdef LFS3_CKFETCHES +static inline bool lfs3_m_isckfetches(uint32_t flags) { + (void)flags; + #ifdef LFS3_YES_CKFETCHES + return true; + #else + return flags & LFS3_M_CKFETCHES; + #endif +} +#endif + +#ifdef LFS3_CKMETAPARITY +static inline bool lfs3_m_isckparity(uint32_t flags) { + (void)flags; + #ifdef LFS3_YES_CKMETAPARITY + return true; + #else + return flags & LFS3_M_CKMETAPARITY; + #endif +} +#endif + +#ifdef LFS3_CKDATACKSUMS +static inline bool lfs3_m_isckdatacksums(uint32_t flags) { + (void)flags; + #ifdef LFS3_YES_CKDATACKSUMS + return true; + #else + return flags & LFS3_M_CKDATACKSUMS; + #endif +} +#endif + +// format flags +#ifdef LFS3_GBMAP +static inline bool lfs3_f_isgbmap(uint32_t flags) { + (void)flags; + #ifdef LFS3_YES_GBMAP + return true; + #else + return flags & LFS3_F_GBMAP; + #endif +} +#endif + + + +/// Handles - opened mdir things /// + +// we maintain an invasive linked-list of all opened mdirs in order to +// keep metadata state in-sync +// +// each handle stores its type in the flags field, and can be casted to +// update type-specific state + +static bool lfs3_handle_isopen(const lfs3_t *lfs3, const lfs3_handle_t *h) { + for (lfs3_handle_t *h_ = lfs3->handles; h_; h_ = h_->next) { + if (h_ == h) { + return true; + } + } + + return false; +} + +static void lfs3_handle_open(lfs3_t *lfs3, lfs3_handle_t *h) { + LFS3_ASSERT(!lfs3_handle_isopen(lfs3, h)); + // add to opened list + h->next = lfs3->handles; + lfs3->handles = h; +} + +static bool lfs3_handle_close(lfs3_t *lfs3, lfs3_handle_t *h) { + // remove from opened list + for (lfs3_handle_t **h_ = &lfs3->handles; *h_; h_ = &(*h_)->next) { + if (*h_ == h) { + *h_ = (*h_)->next; + return true; + } + } + + return false; +} + +// check if a given mid is open +static bool lfs3_mid_isopen(const lfs3_t *lfs3, + lfs3_smid_t mid, uint32_t mask) { + for (lfs3_handle_t *h = lfs3->handles; h; h = h->next) { + // we really only care about regular open files here, all + // others are either transient (dirs) or fake (orphans) + if (lfs3_o_type(h->flags) == LFS3_TYPE_REG + && h->mdir.mid == mid + // allow caller to ignore files with specific flags + && !(h->flags & ~mask)) { + return true; + } + } + + return false; +} + +// traversal things +// +// we use the traversal handle itself as a cursor in the handle list, +// this avoids entangling too many pointers at the cost of needing more +// iterations through the handle list + +static void lfs3_handle_rewind(lfs3_t *lfs3, lfs3_handle_t *h) { + bool entangled = lfs3_handle_close(lfs3, h); + h->next = lfs3->handles; + if (entangled) { + lfs3->handles = h; + } +} + +// seek _after_ h_ +static void lfs3_handle_seek(lfs3_t *lfs3, lfs3_handle_t *h, + lfs3_handle_t **h_) { + bool entangled = lfs3_handle_close(lfs3, h); + h->next = *h_; + if (entangled) { + *h_ = h; + } +} + + + +/// Global-state things /// + +// grm (global remove) things +static inline lfs3_size_t lfs3_grm_count_(const lfs3_grm_t *grm) { + return (grm->queue[0] != 0) + (grm->queue[1] != 0); +} + +static inline lfs3_size_t lfs3_grm_count(const lfs3_t *lfs3) { + return lfs3_grm_count_(&lfs3->grm); +} + +#ifndef LFS3_RDONLY +static inline void lfs3_grm_push(lfs3_t *lfs3, lfs3_smid_t mid) { + // note mid=0.0 always maps to the root bookmark and should never + // be grmed + LFS3_ASSERT(mid != 0); + LFS3_ASSERT(lfs3->grm.queue[1] == 0); + lfs3->grm.queue[1] = lfs3->grm.queue[0]; + lfs3->grm.queue[0] = mid; +} +#endif + +#ifndef LFS3_RDONLY +static inline lfs3_smid_t lfs3_grm_pop(lfs3_t *lfs3) { + lfs3_smid_t mid = lfs3->grm.queue[0]; + lfs3->grm.queue[0] = lfs3->grm.queue[1]; + lfs3->grm.queue[1] = 0; + return mid; +} +#endif + +static inline bool lfs3_grm_ismidrm(const lfs3_t *lfs3, lfs3_smid_t mid) { + return mid != 0 + && (lfs3->grm.queue[0] == mid + || lfs3->grm.queue[1] == mid); +} + +#ifndef LFS3_RDONLY +static lfs3_data_t lfs3_data_fromgrm(const lfs3_grm_t *grm, + uint8_t buffer[static LFS3_GRM_DSIZE]) { + // make sure to zero so we don't leak any info + lfs3_memset(buffer, 0, LFS3_GRM_DSIZE); + + // encode grms + lfs3_size_t count = lfs3_grm_count_(grm); + lfs3_ssize_t d = 0; + for (lfs3_size_t i = 0; i < count; i++) { + lfs3_ssize_t d_ = lfs3_toleb128(grm->queue[i], &buffer[d], 5); + if (d_ < 0) { + LFS3_UNREACHABLE(); + } + d += d_; + } + + return LFS3_DATA_BUF(buffer, lfs3_memlen(buffer, LFS3_GRM_DSIZE)); +} +#endif + +// required by lfs3_data_readgrm +static inline lfs3_mid_t lfs3_mtree_weight(lfs3_t *lfs3); + +static int lfs3_data_readgrm(lfs3_t *lfs3, lfs3_data_t *data, + lfs3_grm_t *grm) { + // clear first + grm->queue[0] = 0; + grm->queue[1] = 0; + + // decode grms, these are terminated by either a null (mid=0) or the + // size of the grm buffer + for (lfs3_size_t i = 0; i < 2; i++) { + lfs3_mid_t mid; + int err = lfs3_data_readleb128(lfs3, data, &mid); + if (err) { + return err; + } + + // null grm? + if (!mid) { + break; + } + + // grm inside mtree? + LFS3_ASSERT(mid < lfs3_mtree_weight(lfs3)); + grm->queue[i] = mid; + } + + return 0; +} + +// predeclarations of other gstate, needed below +#if !defined(LFS3_RDONLY) && defined(LFS3_GBMAP) +static lfs3_data_t lfs3_data_fromgbmap(const lfs3_gbmap_t *gbmap, + uint8_t buffer[static LFS3_GBMAP_DSIZE]); +#endif +#ifdef LFS3_GBMAP +static int lfs3_data_readgbmap(lfs3_t *lfs3, lfs3_data_t *data, + lfs3_gbmap_t *gbmap); +#endif + + +// some mdir-related gstate things we need + +// zero any pending gdeltas +static void lfs3_fs_zerogdelta(lfs3_t *lfs3) { + // TODO one cool trick would be to make these all contiguous so + // zeroing is one memset + + // zero the gcksumdelta + lfs3->gcksum_d = 0; + + // zero the grmdelta + lfs3_memset(lfs3->grm_d, 0, LFS3_GRM_DSIZE); + + // zero the gbmapdelta + // + // note we do this unconditionally! before figuring out if littlefs + // actually configured to use the gbmap + #ifdef LFS3_GBMAP + lfs3_memset(lfs3->gbmap_d, 0, LFS3_GBMAP_DSIZE); + #endif +} + +// commit any pending gdeltas +#ifndef LFS3_RDONLY +static void lfs3_fs_commitgdelta(lfs3_t *lfs3) { + // keep track of the on-disk gcksum + lfs3->gcksum_p = lfs3->gcksum; + + // keep track of the on-disk grm + lfs3_data_fromgrm(&lfs3->grm, lfs3->grm_p); + + #ifdef LFS3_GBMAP + // keep track of the on-disk gbmap + if (lfs3_f_isgbmap(lfs3->flags)) { + // keep track of both the committed gstate and btree for + // traversals + lfs3->gbmap.b_p = lfs3->gbmap.b; + lfs3_data_fromgbmap(&lfs3->gbmap, lfs3->gbmap_p); + + // if disabled, we still want to keep track of the on-disk gstate + // in case the user wants to re-enable the gbmap + } else { + lfs3_memxor(lfs3->gbmap_p, lfs3->gbmap_d, LFS3_GBMAP_DSIZE); + } + #endif +} +#endif + +// revert gstate to on-disk state +#ifndef LFS3_RDONLY +static void lfs3_fs_revertgdelta(lfs3_t *lfs3) { + // revert to the on-disk gcksum + lfs3->gcksum = lfs3->gcksum_p; + + // revert to the on-disk grm + int err = lfs3_data_readgrm(lfs3, + &LFS3_DATA_BUF(lfs3->grm_p, LFS3_GRM_DSIZE), + &lfs3->grm); + if (err) { + LFS3_UNREACHABLE(); + } + + // note we do _not_ revert the on-disk gbmap + // + // if we did, any in-flight state would be lost +} +#endif + +// append and consume any pending gstate +#ifndef LFS3_RDONLY +static int lfs3_rbyd_appendgdelta(lfs3_t *lfs3, lfs3_rbyd_t *rbyd) { + // note gcksums are a special case and handled directly in + // lfs3_mdir_commit___/lfs3_rbyd_appendcksum_ + + // pending grm state? + uint8_t grmdelta_[LFS3_GRM_DSIZE]; + lfs3_data_fromgrm(&lfs3->grm, grmdelta_); + lfs3_memxor(grmdelta_, lfs3->grm_p, LFS3_GRM_DSIZE); + lfs3_memxor(grmdelta_, lfs3->grm_d, LFS3_GRM_DSIZE); + + if (lfs3_memlen(grmdelta_, LFS3_GRM_DSIZE) != 0) { + // make sure to xor any existing delta + lfs3_data_t data; + lfs3_stag_t tag = lfs3_rbyd_lookup(lfs3, rbyd, -1, LFS3_TAG_GRMDELTA, + &data); + if (tag < 0 && tag != LFS3_ERR_NOENT) { + return tag; + } + + uint8_t grmdelta[LFS3_GRM_DSIZE]; + lfs3_memset(grmdelta, 0, LFS3_GRM_DSIZE); + if (tag != LFS3_ERR_NOENT) { + lfs3_ssize_t d = lfs3_data_read(lfs3, &data, + grmdelta, LFS3_GRM_DSIZE); + if (d < 0) { + return d; + } + } + + lfs3_memxor(grmdelta_, grmdelta, LFS3_GRM_DSIZE); + + // append to our rbyd, replacing any existing delta + lfs3_size_t size = lfs3_memlen(grmdelta_, LFS3_GRM_DSIZE); + int err = lfs3_rbyd_appendrattr(lfs3, rbyd, -1, LFS3_RATTR_BUF( + // opportunistically remove this tag if delta is all zero + (size == 0) + ? LFS3_tag_RM | LFS3_TAG_GRMDELTA + : LFS3_TAG_GRMDELTA, 0, + grmdelta_, size)); + if (err) { + return err; + } + } + + // pending gbmap state? + #ifdef LFS3_GBMAP + if (lfs3_f_isgbmap(lfs3->flags)) { + // lookahead and gbmap window offsets should always be in sync + // + // we probably don't need the duplicate fields, but it certainly + // makes the code simpler + LFS3_ASSERT(lfs3->gbmap.window + == (lfs3->lookahead.window + lfs3->lookahead.off) + % lfs3->block_count); + + uint8_t gbmapdelta_[LFS3_GBMAP_DSIZE]; + lfs3_data_fromgbmap(&lfs3->gbmap, gbmapdelta_); + lfs3_memxor(gbmapdelta_, lfs3->gbmap_p, LFS3_GBMAP_DSIZE); + lfs3_memxor(gbmapdelta_, lfs3->gbmap_d, LFS3_GBMAP_DSIZE); + + if (lfs3_memlen(gbmapdelta_, LFS3_GBMAP_DSIZE) != 0) { + // make sure to xor any existing delta + lfs3_data_t data; + lfs3_stag_t tag = lfs3_rbyd_lookup(lfs3, rbyd, + -1, LFS3_TAG_GBMAPDELTA, + &data); + if (tag < 0 && tag != LFS3_ERR_NOENT) { + return tag; + } + + uint8_t gbmapdelta[LFS3_GBMAP_DSIZE]; + lfs3_memset(gbmapdelta, 0, LFS3_GBMAP_DSIZE); + if (tag != LFS3_ERR_NOENT) { + lfs3_ssize_t d = lfs3_data_read(lfs3, &data, + gbmapdelta, LFS3_GBMAP_DSIZE); + if (d < 0) { + return d; + } + } + + lfs3_memxor(gbmapdelta_, gbmapdelta, LFS3_GBMAP_DSIZE); + + // append to our rbyd, replacing any existing delta + lfs3_size_t size = lfs3_memlen(gbmapdelta_, LFS3_GBMAP_DSIZE); + int err = lfs3_rbyd_appendrattr(lfs3, rbyd, -1, LFS3_RATTR_BUF( + // opportunistically remove this tag if delta is all zero + (size == 0) + ? LFS3_tag_RM | LFS3_TAG_GBMAPDELTA + : LFS3_TAG_GBMAPDELTA, 0, + gbmapdelta_, size)); + if (err) { + return err; + } + } + } + #endif + + return 0; +} +#endif + +static int lfs3_fs_consumegdelta(lfs3_t *lfs3, const lfs3_mdir_t *mdir) { + // consume any gcksum deltas + lfs3->gcksum_d ^= mdir->gcksumdelta; + + // consume any grm deltas + lfs3_data_t data; + lfs3_stag_t tag = lfs3_rbyd_lookup(lfs3, &mdir->r, -1, LFS3_TAG_GRMDELTA, + &data); + if (tag < 0 && tag != LFS3_ERR_NOENT) { + return tag; + } + + if (tag != LFS3_ERR_NOENT) { + uint8_t grmdelta[LFS3_GRM_DSIZE]; + lfs3_ssize_t d = lfs3_data_read(lfs3, &data, + grmdelta, LFS3_GRM_DSIZE); + if (d < 0) { + return d; + } + + lfs3_memxor(lfs3->grm_d, grmdelta, d); + } + + // consume any gbmap deltas + // + // note we do this unconditionally! before figuring out if littlefs + // actually configured to use the gbmap + #ifdef LFS3_GBMAP + tag = lfs3_rbyd_lookup(lfs3, &mdir->r, -1, LFS3_TAG_GBMAPDELTA, + &data); + if (tag != LFS3_ERR_NOENT) { + uint8_t gbmapdelta[LFS3_GBMAP_DSIZE]; + lfs3_ssize_t d = lfs3_data_read(lfs3, &data, + gbmapdelta, LFS3_GBMAP_DSIZE); + if (d < 0) { + return d; + } + + lfs3_memxor(lfs3->gbmap_d, gbmapdelta, d); + } + #endif + + return 0; +} + + + +/// Revision count things /// + +// in mdirs, our revision count is broken down into 1-4 parts: +// +// vvvv---- -------- -------- -------- +// vvvvrrrr rrrrrr-- -------- -------- +// vvvvrrrr rrrrrrnn nnnnnnnn nnnnnnnn +// vvvvrrrr rrrrrrnn nnnnnnnn dddddddd +// '-.''----.----''----.- - - '---.--' +// '------|----------|----------|---- 4-bit relocation revision +// '----------|----------|---- recycle-bits recycle counter +// '----------|---- pseudorandom noise (if revnoise) +// '---- h, i, m, or b (if revdbg) +// -11-1--- - h = mroot anchor +// -11-1--1 - i = mroot +// -11-11-1 - m = mdir +// -11---1- - b = btree node +// + +// needed in lfs3_rev_init +static inline bool lfs3_mdir_ismrootanchor(const lfs3_mdir_t *mdir); +static inline int lfs3_mdir_cmp(const lfs3_mdir_t *a, const lfs3_mdir_t *b); + +#ifndef LFS3_RDONLY +static inline uint32_t lfs3_rev_init(lfs3_t *lfs3, const lfs3_mdir_t *mdir, + uint32_t rev) { + (void)lfs3; + (void)mdir; + // we really only care about the top revision bits here + rev &= ~((1 << 28)-1); + // increment revision + rev += 1 << 28; + // xor in pseudorandom noise? + #ifdef LFS3_REVNOISE + if (lfs3_m_isrevnoise(lfs3->flags)) { + rev ^= ((1 << (28-lfs3_smax(lfs3->recycle_bits, 0)))-1) + // we need to use gcksum_p because we have be in the + // middle of updating the gcksum + & lfs3->gcksum_p; + } + #endif + // include debug bits? + #ifdef LFS3_REVDBG + if (lfs3_m_isrevdbg(lfs3->flags)) { + uint32_t mask = (1 << (28-lfs3_smax(lfs3->recycle_bits, 0)))-1; + // mroot anchor? + if (lfs3_mdir_ismrootanchor(mdir)) { + rev = (rev & ~(mask & 0xff)) | (mask & 0x68); + // mroot? + } else if (mdir->mid <= -1 + || lfs3_mdir_cmp(mdir, &lfs3->mroot) == 0) { + rev = (rev & ~(mask & 0xff)) | (mask & 0x69); + // mdir? + } else { + rev = (rev & ~(mask & 0xff)) | (mask & 0x6d); + } + } + #endif + return rev; +} +#endif + +// btrees don't normally need revision counts, but we make use of them +// if revdbg or revnoise is enabled +#ifndef LFS3_RDONLY +static inline uint32_t lfs3_rev_btree(lfs3_t *lfs3) { + (void)lfs3; + uint32_t rev = 0; + // xor in pseudorandom noise? + #ifdef LFS3_REVNOISE + if (lfs3_m_isrevnoise(lfs3->flags)) { + // keep the top nibble zero + rev ^= 0x0fffffff + // we need to use gcksum_p because we have be in the + // middle of updating the gcksum + & lfs3->gcksum_p; + } + #endif + // include debug bits? + #ifdef LFS3_REVDBG + if (lfs3_m_isrevdbg(lfs3->flags)) { + rev = (rev & ~0xff) | 0x62; + } + #endif + return rev; +} +#endif + +#ifndef LFS3_RDONLY +static inline bool lfs3_rev_needsrelocation(lfs3_t *lfs3, uint32_t rev) { + if (lfs3->recycle_bits == -1) { + return false; + } + + // does out recycle counter overflow? + uint32_t rev_ = rev + (1 << (28-lfs3_smax(lfs3->recycle_bits, 0))); + return (rev_ >> 28) != (rev >> 28); +} +#endif + +#ifndef LFS3_RDONLY +static inline uint32_t lfs3_rev_inc(lfs3_t *lfs3, const lfs3_mdir_t *mdir, + uint32_t rev) { + (void)mdir; + // increment recycle counter/revision + rev += 1 << (28-lfs3_smax(lfs3->recycle_bits, 0)); + // xor in pseudorandom noise? + #ifdef LFS3_REVNOISE + if (lfs3_m_isrevnoise(lfs3->flags)) { + rev ^= ((1 << (28-lfs3_smax(lfs3->recycle_bits, 0)))-1) + // we need to use gcksum_p because we have be in the + // middle of updating the gcksum + & lfs3->gcksum_p; + } + #endif + // include debug bits? + #ifdef LFS3_REVDBG + if (lfs3_m_isrevdbg(lfs3->flags)) { + uint32_t mask = (1 << (28-lfs3_smax(lfs3->recycle_bits, 0)))-1; + // mroot anchor? + if (lfs3_mdir_ismrootanchor(mdir)) { + rev = (rev & ~(mask & 0xff)) | (mask & 0x68); + // mroot? + } else if (mdir->mid <= -1 + || lfs3_mdir_cmp(mdir, &lfs3->mroot) == 0) { + rev = (rev & ~(mask & 0xff)) | (mask & 0x69); + // mdir? + } else { + rev = (rev & ~(mask & 0xff)) | (mask & 0x6d); + } + } + #endif + return rev; +} +#endif + + + +/// Metadata-pair stuff /// + +// mdir convenience functions +#ifndef LFS3_RDONLY +static inline void lfs3_mdir_claim(lfs3_mdir_t *mdir) { + // mark erased state as invalid, we only fallback on this if a + // commit fails, and at that point it's unlikely we'll be able to + // reuse the block + mdir->r.eoff = -1; +} +#endif + +static inline int lfs3_mdir_cmp(const lfs3_mdir_t *a, const lfs3_mdir_t *b) { + return lfs3_mptr_cmp(a->r.blocks, b->r.blocks); +} + +static inline bool lfs3_mdir_ismrootanchor(const lfs3_mdir_t *mdir) { + return lfs3_mptr_ismrootanchor(mdir->r.blocks); +} + +static inline void lfs3_mdir_sync(lfs3_mdir_t *a, const lfs3_mdir_t *b) { + // copy over everything but the mid + a->r = b->r; + a->gcksumdelta = b->gcksumdelta; +} + +// mdir operations +static int lfs3_mdir_fetch(lfs3_t *lfs3, lfs3_mdir_t *mdir, + lfs3_smid_t mid, const lfs3_block_t mptr[static 2]) { + // create a copy of the mptr, both so we can swap the blocks to keep + // track of the current revision, and to prevents issues if mptr + // references the blocks in the mdir + lfs3_block_t blocks[2] = {mptr[0], mptr[1]}; + // read both revision counts, try to figure out which block + // has the most recent revision + uint32_t revs[2] = {0, 0}; + for (int i = 0; i < 2; i++) { + int err = lfs3_bd_read(lfs3, blocks[0], 0, 0, + &revs[0], sizeof(uint32_t)); + if (err && err != LFS3_ERR_CORRUPT) { + return err; + } + revs[i] = lfs3_fromle32(&revs[i]); + + if (i == 0 + || err == LFS3_ERR_CORRUPT + || lfs3_scmp(revs[1], revs[0]) > 0) { + LFS3_SWAP(lfs3_block_t, &blocks[0], &blocks[1]); + LFS3_SWAP(uint32_t, &revs[0], &revs[1]); + } + } + + // try to fetch rbyds in the order of most recent to least recent + for (int i = 0; i < 2; i++) { + int err = lfs3_rbyd_fetch_(lfs3, + &mdir->r, &mdir->gcksumdelta, + blocks[0], 0); + if (err && err != LFS3_ERR_CORRUPT) { + return err; + } + + if (err != LFS3_ERR_CORRUPT) { + mdir->mid = mid; + // keep track of other block for compactions + mdir->r.blocks[1] = blocks[1]; + #ifdef LFS3_DBGMDIRFETCHES + LFS3_DEBUG("Fetched mdir %"PRId32" " + "0x{%"PRIx32",%"PRIx32"}.%"PRIx32" w%"PRId32", " + "cksum %"PRIx32, + lfs3_dbgmbid(lfs3, mdir->mid), + mdir->r.blocks[0], mdir->r.blocks[1], + lfs3_rbyd_trunk(&mdir->r), + mdir->r.weight, + mdir->r.cksum); + #endif + return 0; + } + + LFS3_SWAP(lfs3_block_t, &blocks[0], &blocks[1]); + LFS3_SWAP(uint32_t, &revs[0], &revs[1]); + } + + // could not find a non-corrupt rbyd + return LFS3_ERR_CORRUPT; +} + +static int lfs3_data_fetchmdir(lfs3_t *lfs3, + lfs3_data_t *data, lfs3_smid_t mid, + lfs3_mdir_t *mdir) { + // decode mptr and fetch + int err = lfs3_data_readmptr(lfs3, data, + mdir->r.blocks); + if (err) { + return err; + } + + return lfs3_mdir_fetch(lfs3, mdir, mid, mdir->r.blocks); +} + +static lfs3_tag_t lfs3_mdir_nametag(const lfs3_t *lfs3, const lfs3_mdir_t *mdir, + lfs3_smid_t mid, lfs3_tag_t tag) { + (void)mdir; + // intercept pending grms here and pretend they're orphaned + // stickynotes + // + // fortunately pending grms/orphaned stickynotes have roughly the + // same semantics, and this makes it easier to manage the implied + // mid gap in higher-levels + if (lfs3_grm_ismidrm(lfs3, mid)) { + return LFS3_tag_ORPHAN; + + // if we find a stickynote, check to see if there are any open + // in-sync file handles to decide if it really exists + } else if (tag == LFS3_TAG_STICKYNOTE + && !lfs3_mid_isopen(lfs3, mid, + ~LFS3_o_ZOMBIE & ~LFS3_O_DESYNC)) { + return LFS3_tag_ORPHAN; + + // map unknown types -> LFS3_tag_UNKNOWN, this simplifies higher + // levels and prevents collisions with internal types + // + // Note future types should probably come with WCOMPAT flags, and be + // at least reported on non-supporting filesystems + } else if (tag < LFS3_TAG_REG || tag > LFS3_TAG_BOOKMARK) { + return LFS3_tag_UNKNOWN; + } + + return tag; +} + +static lfs3_stag_t lfs3_mdir_lookupnext(lfs3_t *lfs3, const lfs3_mdir_t *mdir, + lfs3_tag_t tag, + lfs3_data_t *data_) { + lfs3_srid_t rid__; + lfs3_stag_t tag__ = lfs3_rbyd_lookupnext(lfs3, &mdir->r, + lfs3_mrid(lfs3, mdir->mid), tag, + &rid__, NULL, data_); + if (tag__ < 0) { + return tag__; + } + + // this is very similar to lfs3_rbyd_lookupnext, but we error if + // lookupnext would change mids + if (rid__ != lfs3_mrid(lfs3, mdir->mid)) { + return LFS3_ERR_NOENT; + } + + // map name tags to understood types + if (lfs3_tag_suptype(tag__) == LFS3_TAG_NAME) { + tag__ = lfs3_mdir_nametag(lfs3, mdir, mdir->mid, tag__); + } + + return tag__; +} + +static lfs3_stag_t lfs3_mdir_lookup(lfs3_t *lfs3, const lfs3_mdir_t *mdir, + lfs3_tag_t tag, + lfs3_data_t *data_) { + lfs3_stag_t tag__ = lfs3_mdir_lookupnext(lfs3, mdir, lfs3_tag_key(tag), + data_); + if (tag__ < 0) { + return tag__; + } + + // lookup finds the next-smallest tag, all we need to do is fail if it + // picks up the wrong tag + if ((tag__ & lfs3_tag_mask(tag)) != (tag & lfs3_tag_mask(tag))) { + return LFS3_ERR_NOENT; + } + + return tag__; +} + + + +/// Metadata-tree things /// + +static inline lfs3_mid_t lfs3_mtree_weight(lfs3_t *lfs3) { + return lfs3_max(lfs3->mtree.r.weight, 1 << lfs3->mbits); +} + +// lookup mdir containing a given mid +static int lfs3_mtree_lookup(lfs3_t *lfs3, lfs3_smid_t mid, + lfs3_mdir_t *mdir_) { + // looking up mid=-1 is probably a mistake + LFS3_ASSERT(mid >= 0); + + // out of bounds? + if ((lfs3_mid_t)mid >= lfs3_mtree_weight(lfs3)) { + return LFS3_ERR_NOENT; + } + + // looking up mroot? + if (lfs3->mtree.r.weight == 0) { + // treat inlined mdir as mid=0 + mdir_->mid = mid; + lfs3_mdir_sync(mdir_, &lfs3->mroot); + return 0; + + // look up mdir in actual mtree + } else { + lfs3_bid_t bid; + lfs3_srid_t rid; + lfs3_bid_t weight; + lfs3_data_t data; + lfs3_stag_t tag = lfs3_btree_lookupnext_(lfs3, &lfs3->mtree, mid, + &bid, &mdir_->r, &rid, &weight, &data); + if (tag < 0) { + LFS3_ASSERT(tag != LFS3_ERR_NOENT); + return tag; + } + LFS3_ASSERT((lfs3_sbid_t)bid == lfs3_mbid(lfs3, mid)); + LFS3_ASSERT(weight == (lfs3_bid_t)(1 << lfs3->mbits)); + LFS3_ASSERT(tag == LFS3_TAG_MNAME + || tag == LFS3_TAG_MDIR); + + // if we found an mname, lookup the mdir + if (tag == LFS3_TAG_MNAME) { + tag = lfs3_rbyd_lookup(lfs3, &mdir_->r, rid, LFS3_TAG_MDIR, + &data); + if (tag < 0) { + LFS3_ASSERT(tag != LFS3_ERR_NOENT); + return tag; + } + } + + // fetch mdir + return lfs3_data_fetchmdir(lfs3, &data, mid, + mdir_); + } +} + +#ifndef LFS3_RDONLY +static int lfs3_mtree_commit(lfs3_t *lfs3, lfs3_btree_t *mtree, + lfs3_bid_t bid, const lfs3_rattr_t *rattrs, lfs3_size_t rattr_count) { + return lfs3_btree_commit(lfs3, mtree, bid, rattrs, rattr_count); +} +#endif + + + +/// Mdir commit logic /// + +// this is the gooey atomic center of littlefs +// +// any mutation must go through lfs3_mdir_commit to persist on disk +// +// this makes lfs3_mdir_commit also responsible for propagating changes +// up through the mtree/mroot chain, and through any internal structures, +// making lfs3_mdir_commit quite involved and a bit of a mess. + +// low-level mdir operations needed by lfs3_mdir_commit +#ifndef LFS3_RDONLY +static int lfs3_mdir_alloc___(lfs3_t *lfs3, lfs3_mdir_t *mdir, + lfs3_smid_t mid, bool partial) { + // assign the mid + mdir->mid = mid; + // default to zero gcksumdelta + mdir->gcksumdelta = 0; + + if (!partial) { + // allocate one block without an erase + lfs3_sblock_t block = lfs3_alloc(lfs3, 0); + if (block < 0) { + return block; + } + mdir->r.blocks[1] = block; + } + + // read the new revision count + // + // we use whatever is on-disk to avoid needing to rewrite the + // redund block + uint32_t rev; + int err = lfs3_bd_read(lfs3, mdir->r.blocks[1], 0, 0, + &rev, sizeof(uint32_t)); + if (err && err != LFS3_ERR_CORRUPT) { + return err; + } + // note we allow corrupt errors here, as long as they are consistent + rev = (err != LFS3_ERR_CORRUPT) ? lfs3_fromle32(&rev) : 0; + // reset recycle bits in revision count and increment + rev = lfs3_rev_init(lfs3, mdir, rev); + +relocate:; + // allocate another block with an erase + lfs3_sblock_t block = lfs3_alloc(lfs3, LFS3_ALLOC_ERASE); + if (block < 0) { + return block; + } + mdir->r.blocks[0] = block; + mdir->r.weight = 0; + mdir->r.trunk = 0; + mdir->r.eoff = 0; + mdir->r.cksum = 0; + + // write our revision count + err = lfs3_rbyd_appendrev(lfs3, &mdir->r, rev); + if (err) { + // bad prog? try another block + if (err == LFS3_ERR_CORRUPT) { + goto relocate; + } + return err; + } + + return 0; +} +#endif + +#ifndef LFS3_RDONLY +static int lfs3_mdir_swap___(lfs3_t *lfs3, lfs3_mdir_t *mdir_, + const lfs3_mdir_t *mdir, bool force) { + // assign the mid + mdir_->mid = mdir->mid; + // reset to zero gcksumdelta, upper layers should handle this + mdir_->gcksumdelta = 0; + + // first thing we need to do is read our current revision count + uint32_t rev; + int err = lfs3_bd_read(lfs3, mdir->r.blocks[0], 0, 0, + &rev, sizeof(uint32_t)); + if (err && err != LFS3_ERR_CORRUPT) { + return err; + } + // note we allow corrupt errors here, as long as they are consistent + rev = (err != LFS3_ERR_CORRUPT) ? lfs3_fromle32(&rev) : 0; + // increment our revision count + rev = lfs3_rev_inc(lfs3, mdir_, rev); + + // decide if we need to relocate + if (!force && lfs3_rev_needsrelocation(lfs3, rev)) { + return LFS3_ERR_NOSPC; + } + + // swap our blocks + mdir_->r.blocks[0] = mdir->r.blocks[1]; + mdir_->r.blocks[1] = mdir->r.blocks[0]; + mdir_->r.weight = 0; + mdir_->r.trunk = 0; + mdir_->r.eoff = 0; + mdir_->r.cksum = 0; + + // erase, preparing for compact + err = lfs3_bd_erase(lfs3, mdir_->r.blocks[0]); + if (err) { + return err; + } + + // increment our revision count and write it to our rbyd + err = lfs3_rbyd_appendrev(lfs3, &mdir_->r, rev); + if (err) { + return err; + } + + return 0; +} +#endif + +// low-level mdir commit, does not handle mtree/mlist/compaction/etc +#ifndef LFS3_RDONLY +static int lfs3_mdir_commit___(lfs3_t *lfs3, lfs3_mdir_t *mdir_, + lfs3_srid_t start_rid, lfs3_srid_t end_rid, + lfs3_smid_t mid, const lfs3_rattr_t *rattrs, lfs3_size_t rattr_count) { + // since we only ever commit to one mid or split, we can ignore the + // entire rattr-list if our mid is out of range + lfs3_srid_t rid = lfs3_mrid(lfs3, mid); + if (rid >= start_rid + // note the use of rid+1 and unsigned comparison here to + // treat end_rid=-1 as "unbounded" in such a way that rid=-1 + // is still included + && (lfs3_size_t)(rid + 1) <= (lfs3_size_t)end_rid) { + + for (lfs3_size_t i = 0; i < rattr_count; i++) { + // we just happen to never split in an mdir commit + LFS3_ASSERT(!(i > 0 && lfs3_rattr_isinsert(rattrs[i]))); + + // rattr lists can be chained, but only tail-recursively + if (rattrs[i].tag == LFS3_tag_RATTRS) { + // must be the last tag + LFS3_ASSERT(i == rattr_count-1); + const lfs3_rattr_t *rattrs_ = rattrs[i].u.etc; + lfs3_size_t rattr_count_ = rattrs[i].count; + + // switch to chained rattr-list + rattrs = rattrs_; + rattr_count = rattr_count_; + i = -1; + continue; + + // shrub tags append a set of attributes to an unrelated trunk + // in our rbyd + } else if (rattrs[i].tag == LFS3_tag_SHRUBCOMMIT) { + const lfs3_shrubcommit_t *shrubcommit = rattrs[i].u.etc; + lfs3_bshrub_t *bshrub_ = shrubcommit->bshrub; + lfs3_srid_t rid_ = shrubcommit->rid; + const lfs3_rattr_t *rattrs_ = shrubcommit->rattrs; + lfs3_size_t rattr_count_ = shrubcommit->rattr_count; + + // reset shrub if it doesn't live in our block, this happens + // when converting from a btree + if (!lfs3_bshrub_isbshrub(bshrub_)) { + bshrub_->b_.blocks[0] = mdir_->r.blocks[0]; + bshrub_->b_.trunk = LFS3_RBYD_ISSHRUB | 0; + bshrub_->b_.weight = 0; + } + + // commit to shrub + int err = lfs3_shrub_commit(lfs3, + &mdir_->r, &bshrub_->b_, + rid_, rattrs_, rattr_count_); + if (err) { + return err; + } + + // push a new grm, this tag lets us push grms atomically when + // creating new mids + } else if (rattrs[i].tag == LFS3_tag_GRMPUSH) { + // do nothing here, this is handled up in lfs3_mdir_commit + + // move tags copy over any tags associated with the source's rid + // TODO can this be deduplicated with lfs3_mdir_compact___ more? + // it _really_ wants to be deduplicated + } else if (rattrs[i].tag == LFS3_tag_MOVE) { + const lfs3_mdir_t *mdir__ = rattrs[i].u.etc; + + // skip the name tag, this is always replaced by upper layers + lfs3_stag_t tag = LFS3_TAG_STRUCT-1; + while (true) { + lfs3_data_t data; + tag = lfs3_mdir_lookupnext(lfs3, mdir__, tag+1, + &data); + if (tag < 0) { + if (tag == LFS3_ERR_NOENT) { + break; + } + return tag; + } + + // found an inlined shrub? we need to compact the shrub + // as well to bring it along with us + if (tag == LFS3_TAG_BSHRUB) { + lfs3_shrub_t shrub; + int err = lfs3_data_readshrub(lfs3, mdir__, &data, + &shrub); + if (err) { + return err; + } + + // compact our shrub + err = lfs3_shrub_compact(lfs3, &mdir_->r, &shrub, + &shrub); + if (err) { + return err; + } + + // write our new shrub tag + err = lfs3_rbyd_appendrattr(lfs3, &mdir_->r, + rid - lfs3_smax(start_rid, 0), + LFS3_RATTR_SHRUB(LFS3_TAG_BSHRUB, 0, &shrub)); + if (err) { + return err; + } + + // append the rattr + } else { + int err = lfs3_rbyd_appendrattr(lfs3, &mdir_->r, + rid - lfs3_smax(start_rid, 0), + LFS3_RATTR_DATA(tag, 0, &data)); + if (err) { + return err; + } + } + } + + // we're not quite done! we also need to bring over any + // unsynced files + for (lfs3_handle_t *h = lfs3->handles; h; h = h->next) { + if (lfs3_o_type(h->flags) == LFS3_TYPE_REG + // belongs to our mid? + && h->mdir.mid == mdir__->mid + // is a bshrub? + && lfs3_bshrub_isbshrub((lfs3_bshrub_t*)h) + // only compact once, first compact should + // stage the new block + && ((lfs3_bshrub_t*)h)->b_.blocks[0] + != mdir_->r.blocks[0]) { + int err = lfs3_shrub_compact(lfs3, &mdir_->r, + &((lfs3_bshrub_t*)h)->b_, + &((lfs3_bshrub_t*)h)->b.r); + if (err) { + return err; + } + } + } + + // custom attributes need to be reencoded into our tag format + } else if (rattrs[i].tag == LFS3_tag_ATTRS) { + const struct lfs3_attr *attrs_ = rattrs[i].u.etc; + lfs3_size_t attr_count_ = rattrs[i].count; + + for (lfs3_size_t j = 0; j < attr_count_; j++) { + // skip readonly attrs and lazy attrs + if (lfs3_o_isrdonly(attrs_[j].flags)) { + continue; + } + + // first lets check if the attr changed, we don't want + // to append attrs unless we have to + lfs3_data_t data; + lfs3_stag_t tag = lfs3_mdir_lookup(lfs3, mdir_, + LFS3_TAG_ATTR(attrs_[j].type), + &data); + if (tag < 0 && tag != LFS3_ERR_NOENT) { + return tag; + } + + // does disk match our attr? + lfs3_scmp_t cmp = lfs3_attr_cmp(lfs3, &attrs_[j], + (tag != LFS3_ERR_NOENT) ? &data : NULL); + if (cmp < 0) { + return cmp; + } + + if (cmp == LFS3_CMP_EQ) { + continue; + } + + // append the custom attr + int err = lfs3_rbyd_appendrattr(lfs3, &mdir_->r, + rid - lfs3_smax(start_rid, 0), + // removing or updating? + (lfs3_attr_isnoattr(&attrs_[j])) + ? LFS3_RATTR( + LFS3_tag_RM + | LFS3_TAG_ATTR(attrs_[j].type), 0) + : LFS3_RATTR_DATA( + LFS3_TAG_ATTR(attrs_[j].type), 0, + &LFS3_DATA_BUF( + attrs_[j].buffer, + lfs3_attr_size(&attrs_[j])))); + if (err) { + return err; + } + } + + // write out normal tags normally + } else { + LFS3_ASSERT(!lfs3_tag_isinternal(rattrs[i].tag)); + + int err = lfs3_rbyd_appendrattr(lfs3, &mdir_->r, + rid - lfs3_smax(start_rid, 0), + rattrs[i]); + if (err) { + return err; + } + } + + // adjust rid + rid = lfs3_rattr_nextrid(rattrs[i], rid); + } + } + + // abort the commit if our weight dropped to zero! + // + // If we finish the commit it becomes immediately visible, but we really + // need to atomically remove this mdir from the mtree. Leave the actual + // remove up to upper layers. + if (mdir_->r.weight == 0 + // unless we are an mroot + && !(mdir_->mid <= -1 + || lfs3_mdir_cmp(mdir_, &lfs3->mroot) == 0)) { + // note! we can no longer read from this mdir as our pcache may + // be clobbered + return LFS3_ERR_NOENT; + } + + // append any gstate? + if (start_rid <= -2) { + int err = lfs3_rbyd_appendgdelta(lfs3, &mdir_->r); + if (err) { + return err; + } + } + + // save our canonical cksum + // + // note this is before we calculate gcksumdelta, otherwise + // everything would get all self-referential + uint32_t cksum = mdir_->r.cksum; + + // append gkcsumdelta? + if (start_rid <= -2) { + // figure out changes to our gcksumdelta + mdir_->gcksumdelta ^= lfs3_crc32c_cube(lfs3->gcksum_p) + ^ lfs3_crc32c_cube(lfs3->gcksum ^ cksum) + ^ lfs3->gcksum_d; + + int err = lfs3_rbyd_appendrattr_(lfs3, &mdir_->r, LFS3_RATTR_LE32( + LFS3_TAG_GCKSUMDELTA, 0, mdir_->gcksumdelta)); + if (err) { + return err; + } + } + + // finalize commit + int err = lfs3_rbyd_appendcksum_(lfs3, &mdir_->r, cksum); + if (err) { + return err; + } + + // success? + + // xor our new cksum + lfs3->gcksum ^= mdir_->r.cksum; + + return 0; +} +#endif + +// TODO do we need to include commit overhead here? +#ifndef LFS3_RDONLY +static lfs3_ssize_t lfs3_mdir_estimate___(lfs3_t *lfs3, const lfs3_mdir_t *mdir, + lfs3_srid_t start_rid, lfs3_srid_t end_rid, + lfs3_srid_t *split_rid_) { + // yet another function that is just begging to be deduplicated, but we + // can't because it would be recursive + // + // this is basically the same as lfs3_rbyd_estimate, except we assume all + // rids have weight 1 and have extra handling for opened files, shrubs, etc + + // calculate dsize by starting from the outside ids and working inwards, + // this naturally gives us a split rid + lfs3_srid_t a_rid = lfs3_smax(start_rid, -1); + lfs3_srid_t b_rid = lfs3_min(mdir->r.weight, end_rid); + lfs3_size_t a_dsize = 0; + lfs3_size_t b_dsize = 0; + lfs3_size_t mdir_dsize = 0; + + while (a_rid != b_rid) { + if (a_dsize > b_dsize + // bias so lower dsize >= upper dsize + || (a_dsize == b_dsize && a_rid > b_rid)) { + LFS3_SWAP(lfs3_srid_t, &a_rid, &b_rid); + LFS3_SWAP(lfs3_size_t, &a_dsize, &b_dsize); + } + + if (a_rid > b_rid) { + a_rid -= 1; + } + + lfs3_stag_t tag = 0; + lfs3_size_t dsize_ = 0; + while (true) { + lfs3_srid_t rid_; + lfs3_data_t data; + tag = lfs3_rbyd_lookupnext(lfs3, &mdir->r, + a_rid, tag+1, + &rid_, NULL, &data); + if (tag < 0) { + if (tag == LFS3_ERR_NOENT) { + break; + } + return tag; + } + if (rid_ != a_rid) { + break; + } + + // special handling for shrub trunks, we need to include the + // compacted cost of the shrub in our estimate + // + // this is what would make lfs3_rbyd_estimate recursive, and + // why we need a second function... + // + if (tag == LFS3_TAG_BSHRUB) { + // include the cost of this trunk + dsize_ += LFS3_SHRUB_DSIZE; + + lfs3_shrub_t shrub; + int err = lfs3_data_readshrub(lfs3, mdir, &data, + &shrub); + if (err) { + return err; + } + + lfs3_ssize_t dsize__ = lfs3_shrub_estimate(lfs3, &shrub); + if (dsize__ < 0) { + return dsize__; + } + dsize_ += lfs3->rattr_estimate + dsize__; + + } else { + // include the cost of this tag + dsize_ += lfs3->mattr_estimate + lfs3_data_size(data); + } + } + + // include any opened+unsynced inlined files + // + // this is O(n^2), but littlefs is unlikely to have many open + // files, I suppose if this becomes a problem we could sort + // opened files by mid + for (lfs3_handle_t *h = lfs3->handles; h; h = h->next) { + if (lfs3_o_type(h->flags) == LFS3_TYPE_REG + // belongs to our mdir + rid? + && lfs3_mdir_cmp(&h->mdir, mdir) == 0 + && lfs3_mrid(lfs3, h->mdir.mid) == a_rid + // is a bshrub? + && lfs3_bshrub_isbshrub((lfs3_bshrub_t*)h)) { + lfs3_ssize_t dsize__ = lfs3_shrub_estimate(lfs3, + &((lfs3_bshrub_t*)h)->b.r); + if (dsize__ < 0) { + return dsize__; + } + dsize_ += dsize__; + } + } + + if (a_rid <= -1) { + mdir_dsize += dsize_; + } else { + a_dsize += dsize_; + } + + if (a_rid < b_rid) { + a_rid += 1; + } + } + + if (split_rid_) { + *split_rid_ = a_rid; + } + + return mdir_dsize + a_dsize + b_dsize; +} +#endif + +#ifndef LFS3_RDONLY +static int lfs3_mdir_compact___(lfs3_t *lfs3, + lfs3_mdir_t *mdir_, const lfs3_mdir_t *mdir, + lfs3_srid_t start_rid, lfs3_srid_t end_rid) { + // this is basically the same as lfs3_rbyd_compact, but with special + // handling for inlined trees. + // + // it's really tempting to deduplicate this via recursion! but we + // can't do that here + // + // TODO this true? + // note that any inlined updates here depend on the pre-commit state + // (btree), not the staged state (btree_), this is important, + // we can't trust btree_ after a failed commit + + // assume we keep any gcksumdelta, this will get fixed the first time + // we commit anything + if (start_rid == -2) { + mdir_->gcksumdelta = mdir->gcksumdelta; + } + + // copy over tags in the rbyd in order + lfs3_srid_t rid = lfs3_smax(start_rid, -1); + lfs3_stag_t tag = 0; + while (true) { + lfs3_rid_t weight; + lfs3_data_t data; + tag = lfs3_rbyd_lookupnext(lfs3, &mdir->r, + rid, tag+1, + &rid, &weight, &data); + if (tag < 0) { + if (tag == LFS3_ERR_NOENT) { + break; + } + return tag; + } + // end of range? note the use of rid+1 and unsigned comparison here to + // treat end_rid=-1 as "unbounded" in such a way that rid=-1 is still + // included + if ((lfs3_size_t)(rid + 1) > (lfs3_size_t)end_rid) { + break; + } + + // found an inlined shrub? we need to compact the shrub as well to + // bring it along with us + if (tag == LFS3_TAG_BSHRUB) { + lfs3_shrub_t shrub; + int err = lfs3_data_readshrub(lfs3, mdir, &data, + &shrub); + if (err) { + return err; + } + + // compact our shrub + err = lfs3_shrub_compact(lfs3, &mdir_->r, &shrub, + &shrub); + if (err) { + LFS3_ASSERT(err != LFS3_ERR_RANGE); + return err; + } + + // write the new shrub tag + err = lfs3_rbyd_appendcompactrattr(lfs3, &mdir_->r, + LFS3_RATTR_SHRUB(tag, weight, &shrub)); + if (err) { + LFS3_ASSERT(err != LFS3_ERR_RANGE); + return err; + } + + } else { + // write the tag + int err = lfs3_rbyd_appendcompactrattr(lfs3, &mdir_->r, + LFS3_RATTR_DATA(tag, weight, &data)); + if (err) { + LFS3_ASSERT(err != LFS3_ERR_RANGE); + return err; + } + } + } + + int err = lfs3_rbyd_appendcompaction(lfs3, &mdir_->r, 0); + if (err) { + LFS3_ASSERT(err != LFS3_ERR_RANGE); + return err; + } + + // we're not quite done! we also need to bring over any unsynced files + for (lfs3_handle_t *h = lfs3->handles; h; h = h->next) { + if (lfs3_o_type(h->flags) == LFS3_TYPE_REG + // belongs to our mdir? + && lfs3_mdir_cmp(&h->mdir, mdir) == 0 + && lfs3_mrid(lfs3, h->mdir.mid) >= start_rid + && (lfs3_rid_t)lfs3_mrid(lfs3, h->mdir.mid) + < (lfs3_rid_t)end_rid + // is a bshrub? + && lfs3_bshrub_isbshrub((lfs3_bshrub_t*)h) + // only compact once, first compact should + // stage the new block + && ((lfs3_bshrub_t*)h)->b_.blocks[0] + != mdir_->r.blocks[0]) { + int err = lfs3_shrub_compact(lfs3, &mdir_->r, + &((lfs3_bshrub_t*)h)->b_, + &((lfs3_bshrub_t*)h)->b.r); + if (err) { + LFS3_ASSERT(err != LFS3_ERR_RANGE); + return err; + } + } + } + + return 0; +} +#endif + +// mid-level mdir commit, this one will at least compact on overflow +#ifndef LFS3_RDONLY +static int lfs3_mdir_commit__(lfs3_t *lfs3, + lfs3_mdir_t *mdir_, lfs3_mdir_t *mdir, + lfs3_srid_t start_rid, lfs3_srid_t end_rid, + lfs3_srid_t *split_rid_, + lfs3_smid_t mid, const lfs3_rattr_t *rattrs, lfs3_size_t rattr_count) { + // make a copy + *mdir_ = *mdir; + // mark our mdir as unerased in case we fail + lfs3_mdir_claim(mdir); + // mark any copies of our mdir as unerased in case we fail + if (lfs3_mdir_cmp(mdir, &lfs3->mroot) == 0) { + lfs3_mdir_claim(&lfs3->mroot); + } + for (lfs3_handle_t *h = lfs3->handles; h; h = h->next) { + if (lfs3_mdir_cmp(&h->mdir, mdir) == 0) { + lfs3_mdir_claim(&h->mdir); + } + } + + // try to commit + int err = lfs3_mdir_commit___(lfs3, mdir_, start_rid, end_rid, + mid, rattrs, rattr_count); + if (err) { + if (err == LFS3_ERR_RANGE || err == LFS3_ERR_CORRUPT) { + goto compact; + } + return err; + } + return 0; + +compact:; + // can't commit, can we compact? + bool relocated = false; + bool overrecyclable = true; + + // check if we're within our compaction threshold + lfs3_ssize_t estimate = lfs3_mdir_estimate___(lfs3, mdir, + start_rid, end_rid, + split_rid_); + if (estimate < 0) { + return estimate; + } + + // TODO do we need to include mdir commit overhead here? in rbyd_estimate? + if ((lfs3_size_t)estimate > lfs3->cfg->block_size/2) { + return LFS3_ERR_RANGE; + } + + // swap blocks, increment revision count + err = lfs3_mdir_swap___(lfs3, mdir_, mdir, false); + if (err) { + if (err == LFS3_ERR_NOSPC || err == LFS3_ERR_CORRUPT) { + overrecyclable &= (err != LFS3_ERR_CORRUPT); + goto relocate; + } + return err; + } + + while (true) { + // try to compact + #ifdef LFS3_DBGMDIRCOMMITS + LFS3_DEBUG("Compacting mdir %"PRId32" 0x{%"PRIx32",%"PRIx32"} " + "-> 0x{%"PRIx32",%"PRIx32"}", + lfs3_dbgmbid(lfs3, mdir->mid), + mdir->r.blocks[0], mdir->r.blocks[1], + mdir_->r.blocks[0], mdir_->r.blocks[1]); + #endif + + // don't copy over gcksum if relocating + lfs3_srid_t start_rid_ = start_rid; + if (relocated) { + start_rid_ = lfs3_smax(start_rid_, -1); + } + + // compact our mdir + err = lfs3_mdir_compact___(lfs3, mdir_, mdir, start_rid_, end_rid); + if (err) { + LFS3_ASSERT(err != LFS3_ERR_RANGE); + // bad prog? try another block + if (err == LFS3_ERR_CORRUPT) { + overrecyclable &= relocated; + goto relocate; + } + return err; + } + + // now try to commit again + // + // upper layers should make sure this can't fail by limiting the + // maximum commit size + err = lfs3_mdir_commit___(lfs3, mdir_, start_rid_, end_rid, + mid, rattrs, rattr_count); + if (err) { + LFS3_ASSERT(err != LFS3_ERR_RANGE); + // bad prog? try another block + if (err == LFS3_ERR_CORRUPT) { + overrecyclable &= relocated; + goto relocate; + } + return err; + } + + // consume gcksumdelta if relocated + if (relocated) { + lfs3->gcksum_d ^= mdir->gcksumdelta; + } + return 0; + + relocate:; + // needs relocation? bad prog? ok, try allocating a new mdir + err = lfs3_mdir_alloc___(lfs3, mdir_, mdir->mid, relocated); + if (err && !(err == LFS3_ERR_NOSPC && overrecyclable)) { + return err; + } + relocated = true; + + // no more blocks? wear-leveling falls apart here, but we can try + // without relocating + if (err == LFS3_ERR_NOSPC) { + LFS3_WARN("Overrecycling mdir %"PRId32" 0x{%"PRIx32",%"PRIx32"}", + lfs3_dbgmbid(lfs3, mdir->mid), + mdir->r.blocks[0], mdir->r.blocks[1]); + relocated = false; + overrecyclable = false; + + err = lfs3_mdir_swap___(lfs3, mdir_, mdir, true); + if (err) { + // bad prog? can't do much here, mdir stuck + if (err == LFS3_ERR_CORRUPT) { + LFS3_ERROR("Stuck mdir 0x{%"PRIx32",%"PRIx32"}", + mdir->r.blocks[0], + mdir->r.blocks[1]); + return LFS3_ERR_NOSPC; + } + return err; + } + } + } +} +#endif + +#ifndef LFS3_RDONLY +static int lfs3_mroot_parent(lfs3_t *lfs3, const lfs3_block_t mptr[static 2], + lfs3_mdir_t *mparent_) { + // we only call this when we actually have parents + LFS3_ASSERT(!lfs3_mptr_ismrootanchor(mptr)); + + // scan list of mroots for our requested pair + lfs3_block_t mptr_[2] = { + LFS3_MPTR_MROOTANCHOR()[0], + LFS3_MPTR_MROOTANCHOR()[1]}; + while (true) { + // fetch next possible superblock + lfs3_mdir_t mdir; + int err = lfs3_mdir_fetch(lfs3, &mdir, -1, mptr_); + if (err) { + return err; + } + + // lookup next mroot + lfs3_data_t data; + lfs3_stag_t tag = lfs3_mdir_lookup(lfs3, &mdir, LFS3_TAG_MROOT, + &data); + if (tag < 0) { + LFS3_ASSERT(tag != LFS3_ERR_NOENT); + return tag; + } + + // decode mdir + err = lfs3_data_readmptr(lfs3, &data, mptr_); + if (err) { + return err; + } + + // found our child? + if (lfs3_mptr_cmp(mptr_, mptr) == 0) { + *mparent_ = mdir; + return 0; + } + } +} +#endif + +// needed in lfs3_mdir_commit_ +static inline void lfs3_file_discardleaf(lfs3_file_t *file); + +// high-level mdir commit +// +// this is atomic and updates any opened mdirs, lfs3_t, etc +// +// note that if an error occurs, any gstate is reverted to the on-disk +// state +// +#ifndef LFS3_RDONLY +static int lfs3_mdir_commit_(lfs3_t *lfs3, lfs3_mdir_t *mdir, + const lfs3_rattr_t *rattrs, lfs3_size_t rattr_count) { + // non-mroot mdirs must have weight + LFS3_ASSERT(mdir->mid <= -1 + // note inlined mdirs are mroots with mid != -1 + || lfs3_mdir_cmp(mdir, &lfs3->mroot) == 0 + || mdir->r.weight > 0); + // rid in-bounds? + LFS3_ASSERT(lfs3_mrid(lfs3, mdir->mid) + <= (lfs3_srid_t)mdir->r.weight); + // lfs3->mroot must have mid=-1 + LFS3_ASSERT(lfs3->mroot.mid == -1); + + // play out any rattrs that affect our grm _before_ committing to disk, + // keep in mind we revert to on-disk gstate if we run into an error + lfs3_smid_t mid_ = lfs3_smax(mdir->mid, -1); + for (lfs3_size_t i = 0; i < rattr_count; i++) { + // push a new grm, this tag lets us push grms atomically when + // creating new mids + if (rattrs[i].tag == LFS3_tag_GRMPUSH) { + lfs3_grm_push(lfs3, mid_); + + // adjust pending grms? + } else { + for (int j = 0; j < 2; j++) { + if (lfs3_mbid(lfs3, lfs3->grm.queue[j]) == lfs3_mbid(lfs3, mid_) + && lfs3->grm.queue[j] >= mid_) { + // deleting a pending grm doesn't really make sense + LFS3_ASSERT(lfs3->grm.queue[j] >= mid_ - rattrs[i].weight); + + // adjust the grm + lfs3->grm.queue[j] += rattrs[i].weight; + } + } + } + + // adjust mid + mid_ = lfs3_rattr_nextrid(rattrs[i], mid_); + } + + // flush gdeltas + lfs3_fs_zerogdelta(lfs3); + + // xor our old cksum + lfs3->gcksum ^= mdir->r.cksum; + + // stage any bshrubs + for (lfs3_handle_t *h = lfs3->handles; h; h = h->next) { + if (lfs3_o_type(h->flags) == LFS3_TYPE_REG) { + // a bshrub outside of its mdir means something has gone + // horribly wrong + LFS3_ASSERT(!lfs3_bshrub_isbshrub((lfs3_bshrub_t*)h) + || ((lfs3_bshrub_t*)h)->b.r.blocks[0] + == h->mdir.r.blocks[0]); + ((lfs3_bshrub_t*)h)->b_ = ((lfs3_bshrub_t*)h)->b.r; + } + } + + // attempt to commit/compact the mdir normally + lfs3_mdir_t mdir_[2]; + lfs3_srid_t split_rid; + int err = lfs3_mdir_commit__(lfs3, &mdir_[0], mdir, -2, -1, + &split_rid, + mdir->mid, rattrs, rattr_count); + if (err && err != LFS3_ERR_RANGE + && err != LFS3_ERR_NOENT) { + goto failed; + } + + // keep track of any mroot changes + lfs3_mdir_t mroot_ = lfs3->mroot; + if (!err && lfs3_mdir_cmp(mdir, &lfs3->mroot) == 0) { + lfs3_mdir_sync(&mroot_, &mdir_[0]); + } + + // handle possible mtree updates, this gets a bit messy + lfs3_smid_t mdelta = 0; + lfs3_btree_t mtree_ = lfs3->mtree; + // need to split? + if (err == LFS3_ERR_RANGE) { + // this should not happen unless we can't fit our mroot's metadata + LFS3_ASSERT(lfs3_mdir_cmp(mdir, &lfs3->mroot) != 0 + || lfs3->mtree.r.weight == 0); + + // if we're not the mroot, we need to consume the gstate so + // we don't lose any info during the split + // + // we do this here so we don't have to worry about corner cases + // with dropping mdirs during a split + if (lfs3_mdir_cmp(mdir, &lfs3->mroot) != 0) { + err = lfs3_fs_consumegdelta(lfs3, mdir); + if (err) { + goto failed; + } + } + + for (int i = 0; i < 2; i++) { + // order the split compacts so that that mdir containing our mid + // is committed last, this is a bit of a hack but necessary so + // shrubs are staged correctly + bool l = (lfs3_mrid(lfs3, mdir->mid) < split_rid); + + bool relocated = false; + split_relocate:; + // alloc and compact into new mdirs + err = lfs3_mdir_alloc___(lfs3, &mdir_[i^l], + lfs3_smax(mdir->mid, 0), relocated); + if (err) { + goto failed; + } + relocated = true; + + err = lfs3_mdir_compact___(lfs3, &mdir_[i^l], + mdir, + ((i^l) == 0) ? 0 : split_rid, + ((i^l) == 0) ? split_rid : -1); + if (err) { + LFS3_ASSERT(err != LFS3_ERR_RANGE); + // bad prog? try another block + if (err == LFS3_ERR_CORRUPT) { + goto split_relocate; + } + goto failed; + } + + err = lfs3_mdir_commit___(lfs3, &mdir_[i^l], + ((i^l) == 0) ? 0 : split_rid, + ((i^l) == 0) ? split_rid : -1, + mdir->mid, rattrs, rattr_count); + if (err && err != LFS3_ERR_NOENT) { + LFS3_ASSERT(err != LFS3_ERR_RANGE); + // bad prog? try another block + if (err == LFS3_ERR_CORRUPT) { + goto split_relocate; + } + goto failed; + } + // empty? set weight to zero + if (err == LFS3_ERR_NOENT) { + mdir_[i^l].r.weight = 0; + } + } + + // adjust our sibling's mid after committing rattrs + mdir_[1].mid += (1 << lfs3->mbits); + + LFS3_INFO("Splitting mdir %"PRId32" 0x{%"PRIx32",%"PRIx32"} " + "-> 0x{%"PRIx32",%"PRIx32"}, 0x{%"PRIx32",%"PRIx32"}", + lfs3_dbgmbid(lfs3, mdir->mid), + mdir->r.blocks[0], mdir->r.blocks[1], + mdir_[0].r.blocks[0], mdir_[0].r.blocks[1], + mdir_[1].r.blocks[0], mdir_[1].r.blocks[1]); + + // because of defered commits, children can be reduced to zero + // when splitting, need to catch this here + + // both siblings reduced to zero + if (mdir_[0].r.weight == 0 && mdir_[1].r.weight == 0) { + LFS3_INFO("Dropping mdir %"PRId32" 0x{%"PRIx32",%"PRIx32"}", + lfs3_dbgmbid(lfs3, mdir_[0].mid), + mdir_[0].r.blocks[0], mdir_[0].r.blocks[1]); + LFS3_INFO("Dropping mdir %"PRId32" 0x{%"PRIx32",%"PRIx32"}", + lfs3_dbgmbid(lfs3, mdir_[1].mid), + mdir_[1].r.blocks[0], mdir_[1].r.blocks[1]); + goto dropped; + + // one sibling reduced to zero + } else if (mdir_[0].r.weight == 0) { + LFS3_INFO("Dropping mdir %"PRId32" 0x{%"PRIx32",%"PRIx32"}", + lfs3_dbgmbid(lfs3, mdir_[0].mid), + mdir_[0].r.blocks[0], mdir_[0].r.blocks[1]); + lfs3_mdir_sync(&mdir_[0], &mdir_[1]); + goto relocated; + + // other sibling reduced to zero + } else if (mdir_[1].r.weight == 0) { + LFS3_INFO("Dropping mdir %"PRId32" 0x{%"PRIx32",%"PRIx32"}", + lfs3_dbgmbid(lfs3, mdir_[1].mid), + mdir_[1].r.blocks[0], mdir_[1].r.blocks[1]); + goto relocated; + } + + // no siblings reduced to zero, update our mtree + mdelta = +(1 << lfs3->mbits); + + // lookup first name in sibling to use as the split name + // + // note we need to do this after playing out pending rattrs in + // case they introduce a new name! + lfs3_data_t split_name; + lfs3_stag_t split_tag = lfs3_rbyd_lookup(lfs3, &mdir_[1].r, 0, + LFS3_tag_MASK8 | LFS3_TAG_NAME, + &split_name); + if (split_tag < 0) { + LFS3_ASSERT(split_tag != LFS3_ERR_NOENT); + err = split_tag; + goto failed; + } + + // new mtree? + if (lfs3->mtree.r.weight == 0) { + lfs3_btree_init(&mtree_); + + err = lfs3_mtree_commit(lfs3, &mtree_, + 0, LFS3_RATTRS( + LFS3_RATTR_MPTR( + LFS3_TAG_MDIR, +(1 << lfs3->mbits), + mdir_[0].r.blocks), + LFS3_RATTR_DATA( + LFS3_TAG_MNAME, +(1 << lfs3->mbits), + &split_name), + LFS3_RATTR_MPTR( + LFS3_TAG_MDIR, 0, + mdir_[1].r.blocks))); + if (err) { + goto failed; + } + + // update our mtree + } else { + err = lfs3_mtree_commit(lfs3, &mtree_, + lfs3_mbid(lfs3, mdir->mid), LFS3_RATTRS( + LFS3_RATTR_MPTR( + LFS3_TAG_MDIR, 0, + mdir_[0].r.blocks), + LFS3_RATTR_DATA( + LFS3_TAG_MNAME, +(1 << lfs3->mbits), + &split_name), + LFS3_RATTR_MPTR( + LFS3_TAG_MDIR, 0, + mdir_[1].r.blocks))); + if (err) { + goto failed; + } + } + + // need to drop? + } else if (err == LFS3_ERR_NOENT) { + LFS3_INFO("Dropping mdir %"PRId32" 0x{%"PRIx32",%"PRIx32"}", + lfs3_dbgmbid(lfs3, mdir->mid), + mdir->r.blocks[0], mdir->r.blocks[1]); + // set weight to zero + mdir_[0].r.weight = 0; + + // consume gstate so we don't lose any info + err = lfs3_fs_consumegdelta(lfs3, mdir); + if (err) { + goto failed; + } + + dropped:; + mdelta = -(1 << lfs3->mbits); + + // how can we drop if we have no mtree? + LFS3_ASSERT(lfs3->mtree.r.weight != 0); + + // update our mtree + err = lfs3_mtree_commit(lfs3, &mtree_, + lfs3_mbid(lfs3, mdir->mid), LFS3_RATTRS( + LFS3_RATTR( + LFS3_tag_RM, -(1 << lfs3->mbits)))); + if (err) { + goto failed; + } + + // need to relocate? + } else if (lfs3_mdir_cmp(&mdir_[0], mdir) != 0 + && lfs3_mdir_cmp(mdir, &lfs3->mroot) != 0) { + LFS3_INFO("Relocating mdir %"PRId32" 0x{%"PRIx32",%"PRIx32"} " + "-> 0x{%"PRIx32",%"PRIx32"}", + lfs3_dbgmbid(lfs3, mdir->mid), + mdir->r.blocks[0], mdir->r.blocks[1], + mdir_[0].r.blocks[0], mdir_[0].r.blocks[1]); + + relocated:; + // new mtree? + if (lfs3->mtree.r.weight == 0) { + lfs3_btree_init(&mtree_); + + err = lfs3_mtree_commit(lfs3, &mtree_, + 0, LFS3_RATTRS( + LFS3_RATTR_MPTR( + LFS3_TAG_MDIR, +(1 << lfs3->mbits), + mdir_[0].r.blocks))); + if (err) { + goto failed; + } + + // update our mtree + } else { + err = lfs3_mtree_commit(lfs3, &mtree_, + lfs3_mbid(lfs3, mdir->mid), LFS3_RATTRS( + LFS3_RATTR_MPTR( + LFS3_TAG_MDIR, 0, + mdir_[0].r.blocks))); + if (err) { + goto failed; + } + } + } + + // patch any pending grms + for (int j = 0; j < 2; j++) { + if (lfs3_mbid(lfs3, lfs3->grm.queue[j]) + == lfs3_mbid(lfs3, lfs3_smax(mdir->mid, 0))) { + if (mdelta > 0 + && lfs3_mrid(lfs3, lfs3->grm.queue[j]) + >= (lfs3_srid_t)mdir_[0].r.weight) { + lfs3->grm.queue[j] + += (1 << lfs3->mbits) - mdir_[0].r.weight; + } + } else if (lfs3->grm.queue[j] > mdir->mid) { + lfs3->grm.queue[j] += mdelta; + } + } + + // need to update mtree? + if (lfs3_btree_cmp(&mtree_, &lfs3->mtree) != 0) { + // mtree should never go to zero since we always have a root bookmark + LFS3_ASSERT(mtree_.r.weight > 0); + + // make sure mtree/mroot changes are on-disk before committing + // metadata + err = lfs3_bd_sync(lfs3); + if (err) { + goto failed; + } + + // xor mroot's cksum if we haven't already + if (lfs3_mdir_cmp(mdir, &lfs3->mroot) != 0) { + lfs3->gcksum ^= lfs3->mroot.r.cksum; + } + + // commit new mtree into our mroot + // + // note end_rid=0 here will delete any files leftover from a split + // in our mroot + err = lfs3_mdir_commit__(lfs3, &mroot_, &lfs3->mroot, -2, 0, + NULL, + -1, LFS3_RATTRS( + LFS3_RATTR_BTREE( + LFS3_tag_MASK8 | LFS3_TAG_MTREE, 0, + &mtree_), + // were we committing to the mroot? include any -1 rattrs + (mdir->mid <= -1) + ? LFS3_RATTR_RATTRS(rattrs, rattr_count) + : LFS3_RATTR_NOOP())); + if (err) { + LFS3_ASSERT(err != LFS3_ERR_RANGE); + goto failed; + } + } + + // need to update mroot chain? + if (lfs3_mdir_cmp(&mroot_, &lfs3->mroot) != 0) { + // tail recurse, updating mroots until a commit sticks + lfs3_mdir_t mrootchild = lfs3->mroot; + lfs3_mdir_t mrootchild_ = mroot_; + while (lfs3_mdir_cmp(&mrootchild_, &mrootchild) != 0 + && !lfs3_mdir_ismrootanchor(&mrootchild)) { + // find the mroot's parent + lfs3_mdir_t mrootparent; + err = lfs3_mroot_parent(lfs3, mrootchild.r.blocks, + &mrootparent); + if (err) { + LFS3_ASSERT(err != LFS3_ERR_NOENT); + goto failed; + } + + LFS3_INFO("Relocating mroot 0x{%"PRIx32",%"PRIx32"} " + "-> 0x{%"PRIx32",%"PRIx32"}", + mrootchild.r.blocks[0], mrootchild.r.blocks[1], + mrootchild_.r.blocks[0], mrootchild_.r.blocks[1]); + + // make sure mtree/mroot changes are on-disk before committing + // metadata + err = lfs3_bd_sync(lfs3); + if (err) { + goto failed; + } + + // xor mrootparent's cksum + lfs3->gcksum ^= mrootparent.r.cksum; + + // commit mrootchild + lfs3_mdir_t mrootparent_; + err = lfs3_mdir_commit__(lfs3, &mrootparent_, &mrootparent, -2, -1, + NULL, + -1, LFS3_RATTRS( + LFS3_RATTR_MPTR( + LFS3_TAG_MROOT, 0, + mrootchild_.r.blocks))); + if (err) { + LFS3_ASSERT(err != LFS3_ERR_RANGE); + LFS3_ASSERT(err != LFS3_ERR_NOENT); + goto failed; + } + + mrootchild = mrootparent; + mrootchild_ = mrootparent_; + } + + // no more mroot parents? uh oh, need to extend mroot chain + if (lfs3_mdir_cmp(&mrootchild_, &mrootchild) != 0) { + // mrootchild should be our previous mroot anchor at this point + LFS3_ASSERT(lfs3_mdir_ismrootanchor(&mrootchild)); + LFS3_INFO("Extending mroot 0x{%"PRIx32",%"PRIx32"}" + " -> 0x{%"PRIx32",%"PRIx32"}, 0x{%"PRIx32",%"PRIx32"}", + mrootchild.r.blocks[0], mrootchild.r.blocks[1], + mrootchild.r.blocks[0], mrootchild.r.blocks[1], + mrootchild_.r.blocks[0], mrootchild_.r.blocks[1]); + + // make sure mtree/mroot changes are on-disk before committing + // metadata + err = lfs3_bd_sync(lfs3); + if (err) { + goto failed; + } + + // commit the new mroot anchor + lfs3_mdir_t mrootanchor_; + err = lfs3_mdir_swap___(lfs3, &mrootanchor_, &mrootchild, true); + if (err) { + // bad prog? can't do much here, mroot stuck + if (err == LFS3_ERR_CORRUPT) { + LFS3_ERROR("Stuck mroot 0x{%"PRIx32",%"PRIx32"}", + mrootanchor_.r.blocks[0], + mrootanchor_.r.blocks[1]); + return LFS3_ERR_NOSPC; + } + goto failed; + } + + err = lfs3_mdir_commit___(lfs3, &mrootanchor_, -2, -1, + -1, LFS3_RATTRS( + LFS3_RATTR_BUF( + LFS3_TAG_MAGIC, 0, + "littlefs", 8), + LFS3_RATTR_MPTR( + LFS3_TAG_MROOT, 0, + mrootchild_.r.blocks))); + if (err) { + LFS3_ASSERT(err != LFS3_ERR_RANGE); + LFS3_ASSERT(err != LFS3_ERR_NOENT); + // bad prog? can't do much here, mroot stuck + if (err == LFS3_ERR_CORRUPT) { + LFS3_ERROR("Stuck mroot 0x{%"PRIx32",%"PRIx32"}", + mrootanchor_.r.blocks[0], + mrootanchor_.r.blocks[1]); + return LFS3_ERR_NOSPC; + } + goto failed; + } + } + } + + // sync on-disk state + err = lfs3_bd_sync(lfs3); + if (err) { + return err; + } + + /////////////////////////////////////////////////////////////////////// + // success? update in-device state, we must not error at this point! // + /////////////////////////////////////////////////////////////////////// + + // play out any rattrs that affect internal state + mid_ = lfs3_smax(mdir->mid, -1); + for (lfs3_size_t i = 0; i < rattr_count; i++) { + // adjust any opened mdirs + for (lfs3_handle_t *h = lfs3->handles; h; h = h->next) { + // adjust opened mdirs? + if (lfs3_mdir_cmp(&h->mdir, mdir) == 0 + && h->mdir.mid >= mid_) { + // removed? + if (h->mdir.mid < mid_ - rattrs[i].weight) { + // opened files should turn into stickynote, not + // have their mid removed + LFS3_ASSERT(lfs3_o_type(h->flags) != LFS3_TYPE_REG); + h->flags |= LFS3_o_ZOMBIE; + h->mdir.mid = mid_; + } else { + h->mdir.mid += rattrs[i].weight; + } + } + } + + // adjust mid + mid_ = lfs3_rattr_nextrid(rattrs[i], mid_); + } + + // update internal mdir state + for (lfs3_handle_t *h = lfs3->handles; h; h = h->next) { + // avoid double updating the current mdir + if (&h->mdir == mdir) { + continue; + } + + // update any splits/drops + if (lfs3_mdir_cmp(&h->mdir, mdir) == 0) { + if (mdelta > 0 + && lfs3_mrid(lfs3, h->mdir.mid) + >= (lfs3_srid_t)mdir_[0].r.weight) { + h->mdir.mid += (1 << lfs3->mbits) - mdir_[0].r.weight; + lfs3_mdir_sync(&h->mdir, &mdir_[1]); + } else { + lfs3_mdir_sync(&h->mdir, &mdir_[0]); + } + } else if (h->mdir.mid > mdir->mid) { + h->mdir.mid += mdelta; + } + } + + // update mdir to follow requested rid + if (mdelta > 0 + && mdir->mid <= -1) { + lfs3_mdir_sync(mdir, &mroot_); + } else if (mdelta > 0 + && lfs3_mrid(lfs3, mdir->mid) + >= (lfs3_srid_t)mdir_[0].r.weight) { + mdir->mid += (1 << lfs3->mbits) - mdir_[0].r.weight; + lfs3_mdir_sync(mdir, &mdir_[1]); + } else { + lfs3_mdir_sync(mdir, &mdir_[0]); + } + + // update mroot and mtree + lfs3_mdir_sync(&lfs3->mroot, &mroot_); + lfs3->mtree = mtree_; + + // update any staged bshrubs + for (lfs3_handle_t *h = lfs3->handles; h; h = h->next) { + // update the shrub + if (lfs3_o_type(h->flags) == LFS3_TYPE_REG) { + // if we moved a shrub, we also need to discard any leaves + // that moved + if (((lfs3_bshrub_t*)h)->b_.blocks[0] + != ((lfs3_bshrub_t*)h)->b.r.blocks[0]) { + #ifdef LFS3_BLEAFCACHE + // discard any bshrub leaves that moved + if (((lfs3_bshrub_t*)h)->b.leaf.r.blocks[0] + == ((lfs3_bshrub_t*)h)->b.r.blocks[0]) { + lfs3_bshrub_discardleaf((lfs3_bshrub_t*)h); + } + #endif + + // discard any file leaves that moved + if (lfs3_o_type(h->flags) == LFS3_TYPE_REG + && lfs3_bptr_block(&((lfs3_file_t*)h)->leaf.bptr) + == ((lfs3_bshrub_t*)h)->b.r.blocks[0]) { + lfs3_file_discardleaf((lfs3_file_t*)h); + } + } + + ((lfs3_bshrub_t*)h)->b.r = ((lfs3_bshrub_t*)h)->b_; + } + } + + // update any gstate changes + lfs3_fs_commitgdelta(lfs3); + + // we may have touched any number of mdirs, so assume uncompacted + // until lfs3_fs_gc can prove otherwise + lfs3->flags |= LFS3_I_COMPACTMETA; + + #ifdef LFS3_DBGMDIRCOMMITS + LFS3_DEBUG("Committed mdir %"PRId32" " + "0x{%"PRIx32",%"PRIx32"}.%"PRIx32" w%"PRId32", " + "cksum %"PRIx32, + lfs3_dbgmbid(lfs3, mdir->mid), + mdir->r.blocks[0], mdir->r.blocks[1], + lfs3_rbyd_trunk(&mdir->r), + mdir->r.weight, + mdir->r.cksum); + #endif + return 0; + +failed:; + // revert gstate to on-disk state + lfs3_fs_revertgdelta(lfs3); + return err; +} +#endif + +// by default, lfs3_mdir_commit implicitly checkpoints the block +// allocator, use lfs3_mdir_commit_ to bypass this +// +// allocator checkpoints indicate when any in-flight blocks are at rest, +// i.e. tracked on-disk or in-RAM, so this is what you want if the mdir +// commit represents an atomic transition between at rest filesystem +// states +// +#ifndef LFS3_RDONLY +static int lfs3_mdir_commit(lfs3_t *lfs3, lfs3_mdir_t *mdir, + const lfs3_rattr_t *rattrs, lfs3_size_t rattr_count) { + // checkpoint the allocator + int err = lfs3_alloc_ckpoint(lfs3); + if (err) { + // revert gstate to on-disk state + lfs3_fs_revertgdelta(lfs3); + return err; + } + + // commit to mdir + return lfs3_mdir_commit_(lfs3, mdir, rattrs, rattr_count); +} +#endif + +#ifndef LFS3_RDONLY +static int lfs3_mdir_compact_(lfs3_t *lfs3, lfs3_mdir_t *mdir) { + // the easiest way to do this is to just mark mdir as unerased + // and call lfs3_mdir_commit + lfs3_mdir_claim(mdir); + return lfs3_mdir_commit_(lfs3, mdir, NULL, 0); +} +#endif + +#ifndef LFS3_RDONLY +static int lfs3_mdir_compact(lfs3_t *lfs3, lfs3_mdir_t *mdir) { + // the easiest way to do this is to just mark mdir as unerased + // and call lfs3_mdir_commit + lfs3_mdir_claim(mdir); + return lfs3_mdir_commit(lfs3, mdir, NULL, 0); +} +#endif + + + +/// Mtree path/name lookup /// + +// lookup names in an mdir +// +// if not found, mid will be the best place to insert +static lfs3_stag_t lfs3_mdir_namelookup(lfs3_t *lfs3, const lfs3_mdir_t *mdir, + lfs3_did_t did, const char *name, lfs3_size_t name_len, + lfs3_smid_t *mid_, lfs3_data_t *data_) { + // default to mid_ = 0, this blanket assignment is the only way to + // keep GCC happy + if (mid_) { + *mid_ = 0; + } + + // empty mdir? + if (mdir->r.weight == 0) { + return LFS3_ERR_NOENT; + } + + lfs3_srid_t rid; + lfs3_tag_t tag; + lfs3_scmp_t cmp = lfs3_rbyd_namelookup(lfs3, &mdir->r, + did, name, name_len, + &rid, &tag, NULL, data_); + if (cmp < 0) { + LFS3_ASSERT(cmp != LFS3_ERR_NOENT); + return cmp; + } + + // adjust mid if necessary + // + // note missing mids end up pointing to the next mid + lfs3_smid_t mid = LFS3_MID(lfs3, + mdir->mid, + (cmp < LFS3_CMP_EQ) ? rid+1 : rid); + + // map name tags to understood types + tag = lfs3_mdir_nametag(lfs3, mdir, mid, tag); + + if (mid_) { + *mid_ = mid; + } + return (cmp == LFS3_CMP_EQ) + ? tag + : LFS3_ERR_NOENT; +} + +// lookup names in our mtree +// +// if not found, mid will be the best place to insert +static lfs3_stag_t lfs3_mtree_namelookup(lfs3_t *lfs3, + lfs3_did_t did, const char *name, lfs3_size_t name_len, + lfs3_mdir_t *mdir_, lfs3_data_t *data_) { + // do we only have mroot? + if (lfs3->mtree.r.weight == 0) { + // treat inlined mdir as mid=0 + mdir_->mid = 0; + lfs3_mdir_sync(mdir_, &lfs3->mroot); + + // lookup name in actual mtree + } else { + lfs3_bid_t bid; + lfs3_stag_t tag; + lfs3_bid_t weight; + lfs3_data_t data; + lfs3_scmp_t cmp = lfs3_btree_namelookup(lfs3, &lfs3->mtree, + did, name, name_len, + &bid, (lfs3_tag_t*)&tag, &weight, &data); + if (cmp < 0) { + LFS3_ASSERT(cmp != LFS3_ERR_NOENT); + return cmp; + } + LFS3_ASSERT(weight == (lfs3_bid_t)(1 << lfs3->mbits)); + LFS3_ASSERT(tag == LFS3_TAG_MNAME + || tag == LFS3_TAG_MDIR); + + // if we found an mname, lookup the mdir + if (tag == LFS3_TAG_MNAME) { + tag = lfs3_btree_lookup(lfs3, &lfs3->mtree, bid, LFS3_TAG_MDIR, + &data); + if (tag < 0) { + LFS3_ASSERT(tag != LFS3_ERR_NOENT); + return tag; + } + } + + // fetch the mdir + int err = lfs3_data_fetchmdir(lfs3, &data, bid-((1 << lfs3->mbits)-1), + mdir_); + if (err) { + return err; + } + } + + // and lookup name in our mdir + lfs3_smid_t mid; + lfs3_stag_t tag = lfs3_mdir_namelookup(lfs3, mdir_, + did, name, name_len, + &mid, data_); + if (tag < 0 && tag != LFS3_ERR_NOENT) { + return tag; + } + + // update mdir with best place to insert even if we fail + mdir_->mid = mid; + return tag; +} + + +// special directory-ids +enum { + LFS3_DID_ROOT = 0, +}; + +// some operations on paths +static inline lfs3_size_t lfs3_path_namelen(const char *path) { + return lfs3_strcspn(path, "/"); +} + +static inline bool lfs3_path_islast(const char *path) { + lfs3_size_t name_len = lfs3_path_namelen(path); + return path[name_len + lfs3_strspn(path + name_len, "/")] == '\0'; +} + +static inline bool lfs3_path_isdir(const char *path) { + return path[lfs3_path_namelen(path)] != '\0'; +} + +// lookup a full path in our mtree, updating the path as we descend +// +// the errors get a bit subtle here, and rely on what ends up in the +// path/mdir: +// - tag => file found +// - LFS3_TAG_DIR, mdir.mid>=0 => dir found +// - LFS3_TAG_DIR, mdir.mid=-1 => root found +// - LFS3_ERR_NOENT, islast(path), !isdir(path) => file not found +// - LFS3_ERR_NOENT, islast(path), isdir(path) => dir not found +// - LFS3_ERR_NOENT, !islast(path) => parent not found +// - LFS3_ERR_NOTDIR => parent not a dir +// +// if not found, mdir/did_ will be set to the parent's mdir/did, all +// ready for file creation +// +static lfs3_stag_t lfs3_mtree_pathlookup(lfs3_t *lfs3, const char **path, + lfs3_mdir_t *mdir_, lfs3_did_t *did_) { + // setup root + *mdir_ = lfs3->mroot; + lfs3_stag_t tag = LFS3_TAG_DIR; + lfs3_did_t did = LFS3_DID_ROOT; + + // we reduce path to a single name if we can find it + const char *path_ = *path; + + // empty paths are not allowed + if (path_[0] == '\0') { + return LFS3_ERR_INVAL; + } + + while (true) { + // skip slashes if we're a directory + if (tag == LFS3_TAG_DIR) { + path_ += lfs3_strspn(path_, "/"); + } + lfs3_size_t name_len = lfs3_strcspn(path_, "/"); + + // skip '.' + if (name_len == 1 && lfs3_memcmp(path_, ".", 1) == 0) { + path_ += name_len; + goto next; + } + + // error on unmatched '..', trying to go above root, eh? + if (name_len == 2 && lfs3_memcmp(path_, "..", 2) == 0) { + return LFS3_ERR_INVAL; + } + + // skip if matched by '..' in name + const char *suffix = path_ + name_len; + lfs3_size_t suffix_len; + int depth = 1; + while (true) { + suffix += lfs3_strspn(suffix, "/"); + suffix_len = lfs3_strcspn(suffix, "/"); + if (suffix_len == 0) { + break; + } + + if (suffix_len == 1 && lfs3_memcmp(suffix, ".", 1) == 0) { + // noop + } else if (suffix_len == 2 && lfs3_memcmp(suffix, "..", 2) == 0) { + depth -= 1; + if (depth == 0) { + path_ = suffix + suffix_len; + goto next; + } + } else { + depth += 1; + } + + suffix += suffix_len; + } + + // found end of path, we must be done parsing our path now + if (path_[0] == '\0') { + if (did_) { + *did_ = did; + } + return tag; + } + + // only continue if we hit a directory + if (tag != LFS3_TAG_DIR) { + return (tag == LFS3_tag_ORPHAN) + ? LFS3_ERR_NOENT + : LFS3_ERR_NOTDIR; + } + + // read the next did from the mdir if this is not the root + if (mdir_->mid != -1) { + lfs3_data_t data; + tag = lfs3_mdir_lookup(lfs3, mdir_, LFS3_TAG_DID, + &data); + if (tag < 0) { + return tag; + } + + int err = lfs3_data_readleb128(lfs3, &data, &did); + if (err) { + return err; + } + } + + // update path as we parse + *path = path_; + + // lookup up this name in the mtree + tag = lfs3_mtree_namelookup(lfs3, did, path_, name_len, + mdir_, NULL); + if (tag < 0 && tag != LFS3_ERR_NOENT) { + return tag; + } + + if (tag == LFS3_ERR_NOENT) { + // keep track of where to insert if we can't find path + if (did_) { + *did_ = did; + } + return LFS3_ERR_NOENT; + } + + // go on to next name + path_ += name_len; + next:; + } +} + + + +/// Mtree traversal /// + +// special metadata ids +enum { + // mids < -1 are used to encode traversal states + LFS3_MID_MROOTANCHOR = -5, + LFS3_MID_MTREE = -4, + LFS3_MID_GBMAP = -3, + LFS3_MID_GBMAP_P = -2, + // mid = -1 is reserved for mdir-level tags + // mids >= -1 represent mid-level tags +}; + +// special btree ids +enum { + // bids < -1 are used to encode traversal states + LFS3_BID_MDIR = -2, + // bids >= -1 map to btrv steps +}; + +static void lfs3_mtrv_init(lfs3_mtrv_t *mtrv, uint32_t flags) { + // start at the mroot anchor + mtrv->h.mdir.mid = LFS3_MID_MROOTANCHOR; + mtrv->u.btrv.bid = LFS3_BID_MDIR; + mtrv->h.flags = lfs3_o_typeflags(LFS3_type_TRV) | flags; + mtrv->h.mdir.r.weight = 0; + mtrv->h.mdir.r.blocks[0] = -1; + mtrv->h.mdir.r.blocks[1] = -1; + mtrv->gcksum = 0; +} + +static void lfs3_mtrv_ckpoint(lfs3_mtrv_t *mtrv) { + // mark as ckpointed and dirty + mtrv->h.flags |= LFS3_t_CKPOINTED | LFS3_t_DIRTY | LFS3_t_STALE; + + // when tracked, our mdir should be kept in-sync, but we need to + // discard any btrees/bshrubs that may fall out-of-date + // + // this may revisit seen blocks, but that's ok because this was + // always possible due to CoW references + mtrv->u.btrv.bid = LFS3_BID_MDIR; +} + +static void lfs3_mgc_init(lfs3_mgc_t *mgc, uint32_t flags) { + lfs3_mtrv_init(&mgc->t, flags); +} + +static void lfs3_mgc_ckpoint(lfs3_mgc_t *mgc) { + lfs3_mtrv_ckpoint(&mgc->t); +} + +// low-level traversal _only_ finds blocks +static lfs3_stag_t lfs3_mtree_traverse_(lfs3_t *lfs3, lfs3_mtrv_t *mtrv, + lfs3_bptr_t *bptr_) { +again:; + // fetch a btree/bshrub? + if (mtrv->u.btrv.bid == LFS3_BID_MDIR) { + // default to null btree + lfs3_btree_init(&mtrv->b); + // reset our position in the opened handles + // + // after traversing on-disk bshrubs/btrees, we'll need + // to traverse any open bshrubs/btrees + lfs3_handle_rewind(lfs3, &mtrv->h); + + // fetch mroot anchor (mdir 0x{0,1})? + if (mtrv->h.mdir.mid == LFS3_MID_MROOTANCHOR) { + int err = lfs3_mdir_fetch(lfs3, &mtrv->h.mdir, + LFS3_MID_MTREE, LFS3_MPTR_MROOTANCHOR()); + if (err) { + return err; + } + + // setup mtortoise to detect cycles + mtrv->u.mtortoise.blocks[0] = mtrv->h.mdir.r.blocks[0]; + mtrv->u.mtortoise.blocks[0] = mtrv->h.mdir.r.blocks[1]; + mtrv->u.mtortoise.dist = 0; + mtrv->u.mtortoise.nlog2 = 0; + + // traverse the mroot anchor + bptr_->d.u.buffer = (const uint8_t*)&mtrv->h.mdir; + return LFS3_TAG_MDIR; + + // try to fetch either another mroot in the mroot chain, or + // the mtree if we find it + } else if (mtrv->h.mdir.mid == LFS3_MID_MTREE) { + lfs3_data_t data; + lfs3_stag_t tag = lfs3_mdir_lookup(lfs3, &mtrv->h.mdir, + LFS3_tag_MASK8 | LFS3_TAG_STRUCT, + &data); + if (tag < 0 && tag != LFS3_ERR_NOENT) { + return tag; + } + + // found a new mroot? + if (tag == LFS3_TAG_MROOT) { + // fetch this mroot + int err = lfs3_data_fetchmdir(lfs3, &data, LFS3_MID_MTREE, + &mtrv->h.mdir); + if (err) { + return err; + } + + // detect cycles with Brent's algorithm + // + // note we only check for cycles in the mroot chain, the + // btree inner nodes require checksums of their pointers, + // so creating a valid cycle is actually quite difficult + // + if (lfs3_mptr_cmp( + mtrv->h.mdir.r.blocks, + mtrv->u.mtortoise.blocks) == 0) { + LFS3_ERROR("Cycle detected during mtree traversal " + "0x{%"PRIx32",%"PRIx32"}", + mtrv->h.mdir.r.blocks[0], + mtrv->h.mdir.r.blocks[1]); + return LFS3_ERR_CORRUPT; + } + if (mtrv->u.mtortoise.dist + == (1U << mtrv->u.mtortoise.nlog2)) { + mtrv->u.mtortoise.blocks[0] = mtrv->h.mdir.r.blocks[0]; + mtrv->u.mtortoise.blocks[1] = mtrv->h.mdir.r.blocks[1]; + mtrv->u.mtortoise.dist = 0; + mtrv->u.mtortoise.nlog2 += 1; + } + mtrv->u.mtortoise.dist += 1; + + bptr_->d.u.buffer = (const uint8_t*)&mtrv->h.mdir; + return LFS3_TAG_MDIR; + + // found an mtree? + } else if (tag == LFS3_TAG_MTREE) { + // fetch the root of the mtree + int err = lfs3_data_fetchbtree(lfs3, &data, + &mtrv->b); + if (err) { + return err; + } + + // found something else? + } else if (tag != LFS3_ERR_NOENT) { + LFS3_ERROR("Weird mroot entry? 0x%"PRIx32, tag); + return LFS3_ERR_CORRUPT; + } + + // traverse the gbmap if we have one + } else if (LFS3_IFDEF_GBMAP( + mtrv->h.mdir.mid == LFS3_MID_GBMAP + && lfs3_f_isgbmap(lfs3->flags) + && !lfs3_t_ismtreeonly(mtrv->h.flags), + false)) { + #ifdef LFS3_GBMAP + mtrv->b = lfs3->gbmap.b; + #endif + + // traverse on-disk gbmap if it doesn't match our in-RAM + // snapshot + // + // we need to include this in case the gbmap is rebuilt + // multiple times before an mdir commit + } else if (LFS3_IFDEF_GBMAP( + mtrv->h.mdir.mid == LFS3_MID_GBMAP_P + && lfs3_f_isgbmap(lfs3->flags) + && !lfs3_t_ismtreeonly(mtrv->h.flags) + && lfs3_btree_cmp( + &lfs3->gbmap.b_p, + &lfs3->gbmap.b) != 0, + false)) { + #ifdef LFS3_GBMAP + mtrv->b = lfs3->gbmap.b_p; + #endif + + // fetch the next btree/bshrub + } else if (mtrv->h.mdir.mid >= 0 + && !lfs3_t_ismtreeonly(mtrv->h.flags)) { + // try to fetch bshrub/btree, if we don't find one + // that's ok + int err = lfs3_bshrub_fetch_(lfs3, &mtrv->h.mdir, &mtrv->b); + if (err && err != LFS3_ERR_NOENT) { + return err; + } + } + + mtrv->u.btrv.bid = -1; + } + + // traverse any btrees/bshrubs we find + LFS3_ASSERT(mtrv->u.btrv.bid >= -1); + while (true) { + lfs3_data_t data; + lfs3_stag_t tag = lfs3_btree_traverse(lfs3, &mtrv->b, &mtrv->u.btrv, + NULL, NULL, &data); + if (tag < 0) { + if (tag == LFS3_ERR_NOENT) { + break; + } + return tag; + } + + // found an inner btree node? + if (tag == LFS3_TAG_BRANCH) { + bptr_->d = data; + return LFS3_TAG_BRANCH; + + // found an indirect block? + } else if (tag == LFS3_TAG_BLOCK) { + int err = lfs3_data_readbptr(lfs3, &data, + bptr_); + if (err) { + return err; + } + + return LFS3_TAG_BLOCK; + } + } + + // done with this btree/bshrub? search our opened handle list + // for any unsynced bshrubs/btrees related to this mid + // + // yes this grows potentially O(n^2) in-ram, but do we care? + // + // note we can skip this when rdonly, which saves a bit of code + if (!lfs3_m_isrdonly(lfs3->flags)) { + for (lfs3_handle_t *h = mtrv->h.next; h; h = h->next) { + // found one? + if (h->mdir.mid == mtrv->h.mdir.mid + && lfs3_o_type(h->flags) == LFS3_TYPE_REG + && lfs3_o_isunsync(h->flags)) { + // found one! + const lfs3_file_t *file = (const lfs3_file_t*)h; + mtrv->b = file->b.b; + mtrv->u.btrv.bid = -1; + + // move our handle to make progress + // + // this looks scary with lfs3_handle_seek running in + // O(n), but, because we only visit each unique mid + + // handle once, in total this should still run O(n^2) + // in-ram + lfs3_handle_seek(lfs3, &mtrv->h, &h->next); + + // wait, do we have an ungrafted leaf? + if (lfs3_o_isungraft(file->b.h.flags)) { + *bptr_ = file->leaf.bptr; + return LFS3_TAG_BLOCK; + } + + goto again; + } + } + } + + // done with this mid? transition to next mid + // TODO is this correct? + if (mtrv->h.mdir.mid >= 0 + && (lfs3_t_ismtreeonly(mtrv->h.flags) + || lfs3_mrid(lfs3, mtrv->h.mdir.mid) + >= (lfs3_srid_t)mtrv->h.mdir.r.weight-1)) { + mtrv->h.mdir.mid = lfs3_mbid(lfs3, mtrv->h.mdir.mid) + 1; + } else { + mtrv->h.mdir.mid += 1; + } + mtrv->u.btrv.bid = LFS3_BID_MDIR; + + // fetch mdirs on first access + // + // note lfs3_mrid maps mids<=-1 => rid=-1 + if (lfs3_mrid(lfs3, mtrv->h.mdir.mid) == 0) { + int err = lfs3_mtree_lookup(lfs3, mtrv->h.mdir.mid, + &mtrv->h.mdir); + if (err) { + return err; + } + + // traverse this mdir, but don't repeat the mroot + if (lfs3->mtree.r.weight != 0) { + bptr_->d.u.buffer = (const uint8_t*)&mtrv->h.mdir; + return LFS3_TAG_MDIR; + } + } + + goto again; +} + +// needed in lfs3_mtree_traverse +static void lfs3_alloc_markinusebptr(lfs3_t *lfs3, + lfs3_tag_t tag, const lfs3_bptr_t *bptr); + +// high-level immutable traversal, handle extra features here, +// but no mutation! (we're called in lfs3_alloc, so things would end up +// recursive, which would be a bit bad!) +static lfs3_stag_t lfs3_mtree_traverse(lfs3_t *lfs3, lfs3_mtrv_t *mtrv, + lfs3_bptr_t *bptr_) { + lfs3_stag_t tag = lfs3_mtree_traverse_(lfs3, mtrv, + bptr_); + if (tag < 0) { + // end of traversal? + if (tag == LFS3_ERR_NOENT) { + goto eot; + } + return tag; + } + + // validate mdirs? mdir checksums are already validated in + // lfs3_mdir_fetch, but this doesn't prevent rollback issues, where + // the most recent commit is corrupted but a previous outdated + // commit appears valid + // + // this is where the gcksum comes in, which we can recalculate to + // check if the filesystem state on-disk is as expected + // + // we also compare mdir checksums with any open mdirs to try to + // avoid traversing any outdated bshrubs/btrees + if ((lfs3_t_isckmeta(mtrv->h.flags) + || lfs3_t_isckdata(mtrv->h.flags)) + && tag == LFS3_TAG_MDIR) { + lfs3_mdir_t *mdir = (lfs3_mdir_t*)bptr_->d.u.buffer; + + // check cksum matches our mroot + if (lfs3_mdir_cmp(mdir, &lfs3->mroot) == 0 + && mdir->r.cksum != lfs3->mroot.r.cksum) { + LFS3_ERROR("Found mroot cksum mismatch " + "0x{%"PRIx32",%"PRIx32"}, " + "cksum %08"PRIx32" (!= %08"PRIx32")", + mdir->r.blocks[0], + mdir->r.blocks[1], + mdir->r.cksum, + lfs3->mroot.r.cksum); + return LFS3_ERR_CORRUPT; + } + + // check cksum matches any open mdirs + for (lfs3_handle_t *h = lfs3->handles; h; h = h->next) { + if (lfs3_mdir_cmp(&h->mdir, mdir) == 0 + && h->mdir.r.cksum != mdir->r.cksum) { + LFS3_ERROR("Found mdir cksum mismatch %"PRId32" " + "0x{%"PRIx32",%"PRIx32"}, " + "cksum %08"PRIx32" (!= %08"PRIx32")", + lfs3_dbgmbid(lfs3, mdir->mid), + mdir->r.blocks[0], + mdir->r.blocks[1], + mdir->r.cksum, + h->mdir.r.cksum); + return LFS3_ERR_CORRUPT; + } + } + + // recalculate gcksum + mtrv->gcksum ^= mdir->r.cksum; + } + + // validate btree nodes? + // + // this may end up revalidating some btree nodes when ckfetches + // is enabled, but we need to revalidate cached btree nodes or + // we risk missing errors in ckmeta scans + if ((lfs3_t_isckmeta(mtrv->h.flags) + || lfs3_t_isckdata(mtrv->h.flags)) + && tag == LFS3_TAG_BRANCH) { + lfs3_rbyd_t *rbyd = (lfs3_rbyd_t*)bptr_->d.u.buffer; + int err = lfs3_rbyd_fetchck(lfs3, rbyd, + rbyd->blocks[0], rbyd->trunk, + rbyd->cksum); + if (err) { + return err; + } + } + + // validate data blocks? + if (lfs3_t_isckdata(mtrv->h.flags) + && tag == LFS3_TAG_BLOCK) { + int err = lfs3_bptr_ck(lfs3, bptr_); + if (err) { + return err; + } + } + + return tag; + +eot:; + // compare gcksum with in-RAM gcksum + if ((lfs3_t_isckmeta(mtrv->h.flags) + || lfs3_t_isckdata(mtrv->h.flags)) + && !lfs3_t_isckpointed(mtrv->h.flags) + && mtrv->gcksum != lfs3->gcksum) { + LFS3_ERROR("Found gcksum mismatch, cksum %08"PRIx32" (!= %08"PRIx32")", + mtrv->gcksum, + lfs3->gcksum); + return LFS3_ERR_CORRUPT; + } + + // was ckmeta/ckdata successful? we only consider our filesystem + // checked if we weren't mutated + if ((lfs3_t_isckmeta(mtrv->h.flags) + || lfs3_t_isckdata(mtrv->h.flags)) + && !lfs3_t_ismtreeonly(mtrv->h.flags) + && !lfs3_t_isckpointed(mtrv->h.flags)) { + lfs3->flags &= ~LFS3_I_CKMETA; + } + if (lfs3_t_isckdata(mtrv->h.flags) + && !lfs3_t_ismtreeonly(mtrv->h.flags) + && !lfs3_t_isckpointed(mtrv->h.flags)) { + lfs3->flags &= ~LFS3_I_CKDATA; + } + + return LFS3_ERR_NOENT; +} + +// needed in lfs3_mtree_gc +static int lfs3_mdir_mkconsistent(lfs3_t *lfs3, lfs3_mdir_t *mdir); +static inline void lfs3_alloc_ckpoint_(lfs3_t *lfs3); +static void lfs3_alloc_adopt(lfs3_t *lfs3, lfs3_block_t known); +static int lfs3_alloc_zerogbmap(lfs3_t *lfs3, lfs3_btree_t *gbmap); +static int lfs3_gbmap_markbptr(lfs3_t *lfs3, lfs3_btree_t *gbmap, + lfs3_tag_t tag, const lfs3_bptr_t *bptr, + lfs3_tag_t tag_); +static void lfs3_alloc_adoptgbmap(lfs3_t *lfs3, + const lfs3_btree_t *gbmap, lfs3_block_t known); + +// high-level mutating traversal, handle extra features that require +// mutation here +static lfs3_stag_t lfs3_mtree_gc(lfs3_t *lfs3, lfs3_mgc_t *mgc, + lfs3_bptr_t *bptr_) { + // start of traversal? + if (mgc->t.h.mdir.mid == LFS3_MID_MROOTANCHOR) { + #ifndef LFS3_RDONLY + // checkpoint the allocator to maximize any lookahead scans + // + // note we try to repopupate even if the lookahead flag isn't + // set because there's no real downside + if (lfs3_t_islookahead(mgc->t.h.flags) + && !lfs3_t_ismtreeonly(mgc->t.h.flags) + && !lfs3_t_isckpointed(mgc->t.h.flags)) { + lfs3_alloc_ckpoint_(lfs3); + // keep our own ckpointed flag clear + mgc->t.h.flags &= ~LFS3_t_CKPOINTED & ~LFS3_t_DIRTY; + } + #endif + + #if !defined(LFS3_RDONLY) && defined(LFS3_GBMAP) + // create a new gbmap snapshot + // + // note we _don't_ try to repopulate if the lookgbmap flag isn't + // set because repopulating the gbmap requires disk writes and + // is potentially destructive + // + // note because we bail as soon as a ckpoint is triggered + // (lfs3_t_isckpointed), we don't need to include this snapshot + // in traversals, the ckpointed flag also means we don't need to + // worry about this repopulation condition becoming true later + if (lfs3_t_islookgbmap(mgc->t.h.flags) + && lfs3_f_isgbmap(lfs3->flags) + && lfs3_t_islookgbmap(lfs3->flags) + && !lfs3_t_ismtreeonly(mgc->t.h.flags) + && !lfs3_t_isckpointed(mgc->t.h.flags)) { + // at least checkpoint the lookahead buffer + lfs3_alloc_ckpoint_(lfs3); + + // create a copy of the gbmap + mgc->gbmap_ = lfs3->gbmap.b; + + // mark any in-use blocks as free + // + // we do this instead of creating a new gbmap to (1) preserve any + // erased/bad info and (2) try to best use any available + // erased-state + int err = lfs3_alloc_zerogbmap(lfs3, &mgc->gbmap_); + if (err) { + return err; + } + + // keep our own ckpointed flag clear + mgc->t.h.flags &= ~LFS3_t_CKPOINTED & ~LFS3_t_DIRTY; + } + #endif + } + +again:; + lfs3_stag_t tag = lfs3_mtree_traverse(lfs3, &mgc->t, + bptr_); + if (tag < 0) { + // end of traversal? + if (tag == LFS3_ERR_NOENT) { + goto eot; + } + return tag; + } + + #ifndef LFS3_RDONLY + // mark in-use blocks in lookahead? + if (lfs3_t_islookahead(mgc->t.h.flags) + && !lfs3_t_ismtreeonly(mgc->t.h.flags) + && !lfs3_t_isckpointed(mgc->t.h.flags)) { + lfs3_alloc_markinusebptr(lfs3, tag, bptr_); + } + + // mark in-use blocks in gbmap? + #ifdef LFS3_GBMAP + if (lfs3_t_islookgbmap(mgc->t.h.flags) + && lfs3_f_isgbmap(lfs3->flags) + && lfs3_t_islookgbmap(lfs3->flags) + && !lfs3_t_ismtreeonly(mgc->t.h.flags) + && !lfs3_t_isckpointed(mgc->t.h.flags)) { + int err = lfs3_gbmap_markbptr(lfs3, &mgc->gbmap_, tag, bptr_, + LFS3_TAG_BMINUSE); + if (err) { + return err; + } + } + #endif + + // mkconsistencing mdirs? + if (lfs3_t_ismkconsistent(mgc->t.h.flags) + && lfs3_t_ismkconsistent(lfs3->flags) + && tag == LFS3_TAG_MDIR) { + lfs3_mdir_t *mdir = (lfs3_mdir_t*)bptr_->d.u.buffer; + uint32_t dirty = mgc->t.h.flags; + int err = lfs3_mdir_mkconsistent(lfs3, mdir); + if (err) { + return err; + } + + // reset dirty flag + mgc->t.h.flags &= ~LFS3_t_DIRTY | dirty; + // make sure we clear any zombie flags + mgc->t.h.flags &= ~LFS3_o_ZOMBIE; + + // did this drop our mdir? + if (mdir->mid >= 0 && mdir->r.weight == 0) { + // continue traversal + // TODO big hack! is it big enough? + mgc->t.h.mdir.mid -= 1; + mgc->t.u.btrv.bid = LFS3_BID_MDIR; + goto again; + } + } + + // compacting mdirs? + if (lfs3_t_compactmeta(mgc->t.h.flags) + && tag == LFS3_TAG_MDIR + // exceed compaction threshold? + && lfs3_rbyd_eoff(&((lfs3_mdir_t*)bptr_->d.u.buffer)->r) + > ((lfs3->cfg->gc_compactmeta_thresh) + ? lfs3->cfg->gc_compactmeta_thresh + : lfs3->cfg->block_size - lfs3->cfg->block_size/8)) { + lfs3_mdir_t *mdir = (lfs3_mdir_t*)bptr_->d.u.buffer; + LFS3_INFO("Compacting mdir %"PRId32" 0x{%"PRIx32",%"PRIx32"} " + "(%"PRId32" > %"PRId32")", + lfs3_dbgmbid(lfs3, mdir->mid), + mdir->r.blocks[0], + mdir->r.blocks[1], + lfs3_rbyd_eoff(&mdir->r), + (lfs3->cfg->gc_compactmeta_thresh) + ? lfs3->cfg->gc_compactmeta_thresh + : lfs3->cfg->block_size - lfs3->cfg->block_size/8); + // compact the mdir + uint32_t dirty = mgc->t.h.flags; + int err = lfs3_mdir_compact(lfs3, mdir); + if (err) { + return err; + } + + // reset dirty flag + mgc->t.h.flags &= ~LFS3_t_DIRTY | dirty; + } + #endif + + return tag; + +eot:; + #ifndef LFS3_RDONLY + // was gbmap scan successful? + // + // this is structured this way because only one repopulation + // scan can succeed at a time, if gbmap succeeds it invalidates the + // lookahead scan with the new gbmap + // + // gbmap takes priority because it actually writes to disk + if (LFS3_IFDEF_GBMAP( + lfs3_t_islookgbmap(mgc->t.h.flags) + && lfs3_f_isgbmap(lfs3->flags) + && lfs3_t_islookgbmap(lfs3->flags) + && !lfs3_t_ismtreeonly(mgc->t.h.flags) + && !lfs3_t_isckpointed(mgc->t.h.flags), + false)) { + #ifdef LFS3_GBMAP + lfs3_alloc_adoptgbmap(lfs3, &mgc->gbmap_, lfs3->lookahead.ckpoint); + #endif + + // was lookahead scan successful? + } else if (lfs3_t_islookahead(mgc->t.h.flags) + && !lfs3_t_ismtreeonly(mgc->t.h.flags) + && !lfs3_t_isckpointed(mgc->t.h.flags)) { + lfs3_alloc_adopt(lfs3, lfs3->lookahead.ckpoint); + } + + // was mkconsistent successful? + if (lfs3_t_ismkconsistent(mgc->t.h.flags) + && !lfs3_t_isdirty(mgc->t.h.flags)) { + lfs3->flags &= ~LFS3_I_MKCONSISTENT; + } + + // was compaction successful? note we may need multiple passes if + // we want to be sure everything is compacted + if (lfs3_t_compactmeta(mgc->t.h.flags) + && !lfs3_t_isckpointed(mgc->t.h.flags)) { + lfs3->flags &= ~LFS3_I_COMPACTMETA; + } + #endif + + return LFS3_ERR_NOENT; +} + + + + +/// Optional on-disk block map /// + +#ifdef LFS3_GBMAP +static void lfs3_gbmap_init(lfs3_gbmap_t *gbmap) { + gbmap->window = 0; + gbmap->known = 0; + lfs3_btree_init(&gbmap->b); + lfs3_btree_init(&gbmap->b_p); +} +#endif + +#if !defined(LFS3_RDONLY) && defined(LFS3_GBMAP) +static lfs3_data_t lfs3_data_fromgbmap(const lfs3_gbmap_t *gbmap, + uint8_t buffer[static LFS3_GBMAP_DSIZE]) { + // window should not exceed 31-bits + LFS3_ASSERT(gbmap->window <= 0x7fffffff); + // known should not exceed 31-bits + LFS3_ASSERT(gbmap->known <= 0x7fffffff); + + // make sure to zero so we don't leak any info + lfs3_memset(buffer, 0, LFS3_GBMAP_DSIZE); + + lfs3_ssize_t d = 0; + lfs3_ssize_t d_ = lfs3_toleb128(gbmap->window, &buffer[d], 5); + if (d_ < 0) { + LFS3_UNREACHABLE(); + } + d += d_; + + d_ = lfs3_toleb128(gbmap->known, &buffer[d], 5); + if (d_ < 0) { + LFS3_UNREACHABLE(); + } + d += d_; + + lfs3_data_t data = lfs3_data_frombranch(&gbmap->b.r, &buffer[d]); + d += lfs3_data_size(data); + + return LFS3_DATA_BUF(buffer, lfs3_memlen(buffer, LFS3_GBMAP_DSIZE)); +} +#endif + +#ifdef LFS3_GBMAP +static int lfs3_data_readgbmap(lfs3_t *lfs3, lfs3_data_t *data, + lfs3_gbmap_t *gbmap) { + int err = lfs3_data_readleb128(lfs3, data, &gbmap->window); + if (err) { + return err; + } + + err = lfs3_data_readleb128(lfs3, data, &gbmap->known); + if (err) { + return err; + } + + err = lfs3_data_readbranch(lfs3, data, lfs3->block_count, + &gbmap->b.r); + if (err) { + return err; + } + + #ifdef LFS3_BLEAFCACHE + // make sure to zero btree leaf + lfs3_btree_discardleaf(&gbmap->b); + #endif + // and keep track of the committed gbmap for traversals + gbmap->b_p = gbmap->b; + return 0; +} +#endif + +// on-disk global block-map operations + +#ifdef LFS3_GBMAP +static lfs3_stag_t lfs3_gbmap_lookupnext(lfs3_t *lfs3, lfs3_btree_t *gbmap, + lfs3_bid_t bid, + lfs3_bid_t *bid_, lfs3_bid_t *weight_) { + return lfs3_btree_lookupnext(lfs3, gbmap, bid, + bid_, weight_, NULL); +} +#endif + +#if !defined(LFS3_RDONLY) && defined(LFS3_GBMAP) +static int lfs3_gbmap_commit(lfs3_t *lfs3, lfs3_btree_t *gbmap, + lfs3_bid_t bid, const lfs3_rattr_t *rattrs, lfs3_size_t rattr_count) { + return lfs3_btree_commit(lfs3, gbmap, bid, rattrs, rattr_count); +} +#endif + +#if !defined(LFS3_RDONLY) && defined(LFS3_GBMAP) +// note while this does takes a weight, it's limited to only a single +// range, cross-range sets are not currently not supported +// +// really this just provides a shortcut for bulk clearing ranges in +// lfs3_alloc_lookgbmap +static int lfs3_gbmap_mark_(lfs3_t *lfs3, lfs3_btree_t *gbmap, + lfs3_block_t block, lfs3_block_t weight, lfs3_tag_t tag) { + // lookup gbmap range + lfs3_bid_t bid__; + lfs3_bid_t weight__; + lfs3_stag_t tag__ = lfs3_gbmap_lookupnext(lfs3, gbmap, block, + &bid__, &weight__); + if (tag__ < 0) { + LFS3_ASSERT(tag__ != LFS3_ERR_NOENT); + return tag__; + } + + // wait, already set to expected type? guess we're done + if (tag__ == tag) { + return 0; + } + + // temporary copy, we definitely _don't_ want to leave this in a + // weird state on error + lfs3_btree_t gbmap_ = *gbmap; + // mark as unfetched in case of error + lfs3_btree_claim(gbmap); + + // weight of new range + lfs3_bid_t weight_ = weight; + + // can we merge with right neighbor? + // + // note if we're in the middle of a range we should never need to + // merge + if (block == bid__ + && block < lfs3->block_count-1) { + lfs3_bid_t r_bid; + lfs3_bid_t r_weight; + lfs3_stag_t r_tag = lfs3_gbmap_lookupnext(lfs3, gbmap, block+1, + &r_bid, &r_weight); + if (r_tag < 0) { + LFS3_ASSERT(r_tag != LFS3_ERR_NOENT); + return r_tag; + } + LFS3_ASSERT(r_weight == r_bid - block); + + if (r_tag == tag) { + // delete to prepare merge + int err = lfs3_gbmap_commit(lfs3, &gbmap_, r_bid, LFS3_RATTRS( + LFS3_RATTR(LFS3_tag_RM, -r_weight))); + if (err) { + return err; + } + + // merge + weight_ += r_weight; + } + } + + // can we merge with left neighbor? + // + // note if we're in the middle of a range we should never need to + // merge + if (block-(weight-1) == bid__-(weight__-1) + && block-(weight-1) > 0) { + lfs3_bid_t l_bid; + lfs3_bid_t l_weight; + lfs3_stag_t l_tag = lfs3_gbmap_lookupnext(lfs3, gbmap, block-weight, + &l_bid, &l_weight); + if (l_tag < 0) { + LFS3_ASSERT(l_tag != LFS3_ERR_NOENT); + return l_tag; + } + LFS3_ASSERT(l_bid == block-weight); + + if (l_tag == tag) { + // delete to prepare merge + int err = lfs3_gbmap_commit(lfs3, &gbmap_, l_bid, LFS3_RATTRS( + LFS3_RATTR(LFS3_tag_RM, -l_weight))); + if (err) { + return err; + } + + // merge + weight_ += l_weight; + // adjust target block/bid + block -= l_weight; + bid__ -= l_weight; + } + } + + // commit new range + // + // if we're in the middle of a range, we need to split and inject + // the new range, possibly creating two neighbors in the process + // + // we do this in a bit of a weird order due to how our commit API + // works + int err = lfs3_gbmap_commit(lfs3, &gbmap_, bid__, LFS3_RATTRS( + (bid__-(weight__-1) < block-(weight-1)) + ? LFS3_RATTR(LFS3_tag_GROW, -((bid__+1) - (block-(weight-1)))) + : LFS3_RATTR(LFS3_tag_RM, -((bid__+1) - (block-(weight-1)))), + LFS3_RATTR(tag, +weight_), + (bid__ > block) + ? LFS3_RATTR(tag__, +(bid__ - block)) + : LFS3_RATTR_NOOP())); + if (err) { + return err; + } + + // done! + *gbmap = gbmap_; + return 0; +} +#endif + +#if !defined(LFS3_RDONLY) && defined(LFS3_GBMAP) +static int lfs3_gbmap_mark(lfs3_t *lfs3, lfs3_btree_t *gbmap, + lfs3_block_t block, lfs3_tag_t tag) { + return lfs3_gbmap_mark_(lfs3, gbmap, block, 1, tag); +} +#endif + +#if !defined(LFS3_RDONLY) && defined(LFS3_GBMAP) +static int lfs3_gbmap_markbptr(lfs3_t *lfs3, lfs3_btree_t *gbmap, + lfs3_tag_t tag, const lfs3_bptr_t *bptr, + lfs3_tag_t tag_) { + const lfs3_block_t *blocks; + lfs3_size_t block_count; + if (tag == LFS3_TAG_MDIR) { + lfs3_mdir_t *mdir = (lfs3_mdir_t*)bptr->d.u.buffer; + blocks = mdir->r.blocks; + block_count = 2; + + } else if (tag == LFS3_TAG_BRANCH) { + lfs3_rbyd_t *rbyd = (lfs3_rbyd_t*)bptr->d.u.buffer; + blocks = rbyd->blocks; + block_count = 1; + + } else if (tag == LFS3_TAG_BLOCK) { + blocks = &bptr->d.u.disk.block; + block_count = 1; + + } else { + LFS3_UNREACHABLE(); + } + + for (lfs3_size_t i = 0; i < block_count; i++) { + int err = lfs3_gbmap_mark(lfs3, gbmap, blocks[i], tag_); + if (err) { + return err; + } + } + + return 0; +} +#endif + + + +/// Block allocator /// + +// needed in lfs3_alloc_ckpoint_ +#ifndef LFS3_RDONLY +static void lfs3_trv_ckpoint_(lfs3_t *lfs3, lfs3_trv_t *trv); +#endif + +// checkpoint only the lookahead buffer +#ifndef LFS3_RDONLY +static inline void lfs3_alloc_ckpoint_(lfs3_t *lfs3) { + // set ckpoint = disk size + lfs3->lookahead.ckpoint = lfs3->block_count; + + // ckpoint traversals, marking them as ckpointed + dirty and + // reseting any btrv state + for (lfs3_handle_t *h = lfs3->handles; h; h = h->next) { + if (lfs3_o_type(h->flags) == LFS3_type_TRV) { + lfs3_mgc_ckpoint((lfs3_mgc_t*)h); + } + } +} +#endif + +// needed in lfs3_alloc_ckpoint +static int lfs3_alloc_lookgbmap(lfs3_t *lfs3); + +// checkpoint the allocator +// +// operations that need to alloc should call this when all in-use blocks +// are tracked, either by the filesystem or an opened mdir +// +// blocks are allocated at most once, and never reallocated, between +// checkpoints +#if !defined(LFS3_RDONLY) +static inline int lfs3_alloc_ckpoint(lfs3_t *lfs3) { + // checkpoint the allocator + lfs3_alloc_ckpoint_(lfs3); + + #ifdef LFS3_GBMAP + // do we need to repopulate the gbmap? + if (lfs3_f_isgbmap(lfs3->flags) + && lfs3->gbmap.known < lfs3_min( + lfs3->cfg->lookgbmap_thresh, + lfs3->block_count)) { + int err = lfs3_alloc_lookgbmap(lfs3); + if (err) { + return err; + } + + // checkpoint the allocator again + lfs3_alloc_ckpoint_(lfs3); + } + #endif + + return 0; +} +#endif + +// discard any lookahead/gbmap windows, this is necessary if block_count +// changes +#ifndef LFS3_RDONLY +static inline void lfs3_alloc_discard(lfs3_t *lfs3) { + // discard lookahead state + lfs3->lookahead.known = 0; + lfs3_memset(lfs3->lookahead.buffer, 0, lfs3->cfg->lookahead_size); + + // discard/resync the gbmap window, the resync is necessary to avoid + // disk changes breaking our mod math + #ifdef LFS3_GBMAP + lfs3->gbmap.window = (lfs3->lookahead.window + lfs3->lookahead.off) + % lfs3->block_count; + lfs3->gbmap.known = 0; + #endif +} +#endif + +// mark a block as in-use +#ifndef LFS3_RDONLY +static void lfs3_alloc_markinuse(lfs3_t *lfs3, lfs3_block_t block) { + // translate to lookahead-relative + lfs3_block_t block_ = (( + (lfs3_sblock_t)(block + - (lfs3->lookahead.window + lfs3->lookahead.off)) + // we only need this mess because C's mod is actually rem, and + // we want real mod in case block_ goes negative + % (lfs3_sblock_t)lfs3->block_count) + + (lfs3_sblock_t)lfs3->block_count) + % (lfs3_sblock_t)lfs3->block_count; + + if (block_ < 8*lfs3->cfg->lookahead_size) { + // mark as in-use + lfs3->lookahead.buffer[ + ((lfs3->lookahead.off + block_) / 8) + % lfs3->cfg->lookahead_size] + |= 1 << ((lfs3->lookahead.off + block_) % 8); + } +} +#endif + +// mark some filesystem object as in-use +#ifndef LFS3_RDONLY +static void lfs3_alloc_markinusebptr(lfs3_t *lfs3, + lfs3_tag_t tag, const lfs3_bptr_t *bptr) { + if (tag == LFS3_TAG_MDIR) { + lfs3_mdir_t *mdir = (lfs3_mdir_t*)bptr->d.u.buffer; + lfs3_alloc_markinuse(lfs3, mdir->r.blocks[0]); + lfs3_alloc_markinuse(lfs3, mdir->r.blocks[1]); + + } else if (tag == LFS3_TAG_BRANCH) { + lfs3_rbyd_t *rbyd = (lfs3_rbyd_t*)bptr->d.u.buffer; + lfs3_alloc_markinuse(lfs3, rbyd->blocks[0]); + + } else if (tag == LFS3_TAG_BLOCK) { + lfs3_alloc_markinuse(lfs3, lfs3_bptr_block(bptr)); + + } else { + LFS3_UNREACHABLE(); + } +} +#endif + +// mark lookahead buffer to match gbmap +#if !defined(LFS3_RDONLY) && defined(LFS3_GBMAP) +static int lfs3_alloc_markinusegbmap(lfs3_t *lfs3, lfs3_btree_t *gbmap, + lfs3_block_t known) { + lfs3_block_t block = lfs3->lookahead.window + lfs3->lookahead.off; + while (block < lfs3->lookahead.window + lfs3->lookahead.off + known) { + lfs3_block_t block_ = block % lfs3->block_count; + lfs3_block_t block__; + lfs3_stag_t tag = lfs3_gbmap_lookupnext(lfs3, gbmap, block_, + &block__, NULL); + if (tag < 0) { + return tag; + } + lfs3_block_t d = (block__+1) - block_; + + // in-use? bad? etc? mark as in-use + if (tag != LFS3_TAG_BMFREE) { + // this could probably be more efficient, but cpu usage is + // not a big priority at the moment + for (lfs3_block_t i = 0; i < d; i++) { + lfs3_alloc_markinuse(lfs3, block_+i); + } + } + + block += d; + } + + return 0; +} +#endif + +// needed in lfs3_alloc_adopt +static lfs3_sblock_t lfs3_alloc_findfree(lfs3_t *lfs3); + +// mark any not-in-use blocks as free +#ifndef LFS3_RDONLY +static void lfs3_alloc_adopt(lfs3_t *lfs3, lfs3_block_t known) { + // make lookahead buffer usable + lfs3->lookahead.known = lfs3_min( + 8*lfs3->cfg->lookahead_size, + known); + + // signal that lookahead is full + lfs3->flags &= ~LFS3_I_LOOKAHEAD; + + // eagerly find the next free block so lookahead scans can make + // the most progress + lfs3_alloc_findfree(lfs3); +} +#endif + +// can we repopulate the lookahead buffer? +#if !defined(LFS3_RDONLY) +static inline bool lfs3_alloc_islookahead(const lfs3_t *lfs3) { + return lfs3->lookahead.known + <= lfs3_min( + lfs3->cfg->gc_lookahead_thresh, + lfs3_min( + 8*lfs3->cfg->lookahead_size-1, + lfs3->block_count-1)); +} +#endif + +// can we repopulate the gbmap? +#if !defined(LFS3_RDONLY) && defined(LFS3_GBMAP) +static inline bool lfs3_alloc_islookgbmap(const lfs3_t *lfs3) { + return lfs3->gbmap.known + <= lfs3_min( + lfs3_max( + lfs3->cfg->gc_lookgbmap_thresh, + lfs3->cfg->lookgbmap_thresh), + lfs3->block_count-1); +} +#endif + +// increment lookahead buffer +#ifndef LFS3_RDONLY +static void lfs3_alloc_inc(lfs3_t *lfs3) { + LFS3_ASSERT(lfs3->lookahead.known > 0); + + // clear lookahead as we increment + lfs3->lookahead.buffer[lfs3->lookahead.off / 8] + &= ~(1 << (lfs3->lookahead.off % 8)); + + // increment next/off + lfs3->lookahead.off += 1; + if (lfs3->lookahead.off == 8*lfs3->cfg->lookahead_size) { + lfs3->lookahead.off = 0; + lfs3->lookahead.window + = (lfs3->lookahead.window + 8*lfs3->cfg->lookahead_size) + % lfs3->block_count; + } + + // decrement size + lfs3->lookahead.known -= 1; + // decrement ckpoint + lfs3->lookahead.ckpoint -= 1; + + // signal that lookahead is no longer full + if (lfs3_alloc_islookahead(lfs3)) { + lfs3->flags |= LFS3_I_LOOKAHEAD; + } + + // decrement gbmap known window + #ifdef LFS3_GBMAP + if (lfs3_f_isgbmap(lfs3->flags)) { + lfs3->gbmap.window = (lfs3->gbmap.window + 1) % lfs3->block_count; + lfs3->gbmap.known = lfs3_smax(lfs3->gbmap.known-1, 0); + + // signal that the gbmap is no longer full + if (lfs3_alloc_islookgbmap(lfs3)) { + lfs3->flags |= LFS3_I_LOOKGBMAP; + } + } + #endif +} +#endif + +// find next free block in lookahead buffer, if there is one +#ifndef LFS3_RDONLY +static lfs3_sblock_t lfs3_alloc_findfree(lfs3_t *lfs3) { + while (lfs3->lookahead.known > 0) { + if (!(lfs3->lookahead.buffer[lfs3->lookahead.off / 8] + & (1 << (lfs3->lookahead.off % 8)))) { + // found a free block + return (lfs3->lookahead.window + lfs3->lookahead.off) + % lfs3->block_count; + } + + lfs3_alloc_inc(lfs3); + } + + return LFS3_ERR_NOSPC; +} +#endif + +// needed in lfs3_alloc +static inline lfs3_size_t lfs3_graft_count(lfs3_size_t graft_count); + +// allocate a block +#ifndef LFS3_RDONLY +static lfs3_sblock_t lfs3_alloc(lfs3_t *lfs3, uint32_t flags) { + while (true) { + // scan our lookahead buffer for free blocks + lfs3_sblock_t block = lfs3_alloc_findfree(lfs3); + if (block < 0 && block != LFS3_ERR_NOSPC) { + return block; + } + + if (block != LFS3_ERR_NOSPC) { + // we should never alloc blocks {0,1} + LFS3_ASSERT(block != 0 && block != 1); + + // erase requested? + if (lfs3_alloc_iserase(flags)) { + int err = lfs3_bd_erase(lfs3, block); + if (err) { + // bad erase? try another block + if (err == LFS3_ERR_CORRUPT) { + lfs3_alloc_inc(lfs3); + continue; + } + return err; + } + } + + // eagerly find the next free block to maximize how many blocks + // lfs3_alloc_ckpoint makes available for scanning + lfs3_alloc_inc(lfs3); + lfs3_alloc_findfree(lfs3); + + #ifdef LFS3_DBGALLOCS + LFS3_DEBUG("Allocated block 0x%"PRIx32", " + "lookahead %"PRId32"/%"PRId32, + block, + lfs3->lookahead.known, + lfs3->block_count); + #endif + return block; + } + + // in order to keep our block allocator from spinning forever when our + // filesystem is full, we mark points where there are no in-flight + // allocations with a checkpoint before starting a set of allocations + // + // if we've looked at all blocks since the last checkpoint, we report + // the filesystem as out of storage + // + if (lfs3->lookahead.ckpoint <= 0) { + LFS3_ERROR("No more free space " + "(lookahead %"PRId32"/%"PRId32")", + lfs3->lookahead.known, + lfs3->block_count); + return LFS3_ERR_NOSPC; + } + + // no blocks in our lookahead buffer? + + // known blocks in our gbmap? + #ifdef LFS3_GBMAP + if (lfs3_f_isgbmap(lfs3->flags) && lfs3->gbmap.known > 0) { + int err = lfs3_alloc_markinusegbmap(lfs3, + &lfs3->gbmap.b, lfs3_min( + lfs3->gbmap.known, + lfs3->lookahead.ckpoint)); + if (err) { + return err; + } + + // with known blocks we can trust the block state to be + // exact + lfs3_alloc_adopt(lfs3, lfs3_min( + lfs3->gbmap.known, + lfs3->lookahead.ckpoint)); + continue; + } + #endif + + // no known blocks? fallback to scanning the filesystem + // + // traverse the filesystem, building up knowledge of what blocks are + // in-use in the next lookahead window + // + lfs3_mtrv_t mtrv; + lfs3_mtrv_init(&mtrv, LFS3_T_RDONLY | LFS3_T_LOOKAHEAD); + while (true) { + lfs3_bptr_t bptr; + lfs3_stag_t tag = lfs3_mtree_traverse(lfs3, &mtrv, + &bptr); + if (tag < 0) { + if (tag == LFS3_ERR_NOENT) { + break; + } + return tag; + } + + // track in-use blocks + lfs3_alloc_markinusebptr(lfs3, tag, &bptr); + } + + // mask out any in-flight graft state + for (lfs3_size_t i = 0; + i < lfs3_graft_count(lfs3->graft_count); + i++) { + lfs3_alloc_markinuse(lfs3, lfs3->graft[i].u.disk.block); + } + + // mark anything not seen as free + lfs3_alloc_adopt(lfs3, lfs3->lookahead.ckpoint); + } +} +#endif + +#if !defined(LFS3_RDONLY) && defined(LFS3_GBMAP) +// note this is not completely atomic, but worst case we just end up with +// only some ranges zeroed +static int lfs3_alloc_zerogbmap(lfs3_t *lfs3, lfs3_btree_t *gbmap) { + lfs3_block_t block__ = -1; + while (true) { + lfs3_block_t weight__; + lfs3_stag_t tag__ = lfs3_gbmap_lookupnext(lfs3, gbmap, block__+1, + &block__, &weight__); + if (tag__ < 0) { + if (tag__ == LFS3_ERR_NOENT) { + break; + } + return tag__; + } + + // mark in-use ranges as free + if (tag__ == LFS3_TAG_BMINUSE) { + int err = lfs3_gbmap_mark_(lfs3, gbmap, block__, weight__, + LFS3_TAG_BMFREE); + if (err) { + return err; + } + } + } + + return 0; +} +#endif + +#if !defined(LFS3_RDONLY) && defined(LFS3_GBMAP) +static void lfs3_alloc_adoptgbmap(lfs3_t *lfs3, + const lfs3_btree_t *gbmap, lfs3_block_t known) { + // adopt new gbmap + lfs3->gbmap.known = known; + lfs3->gbmap.b = *gbmap; + + // signal that gbmap is full + lfs3->flags &= ~LFS3_I_LOOKGBMAP; +} +#endif + +#if !defined(LFS3_RDONLY) && defined(LFS3_GBMAP) +static int lfs3_alloc_lookgbmap(lfs3_t *lfs3) { + LFS3_INFO("Repopulating gbmap (gbmap %"PRId32"/%"PRId32")", + lfs3->gbmap.known, + lfs3->block_count); + + // create a copy of the gbmap + lfs3_btree_t gbmap_ = lfs3->gbmap.b; + + // mark any in-use blocks as free + // + // we do this instead of creating a new gbmap to (1) preserve any + // erased/bad info and (2) try to best use any available + // erased-state + int err = lfs3_alloc_zerogbmap(lfs3, &gbmap_); + if (err) { + return err; + } + + // traverse the filesystem, building up knowledge of what blocks are + // in-use + lfs3_mtrv_t mtrv; + lfs3_mtrv_init(&mtrv, LFS3_T_RDONLY); + while (true) { + lfs3_bptr_t bptr; + lfs3_stag_t tag = lfs3_mtree_traverse(lfs3, &mtrv, + &bptr); + if (tag < 0) { + if (tag == LFS3_ERR_NOENT) { + break; + } + return tag; + } + + // track in-use blocks + err = lfs3_gbmap_markbptr(lfs3, &gbmap_, tag, &bptr, + LFS3_TAG_BMINUSE); + if (err) { + return err; + } + } + + // update gbmap with what we found + // + // we don't commit this to disk immediately, instead we piggypack on + // the next mdir commit, most writes terminate in an mdir commit so + // this avoids extra writing at a risk of needing to repopulate the + // gbmap if we lose power + // + lfs3_alloc_adoptgbmap(lfs3, &gbmap_, lfs3->lookahead.ckpoint); + return 0; +} +#endif + + + + +/// Directory operations /// + +#ifndef LFS3_RDONLY +int lfs3_mkdir(lfs3_t *lfs3, const char *path) { + // prepare our filesystem for writing + int err = lfs3_fs_mkconsistent(lfs3); + if (err) { + return err; + } + + // lookup our parent + lfs3_mdir_t mdir; + lfs3_did_t did; + lfs3_stag_t tag = lfs3_mtree_pathlookup(lfs3, &path, + &mdir, &did); + if (tag < 0 + && !(tag == LFS3_ERR_NOENT + && lfs3_path_islast(path))) { + return tag; + } + // already exists? pretend orphans don't exist + if (tag != LFS3_ERR_NOENT && tag != LFS3_tag_ORPHAN) { + return LFS3_ERR_EXIST; + } + + // check that name fits + const char *name = path; + lfs3_size_t name_len = lfs3_path_namelen(path); + if (name_len > lfs3->name_limit) { + return LFS3_ERR_NAMETOOLONG; + } + + // find an arbitrary directory-id (did) + // + // This could be anything, but we want to have few collisions while + // also being deterministic. Here we use the checksum of the + // filename xored with the parent's did. + // + // did = parent_did xor crc32c(name) + // + // We use crc32c here not because it is a good hash function, but + // because it is convenient. The did doesn't need to be reproducible + // so this isn't a compatibility concern. + // + // We also truncate to make better use of our leb128 encoding. This is + // somewhat arbitrary, but if we truncate too much we risk increasing + // the number of collisions, so we want to aim for ~2x the number dids + // in the system: + // + // dmask = 2*dids + // + // But we don't actually know how many dids are in the system. + // Fortunately, we can guess an upper bound based on the number of + // mdirs in the mtree: + // + // mdirs + // dmask = 2 * ----- + // d + // + // Worst case (or best case?) each directory needs 1 name tag, 1 did + // tag, and 1 bookmark. With our current compaction strategy, each tag + // needs 3t+4 bytes for tag+alts (see our rattr_estimate). And, if + // we assume ~1/2 block utilization due to our mdir split threshold, we + // can multiply everything by 2: + // + // d = 3 * (3t+4) * 2 = 18t + 24 + // + // Assuming t=4 bytes, the minimum tag encoding: + // + // d = 18*4 + 24 = 96 bytes + // + // Rounding down to a power-of-two (again this is all arbitrary), gives + // us ~64 bytes per directory: + // + // mdirs mdirs + // dmask = 2 * ----- = ----- + // 64 32 + // + // This is a nice number because for common NOR flash geometry, + // 4096/32 = 128, so a filesystem with a single mdir encodes dids in a + // single byte. + // + // Note we also need to be careful to catch integer overflow. + // + lfs3_did_t dmask + = (1 << lfs3_min( + lfs3_nlog2(lfs3_mtree_weight(lfs3) >> lfs3->mbits) + + lfs3_nlog2(lfs3->cfg->block_size/32), + 31) + ) - 1; + lfs3_did_t did_ = (did ^ lfs3_crc32c(0, name, name_len)) & dmask; + + // check if we have a collision, if we do, search for the next + // available did + while (true) { + lfs3_stag_t tag_ = lfs3_mtree_namelookup(lfs3, did_, NULL, 0, + &mdir, NULL); + if (tag_ < 0) { + if (tag_ == LFS3_ERR_NOENT) { + break; + } + return tag_; + } + + // try the next did + did_ = (did_ + 1) & dmask; + } + + // found a good did, now to commit to the mtree + // + // A problem: we need to create both: + // 1. the metadata entry + // 2. the bookmark entry + // + // To do this atomically, we first create the bookmark entry with a grm + // to delete-self in case of powerloss, then create the metadata entry + // while atomically cancelling the grm. + // + // This is done automatically by lfs3_mdir_commit to avoid issues with + // mid updates, since the mid technically doesn't exist yet... + + // commit our bookmark and a grm to self-remove in case of powerloss + err = lfs3_mdir_commit(lfs3, &mdir, LFS3_RATTRS( + LFS3_RATTR_NAME( + LFS3_TAG_BOOKMARK, +1, did_, NULL, 0), + LFS3_RATTR( + LFS3_tag_GRMPUSH, 0))); + if (err) { + return err; + } + LFS3_ASSERT(lfs3->grm.queue[0] == mdir.mid); + + // committing our bookmark may have changed the mid of our metadata entry, + // we need to look it up again, we can at least avoid the full path walk + lfs3_stag_t tag_ = lfs3_mtree_namelookup(lfs3, did, name, name_len, + &mdir, NULL); + if (tag_ < 0 && tag_ != LFS3_ERR_NOENT) { + return tag_; + } + LFS3_ASSERT((tag != LFS3_ERR_NOENT) + ? tag_ >= 0 + : tag_ == LFS3_ERR_NOENT); + + // commit our new directory into our parent, zeroing the grm in the + // process + lfs3_grm_pop(lfs3); + err = lfs3_mdir_commit(lfs3, &mdir, LFS3_RATTRS( + LFS3_RATTR_NAME( + LFS3_tag_MASK12 | LFS3_TAG_DIR, + (tag == LFS3_ERR_NOENT) ? +1 : 0, + did, name, name_len), + LFS3_RATTR_LEB128( + LFS3_TAG_DID, 0, did_))); + if (err) { + return err; + } + + // update in-device state + for (lfs3_handle_t *h = lfs3->handles; h; h = h->next) { + // mark any clobbered uncreats as zombied + if (tag != LFS3_ERR_NOENT + && lfs3_o_type(h->flags) == LFS3_TYPE_REG + && h->mdir.mid == mdir.mid) { + h->flags = (h->flags & ~LFS3_o_UNCREAT) + | LFS3_o_ZOMBIE + | LFS3_o_UNSYNC + | LFS3_O_DESYNC; + + // update dir positions + } else if (tag == LFS3_ERR_NOENT + && lfs3_o_type(h->flags) == LFS3_TYPE_DIR + && ((lfs3_dir_t*)h)->did == did + && h->mdir.mid >= mdir.mid) { + ((lfs3_dir_t*)h)->pos += 1; + } + } + + return 0; +} +#endif + +// push a did to grm, but only if the directory is empty +#ifndef LFS3_RDONLY +static int lfs3_grm_pushdid(lfs3_t *lfs3, lfs3_did_t did) { + // first lookup the bookmark entry + lfs3_mdir_t bookmark_mdir; + lfs3_stag_t tag = lfs3_mtree_namelookup(lfs3, did, NULL, 0, + &bookmark_mdir, NULL); + if (tag < 0) { + LFS3_ASSERT(tag != LFS3_ERR_NOENT); + return tag; + } + lfs3_mid_t bookmark_mid = bookmark_mdir.mid; + + // check that the directory is empty + bookmark_mdir.mid += 1; + if (lfs3_mrid(lfs3, bookmark_mdir.mid) + >= (lfs3_srid_t)bookmark_mdir.r.weight) { + tag = lfs3_mtree_lookup(lfs3, + lfs3_mbid(lfs3, bookmark_mdir.mid-1) + 1, + &bookmark_mdir); + if (tag < 0) { + if (tag == LFS3_ERR_NOENT) { + goto empty; + } + return tag; + } + } + + lfs3_data_t data; + tag = lfs3_mdir_lookup(lfs3, &bookmark_mdir, + LFS3_tag_MASK8 | LFS3_TAG_NAME, + &data); + if (tag < 0) { + LFS3_ASSERT(tag != LFS3_ERR_NOENT); + return tag; + } + + lfs3_did_t did_; + int err = lfs3_data_readleb128(lfs3, &data, &did_); + if (err) { + return err; + } + + if (did_ == did) { + return LFS3_ERR_NOTEMPTY; + } + +empty:; + lfs3_grm_push(lfs3, bookmark_mid); + return 0; +} +#endif + +// needed in lfs3_remove +static int lfs3_fs_fixgrm(lfs3_t *lfs3); + +#ifndef LFS3_RDONLY +int lfs3_remove(lfs3_t *lfs3, const char *path) { + // prepare our filesystem for writing + int err = lfs3_fs_mkconsistent(lfs3); + if (err) { + return err; + } + + // lookup our entry + lfs3_mdir_t mdir; + lfs3_did_t did; + lfs3_stag_t tag = lfs3_mtree_pathlookup(lfs3, &path, + &mdir, &did); + if (tag < 0) { + return tag; + } + // pretend orphans don't exist + if (tag == LFS3_tag_ORPHAN) { + return LFS3_ERR_NOENT; + } + + // trying to remove the root dir? + if (mdir.mid == -1) { + return LFS3_ERR_BUSY; + } + + // if we're removing a directory, we need to also remove the + // bookmark entry + lfs3_did_t did_ = 0; + if (tag == LFS3_TAG_DIR) { + // first lets figure out the did + lfs3_data_t data_; + lfs3_stag_t tag_ = lfs3_mdir_lookup(lfs3, &mdir, LFS3_TAG_DID, + &data_); + if (tag_ < 0) { + return tag_; + } + + err = lfs3_data_readleb128(lfs3, &data_, &did_); + if (err) { + return err; + } + + // mark bookmark for removal with grm + err = lfs3_grm_pushdid(lfs3, did_); + if (err) { + return err; + } + } + + // are we removing an opened file? + bool zombie = lfs3_mid_isopen(lfs3, mdir.mid, -1); + + // remove the metadata entry + err = lfs3_mdir_commit(lfs3, &mdir, LFS3_RATTRS( + // create a stickynote if zombied + // + // we use a create+delete here to also clear any rattrs + // and trim the entry size + (zombie) + ? LFS3_RATTR_NAME( + LFS3_tag_MASK12 | LFS3_TAG_STICKYNOTE, 0, + did, path, lfs3_path_namelen(path)) + : LFS3_RATTR( + LFS3_tag_RM, -1))); + if (err) { + return err; + } + + // update in-device state + for (lfs3_handle_t *h = lfs3->handles; h; h = h->next) { + // mark any clobbered uncreats as zombied + if (zombie + && lfs3_o_type(h->flags) == LFS3_TYPE_REG + && h->mdir.mid == mdir.mid) { + h->flags |= LFS3_o_UNCREAT + | LFS3_o_ZOMBIE + | LFS3_o_UNSYNC + | LFS3_O_DESYNC; + + // mark any removed dirs as zombied + } else if (did_ + && lfs3_o_type(h->flags) == LFS3_TYPE_DIR + && ((lfs3_dir_t*)h)->did == did_) { + h->flags |= LFS3_o_ZOMBIE; + + // update dir positions + } else if (lfs3_o_type(h->flags) == LFS3_TYPE_DIR + && ((lfs3_dir_t*)h)->did == did + && h->mdir.mid >= mdir.mid) { + if (lfs3_o_iszombie(h->flags)) { + h->flags &= ~LFS3_o_ZOMBIE; + } else { + ((lfs3_dir_t*)h)->pos -= 1; + } + + // clobber entangled traversals + } else if (lfs3_o_type(h->flags) == LFS3_type_TRV) { + if (lfs3_o_iszombie(h->flags)) { + // TODO should we just not set ZOMBIE on trvs? + h->flags &= ~LFS3_o_ZOMBIE; + } + } + } + + // if we were a directory, we need to clean up, fortunately we can leave + // this up to lfs3_fs_fixgrm + err = lfs3_fs_fixgrm(lfs3); + if (err) { + // TODO is this the right thing to do? we should probably still + // propagate errors to the user + // + // we did complete the remove, so we shouldn't error here, best + // we can do is log this + LFS3_WARN("Failed to clean up grm (%d)", err); + } + + return 0; +} +#endif + +#ifndef LFS3_RDONLY +int lfs3_rename(lfs3_t *lfs3, const char *old_path, const char *new_path) { + // prepare our filesystem for writing + int err = lfs3_fs_mkconsistent(lfs3); + if (err) { + return err; + } + + // lookup old entry + lfs3_mdir_t old_mdir; + lfs3_did_t old_did; + lfs3_stag_t old_tag = lfs3_mtree_pathlookup(lfs3, &old_path, + &old_mdir, &old_did); + if (old_tag < 0) { + return old_tag; + } + // pretend orphans don't exist + if (old_tag == LFS3_tag_ORPHAN) { + return LFS3_ERR_NOENT; + } + + // trying to rename the root? + if (old_mdir.mid == -1) { + return LFS3_ERR_BUSY; + } + + // lookup new entry + lfs3_mdir_t new_mdir; + lfs3_did_t new_did; + lfs3_stag_t new_tag = lfs3_mtree_pathlookup(lfs3, &new_path, + &new_mdir, &new_did); + if (new_tag < 0 + && !(new_tag == LFS3_ERR_NOENT + && lfs3_path_islast(new_path))) { + return new_tag; + } + + // there are a few cases we need to watch out for + lfs3_did_t new_did_ = 0; + if (new_tag == LFS3_ERR_NOENT) { + // if we're a file, don't allow trailing slashes + if (old_tag != LFS3_TAG_DIR && lfs3_path_isdir(new_path)) { + return LFS3_ERR_NOTDIR; + } + + // check that name fits + if (lfs3_path_namelen(new_path) > lfs3->name_limit) { + return LFS3_ERR_NAMETOOLONG; + } + + } else { + // trying to rename the root? + if (new_mdir.mid == -1) { + return LFS3_ERR_BUSY; + } + + // we allow reg <-> stickynote renaming, but renaming a non-dir + // to a dir and a dir to a non-dir is an error + if (old_tag != LFS3_TAG_DIR && new_tag == LFS3_TAG_DIR) { + return LFS3_ERR_ISDIR; + } + if (old_tag == LFS3_TAG_DIR + && new_tag != LFS3_TAG_DIR + // pretend orphans don't exist + && new_tag != LFS3_tag_ORPHAN) { + return LFS3_ERR_NOTDIR; + } + + // renaming to ourself is a noop + if (old_mdir.mid == new_mdir.mid) { + return 0; + } + + // if our destination is a directory, we will be implicitly removing + // the directory, we need to create a grm for this + if (new_tag == LFS3_TAG_DIR) { + // first lets figure out the did + lfs3_data_t data_; + lfs3_stag_t tag_ = lfs3_mdir_lookup(lfs3, &new_mdir, LFS3_TAG_DID, + &data_); + if (tag_ < 0) { + return tag_; + } + + err = lfs3_data_readleb128(lfs3, &data_, &new_did_); + if (err) { + return err; + } + + // mark bookmark for removal with grm + err = lfs3_grm_pushdid(lfs3, new_did_); + if (err) { + return err; + } + } + } + + if (old_tag == LFS3_tag_UNKNOWN) { + // lookup the actual tag + old_tag = lfs3_rbyd_lookup(lfs3, &old_mdir.r, + lfs3_mrid(lfs3, old_mdir.mid), LFS3_tag_MASK8 | LFS3_TAG_NAME, + NULL); + if (old_tag < 0) { + return old_tag; + } + } + + // mark old entry for removal with a grm + lfs3_grm_push(lfs3, old_mdir.mid); + + // rename our entry, copying all tags associated with the old rid to the + // new rid, while also marking the old rid for removal + err = lfs3_mdir_commit(lfs3, &new_mdir, LFS3_RATTRS( + LFS3_RATTR_NAME( + LFS3_tag_MASK12 | old_tag, + (new_tag == LFS3_ERR_NOENT) ? +1 : 0, + new_did, new_path, lfs3_path_namelen(new_path)), + LFS3_RATTR_MOVE(&old_mdir))); + if (err) { + return err; + } + + // update in-device state + for (lfs3_handle_t *h = lfs3->handles; h; h = h->next) { + // mark any clobbered uncreats as zombied + if (new_tag != LFS3_ERR_NOENT + && lfs3_o_type(h->flags) == LFS3_TYPE_REG + && h->mdir.mid == new_mdir.mid) { + h->flags = (h->flags & ~LFS3_o_UNCREAT) + | LFS3_o_ZOMBIE + | LFS3_o_UNSYNC + | LFS3_O_DESYNC; + + // update moved files with the new mdir + } else if (lfs3_o_type(h->flags) == LFS3_TYPE_REG + && h->mdir.mid == lfs3->grm.queue[0]) { + h->mdir = new_mdir; + + // mark any removed dirs as zombied + } else if (new_did_ + && lfs3_o_type(h->flags) == LFS3_TYPE_DIR + && ((lfs3_dir_t*)h)->did == new_did_) { + h->flags |= LFS3_o_ZOMBIE; + + // update dir positions + } else if (lfs3_o_type(h->flags) == LFS3_TYPE_DIR) { + if (new_tag == LFS3_ERR_NOENT + && ((lfs3_dir_t*)h)->did == new_did + && h->mdir.mid >= new_mdir.mid) { + ((lfs3_dir_t*)h)->pos += 1; + } + + if (((lfs3_dir_t*)h)->did == old_did + && h->mdir.mid >= lfs3->grm.queue[0]) { + if (h->mdir.mid == lfs3->grm.queue[0]) { + h->mdir.mid += 1; + } else { + ((lfs3_dir_t*)h)->pos -= 1; + } + } + } + } + + // we need to clean up any pending grms, fortunately we can leave + // this up to lfs3_fs_fixgrm + err = lfs3_fs_fixgrm(lfs3); + if (err) { + // TODO is this the right thing to do? we should probably still + // propagate errors to the user + // + // we did complete the remove, so we shouldn't error here, best + // we can do is log this + LFS3_WARN("Failed to clean up grm (%d)", err); + } + + return 0; +} +#endif + +// this just populates the info struct based on what we found +static int lfs3_stat_(lfs3_t *lfs3, const lfs3_mdir_t *mdir, + lfs3_tag_t tag, lfs3_data_t name, + struct lfs3_info *info) { + // get file type from the tag + info->type = lfs3_tag_subtype(tag); + + // read the file name + LFS3_ASSERT(lfs3_data_size(name) <= LFS3_NAME_MAX); + lfs3_ssize_t name_len = lfs3_data_read(lfs3, &name, + info->name, LFS3_NAME_MAX); + if (name_len < 0) { + return name_len; + } + info->name[name_len] = '\0'; + + // default size to zero + info->size = 0; + + // get file size if we're a regular file + if (tag == LFS3_TAG_REG) { + lfs3_data_t data; + lfs3_stag_t tag = lfs3_mdir_lookup(lfs3, mdir, + LFS3_tag_MASK8 | LFS3_TAG_STRUCT, + &data); + if (tag < 0 && tag != LFS3_ERR_NOENT) { + return tag; + } + + if (tag != LFS3_ERR_NOENT) { + // in bshrubs/btrees, size is always the first field + int err = lfs3_data_readleb128(lfs3, &data, &info->size); + if (err) { + return err; + } + } + } + + return 0; +} + +int lfs3_stat(lfs3_t *lfs3, const char *path, struct lfs3_info *info) { + // lookup our entry + lfs3_mdir_t mdir; + lfs3_stag_t tag = lfs3_mtree_pathlookup(lfs3, &path, + &mdir, NULL); + if (tag < 0) { + return tag; + } + // pretend orphans don't exist + if (tag == LFS3_tag_ORPHAN) { + return LFS3_ERR_NOENT; + } + + // special case for root + if (mdir.mid == -1) { + lfs3_strcpy(info->name, "/"); + info->type = LFS3_TYPE_DIR; + info->size = 0; + return 0; + } + + // fill out our info struct + return lfs3_stat_(lfs3, &mdir, + tag, LFS3_DATA_BUF(path, lfs3_path_namelen(path)), + info); +} + +// needed in lfs3_dir_open +static int lfs3_dir_rewind_(lfs3_t *lfs3, lfs3_dir_t *dir); + +int lfs3_dir_open(lfs3_t *lfs3, lfs3_dir_t *dir, const char *path) { + // already open? + LFS3_ASSERT(!lfs3_handle_isopen(lfs3, &dir->h)); + + // setup dir state + dir->h.flags = lfs3_o_typeflags(LFS3_TYPE_DIR); + + // lookup our directory + lfs3_mdir_t mdir; + lfs3_stag_t tag = lfs3_mtree_pathlookup(lfs3, &path, + &mdir, NULL); + if (tag < 0) { + return tag; + } + // pretend orphans don't exist + if (tag == LFS3_tag_ORPHAN) { + return LFS3_ERR_NOENT; + } + + // read our did from the mdir, unless we're root + if (mdir.mid == -1) { + dir->did = 0; + + } else { + // not a directory? + if (tag != LFS3_TAG_DIR) { + return LFS3_ERR_NOTDIR; + } + + lfs3_data_t data_; + lfs3_stag_t tag_ = lfs3_mdir_lookup(lfs3, &mdir, LFS3_TAG_DID, + &data_); + if (tag_ < 0) { + return tag_; + } + + int err = lfs3_data_readleb128(lfs3, &data_, &dir->did); + if (err) { + return err; + } + } + + // let rewind initialize the pos state + int err = lfs3_dir_rewind_(lfs3, dir); + if (err) { + return err; + } + + // add to tracked mdirs + lfs3_handle_open(lfs3, &dir->h); + return 0; +} + +int lfs3_dir_close(lfs3_t *lfs3, lfs3_dir_t *dir) { + LFS3_ASSERT(lfs3_handle_isopen(lfs3, &dir->h)); + + // remove from tracked mdirs + lfs3_handle_close(lfs3, &dir->h); + return 0; +} + +int lfs3_dir_read(lfs3_t *lfs3, lfs3_dir_t *dir, struct lfs3_info *info) { + LFS3_ASSERT(lfs3_handle_isopen(lfs3, &dir->h)); + + // was our dir removed? + if (lfs3_o_iszombie(dir->h.flags)) { + return LFS3_ERR_NOENT; + } + + // handle dots specially + if (dir->pos == 0) { + lfs3_strcpy(info->name, "."); + info->type = LFS3_TYPE_DIR; + info->size = 0; + dir->pos += 1; + return 0; + } else if (dir->pos == 1) { + lfs3_strcpy(info->name, ".."); + info->type = LFS3_TYPE_DIR; + info->size = 0; + dir->pos += 1; + return 0; + } + + while (true) { + // next mdir? + if (lfs3_mrid(lfs3, dir->h.mdir.mid) + >= (lfs3_srid_t)dir->h.mdir.r.weight) { + int err = lfs3_mtree_lookup(lfs3, + lfs3_mbid(lfs3, dir->h.mdir.mid-1) + 1, + &dir->h.mdir); + if (err) { + return err; + } + } + + // lookup the next name tag + lfs3_data_t data; + lfs3_stag_t tag = lfs3_mdir_lookup(lfs3, &dir->h.mdir, + LFS3_tag_MASK8 | LFS3_TAG_NAME, + &data); + if (tag < 0) { + return tag; + } + + // get the did + lfs3_did_t did; + int err = lfs3_data_readleb128(lfs3, &data, &did); + if (err) { + return err; + } + + // did mismatch? this terminates the dir read + if (did != dir->did) { + return LFS3_ERR_NOENT; + } + + // skip orphans, we pretend these don't exist + if (tag == LFS3_tag_ORPHAN) { + dir->h.mdir.mid += 1; + continue; + } + + // fill out our info struct + err = lfs3_stat_(lfs3, &dir->h.mdir, tag, data, + info); + if (err) { + return err; + } + + // eagerly set to next entry + dir->h.mdir.mid += 1; + dir->pos += 1; + return 0; + } +} + +int lfs3_dir_seek(lfs3_t *lfs3, lfs3_dir_t *dir, lfs3_soff_t off) { + LFS3_ASSERT(lfs3_handle_isopen(lfs3, &dir->h)); + + // do nothing if removed + if (lfs3_o_iszombie(dir->h.flags)) { + return 0; + } + + // first rewind + int err = lfs3_dir_rewind_(lfs3, dir); + if (err) { + return err; + } + + // then seek to the requested offset + // + // note the -2 to adjust for dot entries + lfs3_off_t off_ = off - 2; + while (off_ > 0) { + // next mdir? + if (lfs3_mrid(lfs3, dir->h.mdir.mid) + >= (lfs3_srid_t)dir->h.mdir.r.weight) { + int err = lfs3_mtree_lookup(lfs3, + lfs3_mbid(lfs3, dir->h.mdir.mid-1) + 1, + &dir->h.mdir); + if (err) { + if (err == LFS3_ERR_NOENT) { + break; + } + return err; + } + } + + lfs3_off_t d = lfs3_min( + off_, + dir->h.mdir.r.weight + - lfs3_mrid(lfs3, dir->h.mdir.mid)); + dir->h.mdir.mid += d; + off_ -= d; + } + + dir->pos = off; + return 0; +} + +lfs3_soff_t lfs3_dir_tell(lfs3_t *lfs3, lfs3_dir_t *dir) { + (void)lfs3; + LFS3_ASSERT(lfs3_handle_isopen(lfs3, &dir->h)); + + return dir->pos; +} + +static int lfs3_dir_rewind_(lfs3_t *lfs3, lfs3_dir_t *dir) { + // do nothing if removed + if (lfs3_o_iszombie(dir->h.flags)) { + return 0; + } + + // lookup our bookmark in the mtree + lfs3_stag_t tag = lfs3_mtree_namelookup(lfs3, dir->did, NULL, 0, + &dir->h.mdir, NULL); + if (tag < 0) { + LFS3_ASSERT(tag != LFS3_ERR_NOENT); + return tag; + } + + // eagerly set to next entry + dir->h.mdir.mid += 1; + // reset pos + dir->pos = 0; + return 0; +} + +int lfs3_dir_rewind(lfs3_t *lfs3, lfs3_dir_t *dir) { + LFS3_ASSERT(lfs3_handle_isopen(lfs3, &dir->h)); + + return lfs3_dir_rewind_(lfs3, dir); +} + + + + +/// Custom attribute stuff /// + +static int lfs3_lookupattr(lfs3_t *lfs3, const char *path, uint8_t type, + lfs3_mdir_t *mdir_, lfs3_data_t *data_) { + // lookup our entry + lfs3_stag_t tag = lfs3_mtree_pathlookup(lfs3, &path, + mdir_, NULL); + if (tag < 0) { + return tag; + } + // pretend orphans don't exist + if (tag == LFS3_tag_ORPHAN) { + return LFS3_ERR_NOENT; + } + + // lookup our attr + tag = lfs3_mdir_lookup(lfs3, mdir_, LFS3_TAG_ATTR(type), + data_); + if (tag < 0) { + if (tag == LFS3_ERR_NOENT) { + return LFS3_ERR_NOATTR; + } + return tag; + } + + return 0; +} + +lfs3_ssize_t lfs3_getattr(lfs3_t *lfs3, const char *path, uint8_t type, + void *buffer, lfs3_size_t size) { + // lookup our attr + lfs3_mdir_t mdir; + lfs3_data_t data; + int err = lfs3_lookupattr(lfs3, path, type, + &mdir, &data); + if (err) { + return err; + } + + // read the attr + return lfs3_data_read(lfs3, &data, buffer, size); +} + +lfs3_ssize_t lfs3_sizeattr(lfs3_t *lfs3, const char *path, uint8_t type) { + // lookup our attr + lfs3_mdir_t mdir; + lfs3_data_t data; + int err = lfs3_lookupattr(lfs3, path, type, + &mdir, &data); + if (err) { + return err; + } + + // return the attr size + return lfs3_data_size(data); +} + +#ifndef LFS3_RDONLY +int lfs3_setattr(lfs3_t *lfs3, const char *path, uint8_t type, + const void *buffer, lfs3_size_t size) { + // prepare our filesystem for writing + int err = lfs3_fs_mkconsistent(lfs3); + if (err) { + return err; + } + + // lookup our attr + lfs3_mdir_t mdir; + lfs3_data_t data; + err = lfs3_lookupattr(lfs3, path, type, + &mdir, &data); + if (err && err != LFS3_ERR_NOATTR) { + return err; + } + + // commit our attr + err = lfs3_mdir_commit(lfs3, &mdir, LFS3_RATTRS( + LFS3_RATTR_DATA( + LFS3_TAG_ATTR(type), 0, + &LFS3_DATA_BUF( + buffer, size)))); + if (err) { + return err; + } + + // update any opened files tracking custom attrs + for (lfs3_handle_t *h = lfs3->handles; h; h = h->next) { + if (!(lfs3_o_type(h->flags) == LFS3_TYPE_REG + && h->mdir.mid == mdir.mid + && !lfs3_o_isdesync(h->flags))) { + continue; + } + + lfs3_file_t *file = (lfs3_file_t*)h; + for (lfs3_size_t i = 0; i < file->cfg->attr_count; i++) { + if (!(file->cfg->attrs[i].type == type + && !lfs3_o_iswronly(file->cfg->attrs[i].flags))) { + continue; + } + + lfs3_size_t d = lfs3_min(size, file->cfg->attrs[i].buffer_size); + lfs3_memcpy(file->cfg->attrs[i].buffer, buffer, d); + if (file->cfg->attrs[i].size) { + *file->cfg->attrs[i].size = d; + } + } + } + + return 0; +} +#endif + +#ifndef LFS3_RDONLY +int lfs3_removeattr(lfs3_t *lfs3, const char *path, uint8_t type) { + // prepare our filesystem for writing + int err = lfs3_fs_mkconsistent(lfs3); + if (err) { + return err; + } + + // lookup our attr + lfs3_mdir_t mdir; + err = lfs3_lookupattr(lfs3, path, type, + &mdir, NULL); + if (err) { + return err; + } + + // commit our removal + err = lfs3_mdir_commit(lfs3, &mdir, LFS3_RATTRS( + LFS3_RATTR( + LFS3_tag_RM | LFS3_TAG_ATTR(type), 0))); + if (err) { + return err; + } + + // update any opened files tracking custom attrs + for (lfs3_handle_t *h = lfs3->handles; h; h = h->next) { + if (!(lfs3_o_type(h->flags) == LFS3_TYPE_REG + && h->mdir.mid == mdir.mid + && !lfs3_o_isdesync(h->flags))) { + continue; + } + + lfs3_file_t *file = (lfs3_file_t*)h; + for (lfs3_size_t i = 0; i < file->cfg->attr_count; i++) { + if (!(file->cfg->attrs[i].type == type + && !lfs3_o_iswronly(file->cfg->attrs[i].flags))) { + continue; + } + + if (file->cfg->attrs[i].size) { + *file->cfg->attrs[i].size = LFS3_ERR_NOATTR; + } + } + } + + return 0; +} +#endif + + + + +/// File operations /// + +// file helpers + +static inline void lfs3_file_discardcache(lfs3_file_t *file) { + file->b.h.flags &= ~LFS3_o_UNFLUSH; + file->cache.pos = 0; + file->cache.size = 0; +} + +static inline void lfs3_file_discardleaf(lfs3_file_t *file) { + file->b.h.flags &= ~LFS3_o_UNCRYST & ~LFS3_o_UNGRAFT; + file->leaf.pos = 0; + file->leaf.weight = 0; + lfs3_bptr_discard(&file->leaf.bptr); +} + +static inline void lfs3_file_discardbshrub(lfs3_file_t *file) { + lfs3_bshrub_init(&file->b); +} + +static inline lfs3_size_t lfs3_file_fcachesize(lfs3_t *lfs3, + const lfs3_file_t *file) { + return (file->cfg->fcache_buffer || file->cfg->fcache_size) + ? file->cfg->fcache_size + : lfs3->cfg->fcache_size; +} + +static inline lfs3_off_t lfs3_file_size_(const lfs3_file_t *file) { + return lfs3_max( + file->cache.pos + file->cache.size, + lfs3_max( + file->leaf.pos + file->leaf.weight, + file->b.b.r.weight)); +} + + + +// file operations + +static void lfs3_file_init(lfs3_file_t *file, uint32_t flags, + const struct lfs3_file_cfg *cfg) { + file->cfg = cfg; + file->b.h.flags = lfs3_o_typeflags(LFS3_TYPE_REG) | flags; + file->pos = 0; + lfs3_file_discardcache(file); + lfs3_file_discardleaf(file); + lfs3_file_discardbshrub(file); +} + +static int lfs3_file_fetch(lfs3_t *lfs3, lfs3_file_t *file, uint32_t flags) { + // don't bother reading disk if we're not created or truncating + if (!lfs3_o_isuncreat(flags) && !lfs3_o_istrunc(flags)) { + // fetch the file's bshrub/btree, if there is one + int err = lfs3_bshrub_fetch(lfs3, &file->b); + if (err && err != LFS3_ERR_NOENT) { + return err; + } + + // mark as in-sync + file->b.h.flags &= ~LFS3_o_UNSYNC; + } + + // try to fetch any custom attributes + for (lfs3_size_t i = 0; i < file->cfg->attr_count; i++) { + // skip writeonly attrs + if (lfs3_o_iswronly(file->cfg->attrs[i].flags)) { + continue; + } + + // don't bother reading disk if we're not created yet + if (lfs3_o_isuncreat(flags)) { + if (file->cfg->attrs[i].size) { + *file->cfg->attrs[i].size = LFS3_ERR_NOATTR; + } + continue; + } + + // lookup the attr + lfs3_data_t data; + lfs3_stag_t tag = lfs3_mdir_lookup(lfs3, &file->b.h.mdir, + LFS3_TAG_ATTR(file->cfg->attrs[i].type), + &data); + if (tag < 0 && tag != LFS3_ERR_NOENT) { + return tag; + } + + // read the attr, if it exists + if (tag == LFS3_ERR_NOENT + // awkward case here if buffer_size is LFS3_ERR_NOATTR + || file->cfg->attrs[i].buffer_size == LFS3_ERR_NOATTR) { + if (file->cfg->attrs[i].size) { + *file->cfg->attrs[i].size = LFS3_ERR_NOATTR; + } + } else { + lfs3_ssize_t d = lfs3_data_read(lfs3, &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; +} + +// needed in lfs3_file_opencfg +static void lfs3_file_close_(lfs3_t *lfs3, lfs3_file_t *file); +#ifndef LFS3_RDONLY +static int lfs3_file_sync_(lfs3_t *lfs3, lfs3_file_t *file, + const lfs3_name_t *name); +#endif + +int lfs3_file_opencfg_(lfs3_t *lfs3, lfs3_file_t *file, + const char *path, uint32_t flags, + const struct lfs3_file_cfg *cfg) { + #ifndef LFS3_RDONLY + if (!lfs3_o_isrdonly(flags)) { + // prepare our filesystem for writing + int err = lfs3_fs_mkconsistent(lfs3); + if (err) { + return err; + } + } + #endif + + // setup file state + lfs3_file_init(file, + // mounted with LFS3_M_FLUSH/SYNC? implies LFS3_O_FLUSH/SYNC + flags | (lfs3->flags & (LFS3_M_FLUSH | LFS3_M_SYNC)), + cfg); + + // allocate cache if necessary + // + // wrset is a special lfs3_set specific mode that passes data via + // the file cache, so make sure not to clobber it + if (lfs3_o_iswrset(file->b.h.flags)) { + file->b.h.flags |= LFS3_o_UNFLUSH; + file->cache.buffer = file->cfg->fcache_buffer; + file->cache.pos = 0; + file->cache.size = file->cfg->fcache_size; + } else if (file->cfg->fcache_buffer) { + file->cache.buffer = file->cfg->fcache_buffer; + } else { + file->cache.buffer = lfs3_malloc(lfs3_file_fcachesize(lfs3, file)); + if (!file->cache.buffer) { + return LFS3_ERR_NOMEM; + } + } + + int err; + // lookup our parent + lfs3_did_t did; + lfs3_stag_t tag = lfs3_mtree_pathlookup(lfs3, &path, + &file->b.h.mdir, &did); + if (tag < 0 + && !(tag == LFS3_ERR_NOENT + && lfs3_path_islast(path))) { + err = tag; + goto failed; + } + + // creating a new entry? + if (tag == LFS3_ERR_NOENT || tag == LFS3_tag_ORPHAN) { + if (!lfs3_o_iscreat(file->b.h.flags)) { + err = LFS3_ERR_NOENT; + goto failed; + } + LFS3_ASSERT(!lfs3_o_isrdonly(file->b.h.flags)); + + #ifndef LFS3_RDONLY + // we're a file, don't allow trailing slashes + if (lfs3_path_isdir(path)) { + err = LFS3_ERR_NOTDIR; + goto failed; + } + + // check that name fits + if (lfs3_path_namelen(path) > lfs3->name_limit) { + err = LFS3_ERR_NAMETOOLONG; + goto failed; + } + + // if stickynote, mark as uncreated + unsync + if (tag != LFS3_ERR_NOENT) { + file->b.h.flags |= LFS3_o_UNCREAT | LFS3_o_UNSYNC; + } + #endif + } else { + // wanted to create a new entry? + if (lfs3_o_isexcl(file->b.h.flags)) { + err = LFS3_ERR_EXIST; + goto failed; + } + + // wrong type? + if (tag == LFS3_TAG_DIR) { + err = LFS3_ERR_ISDIR; + goto failed; + } + if (tag == LFS3_tag_UNKNOWN) { + err = LFS3_ERR_NOTSUP; + goto failed; + } + + #ifndef LFS3_RDONLY + // if stickynote, mark as uncreated + unsync + if (tag == LFS3_TAG_STICKYNOTE) { + file->b.h.flags |= LFS3_o_UNCREAT | LFS3_o_UNSYNC; + } + + // if truncating, mark as unsync + if (lfs3_o_istrunc(file->b.h.flags)) { + file->b.h.flags |= LFS3_o_UNSYNC; + } + #endif + } + + // need to create an entry? + #ifndef LFS3_RDONLY + if (tag == LFS3_ERR_NOENT) { + // small file wrset? can we atomically commit everything in one + // commit? currently this is only possible via lfs3_set + if (lfs3_o_iswrset(file->b.h.flags) + && file->cache.size <= lfs3->cfg->shrub_size + && file->cache.size <= lfs3->cfg->fragment_size + && file->cache.size < lfs3_max(lfs3->cfg->crystal_thresh, 1)) { + // we need to mark as unsync for sync to do anything + file->b.h.flags |= LFS3_o_UNSYNC; + + err = lfs3_file_sync_(lfs3, file, &(lfs3_name_t){ + .did=did, + .name=path, + .name_len=lfs3_path_namelen(path)}); + if (err) { + goto failed; + } + + } else { + // create a stickynote entry if we don't have one, this + // reserves the mid until first sync + err = lfs3_mdir_commit(lfs3, &file->b.h.mdir, LFS3_RATTRS( + LFS3_RATTR_NAME( + LFS3_TAG_STICKYNOTE, +1, + did, path, lfs3_path_namelen(path)))); + if (err) { + goto failed; + } + + // mark as uncreated + unsync + file->b.h.flags |= LFS3_o_UNCREAT | LFS3_o_UNSYNC; + } + + // update dir positions + for (lfs3_handle_t *h = lfs3->handles; h; h = h->next) { + if (lfs3_o_type(h->flags) == LFS3_TYPE_DIR + && ((lfs3_dir_t*)h)->did == did + && h->mdir.mid >= file->b.h.mdir.mid) { + ((lfs3_dir_t*)h)->pos += 1; + } + } + } + #endif + + // fetch the file struct and custom attrs + err = lfs3_file_fetch(lfs3, file, file->b.h.flags); + if (err) { + goto failed; + } + + // add to tracked mdirs + lfs3_handle_open(lfs3, &file->b.h); + + // check metadata/data for errors? + if (file->b.h.flags & ( + LFS3_CK_CKMETA + | LFS3_CK_CKDATA)) { + err = lfs3_file_ck(lfs3, file, + file->b.h.flags & ( + LFS3_CK_CKMETA + | LFS3_CK_CKDATA)); + if (err) { + goto failed; + } + } + + return 0; + +failed:; + // clean up resources + lfs3_file_close_(lfs3, file); + return err; +} + +int lfs3_file_opencfg(lfs3_t *lfs3, lfs3_file_t *file, + const char *path, uint32_t flags, + const struct lfs3_file_cfg *cfg) { + // already open? + LFS3_ASSERT(!lfs3_handle_isopen(lfs3, &file->b.h)); + // don't allow the forbidden mode! + LFS3_ASSERT((flags & 3) != 3); + // unknown flags? + LFS3_ASSERT((flags & ~( + LFS3_O_RDONLY + | LFS3_IFDEF_RDONLY(0, LFS3_O_WRONLY) + | LFS3_IFDEF_RDONLY(0, LFS3_O_RDWR) + | LFS3_IFDEF_RDONLY(0, LFS3_O_CREAT) + | LFS3_IFDEF_RDONLY(0, LFS3_O_EXCL) + | LFS3_IFDEF_RDONLY(0, LFS3_O_TRUNC) + | LFS3_IFDEF_RDONLY(0, LFS3_O_APPEND) + | LFS3_O_FLUSH + | LFS3_O_SYNC + | LFS3_O_DESYNC + | LFS3_O_CKMETA + | LFS3_O_CKDATA)) == 0); + // writeable files require a writeable filesystem + LFS3_ASSERT(!lfs3_m_isrdonly(lfs3->flags) || lfs3_o_isrdonly(flags)); + // these flags require a writable file + LFS3_ASSERT(!lfs3_o_isrdonly(flags) || !lfs3_o_iscreat(flags)); + LFS3_ASSERT(!lfs3_o_isrdonly(flags) || !lfs3_o_isexcl(flags)); + LFS3_ASSERT(!lfs3_o_isrdonly(flags) || !lfs3_o_istrunc(flags)); + for (lfs3_size_t i = 0; i < cfg->attr_count; i++) { + // these flags require a writable attr + LFS3_ASSERT(!lfs3_o_isrdonly(cfg->attrs[i].flags) + || !lfs3_o_iscreat(cfg->attrs[i].flags)); + LFS3_ASSERT(!lfs3_o_isrdonly(cfg->attrs[i].flags) + || !lfs3_o_isexcl(cfg->attrs[i].flags)); + } + + return lfs3_file_opencfg_(lfs3, file, path, flags, + cfg); +} + +// default file config +static const struct lfs3_file_cfg lfs3_file_defaultcfg = {0}; + +int lfs3_file_open(lfs3_t *lfs3, lfs3_file_t *file, + const char *path, uint32_t flags) { + return lfs3_file_opencfg(lfs3, file, path, flags, + &lfs3_file_defaultcfg); +} + +// clean up resources +static void lfs3_file_close_(lfs3_t *lfs3, lfs3_file_t *file) { + (void)lfs3; + // remove from tracked mdirs + lfs3_handle_close(lfs3, &file->b.h); + + // clean up memory + if (!file->cfg->fcache_buffer) { + lfs3_free(file->cache.buffer); + } + + // are we orphaning a file? + // + // make sure we check _after_ removing ourselves + #ifndef LFS3_RDONLY + if (lfs3_o_isuncreat(file->b.h.flags) + && !lfs3_mid_isopen(lfs3, file->b.h.mdir.mid, -1)) { + // this can only happen in a rdwr filesystem + LFS3_ASSERT(!lfs3_m_isrdonly(lfs3->flags)); + + // this gets a bit messy, since we're not able to write to the + // filesystem if we're rdonly or desynced, fortunately we have + // a few tricks + + // first try to push onto our grm queue + if (lfs3_grm_count(lfs3) < 2) { + lfs3_grm_push(lfs3, file->b.h.mdir.mid); + + // fallback to just marking the filesystem as inconsistent + } else { + lfs3->flags |= LFS3_I_MKCONSISTENT; + } + } + #endif +} + +// needed in lfs3_file_close +int lfs3_file_sync(lfs3_t *lfs3, lfs3_file_t *file); + +int lfs3_file_close(lfs3_t *lfs3, lfs3_file_t *file) { + LFS3_ASSERT(lfs3_handle_isopen(lfs3, &file->b.h)); + + // don't call lfs3_file_sync if we're readonly or desynced + int err = 0; + if (!lfs3_o_isrdonly(file->b.h.flags) + && !lfs3_o_isdesync(file->b.h.flags)) { + err = lfs3_file_sync(lfs3, file); + } + + // clean up resources + lfs3_file_close_(lfs3, file); + + return err; +} + +// low-level file reading + +static int lfs3_file_lookupnext(lfs3_t *lfs3, LFS3_BCONST lfs3_file_t *file, + lfs3_bid_t bid, + lfs3_bid_t *bid_, lfs3_bid_t *weight_, lfs3_bptr_t *bptr_) { + lfs3_bid_t weight; + lfs3_data_t data; + lfs3_stag_t tag = lfs3_bshrub_lookupnext(lfs3, &file->b, bid, + bid_, &weight, &data); + if (tag < 0) { + return tag; + } + LFS3_ASSERT(tag == LFS3_TAG_DATA + || tag == LFS3_TAG_BLOCK); + + // fetch the bptr/data fragment + int err = lfs3_bptr_fetch(lfs3, bptr_, tag, weight, data); + if (err) { + return err; + } + + if (weight_) { + *weight_ = weight; + } + return 0; +} + +static lfs3_ssize_t lfs3_file_readnext(lfs3_t *lfs3, lfs3_file_t *file, + lfs3_off_t pos, uint8_t *buffer, lfs3_size_t size) { + // the leaf must not be pinned down here + LFS3_ASSERT(!lfs3_o_isuncryst(file->b.h.flags)); + LFS3_ASSERT(!lfs3_o_isungraft(file->b.h.flags)); + + while (true) { + // any data in our leaf? + if (pos >= file->leaf.pos + && pos < file->leaf.pos + file->leaf.weight) { + // any data on disk? + lfs3_off_t pos_ = pos; + if (pos_ < file->leaf.pos + lfs3_bptr_size(&file->leaf.bptr)) { + // note one important side-effect here is a strict + // data hint + lfs3_ssize_t d = lfs3_min( + size, + lfs3_bptr_size(&file->leaf.bptr) + - (pos_ - file->leaf.pos)); + lfs3_data_t slice = lfs3_data_slice(file->leaf.bptr.d, + pos_ - file->leaf.pos, + d); + d = lfs3_data_read(lfs3, &slice, + buffer, d); + if (d < 0) { + return d; + } + + pos_ += d; + buffer += d; + size -= d; + } + + // found a hole? fill with zeros + lfs3_ssize_t d = lfs3_min( + size, + file->leaf.pos+file->leaf.weight - pos_); + lfs3_memset(buffer, 0, d); + + pos_ += d; + buffer += d; + size -= d; + + return pos_ - pos; + } + + // fetch a new leaf + lfs3_bid_t bid; + lfs3_bid_t weight; + lfs3_bptr_t bptr; + int err = lfs3_file_lookupnext(lfs3, file, pos, + &bid, &weight, &bptr); + if (err) { + return err; + } + + file->leaf.pos = bid - (weight-1); + file->leaf.weight = weight; + file->leaf.bptr = bptr; + } +} + +// high-level file reading + +lfs3_ssize_t lfs3_file_read(lfs3_t *lfs3, lfs3_file_t *file, + void *buffer, lfs3_size_t size) { + LFS3_ASSERT(lfs3_handle_isopen(lfs3, &file->b.h)); + // can't read from writeonly files + LFS3_ASSERT(!lfs3_o_iswronly(file->b.h.flags)); + LFS3_ASSERT(file->pos + size <= 0x7fffffff); + + lfs3_off_t pos_ = file->pos; + uint8_t *buffer_ = buffer; + while (size > 0 && pos_ < lfs3_file_size_(file)) { + // keep track of the next highest priority data offset + lfs3_ssize_t d = lfs3_min(size, lfs3_file_size_(file) - pos_); + + // any data in our cache? + if (pos_ < file->cache.pos + file->cache.size + && file->cache.size != 0) { + if (pos_ >= file->cache.pos) { + lfs3_ssize_t d_ = lfs3_min( + d, + file->cache.size - (pos_ - file->cache.pos)); + lfs3_memcpy(buffer_, + &file->cache.buffer[pos_ - file->cache.pos], + d_); + + pos_ += d_; + buffer_ += d_; + size -= d_; + d -= d_; + continue; + } + + // cached data takes priority + d = lfs3_min(d, file->cache.pos - pos_); + } + + // any data in our btree? + if (pos_ < lfs3_max( + file->leaf.pos + file->leaf.weight, + file->b.b.r.weight)) { + if (!lfs3_o_isuncryst(file->b.h.flags) + && !lfs3_o_isungraft(file->b.h.flags)) { + // bypass cache? + if ((lfs3_size_t)d >= lfs3_file_fcachesize(lfs3, file)) { + lfs3_ssize_t d_ = lfs3_file_readnext(lfs3, file, + pos_, buffer_, d); + if (d_ < 0) { + LFS3_ASSERT(d_ != LFS3_ERR_NOENT); + return d_; + } + + pos_ += d_; + buffer_ += d_; + size -= d_; + continue; + } + + // try to fill our cache with some data + if (!lfs3_o_isunflush(file->b.h.flags)) { + lfs3_ssize_t d_ = lfs3_file_readnext(lfs3, file, + pos_, file->cache.buffer, d); + if (d_ < 0) { + LFS3_ASSERT(d != LFS3_ERR_NOENT); + return d_; + } + file->cache.pos = pos_; + file->cache.size = d_; + continue; + } + } + + // flush our cache so the above can't fail + // + // note that flush does not change the actual file data, so if + // a read fails it's ok to fall back to our flushed state + // + int err = lfs3_file_flush(lfs3, file); + if (err) { + return err; + } + lfs3_file_discardcache(file); + continue; + } + + // found a hole? fill with zeros + lfs3_memset(buffer_, 0, d); + + pos_ += d; + buffer_ += d; + size -= d; + } + + // update file and return amount read + lfs3_size_t read = pos_ - file->pos; + file->pos = pos_; + return read; +} + +// low-level file writing + +#ifndef LFS3_RDONLY +static int lfs3_file_commit(lfs3_t *lfs3, lfs3_file_t *file, + lfs3_bid_t bid, const lfs3_rattr_t *rattrs, lfs3_size_t rattr_count) { + return lfs3_bshrub_commit(lfs3, &file->b, + bid, rattrs, rattr_count); +} +#endif + +// use this flag to indicate bptr vs concatenated data fragments +#define LFS3_GRAFT_ISBPTR 0x80000000 + +static inline bool lfs3_graft_isbptr(lfs3_size_t graft_count) { + return graft_count & LFS3_GRAFT_ISBPTR; +} + +static inline lfs3_size_t lfs3_graft_count(lfs3_size_t graft_count) { + return graft_count & ~LFS3_GRAFT_ISBPTR; +} + +// graft bptr/fragments into our bshrub/btree +#ifndef LFS3_RDONLY +static int lfs3_file_graft_(lfs3_t *lfs3, lfs3_file_t *file, + lfs3_off_t pos, lfs3_off_t weight, lfs3_soff_t delta, + const lfs3_data_t *graft, lfs3_ssize_t graft_count) { + // note! we must never allow our btree size to overflow, even + // temporarily + + // can't carve more than the graft weight + LFS3_ASSERT(delta >= -(lfs3_soff_t)weight); + + // carving the entire tree? revert to no bshrub/btree + if (pos == 0 + && weight >= file->b.b.r.weight + && delta == -(lfs3_soff_t)weight) { + lfs3_file_discardbshrub(file); + return 0; + } + + // keep track of in-flight graft state + // + // normally, in-flight state would be protected by the block + // allocator's checkpoint mechanism, where checkpoints prevent double + // allocation of new blocks while the old copies remain tracked + // + // but we don't track the original bshrub copy during grafting! + // + // in theory, we could track 3 copies of the bshrub/btree: before + // after, and mid-graft (we need the mid-graft copy to survive mdir + // compactions), but that would add a lot of complexity/state to a + // critical function on the stack hot-path + // + // instead, we can just explicitly track any in-flight graft state to + // make sure we don't allocate these blocks in-between commits + // + lfs3->graft = graft; + lfs3->graft_count = graft_count; + + // try to merge commits where possible + lfs3_bid_t bid = file->b.b.r.weight; + lfs3_rattr_t rattrs[3]; + lfs3_size_t rattr_count = 0; + lfs3_bptr_t l; + lfs3_bptr_t r; + int err; + + // need a hole? + if (pos > file->b.b.r.weight) { + // can we coalesce? + if (file->b.b.r.weight > 0) { + bid = lfs3_min(bid, file->b.b.r.weight-1); + rattrs[rattr_count++] = LFS3_RATTR( + LFS3_tag_GROW, +(pos - file->b.b.r.weight)); + + // new hole + } else { + bid = lfs3_min(bid, file->b.b.r.weight); + rattrs[rattr_count++] = LFS3_RATTR( + LFS3_TAG_DATA, +(pos - file->b.b.r.weight)); + } + } + + // try to carve any existing data + lfs3_rattr_t r_rattr_ = {.tag=0}; + while (pos < file->b.b.r.weight) { + lfs3_bid_t weight_; + lfs3_bptr_t bptr_; + err = lfs3_file_lookupnext(lfs3, file, pos, + &bid, &weight_, &bptr_); + if (err) { + LFS3_ASSERT(err != LFS3_ERR_NOENT); + goto failed; + } + + // note, an entry can be both a left and right sibling + l = bptr_; + lfs3_bptr_slice(&l, + -1, + pos - (bid-(weight_-1))); + r = bptr_; + lfs3_bptr_slice(&r, + pos+weight - (bid-(weight_-1)), + -1); + + // found left sibling? + if (bid-(weight_-1) < pos) { + // can we get away with a grow attribute? + if (lfs3_bptr_size(&bptr_) == lfs3_bptr_size(&l)) { + rattrs[rattr_count++] = LFS3_RATTR( + LFS3_tag_GROW, -(bid+1 - pos)); + + // carve fragment? + } else if (!lfs3_bptr_isbptr(&bptr_) + // carve bptr into fragment? + || (lfs3_bptr_size(&l) <= lfs3->cfg->fragment_size + && lfs3_bptr_size(&l) + < lfs3_max(lfs3->cfg->crystal_thresh, 1))) { + rattrs[rattr_count++] = LFS3_RATTR_DATA( + LFS3_tag_GROW | LFS3_tag_MASK8 | LFS3_TAG_DATA, + -(bid+1 - pos), + &l.d); + + // carve bptr? + } else { + rattrs[rattr_count++] = LFS3_RATTR_BPTR( + LFS3_tag_GROW | LFS3_tag_MASK8 | LFS3_TAG_BLOCK, + -(bid+1 - pos), + &l); + } + + // completely overwriting this entry? + } else { + rattrs[rattr_count++] = LFS3_RATTR( + LFS3_tag_RM, -weight_); + } + + // spans more than one entry? we can't do everything in one + // commit because it might span more than one btree leaf, so + // commit what we have and move on to next entry + if (pos+weight > bid+1) { + LFS3_ASSERT(lfs3_bptr_size(&r) == 0); + LFS3_ASSERT(rattr_count <= sizeof(rattrs)/sizeof(lfs3_rattr_t)); + + err = lfs3_file_commit(lfs3, file, bid, + rattrs, rattr_count); + if (err) { + goto failed; + } + + delta += lfs3_min(weight, bid+1 - pos); + weight -= lfs3_min(weight, bid+1 - pos); + rattr_count = 0; + continue; + } + + // found right sibling? + if (pos+weight < bid+1) { + // can we coalesce a hole? + if (lfs3_bptr_size(&r) == 0) { + delta += bid+1 - (pos+weight); + + // carve fragment? + } else if (!lfs3_bptr_isbptr(&bptr_) + // carve bptr into fragment? + || (lfs3_bptr_size(&r) <= lfs3->cfg->fragment_size + && lfs3_bptr_size(&r) + < lfs3_max(lfs3->cfg->crystal_thresh, 1))) { + r_rattr_ = LFS3_RATTR_DATA( + LFS3_TAG_DATA, bid+1 - (pos+weight), + &r.d); + + // carve bptr? + } else { + r_rattr_ = LFS3_RATTR_BPTR( + LFS3_TAG_BLOCK, bid+1 - (pos+weight), + &r); + } + } + + delta += lfs3_min(weight, bid+1 - pos); + weight -= lfs3_min(weight, bid+1 - pos); + break; + } + + // append our data + if (weight + delta > 0) { + lfs3_size_t dsize = 0; + for (lfs3_size_t i = 0; i < lfs3_graft_count(graft_count); i++) { + dsize += lfs3_data_size(graft[i]); + } + + // can we coalesce a hole? + if (dsize == 0 && pos > 0) { + bid = lfs3_min(bid, file->b.b.r.weight-1); + rattrs[rattr_count++] = LFS3_RATTR( + LFS3_tag_GROW, +(weight + delta)); + + // need a new hole? + } else if (dsize == 0) { + bid = lfs3_min(bid, file->b.b.r.weight); + rattrs[rattr_count++] = LFS3_RATTR( + LFS3_TAG_DATA, +(weight + delta)); + + // append a new fragment? + } else if (!lfs3_graft_isbptr(graft_count)) { + bid = lfs3_min(bid, file->b.b.r.weight); + rattrs[rattr_count++] = LFS3_RATTR_CAT_( + LFS3_TAG_DATA, +(weight + delta), + graft, graft_count); + + // append a new bptr? + } else { + bid = lfs3_min(bid, file->b.b.r.weight); + rattrs[rattr_count++] = LFS3_RATTR_BPTR( + LFS3_TAG_BLOCK, +(weight + delta), + (const lfs3_bptr_t*)graft); + } + } + + // and don't forget the right sibling + if (r_rattr_.tag) { + rattrs[rattr_count++] = r_rattr_; + } + + // commit pending rattrs + if (rattr_count > 0) { + LFS3_ASSERT(rattr_count <= sizeof(rattrs)/sizeof(lfs3_rattr_t)); + + err = lfs3_file_commit(lfs3, file, bid, + rattrs, rattr_count); + if (err) { + goto failed; + } + } + + lfs3->graft = NULL; + lfs3->graft_count = 0; + return 0; + +failed:; + lfs3->graft = NULL; + lfs3->graft_count = 0; + return err; +} +#endif + +// graft any ungrafted leaves +#ifndef LFS3_RDONLY +static int lfs3_file_graft(lfs3_t *lfs3, lfs3_file_t *file) { + // do nothing if our file is already grafted + if (!lfs3_o_isungraft(file->b.h.flags)) { + return 0; + } + // ungrafted files must be unsynced + LFS3_ASSERT(lfs3_o_isunsync(file->b.h.flags)); + + // checkpoint the allocator + int err = lfs3_alloc_ckpoint(lfs3); + if (err) { + return err; + } + + // graft into the tree + err = lfs3_file_graft_(lfs3, file, + file->leaf.pos, file->leaf.weight, 0, + &file->leaf.bptr.d, LFS3_GRAFT_ISBPTR | 1); + if (err) { + return err; + } + + // mark as grafted + file->b.h.flags &= ~LFS3_o_UNGRAFT; + return 0; +} +#endif + +// note the slightly unique behavior when crystal_min=-1: +// - crystal_min=-1 => crystal_min=crystal_max +// - crystal_max=-1 => crystal_max=unbounded +// +// this helps avoid duplicate arguments with tight crystal bounds, if +// you really want to crystallize as little as possible, use +// crystal_min=0 +// +#ifndef LFS3_RDONLY +// this LFS3_NOINLINE is to force lfs3_file_crystallize__ off the stack +// hot-path +LFS3_NOINLINE +static int lfs3_file_crystallize_(lfs3_t *lfs3, lfs3_file_t *file, + lfs3_off_t block_pos, + lfs3_ssize_t crystal_min, lfs3_ssize_t crystal_max, + lfs3_off_t pos, const uint8_t *buffer, lfs3_size_t size) { + // align to prog_size, limit to block_size and theoretical file size + lfs3_off_t crystal_limit = lfs3_min( + block_pos + lfs3_min( + lfs3_aligndown( + (lfs3_off_t)crystal_max, + lfs3_min( + lfs3->cfg->prog_size, + lfs3_max(lfs3->cfg->crystal_thresh, 1))), + lfs3->cfg->block_size), + lfs3_max( + pos + size, + file->b.b.r.weight)); + + // resuming crystallization? or do we need to allocate a new block? + if (!lfs3_o_isuncryst(file->b.h.flags)) { + goto relocate; + } + + // only blocks can be uncrystallized + LFS3_ASSERT(lfs3_bptr_isbptr(&file->leaf.bptr)); + LFS3_ASSERT(lfs3_bptr_iserased(&file->leaf.bptr)); + + // uncrystallized blocks shouldn't be truncated or anything + LFS3_ASSERT(file->leaf.pos - lfs3_bptr_off(&file->leaf.bptr) + == block_pos); + LFS3_ASSERT(lfs3_bptr_off(&file->leaf.bptr) + + lfs3_bptr_size(&file->leaf.bptr) + == lfs3_bptr_cksize(&file->leaf.bptr)); + LFS3_ASSERT(lfs3_bptr_size(&file->leaf.bptr) + == file->leaf.weight); + + // before we write, claim the erased state! + for (lfs3_handle_t *h = lfs3->handles; h; h = h->next) { + if (lfs3_o_type(h->flags) == LFS3_TYPE_REG + && h != &file->b.h + && lfs3_bptr_block(&((lfs3_file_t*)h)->leaf.bptr) + == lfs3_bptr_block(&file->leaf.bptr)) { + lfs3_bptr_claim(&((lfs3_file_t*)h)->leaf.bptr); + } + } + + // copy things in case we hit an error + lfs3_sblock_t block_ = lfs3_bptr_block(&file->leaf.bptr); + lfs3_size_t off_ = lfs3_bptr_off(&file->leaf.bptr); + lfs3_off_t pos_ = block_pos + + lfs3_bptr_off(&file->leaf.bptr) + + lfs3_bptr_size(&file->leaf.bptr); + lfs3->pcksum = lfs3_bptr_cksum(&file->leaf.bptr); + while (true) { + // crystallize data into our block + // + // i.e. eagerly merge any right neighbors unless that would put + // us over our crystal_size/block_size + while (pos_ < crystal_limit) { + // keep track of the next highest priority data offset + lfs3_ssize_t d = crystal_limit - pos_; + + // any data in our buffer? + if (pos_ < pos + size && size > 0) { + if (pos_ >= pos) { + lfs3_ssize_t d_ = lfs3_min( + d, + size - (pos_ - pos)); + int err = lfs3_bd_prog(lfs3, block_, pos_ - block_pos, + &buffer[pos_ - pos], d_, + &lfs3->pcksum); + if (err) { + LFS3_ASSERT(err != LFS3_ERR_RANGE); + // bad prog? try another block + if (err == LFS3_ERR_CORRUPT) { + goto relocate; + } + return err; + } + + pos_ += d_; + d -= d_; + continue; + } + + // buffered data takes priority + d = lfs3_min(d, pos - pos_); + } + + // any data in our leaf? + // + // yes, we can hit this if we had to relocate + if (pos_ < file->leaf.pos + lfs3_bptr_size(&file->leaf.bptr)) { + if (pos_ >= file->leaf.pos) { + // note one important side-effect here is a strict + // data hint + lfs3_ssize_t d_ = lfs3_min( + d, + lfs3_bptr_size(&file->leaf.bptr) + - (pos_ - file->leaf.pos)); + int err = lfs3_bd_progdata(lfs3, block_, pos_ - block_pos, + lfs3_data_slice(file->leaf.bptr.d, + pos_ - file->leaf.pos, + d_), + &lfs3->pcksum); + if (err) { + LFS3_ASSERT(err != LFS3_ERR_RANGE); + // bad prog? try another block + if (err == LFS3_ERR_CORRUPT) { + goto relocate; + } + return err; + } + + pos_ += d_; + d -= d_; + continue; + } + + // leaf takes priority + d = lfs3_min(d, file->leaf.pos - pos_); + } + + // any data on disk? + if (pos_ < file->b.b.r.weight) { + lfs3_bid_t bid__; + lfs3_bid_t weight__; + lfs3_bptr_t bptr__; + int err = lfs3_file_lookupnext(lfs3, file, pos_, + &bid__, &weight__, &bptr__); + if (err) { + LFS3_ASSERT(err != LFS3_ERR_NOENT); + return err; + } + + // is this data a pure hole? stop early to (FUTURE) + // better leverage erased-state in sparse files, and to + // try to avoid writing a bunch of unnecessary zeros + if ((pos_ >= bid__-(weight__-1) + lfs3_bptr_size(&bptr__) + // does this data exceed our block_size? also + // stop early to try to avoid messing up + // block alignment + || (bid__-(weight__-1) + lfs3_bptr_size(&bptr__)) + - block_pos + > lfs3->cfg->block_size) + // but make sure to include all of the requested + // crystal if explicit, otherwise above loops + // may never terminate + && (lfs3_soff_t)(pos_ - block_pos) + >= (lfs3_soff_t)lfs3_min( + crystal_min, + crystal_max)) { + // if we hit this condition, mark as crystallized, + // attempting resume crystallization will not make + // progress + file->b.h.flags &= ~LFS3_o_UNCRYST; + break; + } + + if (pos_ < bid__-(weight__-1) + lfs3_bptr_size(&bptr__)) { + // note one important side-effect here is a strict + // data hint + lfs3_ssize_t d_ = lfs3_min( + d, + lfs3_bptr_size(&bptr__) + - (pos_ - (bid__-(weight__-1)))); + err = lfs3_bd_progdata(lfs3, block_, pos_ - block_pos, + lfs3_data_slice(bptr__.d, + pos_ - (bid__-(weight__-1)), + d_), + &lfs3->pcksum); + if (err) { + LFS3_ASSERT(err != LFS3_ERR_RANGE); + // bad prog? try another block + if (err == LFS3_ERR_CORRUPT) { + goto relocate; + } + return err; + } + + pos_ += d_; + d -= d_; + } + + // found a hole? just make sure next leaf takes priority + d = lfs3_min(d, bid__+1 - pos_); + } + + // found a hole? fill with zeros + int err = lfs3_bd_set(lfs3, block_, pos_ - block_pos, + 0, d, + &lfs3->pcksum); + if (err) { + LFS3_ASSERT(err != LFS3_ERR_RANGE); + // bad prog? try another block + if (err == LFS3_ERR_CORRUPT) { + goto relocate; + } + return err; + } + + pos_ += d; + } + + // if we're fully crystallized, mark as crystallized + // + // note some special conditions may also clear this flag in the + // above loop + // + // and don't worry, we can still resume crystallization if we + // write to the tracked erased state + if (pos_ - block_pos == lfs3->cfg->block_size + || pos_ == lfs3_max( + pos + size, + file->b.b.r.weight)) { + file->b.h.flags &= ~LFS3_o_UNCRYST; + } + + // a bit of a hack here, we need to truncate our block to + // prog_size alignment to avoid padding issues + // + // doing this retroactively to the pcache greatly simplifies the + // above loop, though we may end up reading more than is + // strictly necessary + // + // note we _don't_ do this if prog alignment violates + // crystal_thresh, as this would prevent crystallization when + // crystal_thresh < prog_size, it's a weird case, but this is + // useful for small blocks + lfs3_size_t d = (pos_ - block_pos) % lfs3->cfg->prog_size; + if (d < lfs3_max(lfs3->cfg->crystal_thresh, 1)) { + lfs3->pcache.size -= d; + pos_ -= d; + } + + // finalize our write + int err = lfs3_bd_flush(lfs3, + &lfs3->pcksum); + if (err) { + // bad prog? try another block + if (err == LFS3_ERR_CORRUPT) { + goto relocate; + } + return err; + } + + // and update the leaf bptr + LFS3_ASSERT(pos_ - block_pos >= off_); + LFS3_ASSERT(pos_ - block_pos <= lfs3->cfg->block_size); + file->leaf.pos = block_pos + off_; + file->leaf.weight = pos_ - file->leaf.pos; + lfs3_bptr_init(&file->leaf.bptr, + LFS3_DATA_DISK(block_, off_, pos_ - file->leaf.pos), + // mark as erased, unless crystal_thresh prevented + // prog alignment + (((pos_ - block_pos) % lfs3->cfg->prog_size == 0) + ? LFS3_BPTR_ISERASED + : 0) + | (pos_ - block_pos), + lfs3->pcksum); + + // mark as ungrafted + file->b.h.flags |= LFS3_o_UNGRAFT; + return 0; + + relocate:; + // allocate a new block + // + // if we relocate, we rewrite the entire block from block_pos + // using what we can find in our tree/leaf/cache + // + block_ = lfs3_alloc(lfs3, LFS3_ALLOC_ERASE); + if (block_ < 0) { + return block_; + } + + off_ = 0; + pos_ = block_pos; + lfs3->pcksum = 0; + + // mark as uncrystallized and ungrafted + file->b.h.flags |= LFS3_o_UNCRYST; + } +} +#endif + +#ifndef LFS3_RDONLY +static int lfs3_file_crystallize(lfs3_t *lfs3, lfs3_file_t *file) { + // do nothing if our file is already crystallized + if (!lfs3_o_isuncryst(file->b.h.flags)) { + return 0; + } + // uncrystallized files must be unsynced + LFS3_ASSERT(lfs3_o_isunsync(file->b.h.flags)); + + // checkpoint the allocator + int err = lfs3_alloc_ckpoint(lfs3); + if (err) { + return err; + } + + // finish crystallizing + err = lfs3_file_crystallize_(lfs3, file, + file->leaf.pos - lfs3_bptr_off(&file->leaf.bptr), -1, -1, + 0, NULL, 0); + if (err) { + return err; + } + + // we should have crystallized + LFS3_ASSERT(!lfs3_o_isuncryst(file->b.h.flags)); + return 0; +} +#endif + +#ifndef LFS3_RDONLY +static int lfs3_file_flush_(lfs3_t *lfs3, lfs3_file_t *file, + lfs3_off_t pos, const uint8_t *buffer, lfs3_size_t size) { + // we can skip some btree lookups if we know we are aligned from a + // previous iteration, we already do way too many btree lookups + bool aligned = false; + + // if crystallization is disabled, just skip to writing fragments + if (lfs3->cfg->crystal_thresh > lfs3->cfg->block_size) { + goto fragment; + } + + // iteratively write blocks + while (size > 0) { + // checkpoint the allocator + int err = lfs3_alloc_ckpoint(lfs3); + if (err) { + return err; + } + + // mid-crystallization? can we just resume crystallizing? + // + // note that the threshold to resume crystallization (prog_size), + // is usually much lower than the threshold to start + // crystallization (crystal_thresh) + lfs3_off_t block_start = file->leaf.pos + - lfs3_bptr_off(&file->leaf.bptr); + lfs3_off_t block_end = file->leaf.pos + + lfs3_bptr_size(&file->leaf.bptr); + if (lfs3_bptr_isbptr(&file->leaf.bptr) + && lfs3_bptr_iserased(&file->leaf.bptr) + && pos >= block_end + && pos < block_start + lfs3->cfg->block_size + // if we're more than a crystal away, graft and check crystal + // heuristic before resuming + && pos - block_end < lfs3_max(lfs3->cfg->crystal_thresh, 1) + // need to bail if we can't meet prog alignment + && (pos + size) - block_end >= lfs3_min( + lfs3->cfg->prog_size, + lfs3->cfg->crystal_thresh)) { + // mark as uncrystallized to avoid allocating a new block + file->b.h.flags |= LFS3_o_UNCRYST; + // crystallize + err = lfs3_file_crystallize_(lfs3, file, + block_start, -1, (pos + size) - block_start, + pos, buffer, size); + if (err) { + return err; + } + + // update buffer state + lfs3_ssize_t d = lfs3_max( + file->leaf.pos + lfs3_bptr_size(&file->leaf.bptr), + pos) - pos; + pos += d; + buffer += lfs3_min(d, size); + size -= lfs3_min(d, size); + + // we should be aligned now + aligned = true; + continue; + } + + // if we can't resume crystallization, make sure any incomplete + // crystals are at least grafted into the tree + err = lfs3_file_graft(lfs3, file); + if (err) { + return err; + } + + // before we can start writing, we need to figure out if we have + // enough fragments to start crystallizing + // + // we do this heuristically, by looking up our worst-case + // crystal neighbors and using them as bounds for our current + // crystal + // + // note this can end up including holes in our crystals, but + // that's ok, we probably don't want small holes preventing + // crystallization anyways + + // default to arbitrary alignment + lfs3_off_t crystal_start = pos; + lfs3_off_t crystal_end = pos + size; + + // if we haven't already exceeded our crystallization threshold, + // find left crystal neighbor + lfs3_off_t poke = lfs3_smax( + crystal_start - (lfs3->cfg->crystal_thresh-1), + 0); + if (crystal_end - crystal_start < lfs3->cfg->crystal_thresh + && crystal_start > 0 + && poke < file->b.b.r.weight + // don't bother looking up left after the first block + && !aligned) { + lfs3_bid_t bid; + lfs3_bid_t weight; + lfs3_bptr_t bptr; + err = lfs3_file_lookupnext(lfs3, file, poke, + &bid, &weight, &bptr); + if (err) { + LFS3_ASSERT(err != LFS3_ERR_NOENT); + return err; + } + + // if left crystal neighbor is a fragment and there is no + // obvious hole between our own crystal and our neighbor, + // include as a part of our crystal + if (!lfs3_bptr_isbptr(&bptr) + && lfs3_bptr_size(&bptr) > 0 + // hole? holes can be quite large and shouldn't + // trigger crystallization + && bid-(weight-1) + lfs3_bptr_size(&bptr) >= poke) { + crystal_start = bid-(weight-1); + + // otherwise our neighbor determines our crystal boundary + } else { + crystal_start = lfs3_min(bid+1, crystal_start); + } + } + + // if we haven't already exceeded our crystallization threshold, + // find right crystal neighbor + poke = lfs3_min( + crystal_start + (lfs3->cfg->crystal_thresh-1), + file->b.b.r.weight-1); + if (crystal_end - crystal_start < lfs3->cfg->crystal_thresh + && crystal_end < file->b.b.r.weight) { + lfs3_bid_t bid; + lfs3_bid_t weight; + lfs3_bptr_t bptr; + err = lfs3_file_lookupnext(lfs3, file, poke, + &bid, &weight, &bptr); + if (err) { + LFS3_ASSERT(err != LFS3_ERR_NOENT); + return err; + } + + // if right crystal neighbor is a fragment, include as a part + // of our crystal + if (!lfs3_bptr_isbptr(&bptr) + && lfs3_bptr_size(&bptr) > 0) { + crystal_end = lfs3_max( + bid-(weight-1) + lfs3_bptr_size(&bptr), + crystal_end); + + // otherwise treat as crystal boundary + } else { + crystal_end = lfs3_max( + bid-(weight-1), + crystal_end); + } + } + + // now that we have our crystal guess, we need to decide how to + // write to the file + + // below our crystallization threshold? fallback to writing fragments + // + // note as long as crystal_thresh >= prog_size, this also ensures we + // have enough for prog alignment + if (crystal_end - crystal_start < lfs3->cfg->crystal_thresh) { + goto fragment; + } + + // exceeded crystallization threshold? we need to allocate a + // new block + + // can we resume crystallizing with the fragments on disk? + block_start = file->leaf.pos + - lfs3_bptr_off(&file->leaf.bptr); + block_end = file->leaf.pos + + lfs3_bptr_size(&file->leaf.bptr); + if (lfs3_bptr_isbptr(&file->leaf.bptr) + && lfs3_bptr_iserased(&file->leaf.bptr) + && crystal_start >= block_end + && crystal_start < block_start + lfs3->cfg->block_size) { + // mark as uncrystallized + file->b.h.flags |= LFS3_o_UNCRYST; + // crystallize + err = lfs3_file_crystallize_(lfs3, file, + block_start, -1, crystal_end - block_start, + pos, buffer, size); + if (err) { + return err; + } + + // update buffer state, this may or may not make progress + lfs3_ssize_t d = lfs3_max( + file->leaf.pos + lfs3_bptr_size(&file->leaf.bptr), + pos) - pos; + pos += d; + buffer += lfs3_min(d, size); + size -= lfs3_min(d, size); + + // we should be aligned now + aligned = true; + continue; + } + + // if we're mid-crystallization, finish crystallizing the block + // and graft it into our bshrub/btree + err = lfs3_file_crystallize(lfs3, file); + if (err) { + return err; + } + + err = lfs3_file_graft(lfs3, file); + if (err) { + return err; + } + + // before we can crystallize we need to figure out the best + // block alignment, we use the entry immediately to the left of + // our crystal for this + if (crystal_start > 0 + && file->b.b.r.weight > 0 + // don't bother to lookup left after the first block + && !aligned) { + lfs3_bid_t bid; + lfs3_bid_t weight; + lfs3_bptr_t bptr; + err = lfs3_file_lookupnext(lfs3, file, + lfs3_min( + crystal_start-1, + file->b.b.r.weight-1), + &bid, &weight, &bptr); + if (err) { + LFS3_ASSERT(err != LFS3_ERR_NOENT); + return err; + } + + // is our left neighbor in the same block? + // + // note we use the actual block start here! not the sliced + // view! this avoids excessive recrystallizations when + // fruncating + if (crystal_start - (bid-(weight-1)-lfs3_bptr_off(&bptr)) + < lfs3->cfg->block_size + && lfs3_bptr_size(&bptr) > 0) { + crystal_start = bid-(weight-1); + + // no? is our left neighbor at least our left block neighbor? + // align to block alignment + } else if (crystal_start - (bid-(weight-1)-lfs3_bptr_off(&bptr)) + < 2*lfs3->cfg->block_size + && lfs3_bptr_size(&bptr) > 0) { + crystal_start = bid-(weight-1)-lfs3_bptr_off(&bptr) + + lfs3->cfg->block_size; + } + } + + // start crystallizing! + // + // lfs3_file_crystallize_ handles block allocation/relocation + err = lfs3_file_crystallize_(lfs3, file, + crystal_start, -1, crystal_end - crystal_start, + pos, buffer, size); + if (err) { + return err; + } + + // update buffer state, this may or may not make progress + lfs3_ssize_t d = lfs3_max( + file->leaf.pos + lfs3_bptr_size(&file->leaf.bptr), + pos) - pos; + pos += d; + buffer += lfs3_min(d, size); + size -= lfs3_min(d, size); + + // we should be aligned now + aligned = true; + } + + return 0; + +fragment:; + // crystals should be grafted before we write any fragments + LFS3_ASSERT(!lfs3_o_isungraft(file->b.h.flags)); + + // do we need to discard our leaf? + // + // - we need to discard fragments in case the underlying rbyd + // compacts + // - we need to discard overwritten blocks + // - but we really want to keep non-overwritten blocks in case + // they contain erased-state! + // + // note we need to discard before attempting to graft since a + // single graft may be split up into multiple commits + // + // unfortunately we don't know where our fragment will end up + // until after the commit, so we can't track it in our leaf + // quite yet + if (!lfs3_bptr_isbptr(&file->leaf.bptr) + || (pos < file->leaf.pos + lfs3_bptr_size(&file->leaf.bptr) + && pos + size > file->leaf.pos)) { + lfs3_file_discardleaf(file); + } + + // iteratively write fragments (inlined leaves) + while (size > 0) { + // checkpoint the allocator + int err = lfs3_alloc_ckpoint(lfs3); + if (err) { + return err; + } + + // truncate to our fragment size + lfs3_off_t fragment_start = pos; + lfs3_off_t fragment_end = fragment_start + lfs3_min( + size, + lfs3->cfg->fragment_size); + + lfs3_data_t datas[3]; + lfs3_size_t data_count = 0; + + // do we have a left sibling? don't bother to lookup if fragment + // is already full + if (fragment_end - fragment_start < lfs3->cfg->fragment_size + && fragment_start > 0 + && fragment_start <= file->b.b.r.weight + // don't bother to lookup left after first fragment + && !aligned) { + lfs3_bid_t bid; + lfs3_bid_t weight; + lfs3_bptr_t bptr; + err = lfs3_file_lookupnext(lfs3, file, + fragment_start-1, + &bid, &weight, &bptr); + if (err) { + LFS3_ASSERT(err != LFS3_ERR_NOENT); + return err; + } + + // can we coalesce? + if (bid-(weight-1) + lfs3_bptr_size(&bptr) >= fragment_start + && fragment_end - (bid-(weight-1)) + <= lfs3->cfg->fragment_size) { + datas[data_count++] = lfs3_data_slice(bptr.d, + -1, + fragment_start - (bid-(weight-1))); + + fragment_start = bid-(weight-1); + } + } + + // append our new data + datas[data_count++] = LFS3_DATA_BUF( + buffer, + fragment_end - pos); + + // do we have a right sibling? don't bother to lookup if fragment + // is already full + // + // note this may the same as our left sibling + if (fragment_end - fragment_start < lfs3->cfg->fragment_size + && fragment_end < file->b.b.r.weight) { + lfs3_bid_t bid; + lfs3_bid_t weight; + lfs3_bptr_t bptr; + err = lfs3_file_lookupnext(lfs3, file, + fragment_end, + &bid, &weight, &bptr); + if (err) { + LFS3_ASSERT(err != LFS3_ERR_NOENT); + return err; + } + + // can we coalesce? + if (fragment_end < bid-(weight-1) + lfs3_bptr_size(&bptr) + && bid-(weight-1) + lfs3_bptr_size(&bptr) + - fragment_start + <= lfs3->cfg->fragment_size) { + datas[data_count++] = lfs3_data_slice(bptr.d, + fragment_end - (bid-(weight-1)), + -1); + + fragment_end = bid-(weight-1) + lfs3_bptr_size(&bptr); + } + } + + // make sure we didn't overflow our data buffer + LFS3_ASSERT(data_count <= 3); + + // once we've figured out what fragment to write, graft it into + // our tree + err = lfs3_file_graft_(lfs3, file, + fragment_start, fragment_end - fragment_start, 0, + datas, data_count); + if (err) { + return err; + } + + // update buffer state + lfs3_ssize_t d = fragment_end - pos; + pos += d; + buffer += lfs3_min(d, size); + size -= lfs3_min(d, size); + + // we should be aligned now + aligned = true; + } + + return 0; +} +#endif + + +// high-level file writing + +#ifndef LFS3_RDONLY +lfs3_ssize_t lfs3_file_write(lfs3_t *lfs3, lfs3_file_t *file, + const void *buffer, lfs3_size_t size) { + LFS3_ASSERT(lfs3_handle_isopen(lfs3, &file->b.h)); + // can't write to readonly files + LFS3_ASSERT(!lfs3_o_isrdonly(file->b.h.flags)); + + // size=0 is a bit special and is guaranteed to have no effects on the + // underlying file, this means no updating file pos or file size + // + // since we need to test for this, just return early + if (size == 0) { + return 0; + } + + // would this write make our file larger than our file limit? + int err; + if (size > lfs3->file_limit - file->pos) { + err = LFS3_ERR_FBIG; + goto failed; + } + + // mark as unsynced in case we fail + file->b.h.flags |= LFS3_o_UNSYNC; + + // update pos if we are appending + lfs3_off_t pos = file->pos; + if (lfs3_o_isappend(file->b.h.flags)) { + pos = lfs3_file_size_(file); + } + + const uint8_t *buffer_ = buffer; + lfs3_size_t written = 0; + while (size > 0) { + // bypass cache? + // + // note we flush our cache before bypassing writes, this isn't + // strictly necessary, but enforces a more intuitive write order + // and avoids weird cases with low-level write heuristics + // + if (!lfs3_o_isunflush(file->b.h.flags) + && size >= lfs3_file_fcachesize(lfs3, file)) { + err = lfs3_file_flush_(lfs3, file, + pos, buffer_, size); + if (err) { + goto failed; + } + + // after success, fill our cache with the tail of our write + // + // note we need to clear the cache anyways to avoid any + // out-of-date data + file->cache.pos = pos + size - lfs3_file_fcachesize(lfs3, file); + lfs3_memcpy(file->cache.buffer, + &buffer_[size - lfs3_file_fcachesize(lfs3, file)], + lfs3_file_fcachesize(lfs3, file)); + file->cache.size = lfs3_file_fcachesize(lfs3, file); + + file->b.h.flags &= ~LFS3_o_UNFLUSH; + written += size; + pos += size; + buffer_ += size; + size -= size; + continue; + } + + // try to fill our cache + // + // This is a bit delicate, since our cache contains both old and + // new data, but note: + // + // 1. We only write to yet unused cache memory. + // + // 2. Bypassing the cache above means we only write to the + // cache once, and flush at most twice. + // + if (!lfs3_o_isunflush(file->b.h.flags) + || (pos >= file->cache.pos + && pos <= file->cache.pos + file->cache.size + && pos + < file->cache.pos + + lfs3_file_fcachesize(lfs3, file))) { + // unused cache? we can move it where we need it + if (!lfs3_o_isunflush(file->b.h.flags)) { + file->cache.pos = pos; + file->cache.size = 0; + } + + lfs3_size_t d = lfs3_min( + size, + lfs3_file_fcachesize(lfs3, file) + - (pos - file->cache.pos)); + lfs3_memcpy(&file->cache.buffer[pos - file->cache.pos], + buffer_, + d); + file->cache.size = lfs3_max( + file->cache.size, + pos+d - file->cache.pos); + + file->b.h.flags |= LFS3_o_UNFLUSH; + written += d; + pos += d; + buffer_ += d; + size -= d; + continue; + } + + // flush our cache so the above can't fail + err = lfs3_file_flush_(lfs3, file, + file->cache.pos, file->cache.buffer, file->cache.size); + if (err) { + goto failed; + } + file->b.h.flags &= ~LFS3_o_UNFLUSH; + } + + // update our pos + file->pos = pos; + + // flush if requested + if (lfs3_o_isflush(file->b.h.flags)) { + err = lfs3_file_flush(lfs3, file); + if (err) { + goto failed; + } + } + + // sync if requested + if (lfs3_o_issync(file->b.h.flags)) { + err = lfs3_file_sync(lfs3, file); + if (err) { + goto failed; + } + } + + return written; + +failed:; + // mark as desync so lfs3_file_close doesn't write to disk + file->b.h.flags |= LFS3_O_DESYNC; + return err; +} +#endif + +int lfs3_file_flush(lfs3_t *lfs3, lfs3_file_t *file) { + (void)lfs3; + LFS3_ASSERT(lfs3_handle_isopen(lfs3, &file->b.h)); + + // do nothing if our file is already flushed, crystallized, + // and grafted + if (!lfs3_o_isunflush(file->b.h.flags) + && !lfs3_o_isuncryst(file->b.h.flags) + && !lfs3_o_isungraft(file->b.h.flags)) { + return 0; + } + // unflushed/uncrystallized files must be unsynced + LFS3_ASSERT(lfs3_o_isunsync(file->b.h.flags)); + // unflushed files can't be readonly + LFS3_ASSERT(!lfs3_o_isrdonly(file->b.h.flags)); + + #ifndef LFS3_RDONLY + // flush our cache + int err; + if (lfs3_o_isunflush(file->b.h.flags)) { + err = lfs3_file_flush_(lfs3, file, + file->cache.pos, file->cache.buffer, file->cache.size); + if (err) { + goto failed; + } + + // mark as flushed + file->b.h.flags &= ~LFS3_o_UNFLUSH; + } + + // and crystallize/graft our leaf + err = lfs3_file_crystallize(lfs3, file); + if (err) { + goto failed; + } + + err = lfs3_file_graft(lfs3, file); + if (err) { + goto failed; + } + #endif + + return 0; + + #ifndef LFS3_RDONLY +failed:; + // mark as desync so lfs3_file_close doesn't write to disk + file->b.h.flags |= LFS3_O_DESYNC; + return err; + #endif +} + +#ifndef LFS3_RDONLY +// this LFS3_NOINLINE is to force lfs3_file_sync_ off the stack hot-path +LFS3_NOINLINE +static int lfs3_file_sync_(lfs3_t *lfs3, lfs3_file_t *file, + const lfs3_name_t *name) { + // build a commit of any pending file metadata + lfs3_rattr_t rattrs[4]; + lfs3_size_t rattr_count = 0; + lfs3_data_t name_data; + lfs3_rattr_t shrub_rattrs[1]; + lfs3_size_t shrub_rattr_count = 0; + lfs3_data_t file_data; + lfs3_shrubcommit_t shrub_commit; + + // uncreated files must be unsync + LFS3_ASSERT(!lfs3_o_isuncreat(file->b.h.flags) + || lfs3_o_isunsync(file->b.h.flags)); + // small unflushed files must be unsync + LFS3_ASSERT(!lfs3_o_isunflush(file->b.h.flags) + || lfs3_o_isunsync(file->b.h.flags)); + // uncrystallized leaves should've been flushed or discarded + LFS3_ASSERT(!lfs3_o_isuncryst(file->b.h.flags)); + LFS3_ASSERT(!lfs3_o_isungraft(file->b.h.flags)); + + // pending metadata changes? + if (lfs3_o_isunsync(file->b.h.flags)) { + // explicit name? + if (name) { + rattrs[rattr_count++] = LFS3_RATTR_NAME_( + LFS3_TAG_REG, +1, + name); + + // not created yet? need to convert to normal file + } else if (lfs3_o_isuncreat(file->b.h.flags)) { + // convert stickynote -> reg file + lfs3_stag_t name_tag = lfs3_rbyd_lookup(lfs3, &file->b.h.mdir.r, + lfs3_mrid(lfs3, file->b.h.mdir.mid), LFS3_TAG_STICKYNOTE, + &name_data); + if (name_tag < 0) { + // orphan flag but no stickynote tag? + LFS3_ASSERT(name_tag != LFS3_ERR_NOENT); + return name_tag; + } + + rattrs[rattr_count++] = LFS3_RATTR_DATA( + LFS3_tag_MASK8 | LFS3_TAG_REG, 0, + &name_data); + } + + // pending small file flush? + if (lfs3_o_isunflush(file->b.h.flags)) { + // this only works if the file is entirely in our cache + LFS3_ASSERT(file->cache.pos == 0); + LFS3_ASSERT(file->cache.size == lfs3_file_size_(file)); + + // discard any lingering bshrub state + lfs3_file_discardbshrub(file); + + // build a small shrub commit + if (file->cache.size > 0) { + file_data = LFS3_DATA_BUF( + file->cache.buffer, + file->cache.size); + shrub_rattrs[shrub_rattr_count++] = LFS3_RATTR_DATA( + LFS3_TAG_DATA, +file->cache.size, + &file_data); + + LFS3_ASSERT(shrub_rattr_count + <= sizeof(shrub_rattrs)/sizeof(lfs3_rattr_t)); + shrub_commit.bshrub = &file->b; + shrub_commit.rid = 0; + shrub_commit.rattrs = shrub_rattrs; + shrub_commit.rattr_count = shrub_rattr_count; + rattrs[rattr_count++] = LFS3_RATTR_SHRUBCOMMIT(&shrub_commit); + } + } + + // make sure data is on-disk before committing metadata + if (lfs3_file_size_(file) > 0 + && !lfs3_o_isunflush(file->b.h.flags)) { + int err = lfs3_bd_sync(lfs3); + if (err) { + return err; + } + } + + // zero size files should have no bshrub/btree + LFS3_ASSERT(lfs3_file_size_(file) > 0 + || lfs3_bshrub_isbnull(&file->b)); + + // no bshrub/btree? + if (lfs3_file_size_(file) == 0) { + rattrs[rattr_count++] = LFS3_RATTR( + LFS3_tag_RM | LFS3_tag_MASK8 | LFS3_TAG_STRUCT, 0); + // bshrub? + } else if (lfs3_bshrub_isbshrub(&file->b) + || lfs3_o_isunflush(file->b.h.flags)) { + rattrs[rattr_count++] = LFS3_RATTR_SHRUB( + LFS3_tag_MASK8 | LFS3_TAG_BSHRUB, 0, + // note we use the staged trunk here + &file->b.b_); + // btree? + } else if (lfs3_bshrub_isbtree(&file->b)) { + rattrs[rattr_count++] = LFS3_RATTR_BTREE( + LFS3_tag_MASK8 | LFS3_TAG_BTREE, 0, + &file->b.b); + } else { + LFS3_UNREACHABLE(); + } + } + + // 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 attrs = lfs3_o_isunsync(file->b.h.flags); + if (!attrs) { + for (lfs3_size_t i = 0; i < file->cfg->attr_count; i++) { + // skip readonly attrs and lazy attrs + if (lfs3_o_isrdonly(file->cfg->attrs[i].flags) + || lfs3_a_islazy(file->cfg->attrs[i].flags)) { + continue; + } + + // lookup the attr + lfs3_data_t data; + lfs3_stag_t tag = lfs3_mdir_lookup(lfs3, &file->b.h.mdir, + LFS3_TAG_ATTR(file->cfg->attrs[i].type), + &data); + if (tag < 0 && tag != LFS3_ERR_NOENT) { + return tag; + } + + // does disk match our attr? + lfs3_scmp_t cmp = lfs3_attr_cmp(lfs3, &file->cfg->attrs[i], + (tag != LFS3_ERR_NOENT) ? &data : NULL); + if (cmp < 0) { + return cmp; + } + + if (cmp != LFS3_CMP_EQ) { + attrs = true; + break; + } + } + } + if (attrs) { + // need to append custom attributes + rattrs[rattr_count++] = LFS3_RATTR_ATTRS( + file->cfg->attrs, file->cfg->attr_count); + } + + // pending metadata? looks like we need to write to disk + if (rattr_count > 0) { + // make sure we don't overflow our rattr buffer + LFS3_ASSERT(rattr_count <= sizeof(rattrs)/sizeof(lfs3_rattr_t)); + + // and commit! + int err = lfs3_mdir_commit(lfs3, &file->b.h.mdir, + rattrs, rattr_count); + if (err) { + return err; + } + } + + // update in-device state + for (lfs3_handle_t *h = lfs3->handles; h; h = h->next) { + if (lfs3_o_type(h->flags) == LFS3_TYPE_REG + && h->mdir.mid == file->b.h.mdir.mid + // don't double update + && h != &file->b.h) { + lfs3_file_t *file_ = (lfs3_file_t*)h; + // notify all files of creation + file_->b.h.flags &= ~LFS3_o_UNCREAT; + + // mark desynced files an unsynced + if (lfs3_o_isdesync(file_->b.h.flags)) { + file_->b.h.flags |= LFS3_o_UNSYNC; + + // update synced files + } else { + // update flags + file_->b.h.flags &= ~LFS3_o_UNSYNC + & ~LFS3_o_UNFLUSH + & ~LFS3_o_UNCRYST + & ~LFS3_o_UNGRAFT; + // update shrubs + file_->b.b = file->b.b; + // update leaves + file_->leaf = file->leaf; + + // update caches + // + // note we need to be careful if caches have different + // sizes, prefer the most recent data in this case + lfs3_size_t d = file->cache.size - lfs3_min( + lfs3_file_fcachesize(lfs3, file_), + file->cache.size); + file_->cache.pos = file->cache.pos + d; + lfs3_memcpy(file_->cache.buffer, + file->cache.buffer + d, + file->cache.size - d); + file_->cache.size = file->cache.size - d; + + // update any custom attrs + for (lfs3_size_t i = 0; i < file->cfg->attr_count; i++) { + if (lfs3_o_isrdonly(file->cfg->attrs[i].flags)) { + continue; + } + + for (lfs3_size_t j = 0; j < file_->cfg->attr_count; j++) { + if (!(file_->cfg->attrs[j].type + == file->cfg->attrs[i].type + && !lfs3_o_iswronly( + file_->cfg->attrs[j].flags))) { + continue; + } + + if (lfs3_attr_isnoattr(&file->cfg->attrs[i])) { + if (file_->cfg->attrs[j].size) { + *file_->cfg->attrs[j].size = LFS3_ERR_NOATTR; + } + } else { + lfs3_size_t d = lfs3_min( + lfs3_attr_size(&file->cfg->attrs[i]), + file_->cfg->attrs[j].buffer_size); + lfs3_memcpy(file_->cfg->attrs[j].buffer, + file->cfg->attrs[i].buffer, + d); + if (file_->cfg->attrs[j].size) { + *file_->cfg->attrs[j].size = d; + } + } + } + } + } + } + } + + // mark as synced + file->b.h.flags &= ~LFS3_o_UNCREAT + & ~LFS3_o_UNSYNC + & ~LFS3_o_UNFLUSH + & ~LFS3_o_UNCRYST + & ~LFS3_o_UNGRAFT; + return 0; +} +#endif + +int lfs3_file_sync(lfs3_t *lfs3, lfs3_file_t *file) { + (void)lfs3; + LFS3_ASSERT(lfs3_handle_isopen(lfs3, &file->b.h)); + + // removed? ignore sync requests + if (lfs3_o_iszombie(file->b.h.flags)) { + return 0; + } + + #ifndef LFS3_RDONLY + // can we get away with a small file flush? + // + // this merges the data flush with metadata sync in a single commit + // if the file is small enough to fit in the cache + int err; + if (file->cache.size == lfs3_file_size_(file) + && file->cache.size <= lfs3->cfg->shrub_size + && file->cache.size <= lfs3->cfg->fragment_size + && file->cache.size < lfs3_max(lfs3->cfg->crystal_thresh, 1)) { + // discard any overwritten leaves, this also clears the + // LFS3_o_UNCRYST and LFS3_o_UNGRAFT flags + lfs3_file_discardleaf(file); + + // flush any data in our cache, this is a noop if already flushed + // + // note that flush does not change the actual file data, so if + // flush succeeds but mdir commit fails it's ok to fall back to + // our flushed state + } else { + err = lfs3_file_flush(lfs3, file); + if (err) { + goto failed; + } + } + + // commit any pending metadata to disk + // + // the use of a second function here is mainly to isolate the + // stack costs of lfs3_file_flush and lfs3_file_sync_ + // + err = lfs3_file_sync_(lfs3, file, NULL); + if (err) { + goto failed; + } + #endif + + // clear desync flag + file->b.h.flags &= ~LFS3_O_DESYNC; + return 0; + + #ifndef LFS3_RDONLY +failed:; + file->b.h.flags |= LFS3_O_DESYNC; + return err; + #endif +} + +int lfs3_file_desync(lfs3_t *lfs3, lfs3_file_t *file) { + (void)lfs3; + (void)file; + LFS3_ASSERT(lfs3_handle_isopen(lfs3, &file->b.h)); + + #ifndef LFS3_RDONLY + // mark as desynced + file->b.h.flags |= LFS3_O_DESYNC; + #endif + return 0; +} + +int lfs3_file_resync(lfs3_t *lfs3, lfs3_file_t *file) { + (void)lfs3; + (void)file; + LFS3_ASSERT(lfs3_handle_isopen(lfs3, &file->b.h)); + + #ifndef LFS3_RDONLY + // removed? we can't resync + int err; + if (lfs3_o_iszombie(file->b.h.flags)) { + err = LFS3_ERR_NOENT; + goto failed; + } + + // do nothing if already in-sync + if (lfs3_o_isunsync(file->b.h.flags)) { + // discard cached state + lfs3_file_discardbshrub(file); + lfs3_file_discardcache(file); + lfs3_file_discardleaf(file); + + // refetch the file struct from disk + err = lfs3_file_fetch(lfs3, file, + // don't truncate again! + file->b.h.flags & ~LFS3_O_TRUNC); + if (err) { + goto failed; + } + } + #endif + + // clear desync flag + file->b.h.flags &= ~LFS3_O_DESYNC; + return 0; + + #ifndef LFS3_RDONLY +failed:; + file->b.h.flags |= LFS3_O_DESYNC; + return err; + #endif +} + +// other file operations + +lfs3_soff_t lfs3_file_seek(lfs3_t *lfs3, lfs3_file_t *file, + lfs3_soff_t off, uint32_t whence) { + LFS3_ASSERT(lfs3_handle_isopen(lfs3, &file->b.h)); + + // TODO check for out-of-range? + + // figure out our new file position + lfs3_off_t pos_; + if (whence == LFS3_SEEK_SET) { + pos_ = off; + } else if (whence == LFS3_SEEK_CUR) { + pos_ = file->pos + off; + } else if (whence == LFS3_SEEK_END) { + pos_ = lfs3_file_size_(file) + off; + } else { + LFS3_UNREACHABLE(); + } + + // out of range? + if (pos_ > lfs3->file_limit) { + return LFS3_ERR_INVAL; + } + + // update file position + file->pos = pos_; + return pos_; +} + +lfs3_soff_t lfs3_file_tell(lfs3_t *lfs3, lfs3_file_t *file) { + (void)lfs3; + LFS3_ASSERT(lfs3_handle_isopen(lfs3, &file->b.h)); + + return file->pos; +} + +lfs3_soff_t lfs3_file_rewind(lfs3_t *lfs3, lfs3_file_t *file) { + (void)lfs3; + LFS3_ASSERT(lfs3_handle_isopen(lfs3, &file->b.h)); + + file->pos = 0; + return 0; +} + +lfs3_soff_t lfs3_file_size(lfs3_t *lfs3, lfs3_file_t *file) { + (void)lfs3; + LFS3_ASSERT(lfs3_handle_isopen(lfs3, &file->b.h)); + + return lfs3_file_size_(file); +} + +#ifndef LFS3_RDONLY +int lfs3_file_truncate(lfs3_t *lfs3, lfs3_file_t *file, lfs3_off_t size_) { + LFS3_ASSERT(lfs3_handle_isopen(lfs3, &file->b.h)); + // can't write to readonly files + LFS3_ASSERT(!lfs3_o_isrdonly(file->b.h.flags)); + + // do nothing if our size does not change + lfs3_off_t size = lfs3_file_size_(file); + if (lfs3_file_size_(file) == size_) { + return 0; + } + + // exceeds our file limit? + int err; + if (size_ > lfs3->file_limit) { + err = LFS3_ERR_FBIG; + goto failed; + } + + // mark as unsynced in case we fail + file->b.h.flags |= LFS3_o_UNSYNC; + + // make sure any incomplete are at least grafted + err = lfs3_file_graft(lfs3, file); + if (err) { + return err; + } + + // checkpoint the allocator + err = lfs3_alloc_ckpoint(lfs3); + if (err) { + return err; + } + + // truncate our btree + err = lfs3_file_graft_(lfs3, file, + lfs3_min(size, size_), size - lfs3_min(size, size_), + +size_ - size, + NULL, 0); + if (err) { + goto failed; + } + + // truncate our leaf + // + // note we don't unconditionally discard to match fruncate, where we + // _really_ don't want to discard erased-state + lfs3_bptr_slice(&file->leaf.bptr, + -1, + size_ - lfs3_min(file->leaf.pos, size_)); + file->leaf.weight = lfs3_min( + file->leaf.weight, + size_ - lfs3_min(file->leaf.pos, size_)); + file->leaf.pos = lfs3_min(file->leaf.pos, size_); + // mark as crystallized if this truncates our erased-state + if (lfs3_bptr_off(&file->leaf.bptr) + + lfs3_bptr_size(&file->leaf.bptr) + < lfs3_bptr_cksize(&file->leaf.bptr)) { + lfs3_bptr_claim(&file->leaf.bptr); + file->b.h.flags &= ~LFS3_o_UNCRYST; + } + // discard if our leaf is a fragment, is fragmented, or is completed + // truncated, we can't rely on any in-bshrub/btree state + if (!lfs3_bptr_isbptr(&file->leaf.bptr) + || (lfs3_bptr_size(&file->leaf.bptr) <= lfs3->cfg->fragment_size + && lfs3_bptr_size(&file->leaf.bptr) + < lfs3_max(lfs3->cfg->crystal_thresh, 1))) { + lfs3_file_discardleaf(file); + } + + // truncate our cache + file->cache.size = lfs3_min( + file->cache.size, + size_ - lfs3_min(file->cache.pos, size_)); + file->cache.pos = lfs3_min(file->cache.pos, size_); + // mark as flushed if this completely truncates our cache + if (file->cache.size == 0) { + lfs3_file_discardcache(file); + } + + return 0; + +failed:; + // mark as desync so lfs3_file_close doesn't write to disk + file->b.h.flags |= LFS3_O_DESYNC; + return err; +} +#endif + +#ifndef LFS3_RDONLY +int lfs3_file_fruncate(lfs3_t *lfs3, lfs3_file_t *file, lfs3_off_t size_) { + LFS3_ASSERT(lfs3_handle_isopen(lfs3, &file->b.h)); + // can't write to readonly files + LFS3_ASSERT(!lfs3_o_isrdonly(file->b.h.flags)); + + // do nothing if our size does not change + lfs3_off_t size = lfs3_file_size_(file); + if (size == size_) { + return 0; + } + + // exceeds our file limit? + int err; + if (size_ > lfs3->file_limit) { + err = LFS3_ERR_FBIG; + goto failed; + } + + // mark as unsynced in case we fail + file->b.h.flags |= LFS3_o_UNSYNC; + + // make sure any incomplete are at least grafted + err = lfs3_file_graft(lfs3, file); + if (err) { + return err; + } + + // checkpoint the allocator + err = lfs3_alloc_ckpoint(lfs3); + if (err) { + return err; + } + + // fruncate our btree + err = lfs3_file_graft_(lfs3, file, + 0, lfs3_smax(size - size_, 0), + +size_ - size, + NULL, 0); + if (err) { + goto failed; + } + + // fruncate our leaf + // + // note we _really_ don't want to discard erased-state if possible, + // as fruncate is intended for logging operations, otherwise we'd + // just unconditionally discard the leaf and avoid this hassle + lfs3_bptr_slice(&file->leaf.bptr, + lfs3_min( + lfs3_smax( + size - size_ - file->leaf.pos, + 0), + lfs3_bptr_size(&file->leaf.bptr)), + -1); + file->leaf.weight -= lfs3_min( + lfs3_smax( + size - size_ - file->leaf.pos, + 0), + file->leaf.weight); + file->leaf.pos -= lfs3_smin( + size - size_, + file->leaf.pos); + // discard if our leaf is a fragment, is fragmented, or is completed + // truncated, we can't rely on any in-bshrub/btree state + if (!lfs3_bptr_isbptr(&file->leaf.bptr) + || (lfs3_bptr_size(&file->leaf.bptr) <= lfs3->cfg->fragment_size + && lfs3_bptr_size(&file->leaf.bptr) + < lfs3_max(lfs3->cfg->crystal_thresh, 1))) { + lfs3_file_discardleaf(file); + } + + // fruncate our cache + lfs3_memmove(file->cache.buffer, + &file->cache.buffer[lfs3_min( + lfs3_smax( + size - size_ - file->cache.pos, + 0), + file->cache.size)], + file->cache.size - lfs3_min( + lfs3_smax( + size - size_ - file->cache.pos, + 0), + file->cache.size)); + file->cache.size -= lfs3_min( + lfs3_smax( + size - size_ - file->cache.pos, + 0), + file->cache.size); + file->cache.pos -= lfs3_smin( + size - size_, + file->cache.pos); + // mark as flushed if this completely truncates our cache + if (file->cache.size == 0) { + lfs3_file_discardcache(file); + } + + // fruncate _does_ update pos, to keep the same pos relative to end + // of file, though we can't let pos go negative + file->pos -= lfs3_smin( + size - size_, + file->pos); + + return 0; + +failed:; + // mark as desync so lfs3_file_close doesn't write to disk + file->b.h.flags |= LFS3_O_DESYNC; + return err; +} +#endif + +// file check function +int lfs3_file_ck(lfs3_t *lfs3, lfs3_file_t *file, uint32_t flags) { + LFS3_ASSERT(lfs3_handle_isopen(lfs3, &file->b.h)); + // can't read from writeonly files + LFS3_ASSERT(!lfs3_o_iswronly(file->b.h.flags)); + // unknown ck flags? note only some ck flags work on files + LFS3_ASSERT((flags & ~( + LFS3_CK_CKMETA + | LFS3_CK_CKDATA)) == 0); + + // validate ungrafted data block? + if (lfs3_t_isckdata(flags) + && lfs3_o_isungraft(file->b.h.flags)) { + LFS3_ASSERT(lfs3_bptr_isbptr(&file->leaf.bptr)); + int err = lfs3_bptr_ck(lfs3, &file->leaf.bptr); + if (err) { + return err; + } + } + + // traverse the file's bshrub/btree + lfs3_btrv_t btrv; + lfs3_btrv_init(&btrv); + while (true) { + lfs3_data_t data; + lfs3_stag_t tag = lfs3_bshrub_traverse(lfs3, &file->b, &btrv, + NULL, NULL, &data); + if (tag < 0) { + if (tag == LFS3_ERR_NOENT) { + break; + } + return tag; + } + + // validate btree nodes? + // + // this may end up revalidating some btree nodes when ckfetches + // is enabled, but we need to revalidate cached btree nodes or + // we risk missing errors in ckmeta scans + if ((lfs3_t_isckmeta(flags) + || lfs3_t_isckdata(flags)) + && tag == LFS3_TAG_BRANCH) { + lfs3_rbyd_t *rbyd = (lfs3_rbyd_t*)data.u.buffer; + int err = lfs3_rbyd_fetchck(lfs3, rbyd, + rbyd->blocks[0], rbyd->trunk, + rbyd->cksum); + if (err) { + return err; + } + } + + // validate data blocks? + if (lfs3_t_isckdata(flags) + && tag == LFS3_TAG_BLOCK) { + lfs3_bptr_t bptr; + int err = lfs3_data_readbptr(lfs3, &data, + &bptr); + if (err) { + return err; + } + + err = lfs3_bptr_ck(lfs3, &bptr); + if (err) { + return err; + } + } + } + + return 0; +} + + + +/// Simple key-value API /// + +// a simple key-value API is easier to use if your file fits in RAM, and +// if that's all you need you can potentially compile-out the more +// advanced file operations + +// kv file config, we need to explicitly disable the file cache +static const struct lfs3_file_cfg lfs3_file_kvcfg = { + // TODO is this the best way to do this? + .fcache_buffer = (uint8_t*)1, + .fcache_size = 0, +}; + +lfs3_ssize_t lfs3_get(lfs3_t *lfs3, const char *path, + void *buffer, lfs3_size_t size) { + // we just use the file API here, but with no cache so all reads + // bypass the cache + lfs3_file_t file; + int err = lfs3_file_opencfg(lfs3, &file, path, LFS3_O_RDONLY, + &lfs3_file_kvcfg); + if (err) { + return err; + } + + lfs3_ssize_t size_ = lfs3_file_read(lfs3, &file, buffer, size); + + // unconditionally close + err = lfs3_file_close(lfs3, &file); + // we didn't allocate anything, so this can't fail + LFS3_ASSERT(!err); + + return size_; +} + +lfs3_ssize_t lfs3_size(lfs3_t *lfs3, const char *path) { + // we just use the file API here, but with no cache so all reads + // bypass the cache + lfs3_file_t file; + int err = lfs3_file_opencfg(lfs3, &file, path, LFS3_O_RDONLY, + &lfs3_file_kvcfg); + if (err) { + return err; + } + + lfs3_ssize_t size_ = lfs3_file_size_(&file); + + // unconditionally close + err = lfs3_file_close(lfs3, &file); + // we didn't allocate anything, so this can't fail + LFS3_ASSERT(!err); + + return size_; +} + +#ifndef LFS3_RDONLY +int lfs3_set(lfs3_t *lfs3, const char *path, + const void *buffer, lfs3_size_t size) { + // LFS3_o_WRSET is a special mode specifically to make lfs3_set work + // atomically when possible + // + // - if we need to reserve the mid _and_ we're small, everything is + // committed/broadcasted in lfs3_file_opencfg + // + // - otherwise (exists? stickynote?), we flush/sync/broadcast + // normally in lfs3_file_close, lfs3_file_sync has its own logic + // to try to commit small files atomically + // + struct lfs3_file_cfg cfg = { + .fcache_buffer = (uint8_t*)buffer, + .fcache_size = size, + }; + lfs3_file_t file; + int err = lfs3_file_opencfg_(lfs3, &file, path, + LFS3_o_WRSET | LFS3_O_CREAT | LFS3_O_TRUNC, + &cfg); + if (err) { + return err; + } + + // let close do any remaining work + return lfs3_file_close(lfs3, &file); +} +#endif + + + + +/// High-level filesystem operations /// + +// needed in lfs3_init +static int lfs3_deinit(lfs3_t *lfs3); + +// initialize littlefs state, assert on bad configuration +static int lfs3_init(lfs3_t *lfs3, uint32_t flags, + const struct lfs3_cfg *cfg) { + // unknown flags? + LFS3_ASSERT((flags & ~( + LFS3_IFDEF_RDONLY(0, LFS3_M_RDWR) + | LFS3_M_RDONLY + | LFS3_M_FLUSH + | LFS3_M_SYNC + | LFS3_IFDEF_REVDBG(LFS3_M_REVDBG, 0) + | LFS3_IFDEF_REVNOISE(LFS3_M_REVNOISE, 0) + | LFS3_IFDEF_CKPROGS(LFS3_M_CKPROGS, 0) + | LFS3_IFDEF_CKFETCHES(LFS3_M_CKFETCHES, 0) + | LFS3_IFDEF_CKMETAPARITY(LFS3_M_CKMETAPARITY, 0) + | LFS3_IFDEF_CKDATACKSUMS(LFS3_M_CKDATACKSUMS, 0) + | LFS3_IFDEF_GBMAP(LFS3_F_GBMAP, 0))) == 0); + // TODO this all needs to be cleaned up + lfs3->cfg = cfg; + int err = 0; + + // validate that the lfs3-cfg sizes were initiated properly before + // performing any arithmetic logics with them + LFS3_ASSERT(lfs3->cfg->read_size != 0); + #ifndef LFS3_RDONLY + LFS3_ASSERT(lfs3->cfg->prog_size != 0); + #endif + LFS3_ASSERT(lfs3->cfg->rcache_size != 0); + #ifndef LFS3_RDONLY + LFS3_ASSERT(lfs3->cfg->pcache_size != 0); + #endif + + // cache sizes must be a multiple of their operation sizes + LFS3_ASSERT(lfs3->cfg->rcache_size % lfs3->cfg->read_size == 0); + #ifndef LFS3_RDONLY + LFS3_ASSERT(lfs3->cfg->pcache_size % lfs3->cfg->prog_size == 0); + #endif + + // block_size must be a multiple of both prog/read size + LFS3_ASSERT(lfs3->cfg->block_size % lfs3->cfg->read_size == 0); + #ifndef LFS3_RDONLY + LFS3_ASSERT(lfs3->cfg->block_size % lfs3->cfg->prog_size == 0); + #endif + + // block_size is currently limited to 28-bits + LFS3_ASSERT(lfs3->cfg->block_size <= 0x0fffffff); + + #ifdef LFS3_GC + // unknown gc flags? + LFS3_ASSERT((lfs3->cfg->gc_flags & ~LFS3_GC_ALL) == 0); + + // check that gc_compactmeta_thresh makes sense + // + // metadata can't be compacted below block_size/2, and metadata can't + // exceed a block + LFS3_ASSERT(lfs3->cfg->gc_compactmeta_thresh == 0 + || lfs3->cfg->gc_compactmeta_thresh >= lfs3->cfg->block_size/2); + LFS3_ASSERT(lfs3->cfg->gc_compactmeta_thresh == (lfs3_size_t)-1 + || lfs3->cfg->gc_compactmeta_thresh <= lfs3->cfg->block_size); + #endif + + #ifndef LFS3_RDONLY + // shrub_size must be <= block_size/4 + LFS3_ASSERT(lfs3->cfg->shrub_size <= lfs3->cfg->block_size/4); + // fragment_size must be <= block_size/4 + LFS3_ASSERT(lfs3->cfg->fragment_size <= lfs3->cfg->block_size/4); + #endif + + // TODO move this to mount? + // setup flags + lfs3->flags = flags + // assume we contain orphans until proven otherwise + | LFS3_IFDEF_RDONLY(0, LFS3_I_MKCONSISTENT) + // default to an empty lookahead + | LFS3_IFDEF_RDONLY(0, LFS3_I_LOOKAHEAD) + // default to assuming we need compaction somewhere, worst case + // this just makes lfs3_fs_gc read more than is strictly needed + | LFS3_IFDEF_RDONLY(0, LFS3_I_COMPACTMETA) + // default to needing a ckmeta/ckdata scan + | LFS3_I_CKMETA + | LFS3_I_CKDATA; + + // copy block_count so we can mutate it + lfs3->block_count = lfs3->cfg->block_count; + + // setup read cache + lfs3->rcache.block = 0; + lfs3->rcache.off = 0; + lfs3->rcache.size = 0; + if (lfs3->cfg->rcache_buffer) { + lfs3->rcache.buffer = lfs3->cfg->rcache_buffer; + } else { + lfs3->rcache.buffer = lfs3_malloc(lfs3->cfg->rcache_size); + if (!lfs3->rcache.buffer) { + err = LFS3_ERR_NOMEM; + goto failed; + } + } + + // setup program cache + #ifndef LFS3_RDONLY + lfs3->pcache.block = 0; + lfs3->pcache.off = 0; + lfs3->pcache.size = 0; + if (lfs3->cfg->pcache_buffer) { + lfs3->pcache.buffer = lfs3->cfg->pcache_buffer; + } else { + lfs3->pcache.buffer = lfs3_malloc(lfs3->cfg->pcache_size); + if (!lfs3->pcache.buffer) { + err = LFS3_ERR_NOMEM; + goto failed; + } + } + #endif + + // setup ptail, nothing should actually check off=0 + #ifdef LFS3_CKMETAPARITY + lfs3->ptail.block = 0; + lfs3->ptail.off = 0; + #endif + + // setup lookahead buffer, note mount finishes initializing this after + // we establish a decent pseudo-random seed + #ifndef LFS3_RDONLY + LFS3_ASSERT(lfs3->cfg->lookahead_size > 0); + if (lfs3->cfg->lookahead_buffer) { + lfs3->lookahead.buffer = lfs3->cfg->lookahead_buffer; + } else { + lfs3->lookahead.buffer = lfs3_malloc(lfs3->cfg->lookahead_size); + if (!lfs3->lookahead.buffer) { + err = LFS3_ERR_NOMEM; + goto failed; + } + } + lfs3->lookahead.window = 0; + lfs3->lookahead.off = 0; + lfs3->lookahead.known = 0; + lfs3->lookahead.ckpoint = 0; + lfs3_alloc_discard(lfs3); + #endif + + // check that the size limits are sane + #ifndef LFS3_RDONLY + LFS3_ASSERT(lfs3->cfg->name_limit <= LFS3_NAME_MAX); + lfs3->name_limit = lfs3->cfg->name_limit; + if (!lfs3->name_limit) { + lfs3->name_limit = LFS3_NAME_MAX; + } + + LFS3_ASSERT(lfs3->cfg->file_limit <= LFS3_FILE_MAX); + lfs3->file_limit = lfs3->cfg->file_limit; + if (!lfs3->file_limit) { + lfs3->file_limit = LFS3_FILE_MAX; + } + #endif + + // TODO do we need to recalculate these after mount? + + // find the number of bits to use for recycle counters + // + // Add 1, to include the initial erase, multiply by 2, since we + // alternate which metadata block we erase each compaction, and limit + // to 28-bits so we always have some bits to determine the most recent + // revision. + #ifndef LFS3_RDONLY + if (lfs3->cfg->block_recycles != -1) { + lfs3->recycle_bits = lfs3_min( + lfs3_nlog2(2*(lfs3->cfg->block_recycles+1)+1)-1, + 28); + } else { + lfs3->recycle_bits = -1; + } + #endif + + // calculate the upper-bound cost of a single rbyd attr after compaction + // + // Note that with rebalancing during compaction, we know the number + // of inner nodes is roughly the same as the number of tags. Unfortunately, + // our inner node encoding is rather poor, requiring 2 alts and terminating + // with a 4-byte null tag: + // + // a_0 = 3t + 4 + // + // If we could build each trunk perfectly, we could get this down to only + // 1 alt per tag. But this would require unbounded RAM: + // + // a_inf = 2t + // + // Or, if you build a bounded number of layers perfectly: + // + // 2t 3t + 4 + // a_1 = -- + ------ + // 2 2 + // + // a_n = 2t*(1-2^-n) + (3t + 4)*2^-n + // + // But this would be a tradeoff in code complexity. + // + // The worst-case tag encoding, t, depends on our size-limit and + // block-size. The weight can never exceed size-limit, and the size/jump + // field can never exceed a single block: + // + // t = 2 + log128(file_limit+1) + log128(block_size) + // + // Note this is different from LFS3_TAG_DSIZE, which is the worst case + // tag encoding at compile-time. + // + #ifndef LFS3_RDONLY + uint8_t tag_estimate + = 2 + + (lfs3_nlog2(lfs3->file_limit+1)+7-1)/7 + + (lfs3_nlog2(lfs3->cfg->block_size)+7-1)/7; + LFS3_ASSERT(tag_estimate <= LFS3_TAG_DSIZE); + lfs3->rattr_estimate = 3*tag_estimate + 4; + #endif + + // calculate the upper-bound cost of a single mdir attr after compaction + // + // This is the same as rattr_estimate, except we can assume a weight<=1. + // + #ifndef LFS3_RDONLY + tag_estimate + = 2 + + 1 + + (lfs3_nlog2(lfs3->cfg->block_size)+7-1)/7; + LFS3_ASSERT(tag_estimate <= LFS3_TAG_DSIZE); + lfs3->mattr_estimate = 3*tag_estimate + 4; + #endif + + // calculate the number of bits we need to reserve for mdir rids + // + // Worst case (or best case?) each metadata entry is a single tag. In + // theory each entry also needs a did+name, but with power-of-two + // rounding, this is negligible + // + // Assuming a _perfect_ compaction algorithm (requires unbounded RAM), + // each tag also needs ~1 alt, this gives us: + // + // block_size block_size + // mrids = ---------- = ---------- + // a_inf 2t + // + // Assuming t=4 bytes, the minimum tag encoding: + // + // block_size block_size + // mrids = ---------- = ---------- + // 2*4 8 + // + // Note we can't assume ~1/2 block utilization here, as an mdir may + // temporarily fill with more mids before compaction occurs. + // + // Rounding up to the nearest power of two: + // + // (block_size) + // mbits = nlog2(----------) = nlog2(block_size) - 3 + // ( 8 ) + // + // Note if you divide before the nlog2, make sure to use ceiling + // division for compatibility if block_size is not aligned to 8 bytes. + // + // Note note our actual compaction algorithm is not perfect, and + // requires 3t+4 bytes per tag, or with t=4 bytes => ~block_size/12 + // metadata entries per block. But we intentionally don't leverage this + // to maintain compatibility with a theoretical perfect implementation. + // + lfs3->mbits = lfs3_nlog2(lfs3->cfg->block_size) - 3; + + // zero linked-list of opened mdirs + lfs3->handles = NULL; + + // zero in-flight graft state + #ifndef LFS3_RDONLY + lfs3->graft = NULL; + lfs3->graft_count = 0; + #endif + + // TODO are these zeros accomplished by zerogdelta in mountinited? + // should the zerogdelta be dropped? + // TODO should we just call zerogdelta here? + + // zero gstate + lfs3->gcksum = 0; + #ifndef LFS3_RDONLY + lfs3->gcksum_p = 0; + lfs3->gcksum_d = 0; + #endif + + lfs3->grm.queue[0] = -1; + lfs3->grm.queue[1] = -1; + #ifndef LFS3_RDONLY + lfs3_memset(lfs3->grm_p, 0, LFS3_GRM_DSIZE); + lfs3_memset(lfs3->grm_d, 0, LFS3_GRM_DSIZE); + #endif + + // setup other global gbmap state + #ifdef LFS3_GBMAP + lfs3_gbmap_init(&lfs3->gbmap); + lfs3_memset(lfs3->gbmap_p, 0, LFS3_GBMAP_DSIZE); + lfs3_memset(lfs3->gbmap_d, 0, LFS3_GBMAP_DSIZE); + #endif + + return 0; + +failed:; + lfs3_deinit(lfs3); + return err; +} + +static int lfs3_deinit(lfs3_t *lfs3) { + // free allocated memory + if (!lfs3->cfg->rcache_buffer) { + lfs3_free(lfs3->rcache.buffer); + } + + #ifndef LFS3_RDONLY + if (!lfs3->cfg->pcache_buffer) { + lfs3_free(lfs3->pcache.buffer); + } + #endif + + #ifndef LFS3_RDONLY + if (!lfs3->cfg->lookahead_buffer) { + lfs3_free(lfs3->lookahead.buffer); + } + #endif + + return 0; +} + + + +/// Mount/unmount /// + +// compat flags things + +static inline bool lfs3_wcompat_isgbmap(lfs3_wcompat_t flags) { + return flags & LFS3_WCOMPAT_GBMAP; +} + +// figure out what compat flags the current fs configuration needs +static inline lfs3_rcompat_t lfs3_rcompat(const lfs3_t *lfs3) { + (void)lfs3; + return LFS3_RCOMPAT_MMOSS + | LFS3_RCOMPAT_MTREE + | LFS3_RCOMPAT_BSHRUB + | LFS3_RCOMPAT_BTREE + | LFS3_RCOMPAT_GRM; +} + +static inline lfs3_wcompat_t lfs3_wcompat(const lfs3_t *lfs3) { + (void)lfs3; + return LFS3_WCOMPAT_GCKSUM + | LFS3_IFDEF_GBMAP( + (lfs3_f_isgbmap(lfs3->flags)) ? LFS3_WCOMPAT_GBMAP : 0, + 0) + | LFS3_WCOMPAT_DIR; +} + +static inline lfs3_ocompat_t lfs3_ocompat(const lfs3_t *lfs3) { + (void)lfs3; + return 0; +} + +// compat flags on-disk encoding +// +// little-endian, truncated bits must be assumed zero + +static int lfs3_data_readcompat(lfs3_t *lfs3, lfs3_data_t *data, + uint32_t *compat) { + // allow truncated compat flags + uint8_t buf[4] = {0}; + lfs3_ssize_t d = lfs3_data_read(lfs3, data, buf, 4); + if (d < 0) { + return d; + } + *compat = lfs3_fromle32(buf); + + // if any out-of-range flags are set, set the internal overflow bit, + // this is a compromise in correctness and and compat-flag complexity + // + // we don't really care about performance here + while (lfs3_data_size(*data) > 0) { + uint8_t b; + lfs3_ssize_t d = lfs3_data_read(lfs3, data, &b, 1); + if (d < 0) { + return d; + } + + if (b != 0x00) { + *compat |= 0x80000000; + break; + } + } + + return 0; +} + +// all the compat parsing is basically the same, so try to reuse code + +static inline int lfs3_data_readrcompat(lfs3_t *lfs3, lfs3_data_t *data, + lfs3_rcompat_t *rcompat) { + return lfs3_data_readcompat(lfs3, data, rcompat); +} + +static inline int lfs3_data_readwcompat(lfs3_t *lfs3, lfs3_data_t *data, + lfs3_wcompat_t *wcompat) { + return lfs3_data_readcompat(lfs3, data, wcompat); +} + +static inline int lfs3_data_readocompat(lfs3_t *lfs3, lfs3_data_t *data, + lfs3_ocompat_t *ocompat) { + return lfs3_data_readcompat(lfs3, data, ocompat); +} + + +// disk geometry +// +// note these are stored minus 1 to avoid overflow issues +struct lfs3_geometry { + lfs3_off_t block_size; + lfs3_off_t block_count; +}; + +// geometry on-disk encoding +#ifndef LFS3_RDONLY +static lfs3_data_t lfs3_data_fromgeometry(const lfs3_geometry_t *geometry, + uint8_t buffer[static LFS3_GEOMETRY_DSIZE]) { + lfs3_ssize_t d = 0; + lfs3_ssize_t d_ = lfs3_toleb128(geometry->block_size-1, &buffer[d], 4); + if (d_ < 0) { + LFS3_UNREACHABLE(); + } + d += d_; + + d_ = lfs3_toleb128(geometry->block_count-1, &buffer[d], 5); + if (d_ < 0) { + LFS3_UNREACHABLE(); + } + d += d_; + + return LFS3_DATA_BUF(buffer, d); +} +#endif + +static int lfs3_data_readgeometry(lfs3_t *lfs3, lfs3_data_t *data, + lfs3_geometry_t *geometry) { + int err = lfs3_data_readlleb128(lfs3, data, &geometry->block_size); + if (err) { + return err; + } + + err = lfs3_data_readleb128(lfs3, data, &geometry->block_count); + if (err) { + return err; + } + + geometry->block_size += 1; + geometry->block_count += 1; + return 0; +} + +static int lfs3_mountmroot(lfs3_t *lfs3, const lfs3_mdir_t *mroot) { + // check the disk version + uint8_t version[2] = {0, 0}; + lfs3_data_t data; + lfs3_stag_t tag = lfs3_mdir_lookup(lfs3, mroot, LFS3_TAG_VERSION, + &data); + if (tag < 0 && tag != LFS3_ERR_NOENT) { + return tag; + } + if (tag != LFS3_ERR_NOENT) { + lfs3_ssize_t d = lfs3_data_read(lfs3, &data, version, 2); + if (d < 0) { + return d; + } + } + + if (version[0] != LFS3_DISK_VERSION_MAJOR + || version[1] > LFS3_DISK_VERSION_MINOR) { + LFS3_ERROR("Incompatible version v%"PRId32".%"PRId32" " + "(!= v%"PRId32".%"PRId32")", + version[0], + version[1], + LFS3_DISK_VERSION_MAJOR, + LFS3_DISK_VERSION_MINOR); + return LFS3_ERR_NOTSUP; + } + + // check for any rcompatflags, we must understand these to read + // the filesystem + lfs3_rcompat_t rcompat = lfs3_rcompat(lfs3); + lfs3_rcompat_t rcompat_ = 0; + tag = lfs3_mdir_lookup(lfs3, mroot, LFS3_TAG_RCOMPAT, + &data); + if (tag < 0 && tag != LFS3_ERR_NOENT) { + return tag; + } + if (tag != LFS3_ERR_NOENT) { + int err = lfs3_data_readrcompat(lfs3, &data, &rcompat_); + if (err) { + return err; + } + } + + if (rcompat_ != rcompat) { + LFS3_ERROR("Incompatible rcompat flags 0x%0"PRIx32" (!= 0x%0"PRIx32")", + rcompat_, + rcompat); + return LFS3_ERR_NOTSUP; + } + + // check for any wcompatflags, we must understand these to write + // the filesystem + lfs3_wcompat_t wcompat = lfs3_wcompat(lfs3); + lfs3_wcompat_t wcompat_ = 0; + tag = lfs3_mdir_lookup(lfs3, mroot, LFS3_TAG_WCOMPAT, + &data); + if (tag < 0 && tag != LFS3_ERR_NOENT) { + return tag; + } + if (tag != LFS3_ERR_NOENT) { + int err = lfs3_data_readwcompat(lfs3, &data, &wcompat_); + if (err) { + return err; + } + } + + // optional wcompat flags + lfs3_wcompat_t wmask = ~( + LFS3_IFDEF_GBMAP(LFS3_IFDEF_YES_GBMAP(0, LFS3_WCOMPAT_GBMAP), 0)); + if ((wcompat_ & wmask) != (wcompat & wmask)) { + LFS3_WARN("Incompatible wcompat flags 0x%0"PRIx32" " + "(!= 0x%0"PRIx32" & ~0x%0"PRIx32")", + wcompat_, + wcompat, + ~wmask); + // we can ignore this if rdonly + if (!lfs3_m_isrdonly(lfs3->flags)) { + return LFS3_ERR_NOTSUP; + } + } + + #ifdef LFS3_GBMAP + // using the gbmap? + if (lfs3_wcompat_isgbmap(wcompat_)) { + lfs3->flags |= LFS3_I_GBMAP; + } + #endif + + // we don't bother to check for any ocompatflags, we would just + // ignore these anyways + + // check the on-disk geometry + lfs3_geometry_t geometry; + tag = lfs3_mdir_lookup(lfs3, mroot, LFS3_TAG_GEOMETRY, + &data); + if (tag < 0) { + if (tag == LFS3_ERR_NOENT) { + LFS3_ERROR("No geometry found"); + return LFS3_ERR_INVAL; + } + return tag; + } + int err = lfs3_data_readgeometry(lfs3, &data, &geometry); + if (err) { + return err; + } + + // either block_size matches or it doesn't, we don't support variable + // block_sizes + if (geometry.block_size != lfs3->cfg->block_size) { + LFS3_ERROR("Incompatible block size %"PRId32" (!= %"PRId32")", + geometry.block_size, + lfs3->cfg->block_size); + return LFS3_ERR_NOTSUP; + } + + // on-disk block_count must be <= configured block_count + if (geometry.block_count > lfs3->cfg->block_count) { + LFS3_ERROR("Incompatible block count %"PRId32" (> %"PRId32")", + geometry.block_count, + lfs3->cfg->block_count); + return LFS3_ERR_NOTSUP; + } + + lfs3->block_count = geometry.block_count; + + // read the name limit + lfs3_size_t name_limit = 0xff; + tag = lfs3_mdir_lookup(lfs3, mroot, LFS3_TAG_NAMELIMIT, + &data); + if (tag < 0 && tag != LFS3_ERR_NOENT) { + return tag; + } + if (tag != LFS3_ERR_NOENT) { + err = lfs3_data_readleb128(lfs3, &data, &name_limit); + if (err && err != LFS3_ERR_CORRUPT) { + return err; + } + if (err == LFS3_ERR_CORRUPT) { + name_limit = -1; + } + } + + if (name_limit > lfs3->name_limit) { + LFS3_ERROR("Incompatible name limit %"PRId32" (> %"PRId32")", + name_limit, + lfs3->name_limit); + return LFS3_ERR_NOTSUP; + } + + lfs3->name_limit = name_limit; + + // read the file limit + lfs3_off_t file_limit = 0x7fffffff; + tag = lfs3_mdir_lookup(lfs3, mroot, LFS3_TAG_FILELIMIT, + &data); + if (tag < 0 && tag != LFS3_ERR_NOENT) { + return tag; + } + if (tag != LFS3_ERR_NOENT) { + err = lfs3_data_readleb128(lfs3, &data, &file_limit); + if (err && err != LFS3_ERR_CORRUPT) { + return err; + } + if (err == LFS3_ERR_CORRUPT) { + file_limit = -1; + } + } + + if (file_limit > lfs3->file_limit) { + LFS3_ERROR("Incompatible file limit %"PRId32" (> %"PRId32")", + file_limit, + lfs3->file_limit); + return LFS3_ERR_NOTSUP; + } + + lfs3->file_limit = file_limit; + + // check for unknown configs + tag = lfs3_mdir_lookupnext(lfs3, mroot, LFS3_tag_UNKNOWNCONFIG, + NULL); + if (tag < 0 && tag != LFS3_ERR_NOENT) { + return tag; + } + + if (tag != LFS3_ERR_NOENT + && lfs3_tag_suptype(tag) == LFS3_TAG_CONFIG) { + LFS3_ERROR("Unknown config 0x%04"PRIx16, + tag); + return LFS3_ERR_NOTSUP; + } + + return 0; +} + +static int lfs3_mountinited(lfs3_t *lfs3) { + // TODO should these be in lfs3_init? + + // mark mroot as invalid to prevent lfs3_mtree_traverse from getting + // confused + lfs3->mroot.mid = -1; + lfs3->mroot.r.blocks[0] = -1; + lfs3->mroot.r.blocks[1] = -1; + + // default to no mtree, this is allowed and implies all files are + // inlined in the mroot + lfs3_btree_init(&lfs3->mtree); + + // zero gcksum/gdeltas, we'll read these from our mdirs + lfs3->gcksum = 0; + lfs3_fs_zerogdelta(lfs3); + + // traverse the mtree rooted at mroot 0x{1,0} + // + // we do validate btree inner nodes here, how can we trust our + // mdirs are valid if we haven't checked the btree inner nodes at + // least once? + lfs3_mtrv_t mtrv; + lfs3_mtrv_init(&mtrv, LFS3_T_RDONLY | LFS3_T_MTREEONLY | LFS3_T_CKMETA); + while (true) { + lfs3_bptr_t bptr; + lfs3_stag_t tag = lfs3_mtree_traverse(lfs3, &mtrv, + &bptr); + if (tag < 0) { + if (tag == LFS3_ERR_NOENT) { + break; + } + return tag; + } + + // found an mdir? + if (tag == LFS3_TAG_MDIR) { + lfs3_mdir_t *mdir = (lfs3_mdir_t*)bptr.d.u.buffer; + // found an mroot? + if (mdir->mid <= -1) { + // check for the magic string, all mroot should have this + lfs3_data_t data_; + lfs3_stag_t tag_ = lfs3_mdir_lookup(lfs3, mdir, LFS3_TAG_MAGIC, + &data_); + if (tag_ < 0) { + if (tag_ == LFS3_ERR_NOENT) { + LFS3_ERROR("No littlefs magic found"); + return LFS3_ERR_CORRUPT; + } + return tag_; + } + + // treat corrupted magic as no magic + lfs3_scmp_t cmp = lfs3_data_cmp(lfs3, data_, "littlefs", 8); + if (cmp < 0) { + return cmp; + } + if (cmp != LFS3_CMP_EQ) { + LFS3_ERROR("No littlefs magic found"); + return LFS3_ERR_CORRUPT; + } + + // are we the last mroot? + tag_ = lfs3_mdir_lookup(lfs3, mdir, LFS3_TAG_MROOT, + NULL); + if (tag_ < 0 && tag_ != LFS3_ERR_NOENT) { + return tag_; + } + if (tag_ == LFS3_ERR_NOENT) { + // track active mroot + lfs3_mdir_sync(&lfs3->mroot, mdir); + + // mount/validate config in active mroot + int err = lfs3_mountmroot(lfs3, &lfs3->mroot); + if (err) { + return err; + } + } + } + + // build gcksum out of mdir cksums + lfs3->gcksum ^= mdir->r.cksum; + + // collect any gdeltas from this mdir + int err = lfs3_fs_consumegdelta(lfs3, mdir); + if (err) { + return err; + } + + // found an mtree inner-node? + } else if (tag == LFS3_TAG_BRANCH) { + lfs3_rbyd_t *rbyd = (lfs3_rbyd_t*)bptr.d.u.buffer; + // found the root of the mtree? keep track of this + if (lfs3->mtree.r.weight == 0) { + lfs3->mtree.r = *rbyd; + } + + } else { + LFS3_UNREACHABLE(); + } + } + + // validate gcksum by comparing its cube against the gcksumdeltas + // + // The use of cksum^3 here is important to avoid trivial + // gcksumdeltas. If we use a linear function (cksum, crc32c(cksum), + // cksum^2, etc), the state of the filesystem cancels out when + // calculating a new gcksumdelta: + // + // d_i = t(g') - t(g) + // d_i = t(g + c_i) - t(g) + // d_i = t(g) + t(c_i) - t(g) + // d_i = t(c_i) + // + // Using cksum^3 prevents this from happening: + // + // d_i = (g + c_i)^3 - g^3 + // d_i = (g + c_i)(g + c_i)(g + c_i) - g^3 + // d_i = (g^2 + gc_i + gc_i + c_i^2)(g + c_i) - g^3 + // d_i = (g^2 + c_i^2)(g + c_i) - g^3 + // d_i = g^3 + gc_i^2 + g^2c_i + c_i^3 - g^3 + // d_i = gc_i^2 + g^2c_i + c_i^3 + // + // cksum^3 also has some other nice properties, providing a perfect + // 1->1 mapping of t(g) in 2^31 fields, and losing at most 3-bits of + // info when calculating d_i. + // + if (lfs3_crc32c_cube(lfs3->gcksum) != lfs3->gcksum_d) { + LFS3_ERROR("Found gcksum mismatch, cksum^3 %08"PRIx32" " + "(!= %08"PRIx32")", + lfs3_crc32c_cube(lfs3->gcksum), + lfs3->gcksum_d); + return LFS3_ERR_CORRUPT; + } + + // keep track of the current gcksum + #ifndef LFS3_RDONLY + lfs3->gcksum_p = lfs3->gcksum; + #endif + + // TODO should the consumegdelta above take gstate/gdelta as a parameter? + // keep track of the current gstate on disk + #ifndef LFS3_RDONLY + lfs3_memcpy(lfs3->grm_p, lfs3->grm_d, LFS3_GRM_DSIZE); + #ifdef LFS3_GBMAP + lfs3_memcpy(lfs3->gbmap_p, lfs3->gbmap_d, LFS3_GBMAP_DSIZE); + #endif + #endif + + // decode grm so we can report any removed files as missing + int err = lfs3_data_readgrm(lfs3, + &LFS3_DATA_BUF(lfs3->grm_d, LFS3_GRM_DSIZE), + &lfs3->grm); + if (err) { + // TODO switch to read-only? + return err; + } + + // found pending grms? this should only happen if we lost power + if (lfs3_grm_count(lfs3) == 2) { + LFS3_INFO("Found pending grm %"PRId32".%"PRId32" %"PRId32".%"PRId32, + lfs3_dbgmbid(lfs3, lfs3->grm.queue[0]), + lfs3_dbgmrid(lfs3, lfs3->grm.queue[0]), + lfs3_dbgmbid(lfs3, lfs3->grm.queue[1]), + lfs3_dbgmrid(lfs3, lfs3->grm.queue[1])); + } else if (lfs3_grm_count(lfs3) == 1) { + LFS3_INFO("Found pending grm %"PRId32".%"PRId32, + lfs3_dbgmbid(lfs3, lfs3->grm.queue[0]), + lfs3_dbgmrid(lfs3, lfs3->grm.queue[0])); + } + + #ifndef LFS3_RDONLY + if (LFS3_IFDEF_GBMAP( + lfs3_f_isgbmap(lfs3->flags), + false)) { + #ifdef LFS3_GBMAP + // decode the global block-map + err = lfs3_data_readgbmap(lfs3, + &LFS3_DATA_BUF(lfs3->gbmap_d, LFS3_GBMAP_DSIZE), + &lfs3->gbmap); + if (err) { + // TODO switch to read-only? + return err; + } + + // if we have a gbmap, position our lookahead buffer at the last + // known gbmap window + lfs3->lookahead.window = lfs3->gbmap.window; + + // mark our gbmap as repopulatable if known window is + // <= gc_lookgbmap_thresh + // + // unfortunately the dependency of the gbmap on block allocation + // means this rarely includes the entire disk + if (lfs3_alloc_islookgbmap(lfs3)) { + lfs3->flags |= LFS3_I_LOOKGBMAP; + } + #endif + + } else { + // if we don't have a gbmap, position our lookahead buffer + // pseudo-randomly using our gcksum as a prng + // + // the purpose of this is to avoid bad wear patterns such as always + // allocating blocks near the beginning of disk after a power-loss + // + lfs3->lookahead.window = lfs3->gcksum % lfs3->block_count; + } + #endif + + return 0; +} + +int lfs3_mount(lfs3_t *lfs3, uint32_t flags, + const struct lfs3_cfg *cfg) { + #ifdef LFS3_YES_RDONLY + flags |= LFS3_M_RDONLY; + #endif + #ifdef LFS3_YES_FLUSH + flags |= LFS3_M_FLUSH; + #endif + #ifdef LFS3_YES_SYNC + flags |= LFS3_M_SYNC; + #endif + #ifdef LFS3_YES_REVDBG + flags |= LFS3_M_REVDBG; + #endif + #ifdef LFS3_YES_REVNOISE + flags |= LFS3_M_REVNOISE; + #endif + #ifdef LFS3_YES_CKPROGS + flags |= LFS3_M_CKPROGS; + #endif + #ifdef LFS3_YES_CKFETCHES + flags |= LFS3_M_CKFETCHES; + #endif + #ifdef LFS3_YES_CKMETAPARITY + flags |= LFS3_M_CKMETAPARITY; + #endif + #ifdef LFS3_YES_CKDATACKSUMS + flags |= LFS3_M_CKDATACKSUMS; + #endif + #ifdef LFS3_YES_MKCONSISTENT + flags |= LFS3_M_MKCONSISTENT; + #endif + #ifdef LFS3_YES_LOOKAHEAD + flags |= LFS3_M_LOOKAHEAD; + #endif + #ifdef LFS3_YES_LOOKGBMAP + flags |= LFS3_M_LOOKGBMAP; + #endif + #ifdef LFS3_YES_COMPACTMETA + flags |= LFS3_M_COMPACTMETA; + #endif + #ifdef LFS3_YES_CKMETA + flags |= LFS3_M_CKMETA; + #endif + #ifdef LFS3_YES_CKDATA + flags |= LFS3_M_CKDATA + #endif + + // unknown flags? + LFS3_ASSERT((flags & ~( + LFS3_IFDEF_RDONLY(0, LFS3_M_RDWR) + | LFS3_M_RDONLY + | LFS3_M_FLUSH + | LFS3_M_SYNC + | LFS3_IFDEF_REVDBG(LFS3_M_REVDBG, 0) + | LFS3_IFDEF_REVNOISE(LFS3_M_REVNOISE, 0) + | LFS3_IFDEF_CKPROGS(LFS3_M_CKPROGS, 0) + | LFS3_IFDEF_CKFETCHES(LFS3_M_CKFETCHES, 0) + | LFS3_IFDEF_CKMETAPARITY(LFS3_M_CKMETAPARITY, 0) + | LFS3_IFDEF_CKDATACKSUMS(LFS3_M_CKDATACKSUMS, 0) + | LFS3_IFDEF_RDONLY(0, LFS3_M_MKCONSISTENT) + | LFS3_IFDEF_RDONLY(0, LFS3_M_LOOKAHEAD) + | LFS3_IFDEF_RDONLY(0, + LFS3_IFDEF_GBMAP(LFS3_M_LOOKGBMAP, 0)) + | LFS3_IFDEF_RDONLY(0, LFS3_M_COMPACTMETA) + | LFS3_M_CKMETA + | LFS3_M_CKDATA)) == 0); + // these flags require a writable filesystem + LFS3_ASSERT(!lfs3_m_isrdonly(flags) || !lfs3_t_ismkconsistent(flags)); + LFS3_ASSERT(!lfs3_m_isrdonly(flags) || !lfs3_t_islookahead(flags)); + LFS3_ASSERT(!lfs3_m_isrdonly(flags) || !lfs3_t_islookgbmap(flags)); + LFS3_ASSERT(!lfs3_m_isrdonly(flags) || !lfs3_t_compactmeta(flags)); + + int err = lfs3_init(lfs3, + flags & ( + LFS3_IFDEF_RDONLY(0, LFS3_M_RDWR) + | LFS3_M_RDONLY + | LFS3_M_FLUSH + | LFS3_M_SYNC + | LFS3_IFDEF_REVDBG(LFS3_M_REVDBG, 0) + | LFS3_IFDEF_REVNOISE(LFS3_M_REVNOISE, 0) + | LFS3_IFDEF_CKPROGS(LFS3_M_CKPROGS, 0) + | LFS3_IFDEF_CKFETCHES(LFS3_M_CKFETCHES, 0) + | LFS3_IFDEF_CKMETAPARITY(LFS3_M_CKMETAPARITY, 0) + | LFS3_IFDEF_CKDATACKSUMS(LFS3_M_CKDATACKSUMS, 0)), + cfg); + if (err) { + return err; + } + + err = lfs3_mountinited(lfs3); + if (err) { + goto failed; + } + + // run gc if requested + if (flags & LFS3_GC_ALL) { + err = lfs3_fs_ck(lfs3, flags & LFS3_GC_ALL); + if (err) { + goto failed; + } + } + + // TODO this should use any configured values + LFS3_INFO("Mounted littlefs v%"PRId32".%"PRId32" %"PRId32"x%"PRId32" " + "0x{%"PRIx32",%"PRIx32"}.%"PRIx32" w%"PRId32".%"PRId32", " + "cksum %08"PRIx32, + LFS3_DISK_VERSION_MAJOR, + LFS3_DISK_VERSION_MINOR, + lfs3->cfg->block_size, + lfs3->block_count, + lfs3->mroot.r.blocks[0], + lfs3->mroot.r.blocks[1], + lfs3_rbyd_trunk(&lfs3->mroot.r), + lfs3->mtree.r.weight >> lfs3->mbits, + 1 << lfs3->mbits, + lfs3->gcksum); + + return 0; + +failed:; + // make sure we clean up on error + lfs3_deinit(lfs3); + return err; +} + +int lfs3_unmount(lfs3_t *lfs3) { + // all files/dirs should be closed before lfs3_unmount + LFS3_ASSERT(lfs3->handles == NULL + // special case for our gc traversal handle + || LFS3_IFDEF_GC( + (lfs3->handles == &lfs3->gc.t.h + && lfs3->gc.t.h.next == NULL), + false)); + + return lfs3_deinit(lfs3); +} + + + +/// Format /// + +#if !defined(LFS3_RDONLY) && defined(LFS3_GBMAP) +static int lfs3_formatgbmap(lfs3_t *lfs3) { + // TODO should we try multiple blocks? + // + // TODO if we try multiple blocks we should update test_badblocks + // to test block 3 when gbmap is present + // + // assume we can write gbmap to block 2 + lfs3->gbmap.window = 3 % lfs3->block_count; + lfs3->gbmap.known = lfs3->block_count; + lfs3->gbmap.b.r.blocks[0] = 2; + lfs3->gbmap.b.r.trunk = 0; + lfs3->gbmap.b.r.weight = 0; + lfs3->gbmap.b.r.eoff = 0; + lfs3->gbmap.b.r.cksum = 0; + + int err = lfs3_bd_erase(lfs3, lfs3->gbmap.b.r.blocks[0]); + if (err) { + return err; + } + + #if defined(LFS3_REVDBG) || defined(LFS3_REVNOISE) + // append a revision count? + err = lfs3_rbyd_appendrev(lfs3, &lfs3->gbmap.b.r, lfs3_rev_btree(lfs3)); + if (err) { + return err; + } + #endif + + err = lfs3_rbyd_commit(lfs3, &lfs3->gbmap.b.r, 0, LFS3_RATTRS( + // blocks 0..3 - in-use + LFS3_RATTR(LFS3_TAG_BMINUSE, +3), + // blocks 3..block_count - free + (lfs3->block_count > 3) + ? LFS3_RATTR(LFS3_TAG_BMFREE, +(lfs3->block_count - 3)) + : LFS3_RATTR_NOOP())); + if (err) { + return err; + } + + return 0; +} +#endif + +#ifndef LFS3_RDONLY +static int lfs3_formatinited(lfs3_t *lfs3) { + int err; + // create an initial gbmap + #ifdef LFS3_GBMAP + if (lfs3_f_isgbmap(lfs3->flags)) { + err = lfs3_formatgbmap(lfs3); + if (err) { + return err; + } + } + #endif + + for (int i = 0; i < 2; i++) { + // write superblock to both rbyds in the root mroot to hopefully + // avoid mounting an older filesystem on disk + lfs3_rbyd_t rbyd; + rbyd.blocks[0] = i; + rbyd.trunk = 0; + rbyd.weight = 0; + rbyd.eoff = 0; + rbyd.cksum = 0; + + err = lfs3_bd_erase(lfs3, rbyd.blocks[0]); + if (err) { + return err; + } + + // the initial revision count is arbitrary, but it's nice to have + // something here to tell the initial mroot apart from btree nodes + // (rev=0), it's also useful for start with -1 and 0 in the upper + // bits to help test overflow/sequence comparison + uint32_t rev = (((uint32_t)i-1) << 28) + | (((1 << (28-lfs3_smax(lfs3->recycle_bits, 0)))-1) + & 0x00216968); + err = lfs3_rbyd_appendrev(lfs3, &rbyd, rev); + if (err) { + return err; + } + + // include on-disk gbmap? + // + // TODO this is not the greatest solution, but at least it's + // warning free... alternatives? switch to builder pattern? + #ifdef LFS3_GBMAP + #define LFS3_RATTR_IFDEF_GBMAP \ + (lfs3_f_isgbmap(lfs3->flags)) \ + ? LFS3_RATTR_DATA(LFS3_TAG_GBMAPDELTA, 0, \ + (&((struct {lfs3_data_t d;}){ \ + lfs3_data_fromgbmap(&lfs3->gbmap, \ + lfs3->gbmap_d)}).d)) \ + : LFS3_RATTR_NOOP(), + #else + #define LFS3_RATTR_IFDEF_GBMAP + #endif + + // our initial superblock contains a couple things: + // - our magic string, "littlefs" + // - any format-time configuration + // - the root's bookmark tag, which reserves did = 0 for the root + err = lfs3_rbyd_appendrattrs(lfs3, &rbyd, -1, -1, -1, LFS3_RATTRS( + LFS3_RATTR_BUF( + LFS3_TAG_MAGIC, 0, + "littlefs", 8), + LFS3_RATTR_BUF( + LFS3_TAG_VERSION, 0, + ((const uint8_t[2]){ + LFS3_DISK_VERSION_MAJOR, + LFS3_DISK_VERSION_MINOR}), 2), + LFS3_RATTR_LE32( + LFS3_TAG_RCOMPAT, 0, + lfs3_rcompat(lfs3)), + LFS3_RATTR_LE32( + LFS3_TAG_WCOMPAT, 0, + lfs3_wcompat(lfs3)), + LFS3_RATTR_GEOMETRY( + LFS3_TAG_GEOMETRY, 0, + (&(lfs3_geometry_t){ + lfs3->cfg->block_size, + lfs3->cfg->block_count})), + LFS3_RATTR_LLEB128( + LFS3_TAG_NAMELIMIT, 0, + lfs3->name_limit), + LFS3_RATTR_LEB128( + LFS3_TAG_FILELIMIT, 0, + lfs3->file_limit), + LFS3_RATTR_IFDEF_GBMAP + LFS3_RATTR_NAME( + LFS3_TAG_BOOKMARK, +1, + 0, NULL, 0))); + if (err) { + return err; + } + + // append initial gcksum + uint32_t cksum = rbyd.cksum; + err = lfs3_rbyd_appendrattr_(lfs3, &rbyd, LFS3_RATTR_LE32( + LFS3_TAG_GCKSUMDELTA, 0, lfs3_crc32c_cube(cksum))); + if (err) { + return err; + } + + // and commit + err = lfs3_rbyd_appendcksum_(lfs3, &rbyd, cksum); + if (err) { + return err; + } + } + + // sync on-disk state + err = lfs3_bd_sync(lfs3); + if (err) { + return err; + } + + return 0; +} +#endif + +#ifndef LFS3_RDONLY +int lfs3_format(lfs3_t *lfs3, uint32_t flags, + const struct lfs3_cfg *cfg) { + #ifdef LFS3_YES_GBMAP + flags |= LFS3_F_GBMAP; + #endif + #ifdef LFS3_YES_REVDBG + flags |= LFS3_F_REVDBG; + #endif + #ifdef LFS3_YES_REVNOISE + flags |= LFS3_F_REVNOISE; + #endif + #ifdef LFS3_YES_CKPROGS + flags |= LFS3_F_CKPROGS; + #endif + #ifdef LFS3_YES_CKFETCHES + flags |= LFS3_F_CKFETCHES; + #endif + #ifdef LFS3_YES_CKMETAPARITY + flags |= LFS3_F_CKMETAPARITY; + #endif + #ifdef LFS3_YES_CKDATACKSUMS + flags |= LFS3_F_CKDATACKSUMS; + #endif + #ifdef LFS3_YES_MKCONSISTENT + flags |= LFS3_F_MKCONSISTENT; + #endif + #ifdef LFS3_YES_LOOKAHEAD + flags |= LFS3_F_LOOKAHEAD; + #endif + #ifdef LFS3_YES_LOOKGBMAP + flags |= LFS3_F_LOOKGBMAP; + #endif + #ifdef LFS3_YES_COMPACTMETA + flags |= LFS3_F_COMPACTMETA; + #endif + #ifdef LFS3_YES_CKMETA + flags |= LFS3_F_CKMETA; + #endif + #ifdef LFS3_YES_CKDATA + flags |= LFS3_F_CKDATA; + #endif + + // unknown flags? + LFS3_ASSERT((flags & ~( + LFS3_F_RDWR + | LFS3_IFDEF_GBMAP(LFS3_F_GBMAP, 0) + | LFS3_IFDEF_REVDBG(LFS3_F_REVDBG, 0) + | LFS3_IFDEF_REVNOISE(LFS3_F_REVNOISE, 0) + | LFS3_IFDEF_CKPROGS(LFS3_F_CKPROGS, 0) + | LFS3_IFDEF_CKFETCHES(LFS3_F_CKFETCHES, 0) + | LFS3_IFDEF_CKMETAPARITY(LFS3_F_CKMETAPARITY, 0) + | LFS3_IFDEF_CKDATACKSUMS(LFS3_F_CKDATACKSUMS, 0) + | LFS3_F_MKCONSISTENT + | LFS3_F_LOOKAHEAD + | LFS3_IFDEF_GBMAP(LFS3_F_LOOKGBMAP, 0) + | LFS3_F_COMPACTMETA + | LFS3_F_CKMETA + | LFS3_F_CKDATA)) == 0); + + int err = lfs3_init(lfs3, + flags & ( + LFS3_F_RDWR + | LFS3_IFDEF_GBMAP(LFS3_F_GBMAP, 0) + | LFS3_IFDEF_REVDBG(LFS3_F_REVDBG, 0) + | LFS3_IFDEF_REVNOISE(LFS3_F_REVNOISE, 0) + | LFS3_IFDEF_CKPROGS(LFS3_F_CKPROGS, 0) + | LFS3_IFDEF_CKFETCHES(LFS3_F_CKFETCHES, 0) + | LFS3_IFDEF_CKMETAPARITY(LFS3_F_CKMETAPARITY, 0) + | LFS3_IFDEF_CKDATACKSUMS(LFS3_F_CKDATACKSUMS, 0)), + cfg); + if (err) { + return err; + } + + LFS3_INFO("Formatting littlefs v%"PRId32".%"PRId32" %"PRId32"x%"PRId32, + LFS3_DISK_VERSION_MAJOR, + LFS3_DISK_VERSION_MINOR, + lfs3->cfg->block_size, + lfs3->block_count); + + err = lfs3_formatinited(lfs3); + if (err) { + goto failed; + } + + // test that mount works with our formatted disk + err = lfs3_mountinited(lfs3); + if (err) { + goto failed; + } + + // run gc if requested + if (flags & LFS3_GC_ALL) { + err = lfs3_fs_ck(lfs3, flags & LFS3_GC_ALL); + if (err) { + goto failed; + } + } + + return lfs3_deinit(lfs3); + +failed:; + // make sure we clean up on error + lfs3_deinit(lfs3); + return err; +} +#endif + + + +/// Other filesystem things /// + +int lfs3_fs_stat(lfs3_t *lfs3, struct lfs3_fsinfo *fsinfo) { + // return various filesystem flags + fsinfo->flags = lfs3->flags & ( + LFS3_I_RDONLY + | LFS3_I_FLUSH + | LFS3_I_SYNC + | LFS3_IFDEF_REVDBG(LFS3_I_REVDBG, 0) + | LFS3_IFDEF_REVNOISE(LFS3_I_REVNOISE, 0) + | LFS3_IFDEF_CKPROGS(LFS3_I_CKPROGS, 0) + | LFS3_IFDEF_CKFETCHES(LFS3_I_CKFETCHES, 0) + | LFS3_IFDEF_CKMETAPARITY(LFS3_I_CKMETAPARITY, 0) + | LFS3_IFDEF_CKDATACKSUMS(LFS3_I_CKDATACKSUMS, 0) + | LFS3_IFDEF_RDONLY(0, LFS3_I_MKCONSISTENT) + | LFS3_IFDEF_RDONLY(0, LFS3_I_LOOKAHEAD) + | LFS3_IFDEF_RDONLY(0, LFS3_IFDEF_GBMAP(LFS3_I_LOOKGBMAP, 0)) + | LFS3_IFDEF_RDONLY(0, LFS3_I_COMPACTMETA) + | LFS3_I_CKMETA + | LFS3_I_CKDATA + | LFS3_IFDEF_GBMAP(LFS3_I_GBMAP, 0)); + // some flags we calculate on demand + #ifndef LFS3_RDONLY + fsinfo->flags |= (lfs3_grm_count(lfs3) > 0) ? LFS3_I_MKCONSISTENT : 0; + #endif + + // return filesystem config, this may come from disk + fsinfo->block_size = lfs3->cfg->block_size; + fsinfo->block_count = lfs3->block_count; + fsinfo->name_limit = lfs3->name_limit; + fsinfo->file_limit = lfs3->file_limit; + + return 0; +} + +lfs3_ssize_t lfs3_fs_usage(lfs3_t *lfs3) { + lfs3_size_t count = 0; + lfs3_mtrv_t mtrv; + lfs3_mtrv_init(&mtrv, LFS3_T_RDONLY); + while (true) { + lfs3_bptr_t bptr; + lfs3_stag_t tag = lfs3_mtree_traverse(lfs3, &mtrv, + &bptr); + if (tag < 0) { + if (tag == LFS3_ERR_NOENT) { + break; + } + return tag; + } + + // count the number of blocks we see, yes this may result in duplicates + if (tag == LFS3_TAG_MDIR) { + count += 2; + + } else if (tag == LFS3_TAG_BRANCH) { + count += 1; + + } else if (tag == LFS3_TAG_BLOCK) { + count += 1; + + } else { + LFS3_UNREACHABLE(); + } + } + + return count; +} + +// get the filesystem checksum +int lfs3_fs_cksum(lfs3_t *lfs3, uint32_t *cksum) { + *cksum = lfs3->gcksum; + return 0; +} + + +// consistency stuff + +#ifndef LFS3_RDONLY +static int lfs3_fs_fixgrm(lfs3_t *lfs3) { + if (lfs3_grm_count(lfs3) == 2) { + LFS3_INFO("Fixing grm %"PRId32".%"PRId32" %"PRId32".%"PRId32, + lfs3_dbgmbid(lfs3, lfs3->grm.queue[0]), + lfs3_dbgmrid(lfs3, lfs3->grm.queue[0]), + lfs3_dbgmbid(lfs3, lfs3->grm.queue[1]), + lfs3_dbgmrid(lfs3, lfs3->grm.queue[1])); + } else if (lfs3_grm_count(lfs3) == 1) { + LFS3_INFO("Fixing grm %"PRId32".%"PRId32, + lfs3_dbgmbid(lfs3, lfs3->grm.queue[0]), + lfs3_dbgmrid(lfs3, lfs3->grm.queue[0])); + } + + while (lfs3_grm_count(lfs3) > 0) { + LFS3_ASSERT(lfs3->grm.queue[0] != -1); + + // find our mdir + lfs3_mdir_t mdir; + int err = lfs3_mtree_lookup(lfs3, lfs3->grm.queue[0], + &mdir); + if (err) { + LFS3_ASSERT(err != LFS3_ERR_NOENT); + return err; + } + + // we also use grm to track orphans that need to be cleaned up, + // which means it may not match the on-disk state, which means + // we need to revert manually on error + lfs3_grm_t grm_p = lfs3->grm; + + // mark grm as taken care of + lfs3_grm_pop(lfs3); + + // remove the rid while atomically updating our grm + err = lfs3_mdir_commit(lfs3, &mdir, LFS3_RATTRS( + LFS3_RATTR(LFS3_tag_RM, -1))); + if (err) { + // revert grm manually + lfs3->grm = grm_p; + return err; + } + } + + return 0; +} +#endif + +#ifndef LFS3_RDONLY +static int lfs3_mdir_mkconsistent(lfs3_t *lfs3, lfs3_mdir_t *mdir) { + // save the current mid + lfs3_mid_t mid = mdir->mid; + + // iterate through mids looking for orphans + mdir->mid = LFS3_MID(lfs3, mdir->mid, 0); + int err; + while (lfs3_mrid(lfs3, mdir->mid) < (lfs3_srid_t)mdir->r.weight) { + // is this mid open? well we're not an orphan then, skip + // + // note we can't rely on lfs3_mdir_lookup's internal orphan + // checks as we also need to treat desynced/zombied files as + // non-orphans + if (lfs3_mid_isopen(lfs3, mdir->mid, -1)) { + mdir->mid += 1; + continue; + } + + // is this mid marked as a stickynote? + lfs3_stag_t tag = lfs3_rbyd_lookup(lfs3, &mdir->r, + lfs3_mrid(lfs3, mdir->mid), LFS3_TAG_STICKYNOTE, + NULL); + if (tag < 0) { + if (tag == LFS3_ERR_NOENT) { + mdir->mid += 1; + continue; + } + err = tag; + goto failed; + } + + // we found an orphaned stickynote, remove + LFS3_INFO("Fixing orphaned stickynote %"PRId32".%"PRId32, + lfs3_dbgmbid(lfs3, mdir->mid), + lfs3_dbgmrid(lfs3, mdir->mid)); + + // remove the orphaned stickynote + err = lfs3_mdir_commit(lfs3, mdir, LFS3_RATTRS( + LFS3_RATTR(LFS3_tag_RM, -1))); + if (err) { + goto failed; + } + } + + // restore the current mid + mdir->mid = mid; + return 0; + +failed:; + // restore the current mid + mdir->mid = mid; + return err; +} +#endif + +#ifndef LFS3_RDONLY +static int lfs3_fs_fixorphans(lfs3_t *lfs3) { + // LFS3_T_MKCONSISTENT really just removes orphans + lfs3_mgc_t mgc; + lfs3_mgc_init(&mgc, + LFS3_T_RDWR | LFS3_T_MTREEONLY | LFS3_T_MKCONSISTENT); + while (true) { + lfs3_bptr_t bptr; + lfs3_stag_t tag = lfs3_mtree_gc(lfs3, &mgc, + &bptr); + if (tag < 0) { + if (tag == LFS3_ERR_NOENT) { + break; + } + return tag; + } + } + + return 0; +} +#endif + +// prepare the filesystem for mutation +#ifndef LFS3_RDONLY +int lfs3_fs_mkconsistent(lfs3_t *lfs3) { + // filesystem must be writeable + LFS3_ASSERT(!lfs3_m_isrdonly(lfs3->flags)); + + // fix pending grms + if (lfs3_grm_count(lfs3) > 0) { + int err = lfs3_fs_fixgrm(lfs3); + if (err) { + return err; + } + } + + // fix orphaned stickynotes + // + // this must happen after fixgrm, since removing orphaned + // stickynotes risks outdating the grm + // + if (lfs3_t_ismkconsistent(lfs3->flags)) { + int err = lfs3_fs_fixorphans(lfs3); + if (err) { + return err; + } + } + + // go ahead and checkpoint the allocator + // + // this isn't always needed, but redundant alloc ckpoints are noops, + // so might as well to eagerly populate block allocators and save + // some alloc ckpoint calls + int err = lfs3_alloc_ckpoint(lfs3); + if (err) { + return err; + } + + return 0; +} +#endif + +// low-level filesystem gc +// +// runs the traversal until all work is completed, which may take +// multiple passes +static int lfs3_fs_gc_(lfs3_t *lfs3, lfs3_mgc_t *mgc, + uint32_t flags, lfs3_soff_t steps) { + // fix pending grms if requested + #ifndef LFS3_RDONLY + if (lfs3_t_ismkconsistent(flags) + && lfs3_grm_count(lfs3) > 0) { + int err = lfs3_fs_fixgrm(lfs3); + if (err) { + return err; + } + } + #endif + + // do we have any pending work? + uint32_t pending = flags & lfs3->flags & LFS3_GC_ALL; + + while (pending && (lfs3_off_t)steps > 0) { + // start a new traversal? + if (!lfs3_handle_isopen(lfs3, &mgc->t.h)) { + lfs3_mgc_init(mgc, pending); + lfs3_handle_open(lfs3, &mgc->t.h); + } + + // don't bother with lookahead/gbmap if we've ckpointed + #ifndef LFS3_RDONLY + if (lfs3_t_isckpointed(mgc->t.h.flags)) { + mgc->t.h.flags &= ~LFS3_T_LOOKAHEAD; + #ifdef LFS3_GBMAP + mgc->t.h.flags &= ~LFS3_T_LOOKGBMAP; + #endif + } + #endif + + // will this traversal still make progress? no? start over + if (!(mgc->t.h.flags & LFS3_GC_ALL)) { + lfs3_handle_close(lfs3, &mgc->t.h); + continue; + } + + // do we really need a full traversal? + if (!(mgc->t.h.flags & ( + LFS3_IFDEF_RDONLY(0, LFS3_GC_LOOKAHEAD) + | LFS3_IFDEF_RDONLY(0, + LFS3_IFDEF_GBMAP(LFS3_GC_LOOKGBMAP, 0)) + | LFS3_GC_CKMETA + | LFS3_GC_CKDATA))) { + mgc->t.h.flags |= LFS3_T_MTREEONLY; + } + + // progress gc + lfs3_bptr_t bptr; + lfs3_stag_t tag = lfs3_mtree_gc(lfs3, mgc, + &bptr); + if (tag < 0 && tag != LFS3_ERR_NOENT) { + lfs3_handle_close(lfs3, &mgc->t.h); + return tag; + } + + // end of traversal? + if (tag == LFS3_ERR_NOENT) { + lfs3_handle_close(lfs3, &mgc->t.h); + // clear any pending flags we make progress on + pending &= lfs3->flags & LFS3_GC_ALL; + } + + // decrement steps + if (steps > 0) { + steps -= 1; + } + } + + return 0; +} + +// filesystem check function +// +// this just calls lfs3_fs_gc_ with unbounded steps +int lfs3_fs_ck(lfs3_t *lfs3, uint32_t flags) { + // unknown ck flags? + LFS3_ASSERT((flags & ~LFS3_GC_ALL) == 0); + // these flags require a writable filesystem + LFS3_ASSERT(!lfs3_m_isrdonly(lfs3->flags) + || !lfs3_t_ismkconsistent(flags)); + LFS3_ASSERT(!lfs3_m_isrdonly(lfs3->flags) + || !lfs3_t_islookahead(flags)); + LFS3_ASSERT(!lfs3_m_isrdonly(lfs3->flags) + || !lfs3_t_islookgbmap(flags)); + LFS3_ASSERT(!lfs3_m_isrdonly(lfs3->flags) + || !lfs3_t_compactmeta(flags)); + + // set needs-ck flags, this has the side-effect of signaling ck work + // is incomplete if we encounter an error, which is probably a good + // thing + lfs3->flags |= flags & (LFS3_I_CKMETA | LFS3_I_CKDATA); + + lfs3_mgc_t mgc; + return lfs3_fs_gc_(lfs3, &mgc, flags, -1); +} + +// incremental filesystem gc +// +// perform any pending janitorial work +#ifdef LFS3_GC +int lfs3_fs_gc(lfs3_t *lfs3) { + // unknown gc flags? + LFS3_ASSERT((lfs3->cfg->gc_flags & ~LFS3_GC_ALL) == 0); + // these flags require a writable filesystem + LFS3_ASSERT(!lfs3_m_isrdonly(lfs3->flags) + || !lfs3_t_ismkconsistent(lfs3->cfg->gc_flags)); + LFS3_ASSERT(!lfs3_m_isrdonly(lfs3->flags) + || !lfs3_t_islookahead(lfs3->cfg->gc_flags)); + LFS3_ASSERT(!lfs3_m_isrdonly(lfs3->flags) + || !lfs3_t_islookgbmap(lfs3->cfg->gc_flags)); + LFS3_ASSERT(!lfs3_m_isrdonly(lfs3->flags) + || !lfs3_t_compactmeta(lfs3->cfg->gc_flags)); + + // run gc a configurable number of steps + return lfs3_fs_gc_(lfs3, &lfs3->gc, + lfs3->cfg->gc_flags, + (lfs3->cfg->gc_steps) + ? lfs3->cfg->gc_steps + : 1); +} +#endif + +// unperform janitorial work +int lfs3_fs_unck(lfs3_t *lfs3, uint32_t flags) { + // unknown flags? + LFS3_ASSERT((flags & ~LFS3_GC_ALL) == 0); + + // reset the requested flags + lfs3->flags |= flags; + + // and clear from any ongoing traversals + // + // lfs3_fs_gc will terminate early if it discovers it can no longer + // make progress + #ifdef LFS3_GC + lfs3->gc.t.h.flags &= ~flags; + #endif + + return 0; +} + + +// attempt to grow the filesystem +#ifndef LFS3_RDONLY +int lfs3_fs_grow(lfs3_t *lfs3, lfs3_size_t block_count_) { + // filesystem must be writeable + LFS3_ASSERT(!lfs3_m_isrdonly(lfs3->flags)); + // shrinking the filesystem is not supported + LFS3_ASSERT(block_count_ >= lfs3->block_count); + + // do nothing if block_count doesn't change + if (block_count_ == lfs3->block_count) { + return 0; + } + + // Note we do _not_ call lfs3_fs_mkconsistent here. This is a bit scary, + // but we should be ok as long as we patch grms in lfs3_mdir_commit and + // only commit to the mroot. + // + // Calling lfs3_fs_mkconsistent risks locking our filesystem up trying + // to fix grms/orphans before we can commit the new filesystem size. If + // we don't, we should always be able to recover a stuck filesystem with + // lfs3_fs_grow. + + LFS3_INFO("Growing littlefs %"PRId32"x%"PRId32" -> %"PRId32"x%"PRId32, + lfs3->cfg->block_size, lfs3->block_count, + lfs3->cfg->block_size, block_count_); + + // keep track of our current block_count in case we fail + lfs3_size_t block_count = lfs3->block_count; + + // we can use the new blocks immediately as long as the commit + // with the new block_count is atomic + lfs3->block_count = block_count_; + // discard stale lookahead buffer/gbmap + lfs3_alloc_discard(lfs3); + int err; + + // grow the gbmap if we have one + // + // note this won't actually be committed to disk until mdir commit + #ifdef LFS3_GBMAP + if (lfs3_f_isgbmap(lfs3->flags)) { + // if the last range is free, we can extend it, otherwise we + // need a new range + lfs3_stag_t tag = lfs3_gbmap_lookupnext(lfs3, &lfs3->gbmap.b, + block_count-1, + NULL, NULL); + if (tag < 0) { + LFS3_ASSERT(tag != LFS3_ERR_NOENT); + err = tag; + goto failed; + } + + // checkpoint the lookahead buffer, but _not_ the gbmap, we + // can't repopulate the gbmap until we've resized it + lfs3_alloc_ckpoint_(lfs3); + + // we don't need a copy because this is atomic, and mdir commit + // reverts to the on-disk state if it fails + err = lfs3_gbmap_commit(lfs3, &lfs3->gbmap.b, + (tag == LFS3_TAG_BMFREE) ? block_count-1 : block_count, + LFS3_RATTRS( + LFS3_RATTR( + (tag == LFS3_TAG_BMFREE) + ? LFS3_tag_GROW + : LFS3_TAG_BMFREE, + +(block_count_ - block_count)))); + if (err) { + goto failed; + } + } + #endif + + // update our on-disk config + err = lfs3_mdir_commit(lfs3, &lfs3->mroot, LFS3_RATTRS( + LFS3_RATTR_GEOMETRY( + LFS3_TAG_GEOMETRY, 0, + (&(lfs3_geometry_t){ + lfs3->cfg->block_size, + block_count_})))); + if (err) { + goto failed; + } + + return 0; + +failed:; + // restore block_count + lfs3->block_count = block_count; + // discard clobbered lookahead buffer + lfs3_alloc_discard(lfs3); + // revert to the previous gbmap + #ifdef LFS3_GBMAP + if (lfs3_f_isgbmap(lfs3->flags)) { + lfs3->gbmap.b = lfs3->gbmap.b_p; + } + #endif + + return err; +} +#endif + +// enable the global on-disk block-map +#if !defined(LFS3_RDONLY) && defined(LFS3_GBMAP) && !defined(LFS3_YES_GBMAP) +int lfs3_fs_mkgbmap(lfs3_t *lfs3) { + // error if we already have a gbmap + if (lfs3_f_isgbmap(lfs3->flags)) { + return LFS3_ERR_EXIST; + } + + // prepare our filesystem for writing + int err = lfs3_fs_mkconsistent(lfs3); + if (err) { + return err; + } + + // create an empty gbmap, let lfs3_alloc_ckpoint populate it + lfs3_gbmap_init(&lfs3->gbmap); + + err = lfs3_gbmap_commit(lfs3, &lfs3->gbmap.b, 0, LFS3_RATTRS( + LFS3_RATTR(LFS3_TAG_BMFREE, +lfs3->block_count))); + if (err) { + goto failed; + } + + // go ahead and mark gbmap as in-use internally + lfs3->flags |= LFS3_F_GBMAP; + + // sync gbmap/lookahead windows, note this needs to happen after any + // block allocation in lfs3_gbmap_commit + lfs3->gbmap.window = (lfs3->lookahead.window + lfs3->lookahead.off) + % lfs3->block_count; + + // checkpoint the allocator again, this should trigger a + // repopulation scan + err = lfs3_alloc_ckpoint(lfs3); + if (err) { + goto failed; + } + + // mark the gbmap as in-use on-disk while atomically committing the + // gbmap into gstate + lfs3_wcompat_t wcompat_ = lfs3_wcompat(lfs3); + wcompat_ |= LFS3_WCOMPAT_GBMAP; + + err = lfs3_mdir_commit(lfs3, &lfs3->mroot, LFS3_RATTRS( + LFS3_RATTR_LE32(LFS3_TAG_WCOMPAT, 0, wcompat_))); + if (err) { + goto failed; + } + + return 0; + +failed:; + // if we failed clear the gbmap bit and reset the gbmap to be safe + lfs3->flags &= ~LFS3_F_GBMAP; + lfs3_gbmap_init(&lfs3->gbmap); + return err; +} +#endif + +// disable the global on-disk block-map +#if !defined(LFS3_RDONLY) && defined(LFS3_GBMAP) && !defined(LFS3_YES_GBMAP) +int lfs3_fs_rmgbmap(lfs3_t *lfs3) { + // error if we already don't have a gbmap + if (!lfs3_f_isgbmap(lfs3->flags)) { + return LFS3_ERR_NOENT; + } + + // prepare our filesystem for writing + int err = lfs3_fs_mkconsistent(lfs3); + if (err) { + return err; + } + + // removing the gbmap is relatively easy, we just need to mark the + // gbmap as not in use + // + // this leaves garbage gdeltas around, but these should be cleaned + // up implicitly as mdirs are compacted + lfs3_wcompat_t wcompat_ = lfs3_wcompat(lfs3); + wcompat_ &= ~LFS3_WCOMPAT_GBMAP; + + err = lfs3_mdir_commit(lfs3, &lfs3->mroot, LFS3_RATTRS( + LFS3_RATTR_LE32(LFS3_TAG_WCOMPAT, 0, wcompat_))); + if (err) { + return err; + } + + // on success mark gbmap as not-in-use internally + lfs3->flags &= ~LFS3_F_GBMAP; + return 0; +} +#endif + + + +/// High-level filesystem traversal /// + +// needed in lfs3_trv_open +static int lfs3_trv_rewind_(lfs3_t *lfs3, lfs3_trv_t *trv); + +int lfs3_trv_open(lfs3_t *lfs3, lfs3_trv_t *trv, uint32_t flags) { + // already open? + LFS3_ASSERT(!lfs3_handle_isopen(lfs3, &trv->gc.t.h)); + // unknown flags? + LFS3_ASSERT((flags & ~( + LFS3_IFDEF_RDONLY(0, LFS3_T_RDWR) + | LFS3_T_RDONLY + | LFS3_T_MTREEONLY + | LFS3_T_EXCL + | LFS3_IFDEF_RDONLY(0, LFS3_T_MKCONSISTENT) + | LFS3_IFDEF_RDONLY(0, LFS3_T_LOOKAHEAD) + | LFS3_IFDEF_RDONLY(0, + LFS3_IFDEF_GBMAP(LFS3_T_LOOKGBMAP, 0)) + | LFS3_IFDEF_RDONLY(0, LFS3_T_COMPACTMETA) + | LFS3_T_CKMETA + | LFS3_T_CKDATA)) == 0); + // writeable traversals require a writeable filesystem + LFS3_ASSERT(!lfs3_m_isrdonly(lfs3->flags) || lfs3_t_isrdonly(flags)); + // these flags require a writable traversal + LFS3_ASSERT(!lfs3_t_isrdonly(flags) || !lfs3_t_ismkconsistent(flags)); + LFS3_ASSERT(!lfs3_t_isrdonly(flags) || !lfs3_t_islookahead(flags)); + LFS3_ASSERT(!lfs3_t_isrdonly(flags) || !lfs3_t_islookgbmap(flags)); + LFS3_ASSERT(!lfs3_t_isrdonly(flags) || !lfs3_t_compactmeta(flags)); + // some flags don't make sense when only traversing the mtree + LFS3_ASSERT(!lfs3_t_ismtreeonly(flags) || !lfs3_t_islookahead(flags)); + LFS3_ASSERT(!lfs3_t_ismtreeonly(flags) || !lfs3_t_islookgbmap(flags)); + LFS3_ASSERT(!lfs3_t_ismtreeonly(flags) || !lfs3_t_isckdata(flags)); + + // setup traversal state + trv->gc.t.h.flags = flags | lfs3_o_typeflags(LFS3_type_TRV); + + // let rewind initialize/reset things + int err = lfs3_trv_rewind_(lfs3, trv); + if (err) { + return err; + } + + // add to tracked mdirs + lfs3_handle_open(lfs3, &trv->gc.t.h); + return 0; +} + +int lfs3_trv_close(lfs3_t *lfs3, lfs3_trv_t *trv) { + LFS3_ASSERT(lfs3_handle_isopen(lfs3, &trv->gc.t.h)); + + // remove from tracked mdirs + lfs3_handle_close(lfs3, &trv->gc.t.h); + return 0; +} + +int lfs3_trv_read(lfs3_t *lfs3, lfs3_trv_t *trv, + struct lfs3_tinfo *tinfo) { + LFS3_ASSERT(lfs3_handle_isopen(lfs3, &trv->gc.t.h)); + + // filesystem modified? excl? terminate early + if (lfs3_t_isexcl(trv->gc.t.h.flags) + && lfs3_t_isdirty(trv->gc.t.h.flags)) { + return LFS3_ERR_BUSY; + } + + // check for pending grms every step, just in case some other + // operation introduced new grms + #ifndef LFS3_RDONLY + if (lfs3_t_ismkconsistent(trv->gc.t.h.flags) + && lfs3_grm_count(lfs3) > 0) { + uint32_t dirty = trv->gc.t.h.flags; + int err = lfs3_fs_fixgrm(lfs3); + if (err) { + return err; + } + // reset dirty flag + trv->gc.t.h.flags &= ~LFS3_t_DIRTY | dirty; + } + #endif + + // discard current block queue? + if (lfs3_t_isstale(trv->gc.t.h.flags)) { + trv->blocks[0] = -1; + trv->blocks[1] = -1; + trv->gc.t.h.flags &= ~LFS3_t_STALE; + } + + while (true) { + // some redund blocks left over? + if (trv->blocks[0] != -1) { + // write our traversal info + tinfo->btype = lfs3_t_btype(trv->gc.t.h.flags); + tinfo->block = trv->blocks[0]; + + trv->blocks[0] = trv->blocks[1]; + trv->blocks[1] = -1; + return 0; + } + + // find next block + lfs3_bptr_t bptr; + lfs3_stag_t tag = lfs3_mtree_gc(lfs3, &trv->gc, + &bptr); + if (tag < 0) { + return tag; + } + + // ignore new stale flags + trv->gc.t.h.flags &= ~LFS3_t_STALE; + + // figure out type/blocks + if (tag == LFS3_TAG_MDIR) { + lfs3_mdir_t *mdir = (lfs3_mdir_t*)bptr.d.u.buffer; + lfs3_t_setbtype(&trv->gc.t.h.flags, LFS3_BTYPE_MDIR); + trv->blocks[0] = mdir->r.blocks[0]; + trv->blocks[1] = mdir->r.blocks[1]; + + } else if (tag == LFS3_TAG_BRANCH) { + lfs3_t_setbtype(&trv->gc.t.h.flags, LFS3_BTYPE_BTREE); + lfs3_rbyd_t *rbyd = (lfs3_rbyd_t*)bptr.d.u.buffer; + trv->blocks[0] = rbyd->blocks[0]; + trv->blocks[1] = -1; + + } else if (tag == LFS3_TAG_BLOCK) { + lfs3_t_setbtype(&trv->gc.t.h.flags, LFS3_BTYPE_DATA); + trv->blocks[0] = lfs3_bptr_block(&bptr); + trv->blocks[1] = -1; + + } else { + LFS3_UNREACHABLE(); + } + } +} + +static int lfs3_trv_rewind_(lfs3_t *lfs3, lfs3_trv_t *trv) { + (void)lfs3; + // reset traversal + lfs3_mgc_init(&trv->gc, + (trv->gc.t.h.flags + & ~LFS3_t_DIRTY + & ~LFS3_t_CKPOINTED) + | LFS3_t_STALE); + return 0; +} + +int lfs3_trv_rewind(lfs3_t *lfs3, lfs3_trv_t *trv) { + LFS3_ASSERT(lfs3_handle_isopen(lfs3, &trv->gc.t.h)); + return lfs3_trv_rewind_(lfs3, trv); +} + + + +// that's it! you've reached the end! go home! diff --git a/lfs3.h b/lfs3.h new file mode 100644 index 000000000..fa02e55f7 --- /dev/null +++ b/lfs3.h @@ -0,0 +1,1767 @@ +/* + * The little filesystem + * + * Copyright (c) 2022, The littlefs authors. + * Copyright (c) 2017, Arm Limited. All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + */ +#ifndef LFS3_H +#define LFS3_H + +#include "lfs3_util.h" + + +/// Version info /// + +// Software library version +// Major (top-nibble), incremented on backwards incompatible changes +// Minor (bottom-nibble), incremented on feature additions +#define LFS3_VERSION 0x00000000 +#define LFS3_VERSION_MAJOR (0xffff & (LFS3_VERSION >> 16)) +#define LFS3_VERSION_MINOR (0xffff & (LFS3_VERSION >> 0)) + +// Version of On-disk data structures +// Major (top-nibble), incremented on backwards incompatible changes +// Minor (bottom-nibble), incremented on feature additions +#define LFS3_DISK_VERSION 0x00000000 +#define LFS3_DISK_VERSION_MAJOR (0xffff & (LFS3_DISK_VERSION >> 16)) +#define LFS3_DISK_VERSION_MINOR (0xffff & (LFS3_DISK_VERSION >> 0)) + + +/// Definitions /// + +// Type definitions +typedef uint32_t lfs3_size_t; +typedef int32_t lfs3_ssize_t; + +typedef uint32_t lfs3_off_t; +typedef int32_t lfs3_soff_t; + +typedef uint32_t lfs3_block_t; +typedef int32_t lfs3_sblock_t; + +typedef uint32_t lfs3_rid_t; +typedef int32_t lfs3_srid_t; + +typedef uint16_t lfs3_tag_t; +typedef int16_t lfs3_stag_t; + +typedef uint32_t lfs3_bid_t; +typedef int32_t lfs3_sbid_t; + +typedef uint32_t lfs3_mid_t; +typedef int32_t lfs3_smid_t; + +typedef uint32_t lfs3_did_t; +typedef int32_t lfs3_sdid_t; + +typedef uint32_t lfs3_rcompat_t; +typedef uint32_t lfs3_wcompat_t; +typedef uint32_t lfs3_ocompat_t; + +// Maximum name size in bytes, may be redefined to reduce the size of the +// info struct. Limited to <= 1022. Stored in superblock and must be +// respected by other littlefs drivers. +#ifndef LFS3_NAME_MAX +#define LFS3_NAME_MAX 255 +#endif + +// Maximum size of a file in bytes, may be redefined to limit to support other +// drivers. Limited on disk to <= 2147483647. Stored in superblock and must be +// respected by other littlefs drivers. +#ifndef LFS3_FILE_MAX +#define LFS3_FILE_MAX 2147483647 +#endif + + +// Possible error codes, these are negative to allow +// valid positive return values +enum lfs3_err { + LFS3_ERR_OK = 0, // No error + LFS3_ERR_UNKNOWN = -1, // Unknown error + LFS3_ERR_INVAL = -22, // Invalid parameter + LFS3_ERR_NOTSUP = -95, // Operation not supported + LFS3_ERR_BUSY = -16, // Device or resource busy + LFS3_ERR_IO = -5, // Error during device operation + LFS3_ERR_CORRUPT = -84, // Corrupted + LFS3_ERR_NOENT = -2, // No directory entry + LFS3_ERR_EXIST = -17, // Entry already exists + LFS3_ERR_NOTDIR = -20, // Entry is not a dir + LFS3_ERR_ISDIR = -21, // Entry is a dir + LFS3_ERR_NOTEMPTY = -39, // Dir is not empty + LFS3_ERR_FBIG = -27, // File too large + LFS3_ERR_NOSPC = -28, // No space left on device + LFS3_ERR_NOMEM = -12, // No more memory available + LFS3_ERR_NOATTR = -61, // No data/attr available + LFS3_ERR_NAMETOOLONG = -36, // File name too long + LFS3_ERR_RANGE = -34, // Result out of range +}; + +// File types +// +// LFS3_TYPE_UNKNOWN will always be the largest, including internal +// types, and can be used to deliminate user defined types at higher +// levels +// +enum lfs3_type { + // file types + LFS3_TYPE_REG = 1, // A regular file + LFS3_TYPE_DIR = 2, // A directory file + LFS3_TYPE_STICKYNOTE = 3, // An uncommitted file + LFS3_TYPE_UNKNOWN = 7, // Unknown file type + + // internally used types, don't use these + LFS3_type_BOOKMARK = 4, // Directory bookmark + LFS3_type_ORPHAN = 5, // An orphaned stickynote + LFS3_type_TRV = 6, // An open traversal object +}; + +// File open flags +#define LFS3_O_MODE 3 // The file's access mode +#define LFS3_O_RDONLY 0 // Open a file as read only +#ifndef LFS3_RDONLY +#define LFS3_O_WRONLY 1 // Open a file as write only +#endif +#ifndef LFS3_RDONLY +#define LFS3_O_RDWR 2 // Open a file as read and write +#endif +#ifndef LFS3_RDONLY +#define LFS3_O_CREAT 0x00000004 // Create a file if it does not exist +#endif +#ifndef LFS3_RDONLY +#define LFS3_O_EXCL 0x00000008 // Fail if a file already exists +#endif +#ifndef LFS3_RDONLY +#define LFS3_O_TRUNC 0x00000010 // Truncate the existing file to zero size +#endif +#ifndef LFS3_RDONLY +#define LFS3_O_APPEND 0x00000020 // Move to end of file on every write +#endif +#define LFS3_O_FLUSH 0x00000040 // Flush data on every write +#define LFS3_O_SYNC 0x00000080 // Sync metadata on every write +#define LFS3_O_DESYNC 0x00100000 // Do not sync or recieve file updates +#define LFS3_O_CKMETA 0x00010000 // Check metadata checksums +#define LFS3_O_CKDATA 0x00020000 // Check metadata + data checksums + +// internally used flags, don't use these +#define LFS3_o_WRSET 3 // Open a file as an atomic write +#define LFS3_o_TYPE 0xf0000000 // The file's type +#define LFS3_o_ZOMBIE 0x08000000 // File has been removed +#define LFS3_o_UNCREAT 0x04000000 // File does not exist yet +#define LFS3_o_UNSYNC 0x02000000 // File's metadata does not match disk +#define LFS3_o_UNCRYST 0x01000000 // File's leaf not fully crystallized +#define LFS3_o_UNGRAFT 0x00800000 // File's leaf does not match disk +#define LFS3_o_UNFLUSH 0x00400000 // File's cache does not match disk + +// File seek flags +#define LFS3_SEEK_SET 0 // Seek relative to an absolute position +#define LFS3_SEEK_CUR 1 // Seek relative to the current file position +#define LFS3_SEEK_END 2 // Seek relative to the end of the file + +// Custom attribute flags +#define LFS3_A_MODE 3 // The attr's access mode +#define LFS3_A_RDONLY 0 // Open an attr as read only +#ifndef LFS3_RDONLY +#define LFS3_A_WRONLY 1 // Open an attr as write only +#endif +#ifndef LFS3_RDONLY +#define LFS3_A_RDWR 2 // Open an attr as read and write +#endif +#define LFS3_A_LAZY 0x04 // Only write attr if file changed + +// Filesystem format flags +#ifndef LFS3_RDONLY +#define LFS3_F_MODE 1 // Format's access mode +#endif +#ifndef LFS3_RDONLY +#define LFS3_F_RDWR 0 // Format the filesystem as read and write +#endif +#if !defined(LFS3_RDONLY) && defined(LFS3_GBMAP) +#define LFS3_F_GBMAP 0x02000000 // Use the global on-disk block-map +#endif +#if !defined(LFS3_RDONLY) && defined(LFS3_REVDBG) +#define LFS3_F_REVDBG 0x00000010 // Add debug info to revision counts +#endif +#if !defined(LFS3_RDONLY) && defined(LFS3_REVNOISE) +#define LFS3_F_REVNOISE 0x00000020 // Add noise to revision counts +#endif +#if !defined(LFS3_RDONLY) && defined(LFS3_CKPROGS) +#define LFS3_F_CKPROGS 0x00100000 // Check progs by reading back progged data +#endif +#if !defined(LFS3_RDONLY) && defined(LFS3_CKFETCHES) +#define LFS3_F_CKFETCHES \ + 0x00200000 // Check block checksums before first use +#endif +#if !defined(LFS3_RDONLY) && defined(LFS3_CKMETAPARITY) +#define LFS3_F_CKMETAPARITY \ + 0x00400000 // Check metadata tag parity bits +#endif +#if !defined(LFS3_RDONLY) && defined(LFS3_CKDATACKSUMS) +#define LFS3_F_CKDATACKSUMS \ + 0x01000000 // Check data checksums on reads +#endif +#ifndef LFS3_RDONLY +#define LFS3_F_MKCONSISTENT \ + 0x00000800 // Make the filesystem consistent +#endif +#ifndef LFS3_RDONLY +#define LFS3_F_LOOKAHEAD \ + 0x00001000 // Repopulate lookahead buffer +#endif +#if !defined(LFS3_RDONLY) && defined(LFS3_GBMAP) +#define LFS3_F_LOOKGBMAP \ + 0x00002000 // Repopulate the gbmap +#endif +#ifndef LFS3_RDONLY +#define LFS3_F_COMPACTMETA \ + 0x00008000 // Compact metadata logs +#endif +#ifndef LFS3_RDONLY +#define LFS3_F_CKMETA 0x00010000 // Check metadata checksums +#endif +#ifndef LFS3_RDONLY +#define LFS3_F_CKDATA 0x00020000 // Check metadata + data checksums +#endif + +// Filesystem mount flags +#define LFS3_M_MODE 1 // Mount's access mode +#ifndef LFS3_RDONLY +#define LFS3_M_RDWR 0 // Mount the filesystem as read and write +#endif +#define LFS3_M_RDONLY 1 // Mount the filesystem as read only +#define LFS3_M_FLUSH 0x00000040 // Open all files with LFS3_O_FLUSH +#define LFS3_M_SYNC 0x00000080 // Open all files with LFS3_O_SYNC +#if !defined(LFS3_RDONLY) && defined(LFS3_REVDBG) +#define LFS3_M_REVDBG 0x00000010 // Add debug info to revision counts +#endif +#if !defined(LFS3_RDONLY) && defined(LFS3_REVNOISE) +#define LFS3_M_REVNOISE 0x00000020 // Add noise to revision counts +#endif +#if !defined(LFS3_RDONLY) && defined(LFS3_CKPROGS) +#define LFS3_M_CKPROGS 0x00100000 // Check progs by reading back progged data +#endif +#ifdef LFS3_CKFETCHES +#define LFS3_M_CKFETCHES \ + 0x00200000 // Check block checksums before first use +#endif +#ifdef LFS3_CKMETAPARITY +#define LFS3_M_CKMETAPARITY \ + 0x00400000 // Check metadata tag parity bits +#endif +#ifdef LFS3_CKDATACKSUMS +#define LFS3_M_CKDATACKSUMS \ + 0x01000000 // Check data checksums on reads +#endif +#ifndef LFS3_RDONLY +#define LFS3_M_MKCONSISTENT \ + 0x00000800 // Make the filesystem consistent +#endif +#ifndef LFS3_RDONLY +#define LFS3_M_LOOKAHEAD \ + 0x00001000 // Repopulate lookahead buffer +#endif +#if !defined(LFS3_RDONLY) && defined(LFS3_GBMAP) +#define LFS3_M_LOOKGBMAP \ + 0x00002000 // Repopulate the gbmap +#endif +#ifndef LFS3_RDONLY +#define LFS3_M_COMPACTMETA \ + 0x00008000 // Compact metadata logs +#endif +#define LFS3_M_CKMETA 0x00010000 // Check metadata checksums +#define LFS3_M_CKDATA 0x00020000 // Check metadata + data checksums + +// Filesystem info flags +#define LFS3_I_RDONLY 0x00000001 // Mounted read only +#ifdef LFS3_GBMAP +#define LFS3_I_GBMAP 0x02000000 // Global on-disk block-map in use +#endif +#define LFS3_I_FLUSH 0x00000040 // Mounted with LFS3_M_FLUSH +#define LFS3_I_SYNC 0x00000080 // Mounted with LFS3_M_SYNC +#if !defined(LFS3_RDONLY) && defined(LFS3_REVDBG) +#define LFS3_I_REVDBG 0x00000010 // Mounted with LFS3_M_REVDBG +#endif +#if !defined(LFS3_RDONLY) && defined(LFS3_REVNOISE) +#define LFS3_I_REVNOISE 0x00000020 // Mounted with LFS3_M_REVNOISE +#endif +#if !defined(LFS3_RDONLY) && defined(LFS3_CKPROGS) +#define LFS3_I_CKPROGS 0x00100000 // Mounted with LFS3_M_CKPROGS +#endif +#ifdef LFS3_CKFETCHES +#define LFS3_I_CKFETCHES \ + 0x00200000 // Mounted with LFS3_M_CKFETCHES +#endif +#ifdef LFS3_CKMETAPARITY +#define LFS3_I_CKMETAPARITY \ + 0x00400000 // Mounted with LFS3_M_CKMETAPARITY +#endif +#ifdef LFS3_CKDATACKSUMS +#define LFS3_I_CKDATACKSUMS \ + 0x01000000 // Mounted with LFS3_M_CKDATACKSUMS +#endif +#ifndef LFS3_RDONLY +#define LFS3_I_MKCONSISTENT \ + 0x00000800 // Filesystem needs mkconsistent to write +#endif +#ifndef LFS3_RDONLY +#define LFS3_I_LOOKAHEAD \ + 0x00001000 // Lookahead buffer is not full +#endif +#if !defined(LFS3_RDONLY) && defined(LFS3_GBMAP) +#define LFS3_I_LOOKGBMAP \ + 0x00002000 // The gbmap is not full +#endif +#ifndef LFS3_RDONLY +#define LFS3_I_COMPACTMETA \ + 0x00008000 // Filesystem may have uncompacted metadata +#endif +#define LFS3_I_CKMETA 0x00010000 // Metadata checksums not checked recently +#define LFS3_I_CKDATA 0x00020000 // Data checksums not checked recently + +// Block types +enum lfs3_btype { + LFS3_BTYPE_MDIR = 1, + LFS3_BTYPE_BTREE = 2, + LFS3_BTYPE_DATA = 3, +}; + +// Traversal flags +#define LFS3_T_MODE 1 // The traversal's access mode +#ifndef LFS3_RDONLY +#define LFS3_T_RDWR 0 // Open traversal as read and write +#endif +#define LFS3_T_RDONLY 1 // Open traversal as read only +#define LFS3_T_MTREEONLY \ + 0x00000002 // Only traverse the mtree +#define LFS3_T_EXCL 0x00000008 // Error if filesystem modified +#ifndef LFS3_RDONLY +#define LFS3_T_MKCONSISTENT \ + 0x00000800 // Make the filesystem consistent +#endif +#ifndef LFS3_RDONLY +#define LFS3_T_LOOKAHEAD \ + 0x00001000 // Repopulate lookahead buffer +#endif +#if !defined(LFS3_RDONLY) && defined(LFS3_GBMAP) +#define LFS3_T_LOOKGBMAP \ + 0x00002000 // Repopulate the gbmap +#endif +#ifndef LFS3_RDONLY +#define LFS3_T_COMPACTMETA \ + 0x00008000 // Compact metadata logs +#endif +#define LFS3_T_CKMETA 0x00010000 // Check metadata checksums +#define LFS3_T_CKDATA 0x00020000 // Check metadata + data checksums + +// internally used flags, don't use these +#define LFS3_t_TYPE 0xf0000000 // The traversal's type +#define LFS3_t_BTYPE 0x00f00000 // The current block type +#define LFS3_t_ZOMBIE 0x08000000 // File has been removed +#define LFS3_t_CKPOINTED \ + 0x04000000 // Filesystem ckpointed during traversal +#define LFS3_t_DIRTY 0x02000000 // Filesystem ckpointed outside traversal +#define LFS3_t_STALE 0x01000000 // Block queue probably out-of-date + +// File/filesystem check flags +#ifndef LFS3_RDONLY +#define LFS3_CK_MKCONSISTENT \ + 0x00000800 // Make the filesystem consistent +#endif +#ifndef LFS3_RDONLY +#define LFS3_CK_LOOKAHEAD \ + 0x00001000 // Repopulate lookahead buffer +#endif +#if !defined(LFS3_RDONLY) && defined(LFS3_GBMAP) +#define LFS3_CK_LOOKGBMAP \ + 0x00002000 // Repopulate the gbmap +#endif +#ifndef LFS3_RDONLY +#define LFS3_CK_COMPACTMETA \ + 0x00008000 // Compact metadata logs +#endif +#define LFS3_CK_CKMETA 0x00010000 // Check metadata checksums +#define LFS3_CK_CKDATA 0x00020000 // Check metadata + data checksums + +// GC flags +#ifndef LFS3_RDONLY +#define LFS3_GC_MKCONSISTENT \ + 0x00000800 // Make the filesystem consistent +#endif +#ifndef LFS3_RDONLY +#define LFS3_GC_LOOKAHEAD \ + 0x00001000 // Repopulate lookahead buffer +#endif +#if !defined(LFS3_RDONLY) && defined(LFS3_GBMAP) +#define LFS3_GC_LOOKGBMAP \ + 0x00002000 // Repopulate the gbmap +#endif +#ifndef LFS3_RDONLY +#define LFS3_GC_COMPACTMETA \ + 0x00008000 // Compact metadata logs +#endif +#define LFS3_GC_CKMETA 0x00010000 // Check metadata checksums +#define LFS3_GC_CKDATA 0x00020000 // Check metadata + data checksums + +// an alias for all possible GC work +#define LFS3_GC_ALL ( \ + LFS3_IFDEF_RDONLY(0, LFS3_GC_MKCONSISTENT) \ + | LFS3_IFDEF_RDONLY(0, LFS3_GC_LOOKAHEAD) \ + | LFS3_IFDEF_RDONLY(0, \ + LFS3_IFDEF_GBMAP(LFS3_GC_LOOKGBMAP, 0)) \ + | LFS3_IFDEF_RDONLY(0, LFS3_GC_COMPACTMETA) \ + | LFS3_GC_CKMETA \ + | LFS3_GC_CKDATA) + + +// Configuration provided during initialization of the littlefs +struct lfs3_cfg { + // Opaque user provided context that can be used to pass + // information to the block device operations + void *context; + + // Read a region in a block. Negative error codes are propagated + // to the user. + int (*read)(const struct lfs3_cfg *c, lfs3_block_t block, + lfs3_off_t off, void *buffer, lfs3_size_t size); + + // Program a region in a block. The block must have previously + // been erased. Negative error codes are propagated to the user. + // May return LFS3_ERR_CORRUPT if the block should be considered bad. + #ifndef LFS3_RDONLY + int (*prog)(const struct lfs3_cfg *c, lfs3_block_t block, + lfs3_off_t off, const void *buffer, lfs3_size_t size); + #endif + + // Erase a block. A block must be erased before being programmed. + // The state of an erased block is undefined. Negative error codes + // are propagated to the user. + // May return LFS3_ERR_CORRUPT if the block should be considered bad. + #ifndef LFS3_RDONLY + int (*erase)(const struct lfs3_cfg *c, lfs3_block_t block); + #endif + + // Sync the state of the underlying block device. Negative error codes + // are propagated to the user. + #ifndef LFS3_RDONLY + int (*sync)(const struct lfs3_cfg *c); + #endif + +#ifdef LFS3_THREADSAFE + // Lock the underlying block device. Negative error codes + // are propagated to the user. + int (*lock)(const struct lfs3_cfg *c); + + // Unlock the underlying block device. Negative error codes + // are propagated to the user. + int (*unlock)(const struct lfs3_cfg *c); +#endif + + // Minimum size of a read in bytes. All read operations will be a + // multiple of this value. + lfs3_size_t read_size; + + // Minimum size of a program in bytes. All program operations will be a + // multiple of this value. + #ifndef LFS3_RDONLY + lfs3_size_t prog_size; + #endif + + // Size of an erasable block in bytes. This does not impact ram consumption + // and may be larger than the physical erase size. Must be a multiple of + // the read and program sizes. + lfs3_size_t block_size; + + // Number of erasable blocks on the device. + lfs3_size_t block_count; + + // Number of erase cycles before metadata blocks are relocated for + // wear-leveling. Suggested values are in the range 16-1024. Larger values + // relocate less frequently, improving average performance, at the cost + // of worse wear distribution. Note this ends up rounded down to a + // power-of-2. + // + // 0 results in pure copy-on-write, which may be counter-productive. Set + // to -1 to disable block-level wear-leveling. + #ifndef LFS3_RDONLY + int32_t block_recycles; + #endif + + // Size of the read cache in bytes. Larger caches can improve + // performance by storing more data and reducing the number of disk + // accesses. Must be a multiple of the read size. + lfs3_size_t rcache_size; + + // Size of the program cache in bytes. Larger caches can improve + // performance by storing more data and reducing the number of disk + // accesses. Must be a multiple of the program size. + #ifndef LFS3_RDONLY + lfs3_size_t pcache_size; + #endif + + // Size of file caches in bytes. In addition to filesystem-wide + // read/prog caches, each file gets its own cache to reduce disk + // accesses. + lfs3_size_t fcache_size; + + // Size of the lookahead buffer in bytes. A larger lookahead buffer + // increases the number of blocks found during an allocation scan. The + // lookahead buffer is stored as a compact bitmap, so each byte of RAM + // can track 8 blocks. + #ifndef LFS3_RDONLY + lfs3_size_t lookahead_size; + #endif + + // Flags indicating what gc work to do during lfs3_gc calls. + #ifdef LFS3_GC + uint32_t gc_flags; + #endif + + // Number of gc steps to perform in each call to lfs3_gc, with each + // step being ~1 block of work. + // + // More steps per call will make more progress if interleaved with + // other filesystem operations, but may also introduce more latency. + // steps=1 will do the minimum amount of work to make progress, and + // steps=-1 will not return until all pending janitorial work has + // been completed. + // + // Defaults to steps=1 when zero. + #ifdef LFS3_GC + lfs3_soff_t gc_steps; + #endif + + // Threshold for repopulating the lookahead buffer during gc. This + // can be set lower than the lookahead size to delay gc work when + // only a few blocks have been allocated. + // + // Note this only affects explicit gc operations. During normal + // operations the lookahead buffer is only repopulated when empty. + // + // 0 only repopulates the lookahead buffer when empty, while -1 or + // any value >= 8*lookahead_size repopulates the lookahead buffer + // after any block allocation. + #ifndef LFS3_RDONLY + lfs3_block_t gc_lookahead_thresh; + #endif + + // Threshold for repopulating the gbmap during gc. This can be set + // lower than the disk size to delay gc work when only a few blocks + // have been allocated. + // + // Note this only affects explicit gc operations. During normal + // operations gbmap repopulations are controlled by + // lookgbmap_thresh. + // + // Any value <= lookgbmap_thresh repopulates the gbmap when below + // lookgbmap_thresh, while -1 or any value >= block_count + // repopulates the lookahead buffer after any block allocation. + #if !defined(LFS3_RDONLY) && defined(LFS3_GBMAP) + lfs3_block_t gc_lookgbmap_thresh; + #endif + + // Threshold for metadata compaction during gc in bytes. + // + // Metadata logs that exceed this threshold will be compacted during + // gc operations. Defaults to ~88% block_size when zero, though this + // default may change in the future. + // + // Note this only affects explicit gc operations. During normal + // operations metadata is only compacted when full. + // + // Set to -1 to disable metadata compaction during gc. + #ifndef LFS3_RDONLY + lfs3_size_t gc_compactmeta_thresh; + #endif + + // Optional statically allocated rcache buffer. Must be rcache_size. By + // default lfs3_malloc is used to allocate this buffer. + void *rcache_buffer; + + // Optional statically allocated pcache buffer. Must be pcache_size. By + // default lfs3_malloc is used to allocate this buffer. + #ifndef LFS3_RDONLY + void *pcache_buffer; + #endif + + // Optional statically allocated lookahead buffer. Must be lookahead_size. + // By default lfs3_malloc is used to allocate this buffer. + #ifndef LFS3_RDONLY + void *lookahead_buffer; + #endif + + // Optional upper limit on length of file names in bytes. No downside for + // larger names except the size of the info struct which is controlled by + // the LFS3_NAME_MAX define. Defaults to LFS3_NAME_MAX when zero. Stored in + // superblock and must be respected by other littlefs drivers. + #ifndef LFS3_RDONLY + lfs3_size_t name_limit; + #endif + + // Optional upper limit on files in bytes. No downside for larger files + // but must be <= LFS3_FILE_MAX. Defaults to LFS3_FILE_MAX when zero. Stored + // in superblock and must be respected by other littlefs drivers. + #ifndef LFS3_RDONLY + lfs3_size_t file_limit; + #endif + + // TODO these are pretty low-level details, should we have reasonable + // defaults? need to benchmark. + + // Maximum size of inlined trees (shrubs) in bytes. Shrubs reduce B-tree + // root overhead, but may impact metadata-related performance. Must be <= + // blocksize/4. + // + // 0 disables shrubs. + #ifndef LFS3_RDONLY + lfs3_size_t shrub_size; + #endif + + // Maximum size of a non-block B-tree leaf in bytes. Smaller values may + // make small random-writes cheaper, but increase metadata overhead. Must + // be <= block_size/4. + #ifndef LFS3_RDONLY + lfs3_size_t fragment_size; + #endif + + // TODO crystal_thresh=0 really just means crystal_thresh=1, should we + // allow crystal_thresh=0? crystal_thresh=0 => block_size/16 or + // block_size/8 is probably a better default. need to benchmark. + + // TODO we should probably just assert if crystal_thresh < fragment_size, + // or if crystal_thresh < prog_size, these aren't really valid cases + + // Threshold for compacting multiple fragments into a block. Smaller + // values will crystallize more eagerly, reducing disk usage, but + // increasing the cost of random-writes. + // + // 0 tries to only writes blocks, minimizing disk usage, while -1 or + // any value > block_size only writes fragments, minimizing + // random-write cost. + #ifndef LFS3_RDONLY + lfs3_size_t crystal_thresh; + #endif + + // Threshold for repopulating the global on-disk block-map (gbmap). + // + // When <= this many blocks have a known state, littlefs will + // traverse the filesystem and attempt to repopulate the gbmap. + // Smaller values decrease repopulation frequency and improves + // overall allocator throughput, at the risk of needing to fallback + // to the slower lookahead allocator when empty. + // + // 0 only repopulates the gbmap when empty, minimizing gbmap + // repops at the risk of large latency spikes. + #ifdef LFS3_GBMAP + lfs3_block_t lookgbmap_thresh; + #endif +}; + +// File info structure +struct lfs3_info { + // Type of the file, either LFS3_TYPE_REG or LFS3_TYPE_DIR + uint8_t type; + + // Size of the file, only valid for REG files. Limited to 32-bits. + lfs3_size_t size; + + // Name of the file stored as a null-terminated string. Limited to + // LFS3_NAME_MAX+1, which can be changed by redefining LFS3_NAME_MAX to + // reduce RAM. LFS3_NAME_MAX is stored in superblock and must be + // respected by other littlefs drivers. + char name[LFS3_NAME_MAX+1]; +}; + +// Filesystem info structure +struct lfs3_fsinfo { + // Filesystem flags + uint32_t flags; + + // Size of a logical block in bytes. + lfs3_size_t block_size; + + // Number of logical blocks in the filesystem. + lfs3_size_t block_count; + + // Upper limit on the length of file names in bytes. + lfs3_size_t name_limit; + + // Upper limit on the size of files in bytes. + lfs3_size_t file_limit; +}; + +// Traversal info structure +struct lfs3_tinfo { + // Type of the block + uint8_t btype; + + // Block address + lfs3_block_t block; +}; + +// Custom attribute structure, used to describe custom attributes +// committed atomically during file writes. +struct lfs3_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 + // LFS3_ERR_NOATTR to remove the attr + lfs3_ssize_t buffer_size; + + // Optional pointer to a mutable attr size, updated on read/write, + // set to LFS3_ERR_NOATTR if attr does not exist + // + // Defaults to buffer_size if NULL + lfs3_ssize_t *size; +}; + +// Optional configuration provided during lfs3_file_opencfg +struct lfs3_file_cfg { + // Optional statically allocated file cache buffer. Must be fcache_size. + // By default lfs3_malloc is used to allocate this buffer. + void *fcache_buffer; + + // Size of the file cache in bytes. In addition to filesystem-wide + // read/prog caches, each file gets its own cache to reduce disk + // accesses. Defaults to fcache_size if fcache_buffer is NULL. + lfs3_size_t fcache_size; + + // 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 lfs3_attr *attrs; + + // Number of custom attributes in the list + lfs3_size_t attr_count; +}; + + + +/// On-disk things /// + +// On-disk metadata tags +enum lfs3_tag { + // the null tag is reserved + LFS3_TAG_NULL = 0x0000, + + // config tags + LFS3_TAG_CONFIG = 0x0100, + LFS3_TAG_MAGIC = 0x0131, + LFS3_TAG_VERSION = 0x0134, + LFS3_TAG_RCOMPAT = 0x0135, + LFS3_TAG_WCOMPAT = 0x0136, + LFS3_TAG_OCOMPAT = 0x0137, + LFS3_TAG_GEOMETRY = 0x0138, + LFS3_TAG_NAMELIMIT = 0x0139, + LFS3_TAG_FILELIMIT = 0x013a, + // in-device only, to help find unknown config tags + LFS3_tag_UNKNOWNCONFIG = 0x013b, + + // global-state tags + LFS3_TAG_GDELTA = 0x0200, + LFS3_TAG_GRMDELTA = 0x0230, + LFS3_TAG_GBMAPDELTA = 0x0234, + + // name tags + LFS3_TAG_NAME = 0x0300, + LFS3_TAG_BNAME = 0x0300, + LFS3_TAG_REG = 0x0301, + LFS3_TAG_DIR = 0x0302, + LFS3_TAG_STICKYNOTE = 0x0303, + LFS3_TAG_BOOKMARK = 0x0304, + // in-device only name tags, these should never get written to disk + LFS3_tag_ORPHAN = 0x0305, + LFS3_tag_TRV = 0x0306, + LFS3_tag_UNKNOWN = 0x0307, + // non-file name tags + LFS3_TAG_MNAME = 0x0330, + + // struct tags + LFS3_TAG_STRUCT = 0x0400, + LFS3_TAG_BRANCH = 0x0400, + LFS3_TAG_DATA = 0x0404, + LFS3_TAG_BLOCK = 0x0408, + LFS3_TAG_DID = 0x0420, + LFS3_TAG_BSHRUB = 0x0428, + LFS3_TAG_BTREE = 0x042c, + LFS3_TAG_MROOT = 0x0431, + LFS3_TAG_MDIR = 0x0435, + LFS3_TAG_MTREE = 0x043c, + LFS3_TAG_BMRANGE = 0x0440, + LFS3_TAG_BMFREE = 0x0440, + LFS3_TAG_BMINUSE = 0x0441, + LFS3_TAG_BMERASED = 0x0442, + LFS3_TAG_BMBAD = 0x0443, + + // user/sys attributes + LFS3_TAG_ATTR = 0x0600, + LFS3_TAG_UATTR = 0x0600, + LFS3_TAG_SATTR = 0x0700, + + // shrub tags belong to secondary trees + LFS3_TAG_SHRUB = 0x1000, + + // alt pointers form the inner nodes of our rbyd trees + LFS3_TAG_ALT = 0x4000, + LFS3_TAG_B = 0x0000, + LFS3_TAG_R = 0x2000, + LFS3_TAG_LE = 0x0000, + LFS3_TAG_GT = 0x1000, + + // checksum tags + LFS3_TAG_CKSUM = 0x3000, + LFS3_TAG_PHASE = 0x0003, + LFS3_TAG_PERTURB = 0x0004, + LFS3_TAG_NOTE = 0x3100, + LFS3_TAG_ECKSUM = 0x3200, + LFS3_TAG_GCKSUMDELTA = 0x3300, + + // in-device only tags, these should never get written to disk + LFS3_tag_INTERNAL = 0x0000, + LFS3_tag_RATTRS = 0x0001, + LFS3_tag_SHRUBCOMMIT = 0x0002, + LFS3_tag_GRMPUSH = 0x0003, + LFS3_tag_MOVE = 0x0004, + LFS3_tag_ATTRS = 0x0005, + + // some in-device only tag modifiers + LFS3_tag_RM = 0x8000, + LFS3_tag_GROW = 0x4000, + LFS3_tag_MASK0 = 0x0000, + LFS3_tag_MASK2 = 0x1000, + LFS3_tag_MASK8 = 0x2000, + LFS3_tag_MASK12 = 0x3000, +}; + +// some other tag encodings with their own subfields +#define LFS3_TAG_ALT(c, d, key) \ + (LFS3_TAG_ALT \ + | (0x2000 & (c)) \ + | (0x1000 & (d)) \ + | (0x0fff & (lfs3_tag_t)(key))) + +#define LFS3_TAG_ATTR(attr) \ + (LFS3_TAG_ATTR \ + | ((0x80 & (lfs3_tag_t)(attr)) << 1) \ + | (0x7f & (lfs3_tag_t)(attr))) + + +// On-disk compat flags +// +// - RCOMPAT => Must understand to read the filesystem +// - WCOMPAT => Must understand to write to the filesystem +// - OCOMPAT => No understanding necessary, we don't really use these +// +// note, "understanding" does not necessarily mean support +// +#define LFS3_RCOMPAT_NONSTANDARD 0x00000001 // Non-standard filesystem format +#define LFS3_RCOMPAT_WRONLY 0x00000004 // Reading is disallowed +#define LFS3_RCOMPAT_MMOSS 0x00000010 // May use an inlined mdir +#define LFS3_RCOMPAT_MSPROUT 0x00000020 // May use an mdir pointer +#define LFS3_RCOMPAT_MSHRUB 0x00000040 // May use an inlined mtree +#define LFS3_RCOMPAT_MTREE 0x00000080 // May use an mtree +#define LFS3_RCOMPAT_BMOSS 0x00000100 // Files may use inlined data +#define LFS3_RCOMPAT_BSPROUT 0x00000200 // Files may use block pointers +#define LFS3_RCOMPAT_BSHRUB 0x00000400 // Files may use inlined btrees +#define LFS3_RCOMPAT_BTREE 0x00000800 // Files may use btrees +#define LFS3_RCOMPAT_GRM 0x00010000 // Global-remove in use +// internally used flags +#define LFS3_rcompat_OVERFLOW 0x80000000 // Can't represent all flags + +#define LFS3_WCOMPAT_NONSTANDARD 0x00000001 // Non-standard filesystem format +#define LFS3_WCOMPAT_RDONLY 0x00000002 // Writing is disallowed +#define LFS3_WCOMPAT_GCKSUM 0x00040000 // Global-checksum in use +#define LFS3_WCOMPAT_GBMAP 0x00080000 // Global on-disk block-map in use +#define LFS3_WCOMPAT_DIR 0x01000000 // Directory files in use +// internally used flags +#define LFS3_wcompat_OVERFLOW 0x80000000 // Can't represent all flags + +#define LFS3_OCOMPAT_NONSTANDARD 0x00000001 // Non-standard filesystem format +// internally used flags +#define LFS3_ocompat_OVERFLOW 0x80000000 // Can't represent all flags + + +// On-disk encodings/decodings + +// tag encoding: +// .---+---+---+- -+- -+- -+- -+---+- -+- -+- -. tag: 1 be16 2 bytes +// | tag | weight | size | weight: 1 leb128 <=5 bytes +// '---+---+---+- -+- -+- -+- -+---+- -+- -+- -' size: 1 leb128 <=4 bytes +// total: <=11 bytes +#define LFS3_TAG_DSIZE (2+5+4) + +// le32 encoding: +// .---+---+---+---. total: 1 le32 4 bytes +// | le32 | +// '---+---+---+---' +// +#define LFS3_LE32_DSIZE 4 + +// leb128 encoding: +// .---+- -+- -+- -+- -. total: 1 leb128 <=5 bytes +// | leb128 | +// '---+- -+- -+- -+- -' +// +#define LFS3_LEB128_DSIZE 5 + +// lleb128 encoding: +// .---+- -+- -+- -. total: 1 leb128 <=4 bytes +// | lleb128 | +// '---+- -+- -+- -' +// +#define LFS3_LLEB128_DSIZE 4 + +// geometry encoding +// .---+- -+- -+- -. block_size: 1 leb128 <=4 bytes +// | block_size | block_count: 1 leb128 <=5 bytes +// +---+- -+- -+- -+- -. total: <=9 bytes +// | block_count | +// '---+- -+- -+- -+- -' +// +#define LFS3_GEOMETRY_DSIZE (4+5) + +// grm encoding: +// .- -+- -+- -+- -+- -. mids: 2 leb128s <=2x5 bytes +// ' mids ' total: <=10 bytes +// + + +// ' ' +// '- -+- -+- -+- -+- -' +// +#define LFS3_GRM_DSIZE (5+5) + +// gbmap encoding: +// .---+- -+- -+- -+- -. window: 1 leb128 <=5 bytes +// | window | known: 1 leb128 <=5 bytes +// +---+- -+- -+- -+- -+ block: 1 leb128 <=5 bytes +// | known | trunk: 1 leb128 <=4 bytes +// +---+- -+- -+- -+- -+ cksum: 1 le32 4 bytes +// | block | total: 23 bytes +// +---+- -+- -+- -+- -' +// | trunk | +// +---+- -+- -+- -+ +// | cksum | +// '---+---+---+---' +// +#define LFS3_GBMAP_DSIZE (5+5+5+4+4) + +// branch encoding: +// .---+- -+- -+- -+- -. block: 1 leb128 <=5 bytes +// | block | trunk: 1 leb128 <=4 bytes +// +---+- -+- -+- -+- -' cksum: 1 le32 4 bytes +// | trunk | total: <=13 bytes +// +---+- -+- -+- -+ +// | cksum | +// '---+---+---+---' +// +#define LFS3_BRANCH_DSIZE (5+4+4) + +// bptr encoding: +// .---+- -+- -+- -. size: 1 leb128 <=4 bytes +// | size | block: 1 leb128 <=5 bytes +// +---+- -+- -+- -+- -. off: 1 leb128 <=4 bytes +// | block | cksize: 1 leb128 <=4 bytes +// +---+- -+- -+- -+- -' cksum: 1 le32 4 bytes +// | off | total: <=21 bytes +// +---+- -+- -+- -+ +// | cksize | +// +---+- -+- -+- -+ +// | cksum | +// '---+---+---+---' +// +#define LFS3_BPTR_DSIZE (4+5+4+4+4) + +// btree encoding: +// .---+- -+- -+- -+- -. weight: 1 leb128 <=5 bytes +// | weight | block: 1 leb128 <=5 bytes +// +---+- -+- -+- -+- -+ trunk: 1 leb128 <=4 bytes +// | block | cksum: 1 le32 4 bytes +// +---+- -+- -+- -+- -' total: <=18 bytes +// | trunk | +// +---+- -+- -+- -+ +// | cksum | +// '---+---+---+---' +// +#define LFS3_BTREE_DSIZE (5+LFS3_BRANCH_DSIZE) + +// shrub encoding: +// .---+- -+- -+- -+- -. weight: 1 leb128 <=5 bytes +// | weight | trunk: 1 leb128 <=4 bytes +// +---+- -+- -+- -+- -' total: <=9 bytes +// | trunk | +// '---+- -+- -+- -' +// +#define LFS3_SHRUB_DSIZE (5+4) + +// mptr encoding: +// .---+- -+- -+- -+- -. blocks: 2 leb128s <=2x5 bytes +// | block x 2 | total: <=10 bytes +// + + +// | | +// '---+- -+- -+- -+- -' +// +#define LFS3_MPTR_DSIZE (5+5) + +// ecksum encoding: +// .---+- -+- -+- -. cksize: 1 leb128 <=4 bytes +// | cksize | cksum: 1 le32 4 bytes +// +---+- -+- -+- -+ total: <=8 bytes +// | cksum | +// '---+---+---+---' +// +#define LFS3_ECKSUM_DSIZE (4+4) + + + +/// Internal littlefs structs /// + +// either an on-disk or in-RAM data pointer +// +// note, it's tempting to make this fancier, but we benefit quite a lot +// from the compiler being able to aggresively optimize this struct +// +typedef struct lfs3_data { + // sign2(size)=0b00 => in-RAM buffer + // sign2(size)=0b10 => on-disk data + // sign2(size)=0b11 => on-disk data + cksum + lfs3_size_t size; + union { + const uint8_t *buffer; + struct { + lfs3_block_t block; + lfs3_size_t off; + // optional context for validating data + #ifdef LFS3_CKDATACKSUMS + // sign(cksize)=0 => block not erased + // sign(cksize)=1 => block erased + lfs3_size_t cksize; + uint32_t cksum; + #endif + } disk; + } u; +} lfs3_data_t; + +// a possible block pointer +typedef struct lfs3_bptr { + // sign2(size)=0b00 => in-RAM buffer + // sign2(size)=0b10 => on-disk data + // sign2(size)=0b11 => block pointer + lfs3_data_t d; + #ifndef LFS3_CKDATACKSUMS + // sign(cksize)=0 => block not erased + // sign(cksize)=1 => block erased + lfs3_size_t cksize; + uint32_t cksum; + #endif +} lfs3_bptr_t; + +// littlefs's core metadata log type +typedef struct lfs3_rbyd { + lfs3_rid_t weight; + lfs3_block_t blocks[2]; + // sign(trunk)=0 => normal rbyd + // sign(trunk)=1 => shrub rbyd + lfs3_size_t trunk; + #ifndef LFS3_RDONLY + // sign(eoff) => perturb bit + // eoff=0, trunk=0 => not yet committed + // eoff=0, trunk>0 => not yet fetched + // eoff>=block_size => rbyd not erased/needs compaction + lfs3_size_t eoff; + #endif + uint32_t cksum; +} lfs3_rbyd_t; + +// littlefs's btree representation +// +// technically all we need for btrees is the root rbyd, but tracking the +// most recent leaf helps speed up iteration/subattrs/etc without +// local rbyd allocations -- less code and stack for the same +// performance +typedef struct lfs3_btree { + lfs3_rbyd_t r; + #ifdef LFS3_BLEAFCACHE + struct { + lfs3_bid_t bid; + lfs3_rbyd_t r; + } leaf; + #endif +} lfs3_btree_t; + +// littlefs's atomic metadata log type +typedef struct lfs3_mdir { + lfs3_smid_t mid; + lfs3_rbyd_t r; + uint32_t gcksumdelta; +} lfs3_mdir_t; + +// a handle to an opened mdir for tracking purposes +typedef struct lfs3_handle { + // an invasive linked-list is used to keep things in-sync + struct lfs3_handle *next; + // flags includes the type and type-specific flags + uint32_t flags; + lfs3_mdir_t mdir; +} lfs3_handle_t; + +// a shrub is a secondary trunk in an mdir +typedef lfs3_rbyd_t lfs3_shrub_t; + +// a bshrub is like a btree but with a shrub as a root +typedef struct lfs3_bshrub { + // bshrubs need to be tracked for commits to work + lfs3_handle_t h; + // files contain both an active bshrub and staging bshrub, to allow + // staging during mdir compacts + // trunk=0 => no bshrub/btree + // sign(trunk)=1 => bshrub + // sign(trunk)=0 => btree + lfs3_btree_t b; + #ifndef LFS3_RDONLY + lfs3_shrub_t b_; + #endif +} lfs3_bshrub_t; + +// littlefs file type +typedef struct lfs3_file { + // btree/bshrub stuff is in here + lfs3_bshrub_t b; + const struct lfs3_file_cfg *cfg; + + // current file position + lfs3_off_t pos; + + // in-RAM cache + // + // note this lines up with lfs3_data_t's buffer representation + struct { + lfs3_off_t pos; + lfs3_off_t size; + uint8_t *buffer; + } cache; + + // on-disk leaf bptr + struct { + lfs3_off_t pos; + lfs3_off_t weight; + lfs3_bptr_t bptr; + } leaf; +} lfs3_file_t; + +// littlefs directory type +typedef struct lfs3_dir { + lfs3_handle_t h; + lfs3_did_t did; + lfs3_off_t pos; +} lfs3_dir_t; + +// littlefs traversal type +typedef struct lfs3_btrv { + lfs3_sbid_t bid; + lfs3_rbyd_t rbyd; + lfs3_srid_t rid; +} lfs3_btrv_t; + +typedef struct lfs3_mtortoise { + // this aligns with btrv.bid + lfs3_sbid_t bid; + lfs3_block_t blocks[2]; + lfs3_block_t dist; + uint8_t nlog2; +} lfs3_mtortoise_t; + +typedef struct lfs3_mtrv { + // mtree traversal state, our position in then handle linked-list + // is also used to keep track of what handles we've seen + lfs3_handle_t h; + // current bshrub/btree + lfs3_btree_t b; + union { + // bshrub/btree traversal state + lfs3_btrv_t btrv; + // mtortoise for cycle detection + lfs3_mtortoise_t mtortoise; + } u; + + // recalculate gcksum when traversing with ckmeta + uint32_t gcksum; +} lfs3_mtrv_t; + +typedef struct lfs3_mgc { + // core traversal state + lfs3_mtrv_t t; + + #ifdef LFS3_GBMAP + // repopulate gbmap when traversing with lookgbmap + lfs3_btree_t gbmap_; + #endif +} lfs3_mgc_t; + +typedef struct lfs3_trv { + // core traversal/gc state + lfs3_mgc_t gc; + + // pending blocks, only used in lfs3_trv_read + lfs3_sblock_t blocks[2]; +} lfs3_trv_t; + +// littlefs global state +typedef struct lfs3_grm { + lfs3_smid_t queue[2]; +} lfs3_grm_t; + +typedef struct lfs3_gbmap { + lfs3_block_t window; + lfs3_block_t known; + lfs3_btree_t b; + lfs3_btree_t b_p; +} lfs3_gbmap_t; + + +// The littlefs filesystem type +typedef struct lfs3 { + const struct lfs3_cfg *cfg; + uint32_t flags; + lfs3_size_t block_count; + lfs3_size_t name_limit; + lfs3_off_t file_limit; + + uint8_t mbits; + #ifndef LFS3_RDONLY + int8_t recycle_bits; + uint8_t rattr_estimate; + uint8_t mattr_estimate; + #endif + + // linked-list of opened mdirs + lfs3_handle_t *handles; + + lfs3_mdir_t mroot; + lfs3_btree_t mtree; + + struct lfs3_rcache { + lfs3_block_t block; + lfs3_size_t off; + lfs3_size_t size; + uint8_t *buffer; + } rcache; + + #ifndef LFS3_RDONLY + struct lfs3_pcache { + lfs3_block_t block; + lfs3_size_t off; + lfs3_size_t size; + uint8_t *buffer; + } pcache; + // optional prog-aligned cksum + uint32_t pcksum; + #ifdef LFS3_CKMETAPARITY + struct { + lfs3_block_t block; + // sign(off) => tail parity + lfs3_size_t off; + } ptail; + #endif + #endif + + #ifndef LFS3_RDONLY + struct lfs3_lookahead { + lfs3_block_t window; + lfs3_block_t off; + lfs3_block_t known; + lfs3_block_t ckpoint; + uint8_t *buffer; + } lookahead; + #endif + + #ifndef LFS3_RDONLY + const lfs3_data_t *graft; + lfs3_ssize_t graft_count; + #endif + + // global state + uint32_t gcksum; + #ifndef LFS3_RDONLY + uint32_t gcksum_p; + #endif + // TODO can we actually get rid of grm_d when LFS3_RDONLY? + uint32_t gcksum_d; + + lfs3_grm_t grm; + #ifndef LFS3_RDONLY + uint8_t grm_p[LFS3_GRM_DSIZE]; + #endif + // TODO can we actually get rid of grm_d when LFS3_RDONLY? + uint8_t grm_d[LFS3_GRM_DSIZE]; + + #ifdef LFS3_GBMAP + lfs3_gbmap_t gbmap; + uint8_t gbmap_p[LFS3_GBMAP_DSIZE]; + uint8_t gbmap_d[LFS3_GBMAP_DSIZE]; + #endif + + // optional incremental gc state + #ifdef LFS3_GC + lfs3_mgc_t gc; + #endif +} lfs3_t; + + + +/// Filesystem functions /// + +// Format a block device with the littlefs +// +// Requires a littlefs object and config struct. This clobbers the littlefs +// object, and does not leave the filesystem mounted. The config struct must +// be zeroed for defaults and backwards compatibility. +// +// Returns a negative error code on failure. +#ifndef LFS3_RDONLY +int lfs3_format(lfs3_t *lfs3, uint32_t flags, + const struct lfs3_cfg *cfg); +#endif + +// Mounts a littlefs +// +// Requires a littlefs object and config struct. Multiple filesystems +// may be mounted simultaneously with multiple littlefs objects. Both +// lfs3 and config must be allocated while mounted. The config struct must +// be zeroed for defaults and backwards compatibility. +// +// Returns a negative error code on failure. +int lfs3_mount(lfs3_t *lfs3, uint32_t flags, + const struct lfs3_cfg *cfg); + +// Unmounts a littlefs +// +// Does nothing besides releasing any allocated resources. +// Returns a negative error code on failure. +int lfs3_unmount(lfs3_t *lfs3); + +/// General operations /// + +// Get the value of a file +// +// Returns the number of bytes read, or a negative error code on failure. +// Note this may be less than the on-disk file size if the buffer is not +// large enough. +lfs3_ssize_t lfs3_get(lfs3_t *lfs3, const char *path, + void *buffer, lfs3_size_t size); + +// Get a file's size +// +// Returns the size of the file, or a negative error code on failure. +lfs3_ssize_t lfs3_size(lfs3_t *lfs3, const char *path); + +// Set the value of a file +// +// Returns a negative error code on failure. +#ifndef LFS3_RDONLY +int lfs3_set(lfs3_t *lfs3, const char *path, + const void *buffer, lfs3_size_t size); +#endif + +// Removes a file or directory +// +// If removing a directory, the directory must be empty. +// Returns a negative error code on failure. +#ifndef LFS3_RDONLY +int lfs3_remove(lfs3_t *lfs3, const char *path); +#endif + +// Rename or move a file or directory +// +// If the destination exists, it must match the source in type. +// If the destination is a directory, the directory must be empty. +// +// Returns a negative error code on failure. +#ifndef LFS3_RDONLY +int lfs3_rename(lfs3_t *lfs3, const char *old_path, const char *new_path); +#endif + +// Find info about a file or directory +// +// Fills out the info structure, based on the specified file or directory. +// Returns a negative error code on failure. +int lfs3_stat(lfs3_t *lfs3, const char *path, struct lfs3_info *info); + +// Get a custom attribute +// +// Returns the number of bytes read, or a negative error code on failure. +// Note this may be less than the on-disk attr size if the buffer is not +// large enough. +lfs3_ssize_t lfs3_getattr(lfs3_t *lfs3, const char *path, uint8_t type, + void *buffer, lfs3_size_t size); + +// Get a custom attribute's size +// +// Returns the size of the attribute, or a negative error code on failure. +lfs3_ssize_t lfs3_sizeattr(lfs3_t *lfs3, const char *path, uint8_t type); + +// Set a custom attributes +// +// Returns a negative error code on failure. +#ifndef LFS3_RDONLY +int lfs3_setattr(lfs3_t *lfs3, const char *path, uint8_t type, + const void *buffer, lfs3_size_t size); +#endif + +// Removes a custom attribute +// +// Returns a negative error code on failure. +#ifndef LFS3_RDONLY +int lfs3_removeattr(lfs3_t *lfs3, const char *path, uint8_t type); +#endif + + +/// File operations /// + +// Open a file +// +// The mode that the file is opened in is determined by the flags, which +// are values from the enum lfs3_open_flags that are bitwise-ored together. +// +// Returns a negative error code on failure. +#ifndef LFS3_NO_MALLOC +int lfs3_file_open(lfs3_t *lfs3, lfs3_file_t *file, + const char *path, uint32_t flags); +#endif + +// Open a file with extra configuration +// +// The mode that the file is opened in is determined by the flags, which +// are values from the enum lfs3_open_flags that are bitwise-ored together. +// +// The config struct provides additional config options per file as described +// above. The config struct must remain allocated while the file is open, and +// the config struct must be zeroed for defaults and backwards compatibility. +// +// Returns a negative error code on failure. +int lfs3_file_opencfg(lfs3_t *lfs3, lfs3_file_t *file, + const char *path, uint32_t flags, + const struct lfs3_file_cfg *cfg); + +// Close a file +// +// If the file is not desynchronized, any pending writes are written out +// to storage as though sync had been called. +// +// Releases any allocated resources, even if there is an error. +// +// Readonly and desynchronized files do not touch disk and will always +// return 0. +// +// Returns a negative error code on failure. +int lfs3_file_close(lfs3_t *lfs3, lfs3_file_t *file); + +// Synchronize a file on storage +// +// Any pending writes are written out to storage and other open files. +// +// If the file was desynchronized, it is now marked as synchronized. It will +// now recieve file updates and syncs on close. +// +// Returns a negative error code on failure. +int lfs3_file_sync(lfs3_t *lfs3, lfs3_file_t *file); + +// Flush any buffered data +// +// This does not update metadata and is called implicitly by lfs3_file_sync. +// Calling this explicitly may be useful for preventing write errors in +// read operations. +// +// Returns a negative error code on failure. +int lfs3_file_flush(lfs3_t *lfs3, lfs3_file_t *file); + +// Mark a file as desynchronized +// +// Desynchronized files do not recieve file updates and do not sync on close. +// They effectively act as snapshots of the underlying file at that point +// in time. +// +// If an error occurs during a write operation, the file is implicitly marked +// as desynchronized. +// +// An explicit and successful call to either lfs3_file_sync or +// lfs3_file_resync reverses this, marking the file as synchronized again. +// +// Returns a negative error code on failure. +int lfs3_file_desync(lfs3_t *lfs3, lfs3_file_t *file); + +// Discard unsynchronized changes and mark a file as synchronized +// +// This is effectively the same as closing and reopening the file, and +// may read from disk to figure out file state. +// +// Returns a negative error code on failure. +int lfs3_file_resync(lfs3_t *lfs3, lfs3_file_t *file); + +// Read data from file +// +// Takes a buffer and size indicating where to store the read data. +// Returns the number of bytes read, or a negative error code on failure. +lfs3_ssize_t lfs3_file_read(lfs3_t *lfs3, lfs3_file_t *file, + void *buffer, lfs3_size_t size); + +// Write data to file +// +// Takes a buffer and size indicating the data to write. The file will not +// actually be updated on the storage until either sync or close is called. +// +// Returns the number of bytes written, or a negative error code on failure. +#ifndef LFS3_RDONLY +lfs3_ssize_t lfs3_file_write(lfs3_t *lfs3, lfs3_file_t *file, + const void *buffer, lfs3_size_t size); +#endif + +// Change the position of the file +// +// The change in position is determined by the offset and whence flag. +// Returns the new position of the file, or a negative error code on failure. +lfs3_soff_t lfs3_file_seek(lfs3_t *lfs3, lfs3_file_t *file, + lfs3_soff_t off, uint32_t whence); + +// Truncate/grow the size of the file to the specified size +// +// If size is larger than the current file size, a hole is created, appearing +// as if the file was filled with zeros. +// +// Returns a negative error code on failure. +#ifndef LFS3_RDONLY +int lfs3_file_truncate(lfs3_t *lfs3, lfs3_file_t *file, lfs3_off_t size); +#endif + +// Truncate/grow the file, but from the front +// +// If size is larger than the current file size, a hole is created, appearing +// as if the file was filled with zeros. +// +// Returns a negative error code on failure. +#ifndef LFS3_RDONLY +int lfs3_file_fruncate(lfs3_t *lfs3, lfs3_file_t *file, lfs3_off_t size); +#endif + +// Return the position of the file +// +// Equivalent to lfs3_file_seek(lfs3, file, 0, LFS3_SEEK_CUR) +// Returns the position of the file, or a negative error code on failure. +lfs3_soff_t lfs3_file_tell(lfs3_t *lfs3, lfs3_file_t *file); + +// Change the position of the file to the beginning of the file +// +// Equivalent to lfs3_file_seek(lfs3, file, 0, LFS3_SEEK_SET) +// Returns a negative error code on failure. +int lfs3_file_rewind(lfs3_t *lfs3, lfs3_file_t *file); + +// Return the size of the file +// +// Similar to lfs3_file_seek(lfs3, file, 0, LFS3_SEEK_END) +// Returns the size of the file, or a negative error code on failure. +lfs3_soff_t lfs3_file_size(lfs3_t *lfs3, lfs3_file_t *file); + +// Check a file for errors and other work +// +// Returns LFS3_ERR_CORRUPT if a checksum mismatch is found, or a negative +// error code on failure. +int lfs3_file_ck(lfs3_t *lfs3, lfs3_file_t *file, uint32_t flags); + + +/// Directory operations /// + +// Create a directory +// +// Returns a negative error code on failure. +#ifndef LFS3_RDONLY +int lfs3_mkdir(lfs3_t *lfs3, const char *path); +#endif + +// Open a directory +// +// Once open a directory can be used with read to iterate over files. +// Returns a negative error code on failure. +int lfs3_dir_open(lfs3_t *lfs3, lfs3_dir_t *dir, const char *path); + +// Close a directory +// +// Releases any allocated resources. +// Returns a negative error code on failure. +int lfs3_dir_close(lfs3_t *lfs3, lfs3_dir_t *dir); + +// Read an entry in the directory +// +// Fills out the info structure, based on the specified file or directory. +// Returns 0 on success, LFS3_ERR_NOENT at the end of directory, or a +// negative error code on failure. +int lfs3_dir_read(lfs3_t *lfs3, lfs3_dir_t *dir, struct lfs3_info *info); + +// Change the position of the directory +// +// The new off must be a value previous returned from tell and specifies +// an absolute offset in the directory seek. +// +// Returns a negative error code on failure. +int lfs3_dir_seek(lfs3_t *lfs3, lfs3_dir_t *dir, lfs3_soff_t off); + +// Return the position of the directory +// +// The returned offset is only meant to be consumed by seek and may not make +// sense, but does indicate the current position in the directory iteration. +// +// Returns the position of the directory, or a negative error code on failure. +lfs3_soff_t lfs3_dir_tell(lfs3_t *lfs3, lfs3_dir_t *dir); + +// Change the position of the directory to the beginning of the directory +// +// Returns a negative error code on failure. +int lfs3_dir_rewind(lfs3_t *lfs3, lfs3_dir_t *dir); + + +/// Traversal operations /// + +// Open a traversal +// +// Once open, a traversal can be read from to iterate over all blocks in +// the filesystem. +// +// Returns a negative error code on failure. +int lfs3_trv_open(lfs3_t *lfs3, lfs3_trv_t *trv, uint32_t flags); + +// Close a traversal +// +// Releases any allocated resources. +// Returns a negative error code on failure. +int lfs3_trv_close(lfs3_t *lfs3, lfs3_trv_t *trv); + +// Progress the traversal and read an entry +// +// Fills out the tinfo structure. +// +// Returns 0 on success, LFS3_ERR_NOENT at the end of traversal, or a +// negative error code on failure. +int lfs3_trv_read(lfs3_t *lfs3, lfs3_trv_t *trv, + struct lfs3_tinfo *tinfo); + +// Reset the traversal +// +// Returns a negative error code on failure. +int lfs3_trv_rewind(lfs3_t *lfs3, lfs3_trv_t *trv); + + +/// Filesystem-level filesystem operations + +// Find on-disk info about the filesystem +// +// Fills out the fsinfo structure based on the filesystem found on-disk. +// Returns a negative error code on failure. +int lfs3_fs_stat(lfs3_t *lfs3, struct lfs3_fsinfo *fsinfo); + +// Finds the number of blocks in use by the filesystem +// +// Note: Result is best effort. If files share COW structures, the returned +// usage may be larger than the filesystem actually is. +// +// Returns the number of allocated blocks, or a negative error code on failure. +lfs3_ssize_t lfs3_fs_usage(lfs3_t *lfs3); + +// Get the current filesystem checksum +// +// This is a checksum of all metadata + data in the filesystem, which +// can be stored externally to provide increased protection against +// filesystem corruption. +// +// Note this checksum is order-sensitive. So while it's unlikely two +// filesystems with different contents will have the same checksum, two +// filesystems with the same contents may not have the same checksum. +// +// Also note this is only a 32-bit checksum. Collisions should be +// expected. +// +// Returns a negative error code on failure. +int lfs3_fs_cksum(lfs3_t *lfs3, uint32_t *cksum); + +// Attempt to make the filesystem consistent and ready for writing +// +// Calling this function is not required, consistency will be implicitly +// enforced on the first operation that writes to the filesystem, but this +// function allows the work to be performed earlier and without other +// filesystem changes. +// +// Returns a negative error code on failure. +#ifndef LFS3_RDONLY +int lfs3_fs_mkconsistent(lfs3_t *lfs3); +#endif + +// Check the filesystem for errors and other work +// +// This actually supports all janitorial work, but spins until all work +// is complete. See lfs3_fs_gc for incremental gc. +// +// Returns LFS3_ERR_CORRUPT if a checksum mismatch is found, or a negative +// error code on failure. +int lfs3_fs_ck(lfs3_t *lfs3, uint32_t flags); + +// Perform any janitorial work that may be pending +// +// The exact janitorial work depends on the configured flags and steps. +// +// Calling this function is not required, but may allow the offloading of +// expensive janitorial work to a less time-critical code path. +// +// Returns a negative error code on failure. +#ifdef LFS3_GC +int lfs3_fs_gc(lfs3_t *lfs3); +#endif + +// Mark janitorial work as incomplete +// +// Any info flags passed to lfs3_gc_unck will be reset internally, +// forcing the work to be redone. +// +// This is most useful for triggering new ckmeta/ckdata scans with +// LFS3_I_CANCKMETA and LFS3_I_CANCKDATA. Otherwise littlefs will perform +// only one scan after mount. +// +// Returns a negative error code on failure. +int lfs3_fs_unck(lfs3_t *lfs3, uint32_t flags); + +// Change the number of blocks used by the filesystem +// +// This changes the number of blocks we are currently using and updates +// the superblock with the new block count. +// +// Note: This is irreversible. +// +// Returns a negative error code on failure. +#ifndef LFS3_RDONLY +int lfs3_fs_grow(lfs3_t *lfs3, lfs3_size_t block_count); +#endif + +// Enable the global on-disk block-map +// +// Returns a negative error code on failure. Does nothing if a gbmap +// already exists. +#if !defined(LFS3_RDONLY) && defined(LFS3_GBMAP) && !defined(LFS3_YES_GBMAP) +int lfs3_fs_mkgbmap(lfs3_t *lfs3); +#endif + +// Disable the global on-disk block-map +// +// Returns a negative error code on failure. Does nothing if no gbmap +// is found. +#if !defined(LFS3_RDONLY) && defined(LFS3_GBMAP) && !defined(LFS3_YES_GBMAP) +int lfs3_fs_rmgbmap(lfs3_t *lfs3); +#endif + + +#endif diff --git a/lfs3_util.c b/lfs3_util.c new file mode 100644 index 000000000..41160cc84 --- /dev/null +++ b/lfs3_util.c @@ -0,0 +1,285 @@ +/* + * lfs3 util functions + * + * Copyright (c) 2022, The littlefs authors. + * Copyright (c) 2017, Arm Limited. All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + */ +#include "lfs3_util.h" + +// Only compile if user does not provide custom config +#ifndef LFS3_CONFIG + +// Need lfs3.h for error codes +// TODO should we actually move the error codes to lfs3_util.h? +#include "lfs3.h" + + +// Convert to/from leb128 encoding +ssize_t lfs3_toleb128(uint32_t word, void *buffer, size_t size) { + uint8_t *data = buffer; + + for (size_t i = 0; i < size; i++) { + uint8_t dat = word & 0x7f; + word >>= 7; + if (word != 0) { + data[i] = dat | 0x80; + } else { + data[i] = dat | 0x00; + return i+1; + } + } + + // buffer overflow? + LFS3_UNREACHABLE(); +} + +ssize_t lfs3_fromleb128(uint32_t *word, const void *buffer, size_t size) { + const uint8_t *data = buffer; + + int32_t word_ = 0; + for (size_t i = 0; i < size; i++) { + int32_t dat = data[i]; + word_ |= (dat & 0x7f) << 7*i; + if (!(dat & 0x80)) { + // did we overflow? + if ((word_ >> 7*i) != dat) { + return LFS3_ERR_CORRUPT; + } + + *word = word_; + return i+1; + } + } + + // truncated? + return LFS3_ERR_CORRUPT; +} + + +// crc32c tables (see lfs3_crc32c for more info) +#if !defined(LFS3_SMALLER_CRC32C) \ + && !defined(LFS3_FASTER_CRC32C) \ + && !defined(LFS3_PMUL_CRC32C) +static const uint32_t lfs3_crc32c_table[16] = { + 0x00000000, 0x105ec76f, 0x20bd8ede, 0x30e349b1, + 0x417b1dbc, 0x5125dad3, 0x61c69362, 0x7198540d, + 0x82f63b78, 0x92a8fc17, 0xa24bb5a6, 0xb21572c9, + 0xc38d26c4, 0xd3d3e1ab, 0xe330a81a, 0xf36e6f75, +}; +#endif + +#if defined(LFS3_FASTER_CRC32C) \ + && !defined(LFS3_SMALLER_CRC32C) \ + && !defined(LFS3_PMUL_CRC32C) +static const uint32_t lfs3_crc32c_table[256] = { + 0x00000000, 0xf26b8303, 0xe13b70f7, 0x1350f3f4, + 0xc79a971f, 0x35f1141c, 0x26a1e7e8, 0xd4ca64eb, + 0x8ad958cf, 0x78b2dbcc, 0x6be22838, 0x9989ab3b, + 0x4d43cfd0, 0xbf284cd3, 0xac78bf27, 0x5e133c24, + 0x105ec76f, 0xe235446c, 0xf165b798, 0x030e349b, + 0xd7c45070, 0x25afd373, 0x36ff2087, 0xc494a384, + 0x9a879fa0, 0x68ec1ca3, 0x7bbcef57, 0x89d76c54, + 0x5d1d08bf, 0xaf768bbc, 0xbc267848, 0x4e4dfb4b, + 0x20bd8ede, 0xd2d60ddd, 0xc186fe29, 0x33ed7d2a, + 0xe72719c1, 0x154c9ac2, 0x061c6936, 0xf477ea35, + 0xaa64d611, 0x580f5512, 0x4b5fa6e6, 0xb93425e5, + 0x6dfe410e, 0x9f95c20d, 0x8cc531f9, 0x7eaeb2fa, + 0x30e349b1, 0xc288cab2, 0xd1d83946, 0x23b3ba45, + 0xf779deae, 0x05125dad, 0x1642ae59, 0xe4292d5a, + 0xba3a117e, 0x4851927d, 0x5b016189, 0xa96ae28a, + 0x7da08661, 0x8fcb0562, 0x9c9bf696, 0x6ef07595, + 0x417b1dbc, 0xb3109ebf, 0xa0406d4b, 0x522bee48, + 0x86e18aa3, 0x748a09a0, 0x67dafa54, 0x95b17957, + 0xcba24573, 0x39c9c670, 0x2a993584, 0xd8f2b687, + 0x0c38d26c, 0xfe53516f, 0xed03a29b, 0x1f682198, + 0x5125dad3, 0xa34e59d0, 0xb01eaa24, 0x42752927, + 0x96bf4dcc, 0x64d4cecf, 0x77843d3b, 0x85efbe38, + 0xdbfc821c, 0x2997011f, 0x3ac7f2eb, 0xc8ac71e8, + 0x1c661503, 0xee0d9600, 0xfd5d65f4, 0x0f36e6f7, + 0x61c69362, 0x93ad1061, 0x80fde395, 0x72966096, + 0xa65c047d, 0x5437877e, 0x4767748a, 0xb50cf789, + 0xeb1fcbad, 0x197448ae, 0x0a24bb5a, 0xf84f3859, + 0x2c855cb2, 0xdeeedfb1, 0xcdbe2c45, 0x3fd5af46, + 0x7198540d, 0x83f3d70e, 0x90a324fa, 0x62c8a7f9, + 0xb602c312, 0x44694011, 0x5739b3e5, 0xa55230e6, + 0xfb410cc2, 0x092a8fc1, 0x1a7a7c35, 0xe811ff36, + 0x3cdb9bdd, 0xceb018de, 0xdde0eb2a, 0x2f8b6829, + 0x82f63b78, 0x709db87b, 0x63cd4b8f, 0x91a6c88c, + 0x456cac67, 0xb7072f64, 0xa457dc90, 0x563c5f93, + 0x082f63b7, 0xfa44e0b4, 0xe9141340, 0x1b7f9043, + 0xcfb5f4a8, 0x3dde77ab, 0x2e8e845f, 0xdce5075c, + 0x92a8fc17, 0x60c37f14, 0x73938ce0, 0x81f80fe3, + 0x55326b08, 0xa759e80b, 0xb4091bff, 0x466298fc, + 0x1871a4d8, 0xea1a27db, 0xf94ad42f, 0x0b21572c, + 0xdfeb33c7, 0x2d80b0c4, 0x3ed04330, 0xccbbc033, + 0xa24bb5a6, 0x502036a5, 0x4370c551, 0xb11b4652, + 0x65d122b9, 0x97baa1ba, 0x84ea524e, 0x7681d14d, + 0x2892ed69, 0xdaf96e6a, 0xc9a99d9e, 0x3bc21e9d, + 0xef087a76, 0x1d63f975, 0x0e330a81, 0xfc588982, + 0xb21572c9, 0x407ef1ca, 0x532e023e, 0xa145813d, + 0x758fe5d6, 0x87e466d5, 0x94b49521, 0x66df1622, + 0x38cc2a06, 0xcaa7a905, 0xd9f75af1, 0x2b9cd9f2, + 0xff56bd19, 0x0d3d3e1a, 0x1e6dcdee, 0xec064eed, + 0xc38d26c4, 0x31e6a5c7, 0x22b65633, 0xd0ddd530, + 0x0417b1db, 0xf67c32d8, 0xe52cc12c, 0x1747422f, + 0x49547e0b, 0xbb3ffd08, 0xa86f0efc, 0x5a048dff, + 0x8ecee914, 0x7ca56a17, 0x6ff599e3, 0x9d9e1ae0, + 0xd3d3e1ab, 0x21b862a8, 0x32e8915c, 0xc083125f, + 0x144976b4, 0xe622f5b7, 0xf5720643, 0x07198540, + 0x590ab964, 0xab613a67, 0xb831c993, 0x4a5a4a90, + 0x9e902e7b, 0x6cfbad78, 0x7fab5e8c, 0x8dc0dd8f, + 0xe330a81a, 0x115b2b19, 0x020bd8ed, 0xf0605bee, + 0x24aa3f05, 0xd6c1bc06, 0xc5914ff2, 0x37faccf1, + 0x69e9f0d5, 0x9b8273d6, 0x88d28022, 0x7ab90321, + 0xae7367ca, 0x5c18e4c9, 0x4f48173d, 0xbd23943e, + 0xf36e6f75, 0x0105ec76, 0x12551f82, 0xe03e9c81, + 0x34f4f86a, 0xc69f7b69, 0xd5cf889d, 0x27a40b9e, + 0x79b737ba, 0x8bdcb4b9, 0x988c474d, 0x6ae7c44e, + 0xbe2da0a5, 0x4c4623a6, 0x5f16d052, 0xad7d5351, +}; +#endif + + +// Calculate crc32c incrementally +uint32_t lfs3_crc32c(uint32_t crc, const void *buffer, size_t size) { + // init with 0xffffffff so prefixed zeros affect the crc + const uint8_t *buffer_ = buffer; + crc ^= 0xffffffff; + + // A couple crc32c implementations to choose from. + // + // The default, "small-table" implementation offers a decent performance + // without much additional code-size, reasonable for microcontrollers. For + // anything larger where you really don't care about an extra 1KiB of code + // the "big-table" implementation is probably better. + // + // Some quick measurements with GCC 11 using -Os -mcpu=cortex-m55, with + // instruction counts from QEMU and an input size of 4KiB. Note these are + // not cycle-accurate: + // + // code stack ins ld/st branch + // naive 48 12 221192 4099 36865 + // small-table 124 12 49160 12291 4097 + // big-table 1064 8 32776 8195 4097 + // + // If hardware pmul is present, these tables can be replaced with a + // technique called Barret reduction. + // + // The implementation here provides an example, but pmul hardware is + // often paired with SIMD which you may need to leverage to make the + // result performant. The m55 is a notably bad example in that it + // only has a 16-bit pmul, but can do 8 pmuls simultaneously: + // + // code stack ins ld/st branch + // pmul-naive-1x32 152 52 622603 5123 68609 + // pmul-tuned-8x16 316 72 6364 266 783 + // + // Intel's Fast CRC Computation for Generic Polynomials Using + // PCLMULQDQ Instruction whitepaper is an excellent resource on this + // topic. + // + #if defined(LFS3_SMALLER_CRC32C) && !defined(LFS3_PMUL_CRC32C) + // naive reduce + for (size_t i = 0; i < size; i++) { + crc = crc ^ buffer_[i]; + for (size_t j = 0; j < 8; j++) { + crc = (crc >> 1) ^ ((crc & 1) ? 0x82f63b78 : 0); + } + } + + #elif !defined(LFS3_FASTER_CRC32C) && !defined(LFS3_PMUL_CRC32C) + // reduce via small table + for (size_t i = 0; i < size; i++) { + crc = (crc >> 4) ^ lfs3_crc32c_table[0xf & (crc ^ (buffer_[i] >> 0))]; + crc = (crc >> 4) ^ lfs3_crc32c_table[0xf & (crc ^ (buffer_[i] >> 4))]; + } + + #elif defined(LFS3_FASTER_CRC32C) && !defined(LFS3_PMUL_CRC32C) + // reduce via big table + for (size_t i = 0; i < size; i++) { + crc = (crc >> 8) ^ lfs3_crc32c_table[0xff & (crc ^ buffer_[i])]; + } + + #elif defined(LFS3_PMUL_CRC32C) + // reduce via Barret reduction + for (size_t i = 0; i < size;) { + // align to 32-bits + if ((uintptr_t)&buffer_[i] % sizeof(uint32_t) == 0 + && i+sizeof(uint32_t) < size) { + crc = crc ^ lfs3_fromle32_(&buffer_[i]); + crc = lfs3_pmul( + lfs3_pmul(crc, 0xdea713f1), + 0x82f63b78) + >> 31; + i += 4; + } else { + crc = crc ^ buffer_[i]; + crc = (crc >> 8) + ^ (lfs3_pmul( + lfs3_pmul(crc << 24, 0xdea713f1), + 0x82f63b78) + >> 31); + i += 1; + } + } + + #endif + + // fini with 0xffffffff to cancel out init when called incrementally + crc ^= 0xffffffff; + return crc; +} + +// Multiply two crc32cs in the crc32c ring +uint32_t lfs3_crc32c_mul(uint32_t a, uint32_t b) { + // Multiplication in a crc32c ring involves polynomial + // multiplication modulo the crc32c polynomial to keep things + // finite: + // + // r = a * b mod P + // + // Note because our crc32c is not irreducible, this does not give + // us a finite-field, i.e. division is undefined. Still, + // multiplication has useful properties. + // + + // This gets a bit funky because crc32cs are little-endian, but + // fortunately pmul is symmetric. Though the result is awkwardly + // 63-bits, so we need to shift by 1. + uint64_t r = lfs3_pmul(a, b) << 1; + + // We can accelerate our module with crc32c tables if present, these + // loops may look familiar. + #if defined(LFS3_SMALLER_CRC32C) && !defined(LFS3_PMUL_CRC32C) + // naive reduce + for (int i = 0; i < 32; i++) { + r = (r >> 1) ^ ((r & 1) ? 0x82f63b78 : 0); + } + + #elif !defined(LFS3_FASTER_CRC32C) && !defined(LFS3_PMUL_CRC32C) + // reduce via small table + for (int i = 0; i < 8; i++) { + r = (r >> 4) ^ lfs3_crc32c_table[0xf & r]; + } + + #elif defined(LFS3_FASTER_CRC32C) && !defined(LFS3_PMUL_CRC32C) + // reduce via big table + for (int i = 0; i < 4; i++) { + r = (r >> 8) ^ lfs3_crc32c_table[0xff & r]; + } + + #elif defined(LFS3_PMUL_CRC32C) + // reduce via Barret reduction + r = (r >> 32) + ^ (lfs3_pmul( + lfs3_pmul(r, 0xdea713f1), + 0x82f63b78) + >> 31); + #endif + + return (uint32_t)r; +} + + +#endif diff --git a/lfs3_util.h b/lfs3_util.h new file mode 100644 index 000000000..52ef96b89 --- /dev/null +++ b/lfs3_util.h @@ -0,0 +1,749 @@ +/* + * lfs3 utility functions + * + * Copyright (c) 2022, The littlefs authors. + * Copyright (c) 2017, Arm Limited. All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + */ +#ifndef LFS3_UTIL_H +#define LFS3_UTIL_H + +// Users can override lfs3_util.h with their own configuration by defining +// LFS3_CFG as a header file to include (-DLFS3_CFG=my_cfg.h). +// +// If LFS3_CFG is used, none of the default utils will be emitted and must be +// provided by the config file. To start, I would suggest copying lfs3_util.h +// and modifying as needed. +#ifdef LFS3_CFG +#define LFS3_STRINGIZE(x) LFS3_STRINGIZE2(x) +#define LFS3_STRINGIZE2(x) #x +#include LFS3_STRINGIZE(LFS3_CFG) +#else + + +// Some convenient macro aliases +// TODO move these to something like lfs3_cfg.h? + +// LFS3_BIGGEST enables all opt-in features +#ifdef LFS3_BIGGEST +#ifndef LFS3_REVDBG +#define LFS3_REVDBG +#endif +#ifndef LFS3_REVNOISE +#define LFS3_REVNOISE +#endif +#ifndef LFS3_CKPROGS +#define LFS3_CKPROGS +#endif +#ifndef LFS3_CKFETCHES +#define LFS3_CKFETCHES +#endif +#ifndef LFS3_CKMETAPARITY +#define LFS3_CKMETAPARITY +#endif +#ifndef LFS3_CKDATACKSUMS +#define LFS3_CKDATACKSUMS +#endif +#ifndef LFS3_GC +#define LFS3_GC +#endif +#ifndef LFS3_GBMAP +#define LFS3_GBMAP +#endif +#ifndef LFS3_BLEAFCACHE +#define LFS3_BLEAFCACHE +#endif +#endif + +// LFS3_YES_* variants imply the relevant LFS3_* macro +#ifdef LFS3_YES_RDONLY +#define LFS3_RDONLY +#endif +#ifdef LFS3_YES_REVDBG +#define LFS3_REVDBG +#endif +#ifdef LFS3_YES_REVNOISE +#define LFS3_REVNOISE +#endif +#ifdef LFS3_YES_CKPROGS +#define LFS3_CKPROGS +#endif +#ifdef LFS3_YES_CKFETCHES +#define LFS3_CKFETCHES +#endif +#ifdef LFS3_YES_CKMETAPARITY +#define LFS3_CKMETAPARITY +#endif +#ifdef LFS3_YES_CKDATACKSUMS +#define LFS3_CKDATACKSUMS +#endif +#ifdef LFS3_YES_GC +#define LFS3_GC +#endif +#ifdef LFS3_YES_GBMAP +#define LFS3_GBMAP +#endif +#ifdef LFS3_YES_BLEAFCACHE +#define LFS3_BLEAFCACHE +#endif + +// LFS3_NO_LOG disables all logging macros +#ifdef LFS3_NO_LOG +#ifndef LFS3_NO_DEBUG +#define LFS3_NO_DEBUG +#endif +#ifndef LFS3_NO_INFO +#define LFS3_NO_INFO +#endif +#ifndef LFS3_NO_WARN +#define LFS3_NO_WARN +#endif +#ifndef LFS3_NO_ERROR +#define LFS3_NO_ERROR +#endif +#endif + + +// System includes +#include +#include +#include +#include +#ifndef LFS3_NO_STRINGH +#include +#endif +#ifndef LFS3_NO_MALLOC +#include +#endif +#ifndef LFS3_NO_ASSERT +#include +#endif +#if !defined(LFS3_NO_DEBUG) || \ + !defined(LFS3_NO_INFO) || \ + !defined(LFS3_NO_WARN) || \ + !defined(LFS3_NO_ERROR) || \ + defined(LFS3_YES_TRACE) +#include +#endif + + +// Macros, may be replaced by system specific wrappers. Arguments to these +// macros must not have side-effects as the macros can be removed for a smaller +// code footprint + +// Logging functions +#ifndef LFS3_TRACE +#ifdef LFS3_YES_TRACE +#define LFS3_TRACE_(fmt, ...) \ + printf("%s:%d:trace: " fmt "%s\n", __FILE__, __LINE__, __VA_ARGS__) +#define LFS3_TRACE(...) LFS3_TRACE_(__VA_ARGS__, "") +#else +#define LFS3_TRACE(...) +#endif +#endif + +#ifndef LFS3_DEBUG +#ifndef LFS3_NO_DEBUG +#define LFS3_DEBUG_(fmt, ...) \ + printf("%s:%d:debug: " fmt "%s\n", __FILE__, __LINE__, __VA_ARGS__) +#define LFS3_DEBUG(...) LFS3_DEBUG_(__VA_ARGS__, "") +#else +#define LFS3_DEBUG(...) +#endif +#endif + +#ifndef LFS3_INFO +#ifndef LFS3_NO_INFO +#define LFS3_INFO_(fmt, ...) \ + printf("%s:%d:info: " fmt "%s\n", __FILE__, __LINE__, __VA_ARGS__) +#define LFS3_INFO(...) LFS3_INFO_(__VA_ARGS__, "") +#else +#define LFS3_INFO(...) +#endif +#endif + +#ifndef LFS3_WARN +#ifndef LFS3_NO_WARN +#define LFS3_WARN_(fmt, ...) \ + printf("%s:%d:warn: " fmt "%s\n", __FILE__, __LINE__, __VA_ARGS__) +#define LFS3_WARN(...) LFS3_WARN_(__VA_ARGS__, "") +#else +#define LFS3_WARN(...) +#endif +#endif + +#ifndef LFS3_ERROR +#ifndef LFS3_NO_ERROR +#define LFS3_ERROR_(fmt, ...) \ + printf("%s:%d:error: " fmt "%s\n", __FILE__, __LINE__, __VA_ARGS__) +#define LFS3_ERROR(...) LFS3_ERROR_(__VA_ARGS__, "") +#else +#define LFS3_ERROR(...) +#endif +#endif + +// Runtime assertions +#ifndef LFS3_ASSERT +#ifndef LFS3_NO_ASSERT +#define LFS3_ASSERT(test) assert(test) +#else +#define LFS3_ASSERT(test) +#endif +#endif + +#ifndef LFS3_UNREACHABLE +#ifndef LFS3_NO_ASSERT +#define LFS3_UNREACHABLE() LFS3_ASSERT(false) +#elif !defined(LFS3_NO_BUILTINS) +#define LFS3_UNREACHABLE() __builtin_unreachable() +#else +#define LFS3_UNREACHABLE() +#endif +#endif + + +// Some ifdef conveniences +#ifdef LFS3_RDONLY +#define LFS3_IFDEF_RDONLY(a, b) (a) +#else +#define LFS3_IFDEF_RDONLY(a, b) (b) +#endif + +#ifdef LFS3_REVDBG +#define LFS3_IFDEF_REVDBG(a, b) (a) +#else +#define LFS3_IFDEF_REVDBG(a, b) (b) +#endif + +#ifdef LFS3_YES_REVDBG +#define LFS3_IFDEF_YES_REVDBG(a, b) (a) +#else +#define LFS3_IFDEF_YES_REVDBG(a, b) (b) +#endif + +#ifdef LFS3_REVNOISE +#define LFS3_IFDEF_REVNOISE(a, b) (a) +#else +#define LFS3_IFDEF_REVNOISE(a, b) (b) +#endif + +#ifdef LFS3_YES_REVNOISE +#define LFS3_IFDEF_YES_REVNOISE(a, b) (a) +#else +#define LFS3_IFDEF_YES_REVNOISE(a, b) (b) +#endif + +#ifdef LFS3_CKPROGS +#define LFS3_IFDEF_CKPROGS(a, b) (a) +#else +#define LFS3_IFDEF_CKPROGS(a, b) (b) +#endif + +#ifdef LFS3_CKFETCHES +#define LFS3_IFDEF_CKFETCHES(a, b) (a) +#else +#define LFS3_IFDEF_CKFETCHES(a, b) (b) +#endif + +#ifdef LFS3_CKMETAPARITY +#define LFS3_IFDEF_CKMETAPARITY(a, b) (a) +#else +#define LFS3_IFDEF_CKMETAPARITY(a, b) (b) +#endif + +#ifdef LFS3_CKDATACKSUMS +#define LFS3_IFDEF_CKDATACKSUMS(a, b) (a) +#else +#define LFS3_IFDEF_CKDATACKSUMS(a, b) (b) +#endif + +#ifdef LFS3_GC +#define LFS3_IFDEF_GC(a, b) (a) +#else +#define LFS3_IFDEF_GC(a, b) (b) +#endif + +#ifdef LFS3_GBMAP +#define LFS3_IFDEF_GBMAP(a, b) (a) +#else +#define LFS3_IFDEF_GBMAP(a, b) (b) +#endif + +// TODO other LFS3_IFDEF_YES_* macros? +#ifdef LFS3_YES_GBMAP +#define LFS3_IFDEF_YES_GBMAP(a, b) (a) +#else +#define LFS3_IFDEF_YES_GBMAP(a, b) (b) +#endif + +#ifdef LFS3_BLEAFCACHE +#define LFS3_IFDEF_BLEAFCACHE(a, b) (a) +#else +#define LFS3_IFDEF_BLEAFCACHE(a, b) (b) +#endif + + +// Some function attributes, no way around these + +// Force a function to be inlined +#if !defined(LFS3_NO_BUILTINS) && defined(__GNUC__) +#define LFS3_FORCEINLINE __attribute__((always_inline)) +#else +#define LFS3_FORCEINLINE +#endif + +// Force a function to _not_ be inlined +#if !defined(LFS3_NO_BUILTINS) && defined(__GNUC__) +#define LFS3_NOINLINE __attribute__((noinline)) +#else +#define LFS3_NOINLINE +#endif + + +// Builtin functions, these may be replaced by more efficient +// toolchain-specific implementations. LFS3_NO_BUILTINS falls back to a more +// expensive basic C implementation for debugging purposes +// +// Most of the backup implementations are based on the infamous Bit +// Twiddling Hacks compiled by Sean Eron Anderson: +// https://graphics.stanford.edu/~seander/bithacks.html +// + +// Compile time min/max +#define LFS3_MIN(a, b) (((a) < (b)) ? (a) : (b)) +#define LFS3_MAX(a, b) (((a) > (b)) ? (a) : (b)) + +// Min/max functions for unsigned 32-bit numbers +static inline uint32_t lfs3_min(uint32_t a, uint32_t b) { + return (a < b) ? a : b; +} + +static inline uint32_t lfs3_max(uint32_t a, uint32_t b) { + return (a > b) ? a : b; +} + +static inline int32_t lfs3_smin(int32_t a, int32_t b) { + return (a < b) ? a : b; +} + +static inline int32_t lfs3_smax(int32_t a, int32_t b) { + return (a > b) ? a : b; +} + +// Absolute value of signed numbers +static inline int32_t lfs3_abs(int32_t a) { + return (a < 0) ? -a : a; +} + +// Swap two variables +#define LFS3_SWAP(_t, _a, _b) \ + do { \ + _t *a = _a; \ + _t *b = _b; \ + _t t = *a; \ + *a = *b; \ + *b = t; \ + } while (0) + +// Align to nearest multiple of a size +static inline uint32_t lfs3_aligndown(uint32_t a, uint32_t alignment) { + return a - (a % alignment); +} + +static inline uint32_t lfs3_alignup(uint32_t a, uint32_t alignment) { + return lfs3_aligndown(a + alignment-1, alignment); +} + +// Find the smallest power of 2 greater than or equal to a +static inline uint32_t lfs3_nlog2(uint32_t a) { + // __builtin_clz of zero is undefined, so treat both 0 and 1 specially + if (a <= 1) { + return a; + } + +#if !defined(LFS3_NO_BUILTINS) && (defined(__GNUC__) || defined(__CC_ARM)) + return 32 - __builtin_clz(a-1); +#else + uint32_t r = 0; + uint32_t s; + a -= 1; + s = (a > 0xffff) << 4; a >>= s; r |= s; + s = (a > 0xff ) << 3; a >>= s; r |= s; + s = (a > 0xf ) << 2; a >>= s; r |= s; + s = (a > 0x3 ) << 1; a >>= s; r |= s; + return (r | (a >> 1)) + 1; +#endif +} + +// Count the number of trailing binary zeros in a +// lfs3_ctz(0) may be undefined +static inline uint32_t lfs3_ctz(uint32_t a) { +#if !defined(LFS3_NO_BUILTINS) && defined(__GNUC__) + return __builtin_ctz(a); +#else + return lfs3_nlog2((a & -a) + 1) - 1; +#endif +} + +// Count the number of binary ones in a +static inline uint32_t lfs3_popc(uint32_t a) { +#if !defined(LFS3_NO_BUILTINS) && (defined(__GNUC__) || defined(__CC_ARM)) + return __builtin_popcount(a); +#else + a = a - ((a >> 1) & 0x55555555); + a = (a & 0x33333333) + ((a >> 2) & 0x33333333); + a = (a + (a >> 4)) & 0x0f0f0f0f; + return (a * 0x1010101) >> 24; +#endif +} + +// Returns true if there is an odd number of binary ones in a +static inline bool lfs3_parity(uint32_t a) { +#if !defined(LFS3_NO_BUILTINS) && (defined(__GNUC__) || defined(__CC_ARM)) + return __builtin_parity(a); +#else + a ^= a >> 16; + a ^= a >> 8; + a ^= a >> 4; + return (0x6996 >> (a & 0xf)) & 1; +#endif +} + +// Find the sequence comparison of a and b, this is the distance +// between a and b ignoring overflow +static inline int lfs3_scmp(uint32_t a, uint32_t b) { + return (int)(unsigned)(a - b); +} + +// Perform polynomial/carry-less multiplication +// +// This is a multiply where all adds are replaced with xors. If we view +// a and b as binary polynomials, xor is polynomial addition and pmul is +// polynomial multiplication. +static inline uint64_t lfs3_pmul(uint32_t a, uint32_t b) { + uint64_t r = 0; + uint64_t a_ = a; + while (b) { + if (b & 1) { + r ^= a_; + } + a_ <<= 1; + b >>= 1; + } + return r; +} + + +// Convert to/from 32-bit little-endian +static inline void lfs3_tole32(uint32_t word, void *buffer) { + ((uint8_t*)buffer)[0] = word >> 0; + ((uint8_t*)buffer)[1] = word >> 8; + ((uint8_t*)buffer)[2] = word >> 16; + ((uint8_t*)buffer)[3] = word >> 24; +} + +static inline uint32_t lfs3_fromle32(const void *buffer) { + return (((uint8_t*)buffer)[0] << 0) + | (((uint8_t*)buffer)[1] << 8) + | (((uint8_t*)buffer)[2] << 16) + | (((uint8_t*)buffer)[3] << 24); +} + +// Convert to/from leb128 encoding +// TODO should we really be using ssize_t here and not lfs3_ssize_t? +ssize_t lfs3_toleb128(uint32_t word, void *buffer, size_t size); + +ssize_t lfs3_fromleb128(uint32_t *word, const void *buffer, size_t size); + + +// Compare n bytes of memory +#if !defined(LFS3_NO_STRINGH) +#define lfs3_memcmp memcmp +#elif !defined(LFS3_NO_BUILTINS) +#define lfs3_memcmp __builtin_memcmp +#else +static inline int lfs3_memcmp(const void *a, const void *b, size_t size) { + const uint8_t *a_ = a; + const uint8_t *b_ = b; + for (size_t i = 0; i < size; i++) { + if (a_[i] != b_[i]) { + return (int)a_[i] - (int)b_[i]; + } + } + + return 0; +} +#endif + +// Copy n bytes from src to dst, src and dst must not overlap +#if !defined(LFS3_NO_STRINGH) +#define lfs3_memcpy memcpy +#elif !defined(LFS3_NO_BUILTINS) +#define lfs3_memcpy __builtin_memcpy +#else +static inline void *lfs3_memcpy( + void *restrict dst, const void *restrict src, size_t size) { + uint8_t *dst_ = dst; + const uint8_t *src_ = src; + for (size_t i = 0; i < size; i++) { + dst_[i] = src_[i]; + } + + return dst_; +} +#endif + +// Copy n bytes from src to dst, src and dst may overlap +#if !defined(LFS3_NO_STRINGH) +#define lfs3_memmove memmove +#elif !defined(LFS3_NO_BUILTINS) +#define lfs3_memmove __builtin_memmove +#else +static inline void *lfs3_memmove(void *dst, const void *src, size_t size) { + uint8_t *dst_ = dst; + const uint8_t *src_ = src; + if (dst_ < src_) { + for (size_t i = 0; i < size; i++) { + dst_[i] = src_[i]; + } + } else if (dst_ > src_) { + for (size_t i = 0; i < size; i++) { + dst_[(size-1)-i] = src_[(size-1)-i]; + } + } + + return dst_; +} +#endif + +// Set n bytes to c +#if !defined(LFS3_NO_STRINGH) +#define lfs3_memset memset +#elif !defined(LFS3_NO_BUILTINS) +#define lfs3_memset __builtin_memset +#else +static inline void *lfs3_memset(void *dst, int c, size_t size) { + uint8_t *dst_ = dst; + for (size_t i = 0; i < size; i++) { + dst_[i] = c; + } + + return dst_; +} +#endif + +// Find the first occurrence of c or NULL +#if !defined(LFS3_NO_STRINGH) +#define lfs3_memchr memchr +#else +static inline void *lfs3_memchr(const void *a, int c, size_t size) { + const uint8_t *a_ = a; + for (size_t i = 0; i < size; i++) { + if (a_[i] == c) { + return (void*)&a_[i]; + } + } + + return NULL; +} +#endif + +// Find the first occurrence of anything not c or NULL +static inline void *lfs3_memcchr(const void *a, int c, size_t size) { + const uint8_t *a_ = a; + for (size_t i = 0; i < size; i++) { + if (a_[i] != c) { + return (void*)&a_[i]; + } + } + + return NULL; +} + +// Find the minimum length that includes all non-zero bytes +static inline size_t lfs3_memlen(const void *a, size_t size) { + const uint8_t *a_ = a; + while (size > 0 && a_[size-1] == 0) { + size -= 1; + } + + return size; +} + +// Xor n bytes from b into a +static inline void *lfs3_memxor( + void *restrict a, const void *restrict b, size_t size) { + uint8_t *a_ = a; + const uint8_t *b_ = b; + for (size_t i = 0; i < size; i++) { + a_[i] ^= b_[i]; + } + + return a_; +} + + +// Find the length of a null-terminated string +#if !defined(LFS3_NO_STRINGH) +#define lfs3_strlen strlen +#else +static inline size_t lfs3_strlen(const char *a) { + const char *a_ = a; + while (*a_) { + a_++; + } + + return a_ - a; +} +#endif + +// Compare two null-terminated strings +#if !defined(LFS3_NO_STRINGH) +#define lfs3_strcmp strcmp +#else +static inline int lfs3_strcmp(const char *a, const char *b) { + while (*a && *a == *b) { + a++; + b++; + } + + return (int)*a - (int)*b; +} +#endif + +// Copy a null-terminated string from src to dst +#if !defined(LFS3_NO_STRINGH) +#define lfs3_strcpy strcpy +#else +static inline char *lfs3_strcpy( + char *restrict dst, const char *restrict src) { + char *dst_ = dst; + while (*src) { + *dst_ = *src; + dst_++; + src++; + } + + *dst_ = '\0'; + return dst; +} +#endif + +// Find first occurrence of c or NULL +#ifndef LFS3_NO_STRINGH +#define lfs3_strchr strchr +#else +static inline char *lfs3_strchr(const char *a, int c) { + while (*a) { + if (*a == c) { + return (char*)a; + } + + a++; + } + + return NULL; +} +#endif + +// Find first occurrence of anything not c or NULL +static inline char *lfs3_strcchr(const char *a, int c) { + while (*a) { + if (*a != c) { + return (char*)a; + } + + a++; + } + + return NULL; +} + +// Find length of a that does not contain any char in cs +#ifndef LFS3_NO_STRINGH +#define lfs3_strspn strspn +#else +static inline size_t lfs3_strspn(const char *a, const char *cs) { + const char *a_ = a; + while (*a_) { + const char *cs_ = cs; + while (*cs_) { + if (*a_ != *cs_) { + return a_ - a; + } + cs_++; + } + + a_++; + } + + return a_ - a; +} +#endif + +// Find length of a that only contains chars in cs +#ifndef LFS3_NO_STRINGH +#define lfs3_strcspn strcspn +#else +static inline size_t lfs3_strcspn(const char *a, const char *cs) { + const char *a_ = a; + while (*a_) { + const char *cs_ = cs; + while (*cs_) { + if (*a_ == *cs_) { + return a_ - a; + } + cs_++; + } + + a_++; + } + + return a_ - a; +} +#endif + + +// Odd-parity and even-parity zeros in our crc32c ring +#define LFS3_CRC32C_ODDZERO 0xfca42daf +#define LFS3_CRC32C_EVENZERO 0x00000000 + +// Calculate crc32c incrementally +// +// polynomial = 0x11edc6f41 +// init = 0xffffffff +// fini = 0xffffffff +// +uint32_t lfs3_crc32c(uint32_t crc, const void *buffer, size_t size); + +// Multiply two crc32cs in the crc32c ring +uint32_t lfs3_crc32c_mul(uint32_t a, uint32_t b); + +// Find the cube of a crc32c in the crc32c ring +static inline uint32_t lfs3_crc32c_cube(uint32_t a) { + return lfs3_crc32c_mul(lfs3_crc32c_mul(a, a), a); +} + + +// Allocate memory, only used if buffers are not provided to littlefs +#ifndef LFS3_NO_MALLOC +#define lfs3_malloc malloc +#else +static inline void *lfs3_malloc(size_t size) { + (void)size; + return NULL; +} +#endif + +// Deallocate memory, only used if buffers are not provided to littlefs +#ifndef LFS3_NO_MALLOC +#define lfs3_free free +#else +static inline void lfs3_free(void *p) { + (void)p; +} +#endif + + +#endif +#endif diff --git a/lfs_util.c b/lfs_util.c deleted file mode 100644 index 9cdd1c60e..000000000 --- a/lfs_util.c +++ /dev/null @@ -1,34 +0,0 @@ -/* - * lfs util functions - * - * Copyright (c) 2022, The littlefs authors. - * Copyright (c) 2017, Arm Limited. All rights reserved. - * SPDX-License-Identifier: BSD-3-Clause - */ -#include "lfs_util.h" - -// Only compile if user does not provide custom config -#ifndef LFS_CONFIG - - -// Software CRC implementation with small lookup table -uint32_t lfs_crc(uint32_t crc, const void *buffer, size_t size) { - static const uint32_t rtable[16] = { - 0x00000000, 0x1db71064, 0x3b6e20c8, 0x26d930ac, - 0x76dc4190, 0x6b6b51f4, 0x4db26158, 0x5005713c, - 0xedb88320, 0xf00f9344, 0xd6d6a3e8, 0xcb61b38c, - 0x9b64c2b0, 0x86d3d2d4, 0xa00ae278, 0xbdbdf21c, - }; - - const uint8_t *data = buffer; - - for (size_t i = 0; i < size; i++) { - crc = (crc >> 4) ^ rtable[(crc ^ (data[i] >> 0)) & 0xf]; - crc = (crc >> 4) ^ rtable[(crc ^ (data[i] >> 4)) & 0xf]; - } - - return crc; -} - - -#endif diff --git a/lfs_util.h b/lfs_util.h deleted file mode 100644 index 7f79defd2..000000000 --- a/lfs_util.h +++ /dev/null @@ -1,244 +0,0 @@ -/* - * lfs utility functions - * - * Copyright (c) 2022, The littlefs authors. - * Copyright (c) 2017, Arm Limited. All rights reserved. - * SPDX-License-Identifier: BSD-3-Clause - */ -#ifndef LFS_UTIL_H -#define LFS_UTIL_H - -// Users can override lfs_util.h with their own configuration by defining -// LFS_CONFIG as a header file to include (-DLFS_CONFIG=lfs_config.h). -// -// If LFS_CONFIG is used, none of the default utils will be emitted and must be -// provided by the config file. To start, I would suggest copying lfs_util.h -// and modifying as needed. -#ifdef LFS_CONFIG -#define LFS_STRINGIZE(x) LFS_STRINGIZE2(x) -#define LFS_STRINGIZE2(x) #x -#include LFS_STRINGIZE(LFS_CONFIG) -#else - -// System includes -#include -#include -#include -#include -#include - -#ifndef LFS_NO_MALLOC -#include -#endif -#ifndef LFS_NO_ASSERT -#include -#endif -#if !defined(LFS_NO_DEBUG) || \ - !defined(LFS_NO_WARN) || \ - !defined(LFS_NO_ERROR) || \ - defined(LFS_YES_TRACE) -#include -#endif - -#ifdef __cplusplus -extern "C" -{ -#endif - - -// Macros, may be replaced by system specific wrappers. Arguments to these -// macros must not have side-effects as the macros can be removed for a smaller -// code footprint - -// Logging functions -#ifndef LFS_TRACE -#ifdef LFS_YES_TRACE -#define LFS_TRACE_(fmt, ...) \ - printf("%s:%d:trace: " fmt "%s\n", __FILE__, __LINE__, __VA_ARGS__) -#define LFS_TRACE(...) LFS_TRACE_(__VA_ARGS__, "") -#else -#define LFS_TRACE(...) -#endif -#endif - -#ifndef LFS_DEBUG -#ifndef LFS_NO_DEBUG -#define LFS_DEBUG_(fmt, ...) \ - printf("%s:%d:debug: " fmt "%s\n", __FILE__, __LINE__, __VA_ARGS__) -#define LFS_DEBUG(...) LFS_DEBUG_(__VA_ARGS__, "") -#else -#define LFS_DEBUG(...) -#endif -#endif - -#ifndef LFS_WARN -#ifndef LFS_NO_WARN -#define LFS_WARN_(fmt, ...) \ - printf("%s:%d:warn: " fmt "%s\n", __FILE__, __LINE__, __VA_ARGS__) -#define LFS_WARN(...) LFS_WARN_(__VA_ARGS__, "") -#else -#define LFS_WARN(...) -#endif -#endif - -#ifndef LFS_ERROR -#ifndef LFS_NO_ERROR -#define LFS_ERROR_(fmt, ...) \ - printf("%s:%d:error: " fmt "%s\n", __FILE__, __LINE__, __VA_ARGS__) -#define LFS_ERROR(...) LFS_ERROR_(__VA_ARGS__, "") -#else -#define LFS_ERROR(...) -#endif -#endif - -// Runtime assertions -#ifndef LFS_ASSERT -#ifndef LFS_NO_ASSERT -#define LFS_ASSERT(test) assert(test) -#else -#define LFS_ASSERT(test) -#endif -#endif - - -// Builtin functions, these may be replaced by more efficient -// toolchain-specific implementations. LFS_NO_INTRINSICS falls back to a more -// expensive basic C implementation for debugging purposes - -// Min/max functions for unsigned 32-bit numbers -static inline uint32_t lfs_max(uint32_t a, uint32_t b) { - return (a > b) ? a : b; -} - -static inline uint32_t lfs_min(uint32_t a, uint32_t b) { - return (a < b) ? a : b; -} - -// Align to nearest multiple of a size -static inline uint32_t lfs_aligndown(uint32_t a, uint32_t alignment) { - return a - (a % alignment); -} - -static inline uint32_t lfs_alignup(uint32_t a, uint32_t alignment) { - return lfs_aligndown(a + alignment-1, alignment); -} - -// Find the smallest power of 2 greater than or equal to a -static inline uint32_t lfs_npw2(uint32_t a) { -#if !defined(LFS_NO_INTRINSICS) && (defined(__GNUC__) || defined(__CC_ARM)) - return 32 - __builtin_clz(a-1); -#else - uint32_t r = 0; - uint32_t s; - a -= 1; - s = (a > 0xffff) << 4; a >>= s; r |= s; - s = (a > 0xff ) << 3; a >>= s; r |= s; - s = (a > 0xf ) << 2; a >>= s; r |= s; - s = (a > 0x3 ) << 1; a >>= s; r |= s; - return (r | (a >> 1)) + 1; -#endif -} - -// Count the number of trailing binary zeros in a -// lfs_ctz(0) may be undefined -static inline uint32_t lfs_ctz(uint32_t a) { -#if !defined(LFS_NO_INTRINSICS) && defined(__GNUC__) - return __builtin_ctz(a); -#else - return lfs_npw2((a & -a) + 1) - 1; -#endif -} - -// Count the number of binary ones in a -static inline uint32_t lfs_popc(uint32_t a) { -#if !defined(LFS_NO_INTRINSICS) && (defined(__GNUC__) || defined(__CC_ARM)) - return __builtin_popcount(a); -#else - a = a - ((a >> 1) & 0x55555555); - a = (a & 0x33333333) + ((a >> 2) & 0x33333333); - return (((a + (a >> 4)) & 0xf0f0f0f) * 0x1010101) >> 24; -#endif -} - -// Find the sequence comparison of a and b, this is the distance -// between a and b ignoring overflow -static inline int lfs_scmp(uint32_t a, uint32_t b) { - return (int)(unsigned)(a - b); -} - -// Convert between 32-bit little-endian and native order -static inline uint32_t lfs_fromle32(uint32_t a) { -#if (defined( BYTE_ORDER ) && defined( ORDER_LITTLE_ENDIAN ) && BYTE_ORDER == ORDER_LITTLE_ENDIAN ) || \ - (defined(__BYTE_ORDER ) && defined(__ORDER_LITTLE_ENDIAN ) && __BYTE_ORDER == __ORDER_LITTLE_ENDIAN ) || \ - (defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) - return a; -#elif !defined(LFS_NO_INTRINSICS) && ( \ - (defined( BYTE_ORDER ) && defined( ORDER_BIG_ENDIAN ) && BYTE_ORDER == ORDER_BIG_ENDIAN ) || \ - (defined(__BYTE_ORDER ) && defined(__ORDER_BIG_ENDIAN ) && __BYTE_ORDER == __ORDER_BIG_ENDIAN ) || \ - (defined(__BYTE_ORDER__) && defined(__ORDER_BIG_ENDIAN__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__)) - return __builtin_bswap32(a); -#else - return (((uint8_t*)&a)[0] << 0) | - (((uint8_t*)&a)[1] << 8) | - (((uint8_t*)&a)[2] << 16) | - (((uint8_t*)&a)[3] << 24); -#endif -} - -static inline uint32_t lfs_tole32(uint32_t a) { - return lfs_fromle32(a); -} - -// Convert between 32-bit big-endian and native order -static inline uint32_t lfs_frombe32(uint32_t a) { -#if !defined(LFS_NO_INTRINSICS) && ( \ - (defined( BYTE_ORDER ) && defined( ORDER_LITTLE_ENDIAN ) && BYTE_ORDER == ORDER_LITTLE_ENDIAN ) || \ - (defined(__BYTE_ORDER ) && defined(__ORDER_LITTLE_ENDIAN ) && __BYTE_ORDER == __ORDER_LITTLE_ENDIAN ) || \ - (defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)) - return __builtin_bswap32(a); -#elif (defined( BYTE_ORDER ) && defined( ORDER_BIG_ENDIAN ) && BYTE_ORDER == ORDER_BIG_ENDIAN ) || \ - (defined(__BYTE_ORDER ) && defined(__ORDER_BIG_ENDIAN ) && __BYTE_ORDER == __ORDER_BIG_ENDIAN ) || \ - (defined(__BYTE_ORDER__) && defined(__ORDER_BIG_ENDIAN__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__) - return a; -#else - return (((uint8_t*)&a)[0] << 24) | - (((uint8_t*)&a)[1] << 16) | - (((uint8_t*)&a)[2] << 8) | - (((uint8_t*)&a)[3] << 0); -#endif -} - -static inline uint32_t lfs_tobe32(uint32_t a) { - return lfs_frombe32(a); -} - -// Calculate CRC-32 with polynomial = 0x04c11db7 -uint32_t lfs_crc(uint32_t crc, const void *buffer, size_t size); - -// Allocate memory, only used if buffers are not provided to littlefs -// Note, memory must be 64-bit aligned -static inline void *lfs_malloc(size_t size) { -#ifndef LFS_NO_MALLOC - return malloc(size); -#else - (void)size; - return NULL; -#endif -} - -// Deallocate memory, only used if buffers are not provided to littlefs -static inline void lfs_free(void *p) { -#ifndef LFS_NO_MALLOC - free(p); -#else - (void)p; -#endif -} - - -#ifdef __cplusplus -} /* extern "C" */ -#endif - -#endif -#endif diff --git a/runners/bench_runner.c b/runners/bench_runner.c index ba791b251..4f2b23a98 100644 --- a/runners/bench_runner.c +++ b/runners/bench_runner.c @@ -9,7 +9,7 @@ #endif #include "runners/bench_runner.h" -#include "bd/lfs_emubd.h" +#include "bd/lfs3_emubd.h" #include #include @@ -20,6 +20,7 @@ #include #include #include +#include #include @@ -59,7 +60,7 @@ static void leb16_print(uintmax_t x) { } while (true) { - char nibble = (x & 0xf) | (x > 0xf ? 0x10 : 0); + char nibble = (x & 0xf) | ((x > 0xf) ? 0x10 : 0); printf("%c", (nibble < 10) ? '0'+nibble : 'a'+nibble-10); if (x <= 0xf) { break; @@ -103,318 +104,322 @@ static uintmax_t leb16_parse(const char *s, char **tail) { if (tail) { *tail = (char*)s; } - return neg ? -x : x; + return (neg) ? -x : x; } // bench_runner types -typedef struct bench_geometry { - const char *name; - bench_define_t defines[BENCH_GEOMETRY_DEFINE_COUNT]; -} bench_geometry_t; - typedef struct bench_id { const char *name; - const bench_define_t *defines; + bench_define_t *defines; size_t define_count; } bench_id_t; -// bench suites are linked into a custom ld section -extern struct bench_suite __start__bench_suites; -extern struct bench_suite __stop__bench_suites; - -const struct bench_suite *bench_suites = &__start__bench_suites; -#define BENCH_SUITE_COUNT \ - ((size_t)(&__stop__bench_suites - &__start__bench_suites)) - - // bench define management -typedef struct bench_define_map { - const bench_define_t *defines; - size_t count; -} bench_define_map_t; -typedef struct bench_define_names { - const char *const *names; - size_t count; -} bench_define_names_t; - -intmax_t bench_define_lit(void *data) { - return (intptr_t)data; -} - -#define BENCH_CONST(x) {bench_define_lit, (void*)(uintptr_t)(x)} -#define BENCH_LIT(x) ((bench_define_t)BENCH_CONST(x)) +// implicit defines declared here +#define BENCH_DEFINE(k, v) \ + intmax_t k; + BENCH_IMPLICIT_DEFINES +#undef BENCH_DEFINE -#define BENCH_DEF(k, v) \ - intmax_t bench_define_##k(void *data) { \ +#define BENCH_DEFINE(k, v) \ + intmax_t bench_define_##k(void *data, size_t i) { \ (void)data; \ + (void)i; \ return v; \ } BENCH_IMPLICIT_DEFINES -#undef BENCH_DEF - -#define BENCH_DEFINE_MAP_OVERRIDE 0 -#define BENCH_DEFINE_MAP_EXPLICIT 1 -#define BENCH_DEFINE_MAP_PERMUTATION 2 -#define BENCH_DEFINE_MAP_GEOMETRY 3 -#define BENCH_DEFINE_MAP_IMPLICIT 4 -#define BENCH_DEFINE_MAP_COUNT 5 - -bench_define_map_t bench_define_maps[BENCH_DEFINE_MAP_COUNT] = { - [BENCH_DEFINE_MAP_IMPLICIT] = { - (const bench_define_t[BENCH_IMPLICIT_DEFINE_COUNT]) { - #define BENCH_DEF(k, v) \ - [k##_i] = {bench_define_##k, NULL}, - - BENCH_IMPLICIT_DEFINES - #undef BENCH_DEF - }, - BENCH_IMPLICIT_DEFINE_COUNT, - }, -}; +#undef BENCH_DEFINE -#define BENCH_DEFINE_NAMES_SUITE 0 -#define BENCH_DEFINE_NAMES_IMPLICIT 1 -#define BENCH_DEFINE_NAMES_COUNT 2 - -bench_define_names_t bench_define_names[BENCH_DEFINE_NAMES_COUNT] = { - [BENCH_DEFINE_NAMES_IMPLICIT] = { - (const char *const[BENCH_IMPLICIT_DEFINE_COUNT]){ - #define BENCH_DEF(k, v) \ - [k##_i] = #k, - - BENCH_IMPLICIT_DEFINES - #undef BENCH_DEF - }, - BENCH_IMPLICIT_DEFINE_COUNT, - }, -}; +const bench_define_t bench_implicit_defines[] = { + #define BENCH_DEFINE(k, v) \ + {#k, &k, bench_define_##k, NULL, 1}, -intmax_t *bench_define_cache; -size_t bench_define_cache_count; -unsigned *bench_define_cache_mask; - -const char *bench_define_name(size_t define) { - // lookup in our bench names - for (size_t i = 0; i < BENCH_DEFINE_NAMES_COUNT; i++) { - if (define < bench_define_names[i].count - && bench_define_names[i].names - && bench_define_names[i].names[define]) { - return bench_define_names[i].names[define]; - } - } + BENCH_IMPLICIT_DEFINES + #undef BENCH_DEFINE +}; +const size_t bench_implicit_define_count + = sizeof(bench_implicit_defines) / sizeof(bench_define_t); - return NULL; +// some helpers +intmax_t bench_define_lit(void *data, size_t i) { + (void)i; + return (intptr_t)data; } -bool bench_define_ispermutation(size_t define) { - // is this define specific to the permutation? - for (size_t i = 0; i < BENCH_DEFINE_MAP_IMPLICIT; i++) { - if (define < bench_define_maps[i].count - && bench_define_maps[i].defines[define].cb) { - return true; - } - } +#define BENCH_LIT(name, v) ((bench_define_t){ \ + name, NULL, bench_define_lit, (void*)(uintptr_t)(v), 1}) - return false; -} -intmax_t bench_define(size_t define) { - // is the define in our cache? - if (define < bench_define_cache_count - && (bench_define_cache_mask[define/(8*sizeof(unsigned))] - & (1 << (define%(8*sizeof(unsigned)))))) { - return bench_define_cache[define]; - } +// define mapping +const bench_define_t **bench_defines = NULL; +size_t bench_define_count = 0; +size_t bench_define_capacity = 0; - // lookup in our bench defines - for (size_t i = 0; i < BENCH_DEFINE_MAP_COUNT; i++) { - if (define < bench_define_maps[i].count - && bench_define_maps[i].defines[define].cb) { - intmax_t v = bench_define_maps[i].defines[define].cb( - bench_define_maps[i].defines[define].data); +const bench_define_t **bench_suite_defines = NULL; +size_t bench_suite_define_count = 0; +ssize_t *bench_suite_define_map = NULL; - // insert into cache! - bench_define_cache[define] = v; - bench_define_cache_mask[define / (8*sizeof(unsigned))] - |= 1 << (define%(8*sizeof(unsigned))); +bench_define_t *bench_override_defines = NULL; +size_t bench_override_define_count = 0; - return v; - } - } +size_t bench_define_depth = 1000; - return 0; - // not found? - const char *name = bench_define_name(define); - fprintf(stderr, "error: undefined define %s (%zd)\n", - name ? name : "(unknown)", - define); - assert(false); - exit(-1); +static inline bool bench_define_isdefined(const bench_define_t *define) { + return define->cb; } -void bench_define_flush(void) { - // clear cache between permutations - memset(bench_define_cache_mask, 0, - sizeof(unsigned)*( - (bench_define_cache_count+(8*sizeof(unsigned))-1) - / (8*sizeof(unsigned)))); +static inline bool bench_define_ispermutation(const bench_define_t *define) { + // permutation defines are basically anything that's not implicit + return bench_define_isdefined(define) + && !(define >= bench_implicit_defines + && define + < bench_implicit_defines + + bench_implicit_define_count); } -// geometry updates -const bench_geometry_t *bench_geometry = NULL; - -void bench_define_geometry(const bench_geometry_t *geometry) { - bench_define_maps[BENCH_DEFINE_MAP_GEOMETRY] = (bench_define_map_t){ - geometry->defines, BENCH_GEOMETRY_DEFINE_COUNT}; -} -// override updates -typedef struct bench_override { - const char *name; - const intmax_t *defines; - size_t permutations; -} bench_override_t; - -const bench_override_t *bench_overrides = NULL; -size_t bench_override_count = 0; - -bench_define_t *bench_override_defines = NULL; -size_t bench_override_define_count = 0; -size_t bench_override_define_permutations = 1; -size_t bench_override_define_capacity = 0; - -// suite/perm updates -void bench_define_suite(const struct bench_suite *suite) { - bench_define_names[BENCH_DEFINE_NAMES_SUITE] = (bench_define_names_t){ - suite->define_names, suite->define_count}; - - // make sure our cache is large enough - if (lfs_max(suite->define_count, BENCH_IMPLICIT_DEFINE_COUNT) - > bench_define_cache_count) { - // align to power of two to avoid any superlinear growth - size_t ncount = 1 << lfs_npw2( - lfs_max(suite->define_count, BENCH_IMPLICIT_DEFINE_COUNT)); - bench_define_cache = realloc(bench_define_cache, ncount*sizeof(intmax_t)); - bench_define_cache_mask = realloc(bench_define_cache_mask, - sizeof(unsigned)*( - (ncount+(8*sizeof(unsigned))-1) - / (8*sizeof(unsigned)))); - bench_define_cache_count = ncount; +void bench_define_suite( + const bench_id_t *id, + const struct bench_suite *suite) { + // reset our mapping + bench_define_count = 0; + bench_suite_define_count = 0; + + // make sure we have space for everything, just assume the worst case + if (bench_implicit_define_count + suite->define_count + > bench_define_capacity) { + bench_define_capacity + = bench_implicit_define_count + suite->define_count; + bench_defines = realloc( + bench_defines, + bench_define_capacity*sizeof(const bench_define_t*)); + bench_suite_defines = realloc( + bench_suite_defines, + bench_define_capacity*sizeof(const bench_define_t*)); + bench_suite_define_map = realloc( + bench_suite_define_map, + bench_define_capacity*sizeof(ssize_t)); } - // map any overrides - if (bench_override_count > 0) { - // first figure out the total size of override permutations - size_t count = 0; - size_t permutations = 1; - for (size_t i = 0; i < bench_override_count; i++) { - for (size_t d = 0; - d < lfs_max( - suite->define_count, - BENCH_IMPLICIT_DEFINE_COUNT); - d++) { - // define name match? - const char *name = bench_define_name(d); - if (name && strcmp(name, bench_overrides[i].name) == 0) { - count = lfs_max(count, d+1); - permutations *= bench_overrides[i].permutations; - break; + // first map our implicit defines + for (size_t i = 0; i < bench_implicit_define_count; i++) { + bench_suite_defines[i] = &bench_implicit_defines[i]; + } + bench_suite_define_count = bench_implicit_define_count; + + // build a mapping from suite defines to bench defines + // + // we will use this for both suite and case defines + memset(bench_suite_define_map, -1, + bench_suite_define_count*sizeof(size_t)); + + for (size_t i = 0; i < suite->define_count; i++) { + // assume suite defines are unique so we only need to compare + // against implicit defines, this avoids a O(n^2) + for (size_t j = 0; j < bench_implicit_define_count; j++) { + if (bench_suite_defines[j]->define == suite->defines[i].define) { + bench_suite_define_map[j] = i; + + // don't override implicit defines if we're not defined + if (bench_define_isdefined(&suite->defines[i])) { + bench_suite_defines[j] = &suite->defines[i]; } + goto next_suite_define; } } - bench_override_define_count = count; - bench_override_define_permutations = permutations; - - // make sure our override arrays are big enough - if (count * permutations > bench_override_define_capacity) { - // align to power of two to avoid any superlinear growth - size_t ncapacity = 1 << lfs_npw2(count * permutations); - bench_override_defines = realloc( - bench_override_defines, - sizeof(bench_define_t)*ncapacity); - bench_override_define_capacity = ncapacity; - } - // zero unoverridden defines - memset(bench_override_defines, 0, - sizeof(bench_define_t) * count * permutations); - - // compute permutations - size_t p = 1; - for (size_t i = 0; i < bench_override_count; i++) { - for (size_t d = 0; - d < lfs_max( - suite->define_count, - BENCH_IMPLICIT_DEFINE_COUNT); - d++) { - // define name match? - const char *name = bench_define_name(d); - if (name && strcmp(name, bench_overrides[i].name) == 0) { - // scatter the define permutations based on already - // seen permutations - for (size_t j = 0; j < permutations; j++) { - bench_override_defines[j*count + d] = BENCH_LIT( - bench_overrides[i].defines[(j/p) - % bench_overrides[i].permutations]); - } + // map a new suite define + bench_suite_define_map[bench_suite_define_count] = i; + bench_suite_defines[bench_suite_define_count] = &suite->defines[i]; + bench_suite_define_count += 1; +next_suite_define:; + } - // keep track of how many permutations we've seen so far - p *= bench_overrides[i].permutations; - break; - } + // map any explicit defines + // + // we ignore any out-of-bounds defines here, even though it's likely + // an error + if (id && id->defines) { + for (size_t i = 0; + i < id->define_count && i < bench_suite_define_count; + i++) { + if (bench_define_isdefined(&id->defines[i])) { + // update name/addr + id->defines[i].name = bench_suite_defines[i]->name; + id->defines[i].define = bench_suite_defines[i]->define; + // map and override suite mapping + bench_suite_defines[i] = &id->defines[i]; + bench_suite_define_map[i] = -1; + } + } + } + + // map any override defines + // + // note it's not an error to override a define that doesn't exist + for (size_t i = 0; i < bench_override_define_count; i++) { + for (size_t j = 0; j < bench_suite_define_count; j++) { + if (strcmp( + bench_suite_defines[j]->name, + bench_override_defines[i].name) == 0) { + // update addr + bench_override_defines[i].define + = bench_suite_defines[j]->define; + // map and override suite mapping + bench_suite_defines[j] = &bench_override_defines[i]; + bench_suite_define_map[j] = -1; + goto next_override_define; } } +next_override_define:; } } -void bench_define_perm( +void bench_define_case( + const bench_id_t *id, const struct bench_suite *suite, const struct bench_case *case_, size_t perm) { - if (case_->defines) { - bench_define_maps[BENCH_DEFINE_MAP_PERMUTATION] = (bench_define_map_t){ - case_->defines + perm*suite->define_count, - suite->define_count}; - } else { - bench_define_maps[BENCH_DEFINE_MAP_PERMUTATION] = (bench_define_map_t){ - NULL, 0}; + (void)id; + + // copy over suite defines + for (size_t i = 0; i < bench_suite_define_count; i++) { + // map case define if case define is defined + if (case_->defines + && bench_suite_define_map[i] != -1 + && bench_define_isdefined(&case_->defines[ + perm*suite->define_count + + bench_suite_define_map[i]])) { + bench_defines[i] = &case_->defines[ + perm*suite->define_count + + bench_suite_define_map[i]]; + } else { + bench_defines[i] = bench_suite_defines[i]; + } } + bench_define_count = bench_suite_define_count; } -void bench_define_override(size_t perm) { - bench_define_maps[BENCH_DEFINE_MAP_OVERRIDE] = (bench_define_map_t){ - bench_override_defines + perm*bench_override_define_count, - bench_override_define_count}; -} +void bench_define_permutation(size_t perm) { + // first zero everything, we really don't want reproducibility issues + for (size_t i = 0; i < bench_define_count; i++) { + *bench_defines[i]->define = 0; + } + + // defines may be mutually recursive, which makes evaluation a bit tricky + // + // Rather than doing any clever, we just repeatedly evaluate the + // permutation until values stabilize. If things don't stabilize after + // some number of iterations, error, this likely means defines were + // stuck in a cycle + // + size_t attempt = 0; + while (true) { + const bench_define_t *changed = NULL; + // define-specific permutations are encoded in the case permutation + size_t perm_ = perm; + for (size_t i = 0; i < bench_define_count; i++) { + if (bench_defines[i]->cb) { + intmax_t v = bench_defines[i]->cb( + bench_defines[i]->data, + perm_ % bench_defines[i]->permutations); + if (v != *bench_defines[i]->define) { + *bench_defines[i]->define = v; + changed = bench_defines[i]; + } + + perm_ /= bench_defines[i]->permutations; + } + } + + // stabilized? + if (!changed) { + break; + } -void bench_define_explicit( - const bench_define_t *defines, - size_t define_count) { - bench_define_maps[BENCH_DEFINE_MAP_EXPLICIT] = (bench_define_map_t){ - defines, define_count}; + attempt += 1; + if (bench_define_depth && attempt >= bench_define_depth+1) { + fprintf(stderr, "error: could not resolve recursive defines: %s\n", + changed->name); + exit(-1); + } + } } void bench_define_cleanup(void) { // bench define management can allocate a few things - free(bench_define_cache); - free(bench_define_cache_mask); - free(bench_override_defines); + free(bench_defines); + free(bench_suite_defines); + free(bench_suite_define_map); +} + +size_t bench_define_permutations(void) { + size_t prod = 1; + for (size_t i = 0; i < bench_define_count; i++) { + prod *= (bench_defines[i]->permutations > 0) + ? bench_defines[i]->permutations + : 1; + } + return prod; } +// override define stuff + +typedef struct bench_override_value { + intmax_t start; + intmax_t stop; + // step == 0 indicates a single value + intmax_t step; +} bench_override_value_t; + +typedef struct bench_override_data { + bench_override_value_t *values; + size_t value_count; +} bench_override_data_t; + +intmax_t bench_override_cb(void *data, size_t i) { + const bench_override_data_t *data_ = data; + for (size_t j = 0; j < data_->value_count; j++) { + const bench_override_value_t *v = &data_->values[j]; + // range? + if (v->step) { + size_t range_count; + if (v->step > 0) { + range_count = (v->stop-1 - v->start) / v->step + 1; + } else { + range_count = (v->start-1 - v->stop) / -v->step + 1; + } + + if (i < range_count) { + return i*v->step + v->start; + } + i -= range_count; + // value? + } else { + if (i == 0) { + return v->start; + } + i -= 1; + } + } + + // should never get here + assert(false); + __builtin_unreachable(); +} + -// bench state -extern const bench_geometry_t *bench_geometries; -extern size_t bench_geometry_count; +// bench state const bench_id_t *bench_ids = (const bench_id_t[]) { {NULL, NULL, 0}, }; @@ -423,6 +428,7 @@ size_t bench_id_count = 1; size_t bench_step_start = 0; size_t bench_step_stop = -1; size_t bench_step_step = 1; +bool bench_force = false; const char *bench_disk_path = NULL; const char *bench_trace_path = NULL; @@ -433,9 +439,9 @@ FILE *bench_trace_file = NULL; uint32_t bench_trace_cycles = 0; uint64_t bench_trace_time = 0; uint64_t bench_trace_open_time = 0; -lfs_emubd_sleep_t bench_read_sleep = 0.0; -lfs_emubd_sleep_t bench_prog_sleep = 0.0; -lfs_emubd_sleep_t bench_erase_sleep = 0.0; +lfs3_emubd_sleep_t bench_read_sleep = 0.0; +lfs3_emubd_sleep_t bench_prog_sleep = 0.0; +lfs3_emubd_sleep_t bench_erase_sleep = 0.0; // this determines both the backtrace buffer and the trace printf buffer, if // trace ends up interleaved or truncated this may need to be increased @@ -548,6 +554,11 @@ uint32_t bench_prng(uint32_t *state) { // A simple xorshift32 generator, easily reproducible. Keep in mind // determinism is much more important than actual randomness here. uint32_t x = *state; + // must be non-zero, use uintmax here so that seed=0 is different + // from seed=1 and seed=range(0,n) makes a bit more sense + if (x == 0) { + x = -1; + } x ^= x << 13; x ^= x >> 17; x ^= x << 5; @@ -555,51 +566,136 @@ uint32_t bench_prng(uint32_t *state) { return x; } +// bench factorial +size_t bench_factorial(size_t x) { + size_t y = 1; + for (size_t i = 2; i <= x; i++) { + y *= i; + } + return y; +} + +// bench array permutations +void bench_permutation(size_t i, uint32_t *buffer, size_t size) { + // https://stackoverflow.com/a/7919887 and + // https://stackoverflow.com/a/24257996 helped a lot with this, but + // changed to run in O(n) with no extra memory. This has a tradeoff + // of generating the permutations in an unintuitive order. + + // initialize array + for (size_t j = 0; j < size; j++) { + buffer[j] = j; + } + + for (size_t j = 0; j < size; j++) { + // swap index with digit + // + // .- i%rem --. + // v .----+----. + // [p0 p1 |-> r0 r1 r2 r3] + // + size_t t = buffer[j + (i % (size-j))]; + buffer[j + (i % (size-j))] = buffer[j]; + buffer[j] = t; + // update i + i /= (size-j); + } +} + // bench recording state -static struct lfs_config *bench_cfg = NULL; -static lfs_emubd_io_t bench_last_readed = 0; -static lfs_emubd_io_t bench_last_proged = 0; -static lfs_emubd_io_t bench_last_erased = 0; -lfs_emubd_io_t bench_readed = 0; -lfs_emubd_io_t bench_proged = 0; -lfs_emubd_io_t bench_erased = 0; - -void bench_reset(void) { - bench_readed = 0; - bench_proged = 0; - bench_erased = 0; - bench_last_readed = 0; - bench_last_proged = 0; - bench_last_erased = 0; +typedef struct bench_record { + const char *m; + uintmax_t n; + lfs3_emubd_io_t last_readed; + lfs3_emubd_io_t last_proged; + lfs3_emubd_io_t last_erased; +} bench_record_t; + +static struct lfs3_cfg *bench_cfg = NULL; +static bench_record_t *bench_records; +size_t bench_record_count; +size_t bench_record_capacity; + +void bench_reset(struct lfs3_cfg *cfg) { + bench_cfg = cfg; + bench_record_count = 0; } -void bench_start(void) { +void bench_start(const char *m, uintmax_t n) { + // measure current read/prog/erase assert(bench_cfg); - lfs_emubd_sio_t readed = lfs_emubd_readed(bench_cfg); + lfs3_emubd_sio_t readed = lfs3_emubd_readed(bench_cfg); assert(readed >= 0); - lfs_emubd_sio_t proged = lfs_emubd_proged(bench_cfg); + lfs3_emubd_sio_t proged = lfs3_emubd_proged(bench_cfg); assert(proged >= 0); - lfs_emubd_sio_t erased = lfs_emubd_erased(bench_cfg); + lfs3_emubd_sio_t erased = lfs3_emubd_erased(bench_cfg); assert(erased >= 0); - bench_last_readed = readed; - bench_last_proged = proged; - bench_last_erased = erased; + // allocate a new record + bench_record_t *record = mappend( + (void**)&bench_records, + sizeof(bench_record_t), + &bench_record_count, + &bench_record_capacity); + record->m = m; + record->n = n; + record->last_readed = readed; + record->last_proged = proged; + record->last_erased = erased; } -void bench_stop(void) { +void bench_stop(const char *m) { + // measure current read/prog/erase assert(bench_cfg); - lfs_emubd_sio_t readed = lfs_emubd_readed(bench_cfg); + lfs3_emubd_sio_t readed = lfs3_emubd_readed(bench_cfg); assert(readed >= 0); - lfs_emubd_sio_t proged = lfs_emubd_proged(bench_cfg); + lfs3_emubd_sio_t proged = lfs3_emubd_proged(bench_cfg); assert(proged >= 0); - lfs_emubd_sio_t erased = lfs_emubd_erased(bench_cfg); + lfs3_emubd_sio_t erased = lfs3_emubd_erased(bench_cfg); assert(erased >= 0); - bench_readed += readed - bench_last_readed; - bench_proged += proged - bench_last_proged; - bench_erased += erased - bench_last_erased; + // find our record + for (size_t i = 0; i < bench_record_count; i++) { + if (strcmp(bench_records[i].m, m) == 0) { + // print results + printf("benched %s %jd %"PRIu64" %"PRIu64" %"PRIu64"\n", + bench_records[i].m, + bench_records[i].n, + readed - bench_records[i].last_readed, + proged - bench_records[i].last_proged, + erased - bench_records[i].last_erased); + + // remove our record + memmove(&bench_records[i], + &bench_records[i+1], + bench_record_count-(i+1)); + bench_record_count -= 1; + return; + } + } + + // not found? + fprintf(stderr, "error: bench stopped before it was started (%s)\n", + m); + assert(false); + exit(-1); +} + +void bench_result(const char *m, uintmax_t n, uintmax_t result) { + // we just print these directly + printf("benched %s %jd %"PRIu64"\n", + m, + n, + result); +} + +void bench_fresult(const char *m, uintmax_t n, double result) { + // we just print these directly + printf("benched %s %jd %.6f\n", + m, + n, + result); } @@ -610,14 +706,10 @@ static void perm_printid( (void)suite; // case[:permutation] printf("%s:", case_->name); - for (size_t d = 0; - d < lfs_max( - suite->define_count, - BENCH_IMPLICIT_DEFINE_COUNT); - d++) { - if (bench_define_ispermutation(d)) { + for (size_t d = 0; d < bench_define_count; d++) { + if (bench_define_ispermutation(bench_defines[d])) { leb16_print(d); - leb16_print(BENCH_DEFINE(d)); + leb16_print(*bench_defines[d]->define); } } } @@ -634,26 +726,19 @@ struct bench_seen_branch { struct bench_seen branch; }; -bool bench_seen_insert( - bench_seen_t *seen, - const struct bench_suite *suite, - const struct bench_case *case_) { - (void)case_; - bool was_seen = true; - +bool bench_seen_insert(bench_seen_t *seen) { // use the currently set defines - for (size_t d = 0; - d < lfs_max( - suite->define_count, - BENCH_IMPLICIT_DEFINE_COUNT); - d++) { + bool was_seen = true; + for (size_t d = 0; d < bench_define_count; d++) { // treat unpermuted defines the same as 0 - intmax_t define = bench_define_ispermutation(d) ? BENCH_DEFINE(d) : 0; + intmax_t v = bench_define_ispermutation(bench_defines[d]) + ? *bench_defines[d]->define + : 0; // already seen? struct bench_seen_branch *branch = NULL; for (size_t i = 0; i < seen->branch_count; i++) { - if (seen->branches[i].define == define) { + if (seen->branches[i].define == v) { branch = &seen->branches[i]; break; } @@ -667,7 +752,7 @@ bool bench_seen_insert( sizeof(struct bench_seen_branch), &seen->branch_count, &seen->branch_capacity); - branch->define = define; + branch->define = v; branch->branch = (bench_seen_t){NULL, 0, 0}; } @@ -686,23 +771,23 @@ void bench_seen_cleanup(bench_seen_t *seen) { // iterate through permutations in a bench case static void case_forperm( + const bench_id_t *id, const struct bench_suite *suite, const struct bench_case *case_, - const bench_define_t *defines, - size_t define_count, void (*cb)( void *data, const struct bench_suite *suite, const struct bench_case *case_), void *data) { // explicit permutation? - if (defines) { - bench_define_explicit(defines, define_count); + if (id && id->defines) { + // define case permutation, the exact case perm doesn't matter here + bench_define_case(id, suite, case_, 0); - for (size_t v = 0; v < bench_override_define_permutations; v++) { - // define override permutation - bench_define_override(v); - bench_define_flush(); + size_t permutations = bench_define_permutations(); + for (size_t p = 0; p < permutations; p++) { + // define permutation permutation + bench_define_permutation(p); cb(data, suite, case_); } @@ -710,29 +795,31 @@ static void case_forperm( return; } + // deduplicate permutations with the same defines + // + // this can easily happen when overriding multiple case permutations, + // we can't tell that multiple case permutations don't change defines, + // duplicating results bench_seen_t seen = {NULL, 0, 0}; - for (size_t k = 0; k < case_->permutations; k++) { - // define permutation - bench_define_perm(suite, case_, k); - - for (size_t v = 0; v < bench_override_define_permutations; v++) { - // define override permutation - bench_define_override(v); - - for (size_t g = 0; g < bench_geometry_count; g++) { - // define geometry - bench_define_geometry(&bench_geometries[g]); - bench_define_flush(); + for (size_t k = 0; + k < ((case_->permutations) ? case_->permutations : 1); + k++) { + // define case permutation + bench_define_case(id, suite, case_, k); - // have we seen this permutation before? - bool was_seen = bench_seen_insert(&seen, suite, case_); - if (!(k == 0 && v == 0 && g == 0) && was_seen) { - continue; - } + size_t permutations = bench_define_permutations(); + for (size_t p = 0; p < permutations; p++) { + // define permutation permutation + bench_define_permutation(p); - cb(data, suite, case_); + // have we seen this permutation before? + bool was_seen = bench_seen_insert(&seen); + if (!(k == 0 && p == 0) && was_seen) { + continue; } + + cb(data, suite, case_); } } @@ -752,11 +839,10 @@ void perm_count( const struct bench_case *case_) { struct perm_count_state *state = data; (void)suite; - (void)case_; state->total += 1; - if (case_->filter && !case_->filter()) { + if (!case_->run || !(bench_force || !case_->if_ || case_->if_())) { return; } @@ -766,7 +852,7 @@ void perm_count( // operations we can do static void summary(void) { - printf("%-23s %7s %7s %7s %11s\n", + printf("%-23s %7s %7s %7s %15s\n", "", "flags", "suites", "cases", "perms"); size_t suites = 0; size_t cases = 0; @@ -774,31 +860,38 @@ static void summary(void) { struct perm_count_state perms = {0, 0}; for (size_t t = 0; t < bench_id_count; t++) { - for (size_t i = 0; i < BENCH_SUITE_COUNT; i++) { - bench_define_suite(&bench_suites[i]); + for (size_t i = 0; i < bench_suite_count; i++) { + bench_define_suite(&bench_ids[t], bench_suites[i]); - for (size_t j = 0; j < bench_suites[i].case_count; j++) { + size_t cases_ = 0; + + for (size_t j = 0; j < bench_suites[i]->case_count; j++) { // does neither suite nor case name match? if (bench_ids[t].name && !( strcmp(bench_ids[t].name, - bench_suites[i].name) == 0 + bench_suites[i]->name) == 0 || strcmp(bench_ids[t].name, - bench_suites[i].cases[j].name) == 0)) { + bench_suites[i]->cases[j].name) == 0)) { continue; } cases += 1; + cases_ += 1; case_forperm( - &bench_suites[i], - &bench_suites[i].cases[j], - bench_ids[t].defines, - bench_ids[t].define_count, + &bench_ids[t], + bench_suites[i], + &bench_suites[i]->cases[j], perm_count, &perms); } + // no benches found? + if (!cases_) { + continue; + } + suites += 1; - flags |= bench_suites[i].flags; + flags |= bench_suites[i]->flags; } } @@ -806,9 +899,9 @@ static void summary(void) { sprintf(perm_buf, "%zu/%zu", perms.filtered, perms.total); char flag_buf[64]; sprintf(flag_buf, "%s%s", - (flags & BENCH_REENTRANT) ? "r" : "", - (!flags) ? "-" : ""); - printf("%-23s %7s %7zu %7zu %11s\n", + (flags & BENCH_INTERNAL) ? "i" : "", + (!flags) ? "-" : ""); + printf("%-23s %7s %7zu %7zu %15s\n", "TOTAL", flag_buf, suites, @@ -819,39 +912,38 @@ static void summary(void) { static void list_suites(void) { // at least size so that names fit unsigned name_width = 23; - for (size_t i = 0; i < BENCH_SUITE_COUNT; i++) { - size_t len = strlen(bench_suites[i].name); + for (size_t i = 0; i < bench_suite_count; i++) { + size_t len = strlen(bench_suites[i]->name); if (len > name_width) { name_width = len; } } name_width = 4*((name_width+1+4-1)/4)-1; - printf("%-*s %7s %7s %11s\n", + printf("%-*s %7s %7s %15s\n", name_width, "suite", "flags", "cases", "perms"); for (size_t t = 0; t < bench_id_count; t++) { - for (size_t i = 0; i < BENCH_SUITE_COUNT; i++) { - bench_define_suite(&bench_suites[i]); + for (size_t i = 0; i < bench_suite_count; i++) { + bench_define_suite(&bench_ids[t], bench_suites[i]); size_t cases = 0; struct perm_count_state perms = {0, 0}; - for (size_t j = 0; j < bench_suites[i].case_count; j++) { + for (size_t j = 0; j < bench_suites[i]->case_count; j++) { // does neither suite nor case name match? if (bench_ids[t].name && !( strcmp(bench_ids[t].name, - bench_suites[i].name) == 0 + bench_suites[i]->name) == 0 || strcmp(bench_ids[t].name, - bench_suites[i].cases[j].name) == 0)) { + bench_suites[i]->cases[j].name) == 0)) { continue; } cases += 1; case_forperm( - &bench_suites[i], - &bench_suites[i].cases[j], - bench_ids[t].defines, - bench_ids[t].define_count, + &bench_ids[t], + bench_suites[i], + &bench_suites[i]->cases[j], perm_count, &perms); } @@ -865,11 +957,11 @@ static void list_suites(void) { sprintf(perm_buf, "%zu/%zu", perms.filtered, perms.total); char flag_buf[64]; sprintf(flag_buf, "%s%s", - (bench_suites[i].flags & BENCH_REENTRANT) ? "r" : "", - (!bench_suites[i].flags) ? "-" : ""); - printf("%-*s %7s %7zu %11s\n", + (bench_suites[i]->flags & BENCH_INTERNAL) ? "i" : "", + (!bench_suites[i]->flags) ? "-" : ""); + printf("%-*s %7s %7zu %15s\n", name_width, - bench_suites[i].name, + bench_suites[i]->name, flag_buf, cases, perm_buf); @@ -880,9 +972,9 @@ static void list_suites(void) { static void list_cases(void) { // at least size so that names fit unsigned name_width = 23; - for (size_t i = 0; i < BENCH_SUITE_COUNT; i++) { - for (size_t j = 0; j < bench_suites[i].case_count; j++) { - size_t len = strlen(bench_suites[i].cases[j].name); + for (size_t i = 0; i < bench_suite_count; i++) { + for (size_t j = 0; j < bench_suites[i]->case_count; j++) { + size_t len = strlen(bench_suites[i]->cases[j].name); if (len > name_width) { name_width = len; } @@ -890,27 +982,26 @@ static void list_cases(void) { } name_width = 4*((name_width+1+4-1)/4)-1; - printf("%-*s %7s %11s\n", name_width, "case", "flags", "perms"); + printf("%-*s %7s %15s\n", name_width, "case", "flags", "perms"); for (size_t t = 0; t < bench_id_count; t++) { - for (size_t i = 0; i < BENCH_SUITE_COUNT; i++) { - bench_define_suite(&bench_suites[i]); + for (size_t i = 0; i < bench_suite_count; i++) { + bench_define_suite(&bench_ids[t], bench_suites[i]); - for (size_t j = 0; j < bench_suites[i].case_count; j++) { + for (size_t j = 0; j < bench_suites[i]->case_count; j++) { // does neither suite nor case name match? if (bench_ids[t].name && !( strcmp(bench_ids[t].name, - bench_suites[i].name) == 0 + bench_suites[i]->name) == 0 || strcmp(bench_ids[t].name, - bench_suites[i].cases[j].name) == 0)) { + bench_suites[i]->cases[j].name) == 0)) { continue; } struct perm_count_state perms = {0, 0}; case_forperm( - &bench_suites[i], - &bench_suites[i].cases[j], - bench_ids[t].defines, - bench_ids[t].define_count, + &bench_ids[t], + bench_suites[i], + &bench_suites[i]->cases[j], perm_count, &perms); @@ -918,13 +1009,13 @@ static void list_cases(void) { sprintf(perm_buf, "%zu/%zu", perms.filtered, perms.total); char flag_buf[64]; sprintf(flag_buf, "%s%s", - (bench_suites[i].cases[j].flags & BENCH_REENTRANT) - ? "r" : "", - (!bench_suites[i].cases[j].flags) + (bench_suites[i]->cases[j].flags & BENCH_INTERNAL) + ? "i" : "", + (!bench_suites[i]->cases[j].flags) ? "-" : ""); - printf("%-*s %7s %11s\n", + printf("%-*s %7s %15s\n", name_width, - bench_suites[i].cases[j].name, + bench_suites[i]->cases[j].name, flag_buf, perm_buf); } @@ -935,8 +1026,8 @@ static void list_cases(void) { static void list_suite_paths(void) { // at least size so that names fit unsigned name_width = 23; - for (size_t i = 0; i < BENCH_SUITE_COUNT; i++) { - size_t len = strlen(bench_suites[i].name); + for (size_t i = 0; i < bench_suite_count; i++) { + size_t len = strlen(bench_suites[i]->name); if (len > name_width) { name_width = len; } @@ -945,16 +1036,16 @@ static void list_suite_paths(void) { printf("%-*s %s\n", name_width, "suite", "path"); for (size_t t = 0; t < bench_id_count; t++) { - for (size_t i = 0; i < BENCH_SUITE_COUNT; i++) { + for (size_t i = 0; i < bench_suite_count; i++) { size_t cases = 0; - for (size_t j = 0; j < bench_suites[i].case_count; j++) { + for (size_t j = 0; j < bench_suites[i]->case_count; j++) { // does neither suite nor case name match? if (bench_ids[t].name && !( strcmp(bench_ids[t].name, - bench_suites[i].name) == 0 + bench_suites[i]->name) == 0 || strcmp(bench_ids[t].name, - bench_suites[i].cases[j].name) == 0)) { + bench_suites[i]->cases[j].name) == 0)) { continue; cases += 1; @@ -968,8 +1059,8 @@ static void list_suite_paths(void) { printf("%-*s %s\n", name_width, - bench_suites[i].name, - bench_suites[i].path); + bench_suites[i]->name, + bench_suites[i]->path); } } } @@ -977,9 +1068,9 @@ static void list_suite_paths(void) { static void list_case_paths(void) { // at least size so that names fit unsigned name_width = 23; - for (size_t i = 0; i < BENCH_SUITE_COUNT; i++) { - for (size_t j = 0; j < bench_suites[i].case_count; j++) { - size_t len = strlen(bench_suites[i].cases[j].name); + for (size_t i = 0; i < bench_suite_count; i++) { + for (size_t j = 0; j < bench_suites[i]->case_count; j++) { + size_t len = strlen(bench_suites[i]->cases[j].name); if (len > name_width) { name_width = len; } @@ -989,21 +1080,21 @@ static void list_case_paths(void) { printf("%-*s %s\n", name_width, "case", "path"); for (size_t t = 0; t < bench_id_count; t++) { - for (size_t i = 0; i < BENCH_SUITE_COUNT; i++) { - for (size_t j = 0; j < bench_suites[i].case_count; j++) { + for (size_t i = 0; i < bench_suite_count; i++) { + for (size_t j = 0; j < bench_suites[i]->case_count; j++) { // does neither suite nor case name match? if (bench_ids[t].name && !( strcmp(bench_ids[t].name, - bench_suites[i].name) == 0 + bench_suites[i]->name) == 0 || strcmp(bench_ids[t].name, - bench_suites[i].cases[j].name) == 0)) { + bench_suites[i]->cases[j].name) == 0)) { continue; } printf("%-*s %s\n", name_width, - bench_suites[i].cases[j].name, - bench_suites[i].cases[j].path); + bench_suites[i]->cases[j].name, + bench_suites[i]->cases[j].path); } } } @@ -1024,16 +1115,16 @@ struct list_defines_defines { static void list_defines_add( struct list_defines_defines *defines, - size_t d) { - const char *name = bench_define_name(d); - intmax_t value = BENCH_DEFINE(d); + const bench_define_t *define) { + const char *name = define->name; + intmax_t v = *define->define; // define already in defines? for (size_t i = 0; i < defines->define_count; i++) { if (strcmp(defines->defines[i].name, name) == 0) { // value already in values? for (size_t j = 0; j < defines->defines[i].value_count; j++) { - if (defines->defines[i].values[j] == value) { + if (defines->defines[i].values[j] == v) { return; } } @@ -1042,23 +1133,23 @@ static void list_defines_add( (void**)&defines->defines[i].values, sizeof(intmax_t), &defines->defines[i].value_count, - &defines->defines[i].value_capacity) = value; + &defines->defines[i].value_capacity) = v; return; } } // new define? - struct list_defines_define *define = mappend( + struct list_defines_define *define_ = mappend( (void**)&defines->defines, sizeof(struct list_defines_define), &defines->define_count, &defines->define_capacity); - define->name = name; - define->values = malloc(sizeof(intmax_t)); - define->values[0] = value; - define->value_count = 1; - define->value_capacity = 1; + define_->name = name; + define_->values = malloc(sizeof(intmax_t)); + define_->values[0] = v; + define_->value_count = 1; + define_->value_capacity = 1; } void perm_list_defines( @@ -1070,13 +1161,9 @@ void perm_list_defines( (void)case_; // collect defines - for (size_t d = 0; - d < lfs_max(suite->define_count, - BENCH_IMPLICIT_DEFINE_COUNT); - d++) { - if (d < BENCH_IMPLICIT_DEFINE_COUNT - || bench_define_ispermutation(d)) { - list_defines_add(defines, d); + for (size_t d = 0; d < bench_define_count; d++) { + if (bench_define_isdefined(bench_defines[d])) { + list_defines_add(defines, bench_defines[d]); } } } @@ -1090,41 +1177,35 @@ void perm_list_permutation_defines( (void)case_; // collect permutation_defines - for (size_t d = 0; - d < lfs_max(suite->define_count, - BENCH_IMPLICIT_DEFINE_COUNT); - d++) { - if (bench_define_ispermutation(d)) { - list_defines_add(defines, d); + for (size_t d = 0; d < bench_define_count; d++) { + if (bench_define_ispermutation(bench_defines[d])) { + list_defines_add(defines, bench_defines[d]); } } } -extern const bench_geometry_t builtin_geometries[]; - static void list_defines(void) { struct list_defines_defines defines = {NULL, 0, 0}; // add defines for (size_t t = 0; t < bench_id_count; t++) { - for (size_t i = 0; i < BENCH_SUITE_COUNT; i++) { - bench_define_suite(&bench_suites[i]); + for (size_t i = 0; i < bench_suite_count; i++) { + bench_define_suite(&bench_ids[t], bench_suites[i]); - for (size_t j = 0; j < bench_suites[i].case_count; j++) { + for (size_t j = 0; j < bench_suites[i]->case_count; j++) { // does neither suite nor case name match? if (bench_ids[t].name && !( strcmp(bench_ids[t].name, - bench_suites[i].name) == 0 + bench_suites[i]->name) == 0 || strcmp(bench_ids[t].name, - bench_suites[i].cases[j].name) == 0)) { + bench_suites[i]->cases[j].name) == 0)) { continue; } case_forperm( - &bench_suites[i], - &bench_suites[i].cases[j], - bench_ids[t].defines, - bench_ids[t].define_count, + &bench_ids[t], + bench_suites[i], + &bench_suites[i]->cases[j], perm_list_defines, &defines); } @@ -1153,24 +1234,23 @@ static void list_permutation_defines(void) { // add permutation defines for (size_t t = 0; t < bench_id_count; t++) { - for (size_t i = 0; i < BENCH_SUITE_COUNT; i++) { - bench_define_suite(&bench_suites[i]); + for (size_t i = 0; i < bench_suite_count; i++) { + bench_define_suite(&bench_ids[t], bench_suites[i]); - for (size_t j = 0; j < bench_suites[i].case_count; j++) { + for (size_t j = 0; j < bench_suites[i]->case_count; j++) { // does neither suite nor case name match? if (bench_ids[t].name && !( strcmp(bench_ids[t].name, - bench_suites[i].name) == 0 + bench_suites[i]->name) == 0 || strcmp(bench_ids[t].name, - bench_suites[i].cases[j].name) == 0)) { + bench_suites[i]->cases[j].name) == 0)) { continue; } case_forperm( - &bench_suites[i], - &bench_suites[i].cases[j], - bench_ids[t].defines, - bench_ids[t].define_count, + &bench_ids[t], + bench_suites[i], + &bench_suites[i]->cases[j], perm_list_permutation_defines, &defines); } @@ -1197,19 +1277,23 @@ static void list_permutation_defines(void) { static void list_implicit_defines(void) { struct list_defines_defines defines = {NULL, 0, 0}; - // yes we do need to define a suite, this does a bit of bookeeping - // such as setting up the define cache - bench_define_suite(&(const struct bench_suite){0}); + // yes we do need to define a suite/case, these do a bit of bookeeping + // around mapping defines + bench_define_suite(NULL, + &(const struct bench_suite){0}); + bench_define_case(NULL, + &(const struct bench_suite){0}, + &(const struct bench_case){0}, + 0); - // make sure to include builtin geometries here - extern const bench_geometry_t builtin_geometries[]; - for (size_t g = 0; builtin_geometries[g].name; g++) { - bench_define_geometry(&builtin_geometries[g]); - bench_define_flush(); + size_t permutations = bench_define_permutations(); + for (size_t p = 0; p < permutations; p++) { + // define permutation permutation + bench_define_permutation(p); // add implicit defines - for (size_t d = 0; d < BENCH_IMPLICIT_DEFINE_COUNT; d++) { - list_defines_add(&defines, d); + for (size_t d = 0; d < bench_define_count; d++) { + list_defines_add(&defines, bench_defines[d]); } } @@ -1232,53 +1316,6 @@ static void list_implicit_defines(void) { -// geometries to bench - -const bench_geometry_t builtin_geometries[] = { - {"default", {{0}, BENCH_CONST(16), BENCH_CONST(512), {0}}}, - {"eeprom", {{0}, BENCH_CONST(1), BENCH_CONST(512), {0}}}, - {"emmc", {{0}, {0}, BENCH_CONST(512), {0}}}, - {"nor", {{0}, BENCH_CONST(1), BENCH_CONST(4096), {0}}}, - {"nand", {{0}, BENCH_CONST(4096), BENCH_CONST(32768), {0}}}, - {NULL, {{0}, {0}, {0}, {0}}}, -}; - -const bench_geometry_t *bench_geometries = builtin_geometries; -size_t bench_geometry_count = 5; - -static void list_geometries(void) { - // at least size so that names fit - unsigned name_width = 23; - for (size_t g = 0; builtin_geometries[g].name; g++) { - size_t len = strlen(builtin_geometries[g].name); - if (len > name_width) { - name_width = len; - } - } - name_width = 4*((name_width+1+4-1)/4)-1; - - // yes we do need to define a suite, this does a bit of bookeeping - // such as setting up the define cache - bench_define_suite(&(const struct bench_suite){0}); - - printf("%-*s %7s %7s %7s %7s %11s\n", - name_width, "geometry", "read", "prog", "erase", "count", "size"); - for (size_t g = 0; builtin_geometries[g].name; g++) { - bench_define_geometry(&builtin_geometries[g]); - bench_define_flush(); - printf("%-*s %7ju %7ju %7ju %7ju %11ju\n", - name_width, - builtin_geometries[g].name, - READ_SIZE, - PROG_SIZE, - BLOCK_SIZE, - BLOCK_COUNT, - BLOCK_SIZE*BLOCK_COUNT); - } -} - - - // global bench step count size_t bench_step = 0; @@ -1298,7 +1335,7 @@ void perm_run( bench_step += 1; // filter? - if (case_->filter && !case_->filter()) { + if (!case_->run || !(bench_force || !case_->if_ || case_->if_())) { printf("skipped "); perm_printid(suite, case_); printf("\n"); @@ -1306,42 +1343,32 @@ void perm_run( } // create block device and configuration - lfs_emubd_t bd; + lfs3_emubd_t bd; - struct lfs_config cfg = { + struct lfs3_cfg cfg = { .context = &bd, - .read = lfs_emubd_read, - .prog = lfs_emubd_prog, - .erase = lfs_emubd_erase, - .sync = lfs_emubd_sync, - .read_size = READ_SIZE, - .prog_size = PROG_SIZE, - .block_size = BLOCK_SIZE, - .block_count = BLOCK_COUNT, - .block_cycles = BLOCK_CYCLES, - .cache_size = CACHE_SIZE, - .lookahead_size = LOOKAHEAD_SIZE, + .read = lfs3_emubd_read, + .prog = lfs3_emubd_prog, + .erase = lfs3_emubd_erase, + .sync = lfs3_emubd_sync, + BENCH_CFG }; - struct lfs_emubd_config bdcfg = { - .erase_value = ERASE_VALUE, - .erase_cycles = ERASE_CYCLES, - .badblock_behavior = BADBLOCK_BEHAVIOR, - .disk_path = bench_disk_path, + struct lfs3_emubd_cfg bdcfg = { .read_sleep = bench_read_sleep, .prog_sleep = bench_prog_sleep, .erase_sleep = bench_erase_sleep, + BENCH_BDCFG }; - int err = lfs_emubd_createcfg(&cfg, bench_disk_path, &bdcfg); + int err = lfs3_emubd_createcfg(&cfg, bench_disk_path, &bdcfg); if (err) { fprintf(stderr, "error: could not create block device: %d\n", err); exit(-1); } // run the bench - bench_cfg = &cfg; - bench_reset(); + bench_reset(&cfg); printf("running "); perm_printid(suite, case_); printf("\n"); @@ -1350,14 +1377,10 @@ void perm_run( printf("finished "); perm_printid(suite, case_); - printf(" %"PRIu64" %"PRIu64" %"PRIu64, - bench_readed, - bench_proged, - bench_erased); printf("\n"); // cleanup - err = lfs_emubd_destroy(&cfg); + err = lfs3_emubd_destroy(&cfg); if (err) { fprintf(stderr, "error: could not destroy block device: %d\n", err); exit(-1); @@ -1369,24 +1392,23 @@ static void run(void) { signal(SIGPIPE, SIG_IGN); for (size_t t = 0; t < bench_id_count; t++) { - for (size_t i = 0; i < BENCH_SUITE_COUNT; i++) { - bench_define_suite(&bench_suites[i]); + for (size_t i = 0; i < bench_suite_count; i++) { + bench_define_suite(&bench_ids[t], bench_suites[i]); - for (size_t j = 0; j < bench_suites[i].case_count; j++) { + for (size_t j = 0; j < bench_suites[i]->case_count; j++) { // does neither suite nor case name match? if (bench_ids[t].name && !( strcmp(bench_ids[t].name, - bench_suites[i].name) == 0 + bench_suites[i]->name) == 0 || strcmp(bench_ids[t].name, - bench_suites[i].cases[j].name) == 0)) { + bench_suites[i]->cases[j].name) == 0)) { continue; } case_forperm( - &bench_suites[i], - &bench_suites[i].cases[j], - bench_ids[t].defines, - bench_ids[t].define_count, + &bench_ids[t], + bench_suites[i], + &bench_suites[i]->cases[j], perm_run, NULL); } @@ -1407,21 +1429,21 @@ enum opt_flags { OPT_LIST_DEFINES = 3, OPT_LIST_PERMUTATION_DEFINES = 4, OPT_LIST_IMPLICIT_DEFINES = 5, - OPT_LIST_GEOMETRIES = 6, OPT_DEFINE = 'D', - OPT_GEOMETRY = 'G', + OPT_DEFINE_DEPTH = 6, OPT_STEP = 's', + OPT_FORCE = 7, OPT_DISK = 'd', OPT_TRACE = 't', - OPT_TRACE_BACKTRACE = 7, - OPT_TRACE_PERIOD = 8, - OPT_TRACE_FREQ = 9, - OPT_READ_SLEEP = 10, - OPT_PROG_SLEEP = 11, - OPT_ERASE_SLEEP = 12, + OPT_TRACE_BACKTRACE = 8, + OPT_TRACE_PERIOD = 9, + OPT_TRACE_FREQ = 10, + OPT_READ_SLEEP = 11, + OPT_PROG_SLEEP = 12, + OPT_ERASE_SLEEP = 13, }; -const char *short_opts = "hYlLD:G:s:d:t:"; +const char *short_opts = "hYlLD:s:d:t:"; const struct option long_opts[] = { {"help", no_argument, NULL, OPT_HELP}, @@ -1435,10 +1457,10 @@ const struct option long_opts[] = { no_argument, NULL, OPT_LIST_PERMUTATION_DEFINES}, {"list-implicit-defines", no_argument, NULL, OPT_LIST_IMPLICIT_DEFINES}, - {"list-geometries", no_argument, NULL, OPT_LIST_GEOMETRIES}, {"define", required_argument, NULL, OPT_DEFINE}, - {"geometry", required_argument, NULL, OPT_GEOMETRY}, + {"define-depth", required_argument, NULL, OPT_DEFINE_DEPTH}, {"step", required_argument, NULL, OPT_STEP}, + {"force", no_argument, NULL, OPT_FORCE}, {"disk", required_argument, NULL, OPT_DISK}, {"trace", required_argument, NULL, OPT_TRACE}, {"trace-backtrace", no_argument, NULL, OPT_TRACE_BACKTRACE}, @@ -1460,10 +1482,10 @@ const char *const help_text[] = { "List all defines in this bench-runner.", "List explicit defines in this bench-runner.", "List implicit defines in this bench-runner.", - "List the available disk geometries.", "Override a bench define.", - "Comma-separated list of disk geometries to bench.", - "Comma-separated range of bench permutations to run (start,stop,step).", + "How deep to evaluate recursive defines before erroring.", + "Comma-separated range of permutations to run.", + "Ignore bench filters.", "Direct block device operations to this file.", "Direct trace output to this file.", "Include a backtrace with every trace statement.", @@ -1477,141 +1499,159 @@ const char *const help_text[] = { int main(int argc, char **argv) { void (*op)(void) = run; - size_t bench_override_capacity = 0; - size_t bench_geometry_capacity = 0; + size_t bench_override_define_capacity = 0; size_t bench_id_capacity = 0; // parse options while (true) { int c = getopt_long(argc, argv, short_opts, long_opts, NULL); switch (c) { - // generate help message - case OPT_HELP: { - printf("usage: %s [options] [bench_id]\n", argv[0]); - printf("\n"); - - printf("options:\n"); - size_t i = 0; - while (long_opts[i].name) { - size_t indent; - if (long_opts[i].has_arg == no_argument) { - if (long_opts[i].val >= '0' && long_opts[i].val < 'z') { - indent = printf(" -%c, --%s ", - long_opts[i].val, - long_opts[i].name); - } else { - indent = printf(" --%s ", - long_opts[i].name); - } + // generate help message + case OPT_HELP:; + printf("usage: %s [options] [bench_id]\n", argv[0]); + printf("\n"); + + printf("options:\n"); + size_t i = 0; + while (long_opts[i].name) { + size_t indent; + if (long_opts[i].has_arg == no_argument) { + if (long_opts[i].val >= '0' && long_opts[i].val < 'z') { + indent = printf(" -%c, --%s ", + long_opts[i].val, + long_opts[i].name); } else { - if (long_opts[i].val >= '0' && long_opts[i].val < 'z') { - indent = printf(" -%c %s, --%s %s ", - long_opts[i].val, - long_opts[i].name, - long_opts[i].name, - long_opts[i].name); - } else { - indent = printf(" --%s %s ", - long_opts[i].name, - long_opts[i].name); - } + indent = printf(" --%s ", + long_opts[i].name); } - - // a quick, hacky, byte-level method for text wrapping - size_t len = strlen(help_text[i]); - size_t j = 0; - if (indent < 24) { - printf("%*s %.80s\n", - (int)(24-1-indent), - "", - &help_text[i][j]); - j += 80; + } else { + if (long_opts[i].val >= '0' && long_opts[i].val < 'z') { + indent = printf(" -%c %s, --%s %s ", + long_opts[i].val, + long_opts[i].name, + long_opts[i].name, + long_opts[i].name); } else { - printf("\n"); + indent = printf(" --%s %s ", + long_opts[i].name, + long_opts[i].name); } + } - while (j < len) { - printf("%24s%.80s\n", "", &help_text[i][j]); - j += 80; - } + // a quick, hacky, byte-level method for text wrapping + size_t len = strlen(help_text[i]); + size_t j = 0; + if (indent < 24) { + printf("%*s %.80s\n", + (int)(24-1-indent), + "", + &help_text[i][j]); + j += 80; + } else { + printf("\n"); + } - i += 1; + while (j < len) { + printf("%24s%.80s\n", "", &help_text[i][j]); + j += 80; } - printf("\n"); - exit(0); + i += 1; } - // summary/list flags - case OPT_SUMMARY: - op = summary; - break; - case OPT_LIST_SUITES: - op = list_suites; - break; - case OPT_LIST_CASES: - op = list_cases; - break; - case OPT_LIST_SUITE_PATHS: - op = list_suite_paths; - break; - case OPT_LIST_CASE_PATHS: - op = list_case_paths; - break; - case OPT_LIST_DEFINES: - op = list_defines; - break; - case OPT_LIST_PERMUTATION_DEFINES: - op = list_permutation_defines; - break; - case OPT_LIST_IMPLICIT_DEFINES: - op = list_implicit_defines; - break; - case OPT_LIST_GEOMETRIES: - op = list_geometries; - break; - // configuration - case OPT_DEFINE: { - // allocate space - bench_override_t *override = mappend( - (void**)&bench_overrides, - sizeof(bench_override_t), - &bench_override_count, - &bench_override_capacity); - - // parse into string key/intmax_t value, cannibalizing the - // arg in the process - char *sep = strchr(optarg, '='); - char *parsed = NULL; - if (!sep) { - goto invalid_define; - } - *sep = '\0'; - override->name = optarg; - optarg = sep+1; - - // parse comma-separated permutations - { - override->defines = NULL; - override->permutations = 0; - size_t override_capacity = 0; - while (true) { + + printf("\n"); + exit(0); + + // summary/list flags + case OPT_SUMMARY:; + op = summary; + break; + + case OPT_LIST_SUITES:; + op = list_suites; + break; + + case OPT_LIST_CASES:; + op = list_cases; + break; + + case OPT_LIST_SUITE_PATHS:; + op = list_suite_paths; + break; + + case OPT_LIST_CASE_PATHS:; + op = list_case_paths; + break; + + case OPT_LIST_DEFINES:; + op = list_defines; + break; + + case OPT_LIST_PERMUTATION_DEFINES:; + op = list_permutation_defines; + break; + + case OPT_LIST_IMPLICIT_DEFINES:; + op = list_implicit_defines; + break; + + // configuration + case OPT_DEFINE:; + // allocate space + bench_define_t *override = mappend( + (void**)&bench_override_defines, + sizeof(bench_define_t), + &bench_override_define_count, + &bench_override_define_capacity); + + // parse into string key/intmax_t value, cannibalizing the + // arg in the process + char *sep = strchr(optarg, '='); + char *parsed = NULL; + if (!sep) { + goto invalid_define; + } + *sep = '\0'; + override->name = optarg; + optarg = sep+1; + + // parse comma-separated permutations + { + bench_override_value_t *override_values = NULL; + size_t override_value_count = 0; + size_t override_value_capacity = 0; + size_t override_permutations = 0; + while (true) { + optarg += strspn(optarg, " "); + + if (strncmp(optarg, "range", strlen("range")) == 0) { + // range of values + optarg += strlen("range"); optarg += strspn(optarg, " "); + if (*optarg != '(') { + goto invalid_define; + } + optarg += 1; - if (strncmp(optarg, "range", strlen("range")) == 0) { - // range of values - optarg += strlen("range"); - optarg += strspn(optarg, " "); - if (*optarg != '(') { - goto invalid_define; - } - optarg += 1; + intmax_t start = strtoumax(optarg, &parsed, 0); + intmax_t stop = -1; + intmax_t step = 1; + // allow empty string for start=0 + if (parsed == optarg) { + start = 0; + } + optarg = parsed + strspn(parsed, " "); - intmax_t start = strtoumax(optarg, &parsed, 0); - intmax_t stop = -1; - intmax_t step = 1; - // allow empty string for start=0 + if (*optarg != ',' && *optarg != ')') { + goto invalid_define; + } + + if (*optarg == ',') { + optarg += 1; + stop = strtoumax(optarg, &parsed, 0); + // allow empty string for stop=end if (parsed == optarg) { - start = 0; + stop = -1; } optarg = parsed + strspn(parsed, " "); @@ -1621,235 +1661,121 @@ int main(int argc, char **argv) { if (*optarg == ',') { optarg += 1; - stop = strtoumax(optarg, &parsed, 0); - // allow empty string for stop=end + step = strtoumax(optarg, &parsed, 0); + // allow empty string for stop=1 if (parsed == optarg) { - stop = -1; + step = 1; } optarg = parsed + strspn(parsed, " "); - if (*optarg != ',' && *optarg != ')') { + if (*optarg != ')') { goto invalid_define; } - - if (*optarg == ',') { - optarg += 1; - step = strtoumax(optarg, &parsed, 0); - // allow empty string for stop=1 - if (parsed == optarg) { - step = 1; - } - optarg = parsed + strspn(parsed, " "); - - if (*optarg != ')') { - goto invalid_define; - } - } - } else { - // single value = stop only - stop = start; - start = 0; - } - - if (*optarg != ')') { - goto invalid_define; - } - optarg += 1; - - // calculate the range of values - assert(step != 0); - for (intmax_t i = start; - (step < 0) - ? i > stop - : (uintmax_t)i < (uintmax_t)stop; - i += step) { - *(intmax_t*)mappend( - (void**)&override->defines, - sizeof(intmax_t), - &override->permutations, - &override_capacity) = i; - } - } else if (*optarg != '\0') { - // single value - intmax_t define = strtoimax(optarg, &parsed, 0); - if (parsed == optarg) { - goto invalid_define; } - optarg = parsed + strspn(parsed, " "); - *(intmax_t*)mappend( - (void**)&override->defines, - sizeof(intmax_t), - &override->permutations, - &override_capacity) = define; } else { - break; - } - - if (*optarg == ',') { - optarg += 1; - } - } - } - assert(override->permutations > 0); - break; - -invalid_define: - fprintf(stderr, "error: invalid define: %s\n", optarg); - exit(-1); - } - case OPT_GEOMETRY: { - // reset our geometry scenarios - if (bench_geometry_capacity > 0) { - free((bench_geometry_t*)bench_geometries); - } - bench_geometries = NULL; - bench_geometry_count = 0; - bench_geometry_capacity = 0; - - // parse the comma separated list of disk geometries - while (*optarg) { - // allocate space - bench_geometry_t *geometry = mappend( - (void**)&bench_geometries, - sizeof(bench_geometry_t), - &bench_geometry_count, - &bench_geometry_capacity); - - // parse the disk geometry - optarg += strspn(optarg, " "); - - // named disk geometry - size_t len = strcspn(optarg, " ,"); - for (size_t i = 0; builtin_geometries[i].name; i++) { - if (len == strlen(builtin_geometries[i].name) - && memcmp(optarg, - builtin_geometries[i].name, - len) == 0) { - *geometry = builtin_geometries[i]; - optarg += len; - goto geometry_next; + // single value = stop only + stop = start; + start = 0; } - } - // comma-separated read/prog/erase/count - if (*optarg == '{') { - lfs_size_t sizes[4]; - size_t count = 0; - - char *s = optarg + 1; - while (count < 4) { - char *parsed = NULL; - sizes[count] = strtoumax(s, &parsed, 0); - count += 1; - - s = parsed + strspn(parsed, " "); - if (*s == ',') { - s += 1; - continue; - } else if (*s == '}') { - s += 1; - break; - } else { - goto geometry_unknown; - } + if (*optarg != ')') { + goto invalid_define; } + optarg += 1; - // allow implicit r=p and p=e for common geometries - memset(geometry, 0, sizeof(bench_geometry_t)); - if (count >= 3) { - geometry->defines[READ_SIZE_i] - = BENCH_LIT(sizes[0]); - geometry->defines[PROG_SIZE_i] - = BENCH_LIT(sizes[1]); - geometry->defines[BLOCK_SIZE_i] - = BENCH_LIT(sizes[2]); - } else if (count >= 2) { - geometry->defines[PROG_SIZE_i] - = BENCH_LIT(sizes[0]); - geometry->defines[BLOCK_SIZE_i] - = BENCH_LIT(sizes[1]); + // append range + *(bench_override_value_t*)mappend( + (void**)&override_values, + sizeof(bench_override_value_t), + &override_value_count, + &override_value_capacity) + = (bench_override_value_t){ + .start = start, + .stop = stop, + .step = step, + }; + if (step > 0) { + override_permutations += (stop-1 - start) + / step + 1; } else { - geometry->defines[BLOCK_SIZE_i] - = BENCH_LIT(sizes[0]); - } - if (count >= 4) { - geometry->defines[BLOCK_COUNT_i] - = BENCH_LIT(sizes[3]); + override_permutations += (start-1 - stop) + / -step + 1; } - optarg = s; - goto geometry_next; - } - - // leb16-encoded read/prog/erase/count - if (*optarg == ':') { - lfs_size_t sizes[4]; - size_t count = 0; - - char *s = optarg + 1; - while (true) { - char *parsed = NULL; - uintmax_t x = leb16_parse(s, &parsed); - if (parsed == s || count >= 4) { - break; - } - - sizes[count] = x; - count += 1; - s = parsed; + } else if (*optarg != '\0') { + // single value + intmax_t define = strtoumax(optarg, &parsed, 0); + if (parsed == optarg) { + goto invalid_define; } + optarg = parsed + strspn(parsed, " "); - // allow implicit r=p and p=e for common geometries - memset(geometry, 0, sizeof(bench_geometry_t)); - if (count >= 3) { - geometry->defines[READ_SIZE_i] - = BENCH_LIT(sizes[0]); - geometry->defines[PROG_SIZE_i] - = BENCH_LIT(sizes[1]); - geometry->defines[BLOCK_SIZE_i] - = BENCH_LIT(sizes[2]); - } else if (count >= 2) { - geometry->defines[PROG_SIZE_i] - = BENCH_LIT(sizes[0]); - geometry->defines[BLOCK_SIZE_i] - = BENCH_LIT(sizes[1]); - } else { - geometry->defines[BLOCK_SIZE_i] - = BENCH_LIT(sizes[0]); - } - if (count >= 4) { - geometry->defines[BLOCK_COUNT_i] - = BENCH_LIT(sizes[3]); - } - optarg = s; - goto geometry_next; + // append value + *(bench_override_value_t*)mappend( + (void**)&override_values, + sizeof(bench_override_value_t), + &override_value_count, + &override_value_capacity) + = (bench_override_value_t){ + .start = define, + .step = 0, + }; + override_permutations += 1; + } else { + break; } -geometry_unknown: - // unknown scenario? - fprintf(stderr, "error: unknown disk geometry: %s\n", - optarg); - exit(-1); - -geometry_next: - optarg += strspn(optarg, " "); if (*optarg == ',') { optarg += 1; - } else if (*optarg == '\0') { - break; - } else { - goto geometry_unknown; } } - break; + + // define should be patched in bench_define_suite + override->define = NULL; + override->cb = bench_override_cb; + override->data = malloc(sizeof(bench_override_data_t)); + *(bench_override_data_t*)override->data + = (bench_override_data_t){ + .values = override_values, + .value_count = override_value_count, + }; + override->permutations = override_permutations; + } + break; + + invalid_define:; + fprintf(stderr, "error: invalid define: %s\n", optarg); + exit(-1); + + case OPT_DEFINE_DEPTH:; + parsed = NULL; + bench_define_depth = strtoumax(optarg, &parsed, 0); + if (parsed == optarg) { + fprintf(stderr, "error: invalid define-depth: %s\n", optarg); + exit(-1); + } + break; + + case OPT_STEP:; + parsed = NULL; + bench_step_start = strtoumax(optarg, &parsed, 0); + bench_step_stop = -1; + bench_step_step = 1; + // allow empty string for start=0 + if (parsed == optarg) { + bench_step_start = 0; + } + optarg = parsed + strspn(parsed, " "); + + if (*optarg != ',' && *optarg != '\0') { + goto step_unknown; } - case OPT_STEP: { - char *parsed = NULL; - bench_step_start = strtoumax(optarg, &parsed, 0); - bench_step_stop = -1; - bench_step_step = 1; - // allow empty string for start=0 + + if (*optarg == ',') { + optarg += 1; + bench_step_stop = strtoumax(optarg, &parsed, 0); + // allow empty string for stop=end if (parsed == optarg) { - bench_step_start = 0; + bench_step_stop = -1; } optarg = parsed + strspn(parsed, " "); @@ -1859,104 +1785,100 @@ int main(int argc, char **argv) { if (*optarg == ',') { optarg += 1; - bench_step_stop = strtoumax(optarg, &parsed, 0); - // allow empty string for stop=end + bench_step_step = strtoumax(optarg, &parsed, 0); + // allow empty string for stop=1 if (parsed == optarg) { - bench_step_stop = -1; + bench_step_step = 1; } optarg = parsed + strspn(parsed, " "); - if (*optarg != ',' && *optarg != '\0') { + if (*optarg != '\0') { goto step_unknown; } + } + } else { + // single value = stop only + bench_step_stop = bench_step_start; + bench_step_start = 0; + } - if (*optarg == ',') { - optarg += 1; - bench_step_step = strtoumax(optarg, &parsed, 0); - // allow empty string for stop=1 - if (parsed == optarg) { - bench_step_step = 1; - } - optarg = parsed + strspn(parsed, " "); + break; - if (*optarg != '\0') { - goto step_unknown; - } - } - } else { - // single value = stop only - bench_step_stop = bench_step_start; - bench_step_start = 0; - } + step_unknown:; + fprintf(stderr, "error: invalid step: %s\n", optarg); + exit(-1); - break; -step_unknown: - fprintf(stderr, "error: invalid step: %s\n", optarg); + case OPT_FORCE:; + bench_force = true; + break; + + case OPT_DISK:; + bench_disk_path = optarg; + break; + + case OPT_TRACE:; + bench_trace_path = optarg; + break; + + case OPT_TRACE_BACKTRACE:; + bench_trace_backtrace = true; + break; + + case OPT_TRACE_PERIOD:; + parsed = NULL; + bench_trace_period = strtoumax(optarg, &parsed, 0); + if (parsed == optarg) { + fprintf(stderr, "error: invalid trace-period: %s\n", optarg); exit(-1); } - case OPT_DISK: - bench_disk_path = optarg; - break; - case OPT_TRACE: - bench_trace_path = optarg; - break; - case OPT_TRACE_BACKTRACE: - bench_trace_backtrace = true; - break; - case OPT_TRACE_PERIOD: { - char *parsed = NULL; - bench_trace_period = strtoumax(optarg, &parsed, 0); - if (parsed == optarg) { - fprintf(stderr, "error: invalid trace-period: %s\n", optarg); - exit(-1); - } - break; - } - case OPT_TRACE_FREQ: { - char *parsed = NULL; - bench_trace_freq = strtoumax(optarg, &parsed, 0); - if (parsed == optarg) { - fprintf(stderr, "error: invalid trace-freq: %s\n", optarg); - exit(-1); - } - break; - } - case OPT_READ_SLEEP: { - char *parsed = NULL; - double read_sleep = strtod(optarg, &parsed); - if (parsed == optarg) { - fprintf(stderr, "error: invalid read-sleep: %s\n", optarg); - exit(-1); - } - bench_read_sleep = read_sleep*1.0e9; - break; + break; + + case OPT_TRACE_FREQ:; + parsed = NULL; + bench_trace_freq = strtoumax(optarg, &parsed, 0); + if (parsed == optarg) { + fprintf(stderr, "error: invalid trace-freq: %s\n", optarg); + exit(-1); } - case OPT_PROG_SLEEP: { - char *parsed = NULL; - double prog_sleep = strtod(optarg, &parsed); - if (parsed == optarg) { - fprintf(stderr, "error: invalid prog-sleep: %s\n", optarg); - exit(-1); - } - bench_prog_sleep = prog_sleep*1.0e9; - break; + break; + + case OPT_READ_SLEEP:; + parsed = NULL; + double read_sleep = strtod(optarg, &parsed); + if (parsed == optarg) { + fprintf(stderr, "error: invalid read-sleep: %s\n", optarg); + exit(-1); } - case OPT_ERASE_SLEEP: { - char *parsed = NULL; - double erase_sleep = strtod(optarg, &parsed); - if (parsed == optarg) { - fprintf(stderr, "error: invalid erase-sleep: %s\n", optarg); - exit(-1); - } - bench_erase_sleep = erase_sleep*1.0e9; - break; + bench_read_sleep = read_sleep*1.0e9; + break; + + case OPT_PROG_SLEEP:; + parsed = NULL; + double prog_sleep = strtod(optarg, &parsed); + if (parsed == optarg) { + fprintf(stderr, "error: invalid prog-sleep: %s\n", optarg); + exit(-1); } - // done parsing - case -1: - goto getopt_done; - // unknown arg, getopt prints a message for us - default: + bench_prog_sleep = prog_sleep*1.0e9; + break; + + case OPT_ERASE_SLEEP:; + parsed = NULL; + double erase_sleep = strtod(optarg, &parsed); + if (parsed == optarg) { + fprintf(stderr, "error: invalid erase-sleep: %s\n", optarg); exit(-1); + } + bench_erase_sleep = erase_sleep*1.0e9; + break; + + // done parsing + case -1:; + goto getopt_done; + + // unknown arg, getopt prints a message for us + default:; + exit(-1); } } getopt_done: ; @@ -2005,14 +1927,15 @@ getopt_done: ; if (d >= define_count) { // align to power of two to avoid any superlinear growth - size_t ncount = 1 << lfs_npw2(d+1); + size_t ncount = 1 << lfs3_nlog2(d+1); defines = realloc(defines, ncount*sizeof(bench_define_t)); memset(defines+define_count, 0, (ncount-define_count)*sizeof(bench_define_t)); define_count = ncount; } - defines[d] = BENCH_LIT(v); + // name/define should be patched in bench_define_suite + defines[d] = BENCH_LIT(NULL, v); } } @@ -2033,14 +1956,11 @@ getopt_done: ; // cleanup (need to be done for valgrind benching) bench_define_cleanup(); - if (bench_overrides) { - for (size_t i = 0; i < bench_override_count; i++) { - free((void*)bench_overrides[i].defines); + if (bench_override_defines) { + for (size_t i = 0; i < bench_override_define_count; i++) { + free((void*)bench_override_defines[i].data); } - free((void*)bench_overrides); - } - if (bench_geometry_capacity) { - free((void*)bench_geometries); + free((void*)bench_override_defines); } if (bench_id_capacity) { for (size_t i = 0; i < bench_id_count; i++) { diff --git a/runners/bench_runner.h b/runners/bench_runner.h index 6296c091e..f2d69182e 100644 --- a/runners/bench_runner.h +++ b/runners/bench_runner.h @@ -8,27 +8,35 @@ #define BENCH_RUNNER_H -// override LFS_TRACE +// override LFS3_TRACE void bench_trace(const char *fmt, ...); -#define LFS_TRACE_(fmt, ...) \ +#define LFS3_TRACE_(fmt, ...) \ bench_trace("%s:%d:trace: " fmt "%s\n", \ __FILE__, \ __LINE__, \ __VA_ARGS__) -#define LFS_TRACE(...) LFS_TRACE_(__VA_ARGS__, "") -#define LFS_EMUBD_TRACE(...) LFS_TRACE_(__VA_ARGS__, "") +#define LFS3_TRACE(...) LFS3_TRACE_(__VA_ARGS__, "") +#define LFS3_EMUBD_TRACE(...) LFS3_TRACE_(__VA_ARGS__, "") -// provide BENCH_START/BENCH_STOP macros -void bench_start(void); -void bench_stop(void); +// BENCH_START/BENCH_STOP macros measure readed/proged/erased bytes +// through emubd +void bench_start(const char *m, uintmax_t n); +void bench_stop(const char *m); -#define BENCH_START() bench_start() -#define BENCH_STOP() bench_stop() +#define BENCH_START(m, n) bench_start(m, n) +#define BENCH_STOP(m) bench_stop(m) + +// BENCH_RESULT/BENCH_FRESULT allow for explicit non-io measurements +void bench_result(const char *m, uintmax_t n, uintmax_t result); +void bench_fresult(const char *m, uintmax_t n, double result); + +#define BENCH_RESULT(m, n, result) bench_result(m, n, result) +#define BENCH_FRESULT(m, n, result) bench_fresult(m, n, result) // note these are indirectly included in any generated files -#include "bd/lfs_emubd.h" +#include "bd/lfs3_emubd.h" #include // give source a chance to define feature macros @@ -37,28 +45,31 @@ void bench_stop(void); // generated bench configurations -struct lfs_config; +struct lfs3_cfg; enum bench_flags { - BENCH_REENTRANT = 0x1, + BENCH_INTERNAL = 0x1, }; typedef uint8_t bench_flags_t; typedef struct bench_define { - intmax_t (*cb)(void *data); + const char *name; + intmax_t *define; + intmax_t (*cb)(void *data, size_t i); void *data; + size_t permutations; } bench_define_t; struct bench_case { const char *name; const char *path; bench_flags_t flags; - size_t permutations; const bench_define_t *defines; + size_t permutations; - bool (*filter)(void); - void (*run)(struct lfs_config *cfg); + bool (*if_)(void); + void (*run)(struct lfs3_cfg *cfg); }; struct bench_suite { @@ -66,66 +77,106 @@ struct bench_suite { const char *path; bench_flags_t flags; - const char *const *define_names; + const bench_define_t *defines; size_t define_count; const struct bench_case *cases; size_t case_count; }; +extern const struct bench_suite *const bench_suites[]; +extern const size_t bench_suite_count; + // deterministic prng for pseudo-randomness in benches uint32_t bench_prng(uint32_t *state); #define BENCH_PRNG(state) bench_prng(state) +// generation of specific permutations of an array for exhaustive benching +size_t bench_factorial(size_t x); +void bench_permutation(size_t i, uint32_t *buffer, size_t size); -// access generated bench defines -intmax_t bench_define(size_t define); +#define BENCH_FACTORIAL(x) bench_factorial(x) +#define BENCH_PERMUTATION(i, buffer, size) bench_permutation(i, buffer, size) -#define BENCH_DEFINE(i) bench_define(i) // a few preconfigured defines that control how benches run - -#define READ_SIZE_i 0 -#define PROG_SIZE_i 1 -#define BLOCK_SIZE_i 2 -#define BLOCK_COUNT_i 3 -#define CACHE_SIZE_i 4 -#define LOOKAHEAD_SIZE_i 5 -#define BLOCK_CYCLES_i 6 -#define ERASE_VALUE_i 7 -#define ERASE_CYCLES_i 8 -#define BADBLOCK_BEHAVIOR_i 9 -#define POWERLOSS_BEHAVIOR_i 10 - -#define READ_SIZE bench_define(READ_SIZE_i) -#define PROG_SIZE bench_define(PROG_SIZE_i) -#define BLOCK_SIZE bench_define(BLOCK_SIZE_i) -#define BLOCK_COUNT bench_define(BLOCK_COUNT_i) -#define CACHE_SIZE bench_define(CACHE_SIZE_i) -#define LOOKAHEAD_SIZE bench_define(LOOKAHEAD_SIZE_i) -#define BLOCK_CYCLES bench_define(BLOCK_CYCLES_i) -#define ERASE_VALUE bench_define(ERASE_VALUE_i) -#define ERASE_CYCLES bench_define(ERASE_CYCLES_i) -#define BADBLOCK_BEHAVIOR bench_define(BADBLOCK_BEHAVIOR_i) -#define POWERLOSS_BEHAVIOR bench_define(POWERLOSS_BEHAVIOR_i) - #define BENCH_IMPLICIT_DEFINES \ - BENCH_DEF(READ_SIZE, PROG_SIZE) \ - BENCH_DEF(PROG_SIZE, BLOCK_SIZE) \ - BENCH_DEF(BLOCK_SIZE, 0) \ - BENCH_DEF(BLOCK_COUNT, (1024*1024)/BLOCK_SIZE) \ - BENCH_DEF(CACHE_SIZE, lfs_max(64,lfs_max(READ_SIZE,PROG_SIZE))) \ - BENCH_DEF(LOOKAHEAD_SIZE, 16) \ - BENCH_DEF(BLOCK_CYCLES, -1) \ - BENCH_DEF(ERASE_VALUE, 0xff) \ - BENCH_DEF(ERASE_CYCLES, 0) \ - BENCH_DEF(BADBLOCK_BEHAVIOR, LFS_EMUBD_BADBLOCK_PROGERROR) \ - BENCH_DEF(POWERLOSS_BEHAVIOR, LFS_EMUBD_POWERLOSS_NOOP) - -#define BENCH_GEOMETRY_DEFINE_COUNT 4 -#define BENCH_IMPLICIT_DEFINE_COUNT 11 + /* name value (overridable) */ \ + BENCH_DEFINE(READ_SIZE, 1 ) \ + BENCH_DEFINE(PROG_SIZE, 1 ) \ + BENCH_DEFINE(BLOCK_SIZE, 4096 ) \ + BENCH_DEFINE(BLOCK_COUNT, DISK_SIZE/BLOCK_SIZE ) \ + BENCH_DEFINE(DISK_SIZE, 1024*1024 ) \ + BENCH_DEFINE(BLOCK_RECYCLES, -1 ) \ + BENCH_DEFINE(RCACHE_SIZE, LFS3_MAX(16, READ_SIZE) ) \ + BENCH_DEFINE(PCACHE_SIZE, LFS3_MAX(16, PROG_SIZE) ) \ + BENCH_DEFINE(FCACHE_SIZE, 16 ) \ + BENCH_DEFINE(LOOKAHEAD_SIZE, 16 ) \ + BENCH_DEFINE(GC_FLAGS, LFS3_GC_ALL ) \ + BENCH_DEFINE(GC_STEPS, 0 ) \ + BENCH_DEFINE(GC_LOOKAHEAD_THRESH, -1 ) \ + BENCH_DEFINE(GC_LOOKGBMAP_THRESH, -1 ) \ + BENCH_DEFINE(GC_COMPACTMETA_THRESH, 0 ) \ + BENCH_DEFINE(SHRUB_SIZE, BLOCK_SIZE/4 ) \ + BENCH_DEFINE(FRAGMENT_SIZE, LFS3_MIN(BLOCK_SIZE/8, 512) ) \ + BENCH_DEFINE(CRYSTAL_THRESH, BLOCK_SIZE/8 ) \ + BENCH_DEFINE(LOOKGBMAP_THRESH, BLOCK_COUNT/4 ) \ + BENCH_DEFINE(ERASE_VALUE, 0xff ) \ + BENCH_DEFINE(ERASE_CYCLES, 0 ) \ + BENCH_DEFINE(BADBLOCK_BEHAVIOR, LFS3_EMUBD_BADBLOCK_PROGERROR ) \ + BENCH_DEFINE(POWERLOSS_BEHAVIOR, LFS3_EMUBD_POWERLOSS_ATOMIC ) \ + BENCH_DEFINE(EMUBD_SEED, 0 ) + +// declare defines as global intmax_ts +#define BENCH_DEFINE(k, v) \ + extern intmax_t k; + + BENCH_IMPLICIT_DEFINES +#undef BENCH_DEFINE + +// map defines to cfg struct fields +#define BENCH_CFG \ + .read_size = READ_SIZE, \ + .prog_size = PROG_SIZE, \ + .block_size = BLOCK_SIZE, \ + .block_count = BLOCK_COUNT, \ + .block_recycles = BLOCK_RECYCLES, \ + .rcache_size = RCACHE_SIZE, \ + .pcache_size = PCACHE_SIZE, \ + .fcache_size = FCACHE_SIZE, \ + .lookahead_size = LOOKAHEAD_SIZE, \ + BENCH_GBMAP_CFG \ + BENCH_GC_CFG \ + .gc_lookahead_thresh = GC_LOOKAHEAD_THRESH, \ + .gc_compactmeta_thresh = GC_COMPACTMETA_THRESH, \ + .shrub_size = SHRUB_SIZE, \ + .fragment_size = FRAGMENT_SIZE, \ + .crystal_thresh = CRYSTAL_THRESH, + +#ifdef LFS3_GBMAP +#define BENCH_GBMAP_CFG \ + .gc_lookgbmap_thresh = GC_LOOKGBMAP_THRESH, \ + .lookgbmap_thresh = LOOKGBMAP_THRESH, +#else +#define BENCH_GBMAP_CFG +#endif + +#ifdef LFS3_GC +#define BENCH_GC_CFG \ + .gc_flags = GC_FLAGS, \ + .gc_steps = GC_STEPS, +#else +#define BENCH_GC_CFG +#endif + +#define BENCH_BDCFG \ + .erase_value = ERASE_VALUE, \ + .erase_cycles = ERASE_CYCLES, \ + .badblock_behavior = BADBLOCK_BEHAVIOR, \ + .powerloss_behavior = POWERLOSS_BEHAVIOR, \ + .seed = EMUBD_SEED, #endif diff --git a/runners/test_runner.c b/runners/test_runner.c index abc867c2d..ae7b3a7f2 100644 --- a/runners/test_runner.c +++ b/runners/test_runner.c @@ -9,7 +9,7 @@ #endif #include "runners/test_runner.h" -#include "bd/lfs_emubd.h" +#include "bd/lfs3_emubd.h" #include #include @@ -21,6 +21,7 @@ #include #include #include +#include // some helpers @@ -59,7 +60,7 @@ static void leb16_print(uintmax_t x) { } while (true) { - char nibble = (x & 0xf) | (x > 0xf ? 0x10 : 0); + char nibble = (x & 0xf) | ((x > 0xf) ? 0x10 : 0); printf("%c", (nibble < 10) ? '0'+nibble : 'a'+nibble-10); if (x <= 0xf) { break; @@ -103,342 +104,342 @@ static uintmax_t leb16_parse(const char *s, char **tail) { if (tail) { *tail = (char*)s; } - return neg ? -x : x; + return (neg) ? -x : x; } // test_runner types -typedef struct test_geometry { - const char *name; - test_define_t defines[TEST_GEOMETRY_DEFINE_COUNT]; -} test_geometry_t; - typedef struct test_powerloss { const char *name; void (*run)( - const lfs_emubd_powercycles_t *cycles, - size_t cycle_count, + const struct test_powerloss *powerloss, const struct test_suite *suite, const struct test_case *case_); - const lfs_emubd_powercycles_t *cycles; + const lfs3_emubd_powercycles_t *cycles; size_t cycle_count; } test_powerloss_t; typedef struct test_id { const char *name; - const test_define_t *defines; + test_define_t *defines; size_t define_count; - const lfs_emubd_powercycles_t *cycles; - size_t cycle_count; + test_powerloss_t powerloss; } test_id_t; -// test suites are linked into a custom ld section -extern struct test_suite __start__test_suites; -extern struct test_suite __stop__test_suites; - -const struct test_suite *test_suites = &__start__test_suites; -#define TEST_SUITE_COUNT \ - ((size_t)(&__stop__test_suites - &__start__test_suites)) - - // test define management -typedef struct test_define_map { - const test_define_t *defines; - size_t count; -} test_define_map_t; - -typedef struct test_define_names { - const char *const *names; - size_t count; -} test_define_names_t; -intmax_t test_define_lit(void *data) { - return (intptr_t)data; -} - -#define TEST_CONST(x) {test_define_lit, (void*)(uintptr_t)(x)} -#define TEST_LIT(x) ((test_define_t)TEST_CONST(x)) +// implicit defines declared here +#define TEST_DEFINE(k, v) \ + intmax_t k; + TEST_IMPLICIT_DEFINES +#undef TEST_DEFINE -#define TEST_DEF(k, v) \ - intmax_t test_define_##k(void *data) { \ +#define TEST_DEFINE(k, v) \ + intmax_t test_define_##k(void *data, size_t i) { \ (void)data; \ + (void)i; \ return v; \ } TEST_IMPLICIT_DEFINES -#undef TEST_DEF - -#define TEST_DEFINE_MAP_OVERRIDE 0 -#define TEST_DEFINE_MAP_EXPLICIT 1 -#define TEST_DEFINE_MAP_PERMUTATION 2 -#define TEST_DEFINE_MAP_GEOMETRY 3 -#define TEST_DEFINE_MAP_IMPLICIT 4 -#define TEST_DEFINE_MAP_COUNT 5 - -test_define_map_t test_define_maps[TEST_DEFINE_MAP_COUNT] = { - [TEST_DEFINE_MAP_IMPLICIT] = { - (const test_define_t[TEST_IMPLICIT_DEFINE_COUNT]) { - #define TEST_DEF(k, v) \ - [k##_i] = {test_define_##k, NULL}, - - TEST_IMPLICIT_DEFINES - #undef TEST_DEF - }, - TEST_IMPLICIT_DEFINE_COUNT, - }, -}; +#undef TEST_DEFINE -#define TEST_DEFINE_NAMES_SUITE 0 -#define TEST_DEFINE_NAMES_IMPLICIT 1 -#define TEST_DEFINE_NAMES_COUNT 2 - -test_define_names_t test_define_names[TEST_DEFINE_NAMES_COUNT] = { - [TEST_DEFINE_NAMES_IMPLICIT] = { - (const char *const[TEST_IMPLICIT_DEFINE_COUNT]){ - #define TEST_DEF(k, v) \ - [k##_i] = #k, - - TEST_IMPLICIT_DEFINES - #undef TEST_DEF - }, - TEST_IMPLICIT_DEFINE_COUNT, - }, -}; +const test_define_t test_implicit_defines[] = { + #define TEST_DEFINE(k, v) \ + {#k, &k, test_define_##k, NULL, 1}, -intmax_t *test_define_cache; -size_t test_define_cache_count; -unsigned *test_define_cache_mask; - -const char *test_define_name(size_t define) { - // lookup in our test names - for (size_t i = 0; i < TEST_DEFINE_NAMES_COUNT; i++) { - if (define < test_define_names[i].count - && test_define_names[i].names - && test_define_names[i].names[define]) { - return test_define_names[i].names[define]; - } - } + TEST_IMPLICIT_DEFINES + #undef TEST_DEFINE +}; +const size_t test_implicit_define_count + = sizeof(test_implicit_defines) / sizeof(test_define_t); - return NULL; +// some helpers +intmax_t test_define_lit(void *data, size_t i) { + (void)i; + return (intptr_t)data; } -bool test_define_ispermutation(size_t define) { - // is this define specific to the permutation? - for (size_t i = 0; i < TEST_DEFINE_MAP_IMPLICIT; i++) { - if (define < test_define_maps[i].count - && test_define_maps[i].defines[define].cb) { - return true; - } - } +#define TEST_LIT(name, v) ((test_define_t){ \ + name, NULL, test_define_lit, (void*)(uintptr_t)(v), 1}) - return false; -} -intmax_t test_define(size_t define) { - // is the define in our cache? - if (define < test_define_cache_count - && (test_define_cache_mask[define/(8*sizeof(unsigned))] - & (1 << (define%(8*sizeof(unsigned)))))) { - return test_define_cache[define]; - } - - // lookup in our test defines - for (size_t i = 0; i < TEST_DEFINE_MAP_COUNT; i++) { - if (define < test_define_maps[i].count - && test_define_maps[i].defines[define].cb) { - intmax_t v = test_define_maps[i].defines[define].cb( - test_define_maps[i].defines[define].data); +// define mapping +const test_define_t **test_defines = NULL; +size_t test_define_count = 0; +size_t test_define_capacity = 0; - // insert into cache! - test_define_cache[define] = v; - test_define_cache_mask[define / (8*sizeof(unsigned))] - |= 1 << (define%(8*sizeof(unsigned))); +const test_define_t **test_suite_defines = NULL; +size_t test_suite_define_count = 0; +ssize_t *test_suite_define_map = NULL; - return v; - } - } +test_define_t *test_override_defines = NULL; +size_t test_override_define_count = 0; - return 0; +size_t test_define_depth = 1000; - // not found? - const char *name = test_define_name(define); - fprintf(stderr, "error: undefined define %s (%zd)\n", - name ? name : "(unknown)", - define); - assert(false); - exit(-1); -} -void test_define_flush(void) { - // clear cache between permutations - memset(test_define_cache_mask, 0, - sizeof(unsigned)*( - (test_define_cache_count+(8*sizeof(unsigned))-1) - / (8*sizeof(unsigned)))); +static inline bool test_define_isdefined(const test_define_t *define) { + return define->cb; } -// geometry updates -const test_geometry_t *test_geometry = NULL; - -void test_define_geometry(const test_geometry_t *geometry) { - test_define_maps[TEST_DEFINE_MAP_GEOMETRY] = (test_define_map_t){ - geometry->defines, TEST_GEOMETRY_DEFINE_COUNT}; +static inline bool test_define_ispermutation(const test_define_t *define) { + // permutation defines are basically anything that's not implicit + return test_define_isdefined(define) + && !(define >= test_implicit_defines + && define + < test_implicit_defines + + test_implicit_define_count); } -// override updates -typedef struct test_override { - const char *name; - const intmax_t *defines; - size_t permutations; -} test_override_t; -const test_override_t *test_overrides = NULL; -size_t test_override_count = 0; - -test_define_t *test_override_defines = NULL; -size_t test_override_define_count = 0; -size_t test_override_define_permutations = 1; -size_t test_override_define_capacity = 0; - -// suite/perm updates -void test_define_suite(const struct test_suite *suite) { - test_define_names[TEST_DEFINE_NAMES_SUITE] = (test_define_names_t){ - suite->define_names, suite->define_count}; - - // make sure our cache is large enough - if (lfs_max(suite->define_count, TEST_IMPLICIT_DEFINE_COUNT) - > test_define_cache_count) { - // align to power of two to avoid any superlinear growth - size_t ncount = 1 << lfs_npw2( - lfs_max(suite->define_count, TEST_IMPLICIT_DEFINE_COUNT)); - test_define_cache = realloc(test_define_cache, ncount*sizeof(intmax_t)); - test_define_cache_mask = realloc(test_define_cache_mask, - sizeof(unsigned)*( - (ncount+(8*sizeof(unsigned))-1) - / (8*sizeof(unsigned)))); - test_define_cache_count = ncount; - } - - // map any overrides - if (test_override_count > 0) { - // first figure out the total size of override permutations - size_t count = 0; - size_t permutations = 1; - for (size_t i = 0; i < test_override_count; i++) { - for (size_t d = 0; - d < lfs_max( - suite->define_count, - TEST_IMPLICIT_DEFINE_COUNT); - d++) { - // define name match? - const char *name = test_define_name(d); - if (name && strcmp(name, test_overrides[i].name) == 0) { - count = lfs_max(count, d+1); - permutations *= test_overrides[i].permutations; - break; +void test_define_suite( + const test_id_t *id, + const struct test_suite *suite) { + // reset our mapping + test_define_count = 0; + test_suite_define_count = 0; + + // make sure we have space for everything, just assume the worst case + if (test_implicit_define_count + suite->define_count + > test_define_capacity) { + test_define_capacity + = test_implicit_define_count + suite->define_count; + test_defines = realloc( + test_defines, + test_define_capacity*sizeof(const test_define_t*)); + test_suite_defines = realloc( + test_suite_defines, + test_define_capacity*sizeof(const test_define_t*)); + test_suite_define_map = realloc( + test_suite_define_map, + test_define_capacity*sizeof(ssize_t)); + } + + // first map our implicit defines + for (size_t i = 0; i < test_implicit_define_count; i++) { + test_suite_defines[i] = &test_implicit_defines[i]; + } + test_suite_define_count = test_implicit_define_count; + + // build a mapping from suite defines to test defines + // + // we will use this for both suite and case defines + memset(test_suite_define_map, -1, + test_suite_define_count*sizeof(size_t)); + + for (size_t i = 0; i < suite->define_count; i++) { + // assume suite defines are unique so we only need to compare + // against implicit defines, this avoids a O(n^2) + for (size_t j = 0; j < test_implicit_define_count; j++) { + if (test_suite_defines[j]->define == suite->defines[i].define) { + test_suite_define_map[j] = i; + + // don't override implicit defines if we're not defined + if (test_define_isdefined(&suite->defines[i])) { + test_suite_defines[j] = &suite->defines[i]; } + goto next_suite_define; } } - test_override_define_count = count; - test_override_define_permutations = permutations; - - // make sure our override arrays are big enough - if (count * permutations > test_override_define_capacity) { - // align to power of two to avoid any superlinear growth - size_t ncapacity = 1 << lfs_npw2(count * permutations); - test_override_defines = realloc( - test_override_defines, - sizeof(test_define_t)*ncapacity); - test_override_define_capacity = ncapacity; - } - // zero unoverridden defines - memset(test_override_defines, 0, - sizeof(test_define_t) * count * permutations); - - // compute permutations - size_t p = 1; - for (size_t i = 0; i < test_override_count; i++) { - for (size_t d = 0; - d < lfs_max( - suite->define_count, - TEST_IMPLICIT_DEFINE_COUNT); - d++) { - // define name match? - const char *name = test_define_name(d); - if (name && strcmp(name, test_overrides[i].name) == 0) { - // scatter the define permutations based on already - // seen permutations - for (size_t j = 0; j < permutations; j++) { - test_override_defines[j*count + d] = TEST_LIT( - test_overrides[i].defines[(j/p) - % test_overrides[i].permutations]); - } + // map a new suite define + test_suite_define_map[test_suite_define_count] = i; + test_suite_defines[test_suite_define_count] = &suite->defines[i]; + test_suite_define_count += 1; +next_suite_define:; + } + + // map any explicit defines + // + // we ignore any out-of-bounds defines here, even though it's likely + // an error + if (id && id->defines) { + for (size_t i = 0; + i < id->define_count && i < test_suite_define_count; + i++) { + if (test_define_isdefined(&id->defines[i])) { + // update name/addr + id->defines[i].name = test_suite_defines[i]->name; + id->defines[i].define = test_suite_defines[i]->define; + // map and override suite mapping + test_suite_defines[i] = &id->defines[i]; + test_suite_define_map[i] = -1; + } + } + } - // keep track of how many permutations we've seen so far - p *= test_overrides[i].permutations; - break; - } + // map any override defines + // + // note it's not an error to override a define that doesn't exist + for (size_t i = 0; i < test_override_define_count; i++) { + for (size_t j = 0; j < test_suite_define_count; j++) { + if (strcmp( + test_suite_defines[j]->name, + test_override_defines[i].name) == 0) { + // update addr + test_override_defines[i].define + = test_suite_defines[j]->define; + // map and override suite mapping + test_suite_defines[j] = &test_override_defines[i]; + test_suite_define_map[j] = -1; + goto next_override_define; } } +next_override_define:; } } -void test_define_perm( +void test_define_case( + const test_id_t *id, const struct test_suite *suite, const struct test_case *case_, size_t perm) { - if (case_->defines) { - test_define_maps[TEST_DEFINE_MAP_PERMUTATION] = (test_define_map_t){ - case_->defines + perm*suite->define_count, - suite->define_count}; - } else { - test_define_maps[TEST_DEFINE_MAP_PERMUTATION] = (test_define_map_t){ - NULL, 0}; + (void)id; + + // copy over suite defines + for (size_t i = 0; i < test_suite_define_count; i++) { + // map case define if case define is defined + if (case_->defines + && test_suite_define_map[i] != -1 + && test_define_isdefined(&case_->defines[ + perm*suite->define_count + + test_suite_define_map[i]])) { + test_defines[i] = &case_->defines[ + perm*suite->define_count + + test_suite_define_map[i]]; + } else { + test_defines[i] = test_suite_defines[i]; + } } + test_define_count = test_suite_define_count; } -void test_define_override(size_t perm) { - test_define_maps[TEST_DEFINE_MAP_OVERRIDE] = (test_define_map_t){ - test_override_defines + perm*test_override_define_count, - test_override_define_count}; -} +void test_define_permutation(size_t perm) { + // first zero everything, we really don't want reproducibility issues + for (size_t i = 0; i < test_define_count; i++) { + *test_defines[i]->define = 0; + } + + // defines may be mutually recursive, which makes evaluation a bit tricky + // + // Rather than doing any clever, we just repeatedly evaluate the + // permutation until values stabilize. If things don't stabilize after + // some number of iterations, error, this likely means defines were + // stuck in a cycle + // + size_t attempt = 0; + while (true) { + const test_define_t *changed = NULL; + // define-specific permutations are encoded in the case permutation + size_t perm_ = perm; + for (size_t i = 0; i < test_define_count; i++) { + if (test_defines[i]->cb) { + intmax_t v = test_defines[i]->cb( + test_defines[i]->data, + perm_ % test_defines[i]->permutations); + if (v != *test_defines[i]->define) { + *test_defines[i]->define = v; + changed = test_defines[i]; + } + + perm_ /= test_defines[i]->permutations; + } + } + + // stabilized? + if (!changed) { + break; + } -void test_define_explicit( - const test_define_t *defines, - size_t define_count) { - test_define_maps[TEST_DEFINE_MAP_EXPLICIT] = (test_define_map_t){ - defines, define_count}; + attempt += 1; + if (test_define_depth && attempt >= test_define_depth+1) { + fprintf(stderr, "error: could not resolve recursive defines: %s\n", + changed->name); + exit(-1); + } + } } void test_define_cleanup(void) { // test define management can allocate a few things - free(test_define_cache); - free(test_define_cache_mask); - free(test_override_defines); + free(test_defines); + free(test_suite_defines); + free(test_suite_define_map); } +size_t test_define_permutations(void) { + size_t prod = 1; + for (size_t i = 0; i < test_define_count; i++) { + prod *= (test_defines[i]->permutations > 0) + ? test_defines[i]->permutations + : 1; + } + return prod; +} -// test state -extern const test_geometry_t *test_geometries; -extern size_t test_geometry_count; +// override define stuff -extern const test_powerloss_t *test_powerlosses; -extern size_t test_powerloss_count; +typedef struct test_override_value { + intmax_t start; + intmax_t stop; + // step == 0 indicates a single value + intmax_t step; +} test_override_value_t; +typedef struct test_override_data { + test_override_value_t *values; + size_t value_count; +} test_override_data_t; + +intmax_t test_override_cb(void *data, size_t i) { + const test_override_data_t *data_ = data; + for (size_t j = 0; j < data_->value_count; j++) { + const test_override_value_t *v = &data_->values[j]; + // range? + if (v->step) { + size_t range_count; + if (v->step > 0) { + range_count = (v->stop-1 - v->start) / v->step + 1; + } else { + range_count = (v->start-1 - v->stop) / -v->step + 1; + } + + if (i < range_count) { + return i*v->step + v->start; + } + i -= range_count; + // value? + } else { + if (i == 0) { + return v->start; + } + i -= 1; + } + } + + // should never get here + assert(false); + __builtin_unreachable(); +} + + + +// test state const test_id_t *test_ids = (const test_id_t[]) { - {NULL, NULL, 0, NULL, 0}, + {NULL, NULL, 0, {NULL, NULL, NULL, 0}}, }; size_t test_id_count = 1; size_t test_step_start = 0; size_t test_step_stop = -1; size_t test_step_step = 1; +bool test_force = false; const char *test_disk_path = NULL; const char *test_trace_path = NULL; @@ -449,9 +450,15 @@ FILE *test_trace_file = NULL; uint32_t test_trace_cycles = 0; uint64_t test_trace_time = 0; uint64_t test_trace_open_time = 0; -lfs_emubd_sleep_t test_read_sleep = 0.0; -lfs_emubd_sleep_t test_prog_sleep = 0.0; -lfs_emubd_sleep_t test_erase_sleep = 0.0; +lfs3_emubd_sleep_t test_read_sleep = 0.0; +lfs3_emubd_sleep_t test_prog_sleep = 0.0; +lfs3_emubd_sleep_t test_erase_sleep = 0.0; + +volatile size_t TEST_PLS = 0; + +extern const test_powerloss_t *test_powerlosses; +extern size_t test_powerloss_count; + // this determines both the backtrace buffer and the trace printf buffer, if // trace ends up interleaved or truncated this may need to be increased @@ -558,12 +565,16 @@ void test_trace(const char *fmt, ...) { } } - // test prng uint32_t test_prng(uint32_t *state) { // A simple xorshift32 generator, easily reproducible. Keep in mind // determinism is much more important than actual randomness here. uint32_t x = *state; + // must be non-zero, use uintmax here so that seed=0 is different + // from seed=1 and seed=range(0,n) makes a bit more sense + if (x == 0) { + x = -1; + } x ^= x << 13; x ^= x >> 17; x ^= x << 5; @@ -571,29 +582,61 @@ uint32_t test_prng(uint32_t *state) { return x; } +// test factorial +size_t test_factorial(size_t x) { + size_t y = 1; + for (size_t i = 2; i <= x; i++) { + y *= i; + } + return y; +} + +// test array permutations +void test_permutation(size_t i, uint32_t *buffer, size_t size) { + // https://stackoverflow.com/a/7919887 and + // https://stackoverflow.com/a/24257996 helped a lot with this, but + // changed to run in O(n) with no extra memory. This has a tradeoff + // of generating the permutations in an unintuitive order. + + // initialize array + for (size_t j = 0; j < size; j++) { + buffer[j] = j; + } + + for (size_t j = 0; j < size; j++) { + // swap index with digit + // + // .- i%rem --. + // v .----+----. + // [p0 p1 |-> r0 r1 r2 r3] + // + size_t t = buffer[j + (i % (size-j))]; + buffer[j + (i % (size-j))] = buffer[j]; + buffer[j] = t; + // update i + i /= (size-j); + } +} + // encode our permutation into a reusable id static void perm_printid( const struct test_suite *suite, const struct test_case *case_, - const lfs_emubd_powercycles_t *cycles, + const lfs3_emubd_powercycles_t *cycles, size_t cycle_count) { (void)suite; // case[:permutation[:powercycles]] printf("%s:", case_->name); - for (size_t d = 0; - d < lfs_max( - suite->define_count, - TEST_IMPLICIT_DEFINE_COUNT); - d++) { - if (test_define_ispermutation(d)) { + for (size_t d = 0; d < test_define_count; d++) { + if (test_define_ispermutation(test_defines[d])) { leb16_print(d); - leb16_print(TEST_DEFINE(d)); + leb16_print(*test_defines[d]->define); } } // only print power-cycles if any occured - if (cycles) { + if (cycle_count) { printf(":"); for (size_t i = 0; i < cycle_count; i++) { leb16_print(cycles[i]); @@ -614,26 +657,19 @@ struct test_seen_branch { struct test_seen branch; }; -bool test_seen_insert( - test_seen_t *seen, - const struct test_suite *suite, - const struct test_case *case_) { - (void)case_; - bool was_seen = true; - +bool test_seen_insert(test_seen_t *seen) { // use the currently set defines - for (size_t d = 0; - d < lfs_max( - suite->define_count, - TEST_IMPLICIT_DEFINE_COUNT); - d++) { + bool was_seen = true; + for (size_t d = 0; d < test_define_count; d++) { // treat unpermuted defines the same as 0 - intmax_t define = test_define_ispermutation(d) ? TEST_DEFINE(d) : 0; + intmax_t v = test_define_ispermutation(test_defines[d]) + ? *test_defines[d]->define + : 0; // already seen? struct test_seen_branch *branch = NULL; for (size_t i = 0; i < seen->branch_count; i++) { - if (seen->branches[i].define == define) { + if (seen->branches[i].define == v) { branch = &seen->branches[i]; break; } @@ -647,7 +683,7 @@ bool test_seen_insert( sizeof(struct test_seen_branch), &seen->branch_count, &seen->branch_capacity); - branch->define = define; + branch->define = v; branch->branch = (test_seen_t){NULL, 0, 0}; } @@ -665,24 +701,19 @@ void test_seen_cleanup(test_seen_t *seen) { } static void run_powerloss_none( - const lfs_emubd_powercycles_t *cycles, - size_t cycle_count, + const test_powerloss_t *powerloss, const struct test_suite *suite, const struct test_case *case_); static void run_powerloss_cycles( - const lfs_emubd_powercycles_t *cycles, - size_t cycle_count, + const test_powerloss_t *powerloss, const struct test_suite *suite, const struct test_case *case_); // iterate through permutations in a test case static void case_forperm( + const test_id_t *id, const struct test_suite *suite, const struct test_case *case_, - const test_define_t *defines, - size_t define_count, - const lfs_emubd_powercycles_t *cycles, - size_t cycle_count, void (*cb)( void *data, const struct test_suite *suite, @@ -690,20 +721,18 @@ static void case_forperm( const test_powerloss_t *powerloss), void *data) { // explicit permutation? - if (defines) { - test_define_explicit(defines, define_count); + if (id && id->defines) { + // define case permutation, the exact case perm doesn't matter here + test_define_case(id, suite, case_, 0); - for (size_t v = 0; v < test_override_define_permutations; v++) { - // define override permutation - test_define_override(v); - test_define_flush(); + size_t permutations = test_define_permutations(); + for (size_t p = 0; p < permutations; p++) { + // define permutation permutation + test_define_permutation(p); // explicit powerloss cycles? - if (cycles) { - cb(data, suite, case_, &(test_powerloss_t){ - .run=run_powerloss_cycles, - .cycles=cycles, - .cycle_count=cycle_count}); + if (id && id->powerloss.run) { + cb(data, suite, case_, &id->powerloss); } else { for (size_t p = 0; p < test_powerloss_count; p++) { // skip non-reentrant tests when powerloss testing @@ -720,42 +749,42 @@ static void case_forperm( return; } + // deduplicate permutations with the same defines + // + // this can easily happen when overriding multiple case permutations, + // we can't tell that multiple case permutations don't change defines, + // duplicating results test_seen_t seen = {NULL, 0, 0}; - for (size_t k = 0; k < case_->permutations; k++) { - // define permutation - test_define_perm(suite, case_, k); - - for (size_t v = 0; v < test_override_define_permutations; v++) { - // define override permutation - test_define_override(v); + for (size_t k = 0; + k < ((case_->permutations) ? case_->permutations : 1); + k++) { + // define case permutation + test_define_case(id, suite, case_, k); - for (size_t g = 0; g < test_geometry_count; g++) { - // define geometry - test_define_geometry(&test_geometries[g]); - test_define_flush(); - - // have we seen this permutation before? - bool was_seen = test_seen_insert(&seen, suite, case_); - if (!(k == 0 && v == 0 && g == 0) && was_seen) { - continue; - } + size_t permutations = test_define_permutations(); + for (size_t p = 0; p < permutations; p++) { + // define permutation permutation + test_define_permutation(p); - if (cycles) { - cb(data, suite, case_, &(test_powerloss_t){ - .run=run_powerloss_cycles, - .cycles=cycles, - .cycle_count=cycle_count}); - } else { - for (size_t p = 0; p < test_powerloss_count; p++) { - // skip non-reentrant tests when powerloss testing - if (test_powerlosses[p].run != run_powerloss_none - && !(case_->flags & TEST_REENTRANT)) { - continue; - } + // have we seen this permutation before? + bool was_seen = test_seen_insert(&seen); + if (!(k == 0 && p == 0) && was_seen) { + continue; + } - cb(data, suite, case_, &test_powerlosses[p]); + // explicit powerloss cycles? + if (id && id->powerloss.run) { + cb(data, suite, case_, &id->powerloss); + } else { + for (size_t p = 0; p < test_powerloss_count; p++) { + // skip non-reentrant tests when powerloss testing + if (test_powerlosses[p].run != run_powerloss_none + && !(case_->flags & TEST_REENTRANT)) { + continue; } + + cb(data, suite, case_, &test_powerlosses[p]); } } } @@ -778,12 +807,12 @@ void perm_count( const test_powerloss_t *powerloss) { struct perm_count_state *state = data; (void)suite; - (void)case_; - (void)powerloss; state->total += 1; - if (case_->filter && !case_->filter()) { + // set pls to 1 if running under powerloss so it useful for if predicates + TEST_PLS = (powerloss->run != run_powerloss_none); + if (!case_->run || !(test_force || !case_->if_ || case_->if_())) { return; } @@ -793,7 +822,7 @@ void perm_count( // operations we can do static void summary(void) { - printf("%-23s %7s %7s %7s %11s\n", + printf("%-23s %7s %7s %7s %15s\n", "", "flags", "suites", "cases", "perms"); size_t suites = 0; size_t cases = 0; @@ -801,43 +830,50 @@ static void summary(void) { struct perm_count_state perms = {0, 0}; for (size_t t = 0; t < test_id_count; t++) { - for (size_t i = 0; i < TEST_SUITE_COUNT; i++) { - test_define_suite(&test_suites[i]); + for (size_t i = 0; i < test_suite_count; i++) { + test_define_suite(&test_ids[t], test_suites[i]); + + size_t cases_ = 0; - for (size_t j = 0; j < test_suites[i].case_count; j++) { + for (size_t j = 0; j < test_suites[i]->case_count; j++) { // does neither suite nor case name match? if (test_ids[t].name && !( strcmp(test_ids[t].name, - test_suites[i].name) == 0 + test_suites[i]->name) == 0 || strcmp(test_ids[t].name, - test_suites[i].cases[j].name) == 0)) { + test_suites[i]->cases[j].name) == 0)) { continue; } cases += 1; + cases_ += 1; case_forperm( - &test_suites[i], - &test_suites[i].cases[j], - test_ids[t].defines, - test_ids[t].define_count, - test_ids[t].cycles, - test_ids[t].cycle_count, + &test_ids[t], + test_suites[i], + &test_suites[i]->cases[j], perm_count, &perms); } + // no tests found? + if (!cases_) { + continue; + } + suites += 1; - flags |= test_suites[i].flags; + flags |= test_suites[i]->flags; } } char perm_buf[64]; sprintf(perm_buf, "%zu/%zu", perms.filtered, perms.total); char flag_buf[64]; - sprintf(flag_buf, "%s%s", + sprintf(flag_buf, "%s%s%s%s", + (flags & TEST_INTERNAL) ? "i" : "", (flags & TEST_REENTRANT) ? "r" : "", - (!flags) ? "-" : ""); - printf("%-23s %7s %7zu %7zu %11s\n", + (flags & TEST_FUZZ) ? "f" : "", + (!flags) ? "-" : ""); + printf("%-23s %7s %7zu %7zu %15s\n", "TOTAL", flag_buf, suites, @@ -848,41 +884,38 @@ static void summary(void) { static void list_suites(void) { // at least size so that names fit unsigned name_width = 23; - for (size_t i = 0; i < TEST_SUITE_COUNT; i++) { - size_t len = strlen(test_suites[i].name); + for (size_t i = 0; i < test_suite_count; i++) { + size_t len = strlen(test_suites[i]->name); if (len > name_width) { name_width = len; } } name_width = 4*((name_width+1+4-1)/4)-1; - printf("%-*s %7s %7s %11s\n", + printf("%-*s %7s %7s %15s\n", name_width, "suite", "flags", "cases", "perms"); for (size_t t = 0; t < test_id_count; t++) { - for (size_t i = 0; i < TEST_SUITE_COUNT; i++) { - test_define_suite(&test_suites[i]); + for (size_t i = 0; i < test_suite_count; i++) { + test_define_suite(&test_ids[t], test_suites[i]); size_t cases = 0; struct perm_count_state perms = {0, 0}; - for (size_t j = 0; j < test_suites[i].case_count; j++) { + for (size_t j = 0; j < test_suites[i]->case_count; j++) { // does neither suite nor case name match? if (test_ids[t].name && !( strcmp(test_ids[t].name, - test_suites[i].name) == 0 + test_suites[i]->name) == 0 || strcmp(test_ids[t].name, - test_suites[i].cases[j].name) == 0)) { + test_suites[i]->cases[j].name) == 0)) { continue; } cases += 1; case_forperm( - &test_suites[i], - &test_suites[i].cases[j], - test_ids[t].defines, - test_ids[t].define_count, - test_ids[t].cycles, - test_ids[t].cycle_count, + &test_ids[t], + test_suites[i], + &test_suites[i]->cases[j], perm_count, &perms); } @@ -895,12 +928,14 @@ static void list_suites(void) { char perm_buf[64]; sprintf(perm_buf, "%zu/%zu", perms.filtered, perms.total); char flag_buf[64]; - sprintf(flag_buf, "%s%s", - (test_suites[i].flags & TEST_REENTRANT) ? "r" : "", - (!test_suites[i].flags) ? "-" : ""); - printf("%-*s %7s %7zu %11s\n", + sprintf(flag_buf, "%s%s%s%s", + (test_suites[i]->flags & TEST_INTERNAL) ? "i" : "", + (test_suites[i]->flags & TEST_REENTRANT) ? "r" : "", + (test_suites[i]->flags & TEST_FUZZ) ? "f" : "", + (!test_suites[i]->flags) ? "-" : ""); + printf("%-*s %7s %7zu %15s\n", name_width, - test_suites[i].name, + test_suites[i]->name, flag_buf, cases, perm_buf); @@ -911,9 +946,9 @@ static void list_suites(void) { static void list_cases(void) { // at least size so that names fit unsigned name_width = 23; - for (size_t i = 0; i < TEST_SUITE_COUNT; i++) { - for (size_t j = 0; j < test_suites[i].case_count; j++) { - size_t len = strlen(test_suites[i].cases[j].name); + for (size_t i = 0; i < test_suite_count; i++) { + for (size_t j = 0; j < test_suites[i]->case_count; j++) { + size_t len = strlen(test_suites[i]->cases[j].name); if (len > name_width) { name_width = len; } @@ -921,43 +956,44 @@ static void list_cases(void) { } name_width = 4*((name_width+1+4-1)/4)-1; - printf("%-*s %7s %11s\n", name_width, "case", "flags", "perms"); + printf("%-*s %7s %15s\n", name_width, "case", "flags", "perms"); for (size_t t = 0; t < test_id_count; t++) { - for (size_t i = 0; i < TEST_SUITE_COUNT; i++) { - test_define_suite(&test_suites[i]); + for (size_t i = 0; i < test_suite_count; i++) { + test_define_suite(&test_ids[t], test_suites[i]); - for (size_t j = 0; j < test_suites[i].case_count; j++) { + for (size_t j = 0; j < test_suites[i]->case_count; j++) { // does neither suite nor case name match? if (test_ids[t].name && !( strcmp(test_ids[t].name, - test_suites[i].name) == 0 + test_suites[i]->name) == 0 || strcmp(test_ids[t].name, - test_suites[i].cases[j].name) == 0)) { + test_suites[i]->cases[j].name) == 0)) { continue; } struct perm_count_state perms = {0, 0}; case_forperm( - &test_suites[i], - &test_suites[i].cases[j], - test_ids[t].defines, - test_ids[t].define_count, - test_ids[t].cycles, - test_ids[t].cycle_count, + &test_ids[t], + test_suites[i], + &test_suites[i]->cases[j], perm_count, &perms); char perm_buf[64]; sprintf(perm_buf, "%zu/%zu", perms.filtered, perms.total); char flag_buf[64]; - sprintf(flag_buf, "%s%s", - (test_suites[i].cases[j].flags & TEST_REENTRANT) + sprintf(flag_buf, "%s%s%s%s", + (test_suites[i]->cases[j].flags & TEST_INTERNAL) + ? "i" : "", + (test_suites[i]->cases[j].flags & TEST_REENTRANT) ? "r" : "", - (!test_suites[i].cases[j].flags) + (test_suites[i]->cases[j].flags & TEST_FUZZ) + ? "f" : "", + (!test_suites[i]->cases[j].flags) ? "-" : ""); - printf("%-*s %7s %11s\n", + printf("%-*s %7s %15s\n", name_width, - test_suites[i].cases[j].name, + test_suites[i]->cases[j].name, flag_buf, perm_buf); } @@ -968,8 +1004,8 @@ static void list_cases(void) { static void list_suite_paths(void) { // at least size so that names fit unsigned name_width = 23; - for (size_t i = 0; i < TEST_SUITE_COUNT; i++) { - size_t len = strlen(test_suites[i].name); + for (size_t i = 0; i < test_suite_count; i++) { + size_t len = strlen(test_suites[i]->name); if (len > name_width) { name_width = len; } @@ -978,16 +1014,16 @@ static void list_suite_paths(void) { printf("%-*s %s\n", name_width, "suite", "path"); for (size_t t = 0; t < test_id_count; t++) { - for (size_t i = 0; i < TEST_SUITE_COUNT; i++) { + for (size_t i = 0; i < test_suite_count; i++) { size_t cases = 0; - for (size_t j = 0; j < test_suites[i].case_count; j++) { + for (size_t j = 0; j < test_suites[i]->case_count; j++) { // does neither suite nor case name match? if (test_ids[t].name && !( strcmp(test_ids[t].name, - test_suites[i].name) == 0 + test_suites[i]->name) == 0 || strcmp(test_ids[t].name, - test_suites[i].cases[j].name) == 0)) { + test_suites[i]->cases[j].name) == 0)) { continue; } @@ -1001,8 +1037,8 @@ static void list_suite_paths(void) { printf("%-*s %s\n", name_width, - test_suites[i].name, - test_suites[i].path); + test_suites[i]->name, + test_suites[i]->path); } } } @@ -1010,9 +1046,9 @@ static void list_suite_paths(void) { static void list_case_paths(void) { // at least size so that names fit unsigned name_width = 23; - for (size_t i = 0; i < TEST_SUITE_COUNT; i++) { - for (size_t j = 0; j < test_suites[i].case_count; j++) { - size_t len = strlen(test_suites[i].cases[j].name); + for (size_t i = 0; i < test_suite_count; i++) { + for (size_t j = 0; j < test_suites[i]->case_count; j++) { + size_t len = strlen(test_suites[i]->cases[j].name); if (len > name_width) { name_width = len; } @@ -1022,21 +1058,21 @@ static void list_case_paths(void) { printf("%-*s %s\n", name_width, "case", "path"); for (size_t t = 0; t < test_id_count; t++) { - for (size_t i = 0; i < TEST_SUITE_COUNT; i++) { - for (size_t j = 0; j < test_suites[i].case_count; j++) { + for (size_t i = 0; i < test_suite_count; i++) { + for (size_t j = 0; j < test_suites[i]->case_count; j++) { // does neither suite nor case name match? if (test_ids[t].name && !( strcmp(test_ids[t].name, - test_suites[i].name) == 0 + test_suites[i]->name) == 0 || strcmp(test_ids[t].name, - test_suites[i].cases[j].name) == 0)) { + test_suites[i]->cases[j].name) == 0)) { continue; } printf("%-*s %s\n", name_width, - test_suites[i].cases[j].name, - test_suites[i].cases[j].path); + test_suites[i]->cases[j].name, + test_suites[i]->cases[j].path); } } } @@ -1057,16 +1093,16 @@ struct list_defines_defines { static void list_defines_add( struct list_defines_defines *defines, - size_t d) { - const char *name = test_define_name(d); - intmax_t value = TEST_DEFINE(d); + const test_define_t *define) { + const char *name = define->name; + intmax_t v = *define->define; // define already in defines? for (size_t i = 0; i < defines->define_count; i++) { if (strcmp(defines->defines[i].name, name) == 0) { // value already in values? for (size_t j = 0; j < defines->defines[i].value_count; j++) { - if (defines->defines[i].values[j] == value) { + if (defines->defines[i].values[j] == v) { return; } } @@ -1075,23 +1111,23 @@ static void list_defines_add( (void**)&defines->defines[i].values, sizeof(intmax_t), &defines->defines[i].value_count, - &defines->defines[i].value_capacity) = value; + &defines->defines[i].value_capacity) = v; return; } } // new define? - struct list_defines_define *define = mappend( + struct list_defines_define *define_ = mappend( (void**)&defines->defines, sizeof(struct list_defines_define), &defines->define_count, &defines->define_capacity); - define->name = name; - define->values = malloc(sizeof(intmax_t)); - define->values[0] = value; - define->value_count = 1; - define->value_capacity = 1; + define_->name = name; + define_->values = malloc(sizeof(intmax_t)); + define_->values[0] = v; + define_->value_count = 1; + define_->value_capacity = 1; } void perm_list_defines( @@ -1105,13 +1141,9 @@ void perm_list_defines( (void)powerloss; // collect defines - for (size_t d = 0; - d < lfs_max(suite->define_count, - TEST_IMPLICIT_DEFINE_COUNT); - d++) { - if (d < TEST_IMPLICIT_DEFINE_COUNT - || test_define_ispermutation(d)) { - list_defines_add(defines, d); + for (size_t d = 0; d < test_define_count; d++) { + if (test_define_isdefined(test_defines[d])) { + list_defines_add(defines, test_defines[d]); } } } @@ -1127,43 +1159,35 @@ void perm_list_permutation_defines( (void)powerloss; // collect permutation_defines - for (size_t d = 0; - d < lfs_max(suite->define_count, - TEST_IMPLICIT_DEFINE_COUNT); - d++) { - if (test_define_ispermutation(d)) { - list_defines_add(defines, d); + for (size_t d = 0; d < test_define_count; d++) { + if (test_define_ispermutation(test_defines[d])) { + list_defines_add(defines, test_defines[d]); } } } -extern const test_geometry_t builtin_geometries[]; - static void list_defines(void) { struct list_defines_defines defines = {NULL, 0, 0}; // add defines for (size_t t = 0; t < test_id_count; t++) { - for (size_t i = 0; i < TEST_SUITE_COUNT; i++) { - test_define_suite(&test_suites[i]); + for (size_t i = 0; i < test_suite_count; i++) { + test_define_suite(&test_ids[t], test_suites[i]); - for (size_t j = 0; j < test_suites[i].case_count; j++) { + for (size_t j = 0; j < test_suites[i]->case_count; j++) { // does neither suite nor case name match? if (test_ids[t].name && !( strcmp(test_ids[t].name, - test_suites[i].name) == 0 + test_suites[i]->name) == 0 || strcmp(test_ids[t].name, - test_suites[i].cases[j].name) == 0)) { + test_suites[i]->cases[j].name) == 0)) { continue; } case_forperm( - &test_suites[i], - &test_suites[i].cases[j], - test_ids[t].defines, - test_ids[t].define_count, - test_ids[t].cycles, - test_ids[t].cycle_count, + &test_ids[t], + test_suites[i], + &test_suites[i]->cases[j], perm_list_defines, &defines); } @@ -1192,26 +1216,23 @@ static void list_permutation_defines(void) { // add permutation defines for (size_t t = 0; t < test_id_count; t++) { - for (size_t i = 0; i < TEST_SUITE_COUNT; i++) { - test_define_suite(&test_suites[i]); + for (size_t i = 0; i < test_suite_count; i++) { + test_define_suite(&test_ids[t], test_suites[i]); - for (size_t j = 0; j < test_suites[i].case_count; j++) { + for (size_t j = 0; j < test_suites[i]->case_count; j++) { // does neither suite nor case name match? if (test_ids[t].name && !( strcmp(test_ids[t].name, - test_suites[i].name) == 0 + test_suites[i]->name) == 0 || strcmp(test_ids[t].name, - test_suites[i].cases[j].name) == 0)) { + test_suites[i]->cases[j].name) == 0)) { continue; } case_forperm( - &test_suites[i], - &test_suites[i].cases[j], - test_ids[t].defines, - test_ids[t].define_count, - test_ids[t].cycles, - test_ids[t].cycle_count, + &test_ids[t], + test_suites[i], + &test_suites[i]->cases[j], perm_list_permutation_defines, &defines); } @@ -1238,19 +1259,23 @@ static void list_permutation_defines(void) { static void list_implicit_defines(void) { struct list_defines_defines defines = {NULL, 0, 0}; - // yes we do need to define a suite, this does a bit of bookeeping - // such as setting up the define cache - test_define_suite(&(const struct test_suite){0}); + // yes we do need to define a suite/case, these do a bit of bookeeping + // around mapping defines + test_define_suite(NULL, + &(const struct test_suite){0}); + test_define_case(NULL, + &(const struct test_suite){0}, + &(const struct test_case){0}, + 0); - // make sure to include builtin geometries here - extern const test_geometry_t builtin_geometries[]; - for (size_t g = 0; builtin_geometries[g].name; g++) { - test_define_geometry(&builtin_geometries[g]); - test_define_flush(); + size_t permutations = test_define_permutations(); + for (size_t p = 0; p < permutations; p++) { + // define permutation permutation + test_define_permutation(p); // add implicit defines - for (size_t d = 0; d < TEST_IMPLICIT_DEFINE_COUNT; d++) { - list_defines_add(&defines, d); + for (size_t d = 0; d < test_define_count; d++) { + list_defines_add(&defines, test_defines[d]); } } @@ -1273,92 +1298,34 @@ static void list_implicit_defines(void) { -// geometries to test - -const test_geometry_t builtin_geometries[] = { - {"default", {{0}, TEST_CONST(16), TEST_CONST(512), {0}}}, - {"eeprom", {{0}, TEST_CONST(1), TEST_CONST(512), {0}}}, - {"emmc", {{0}, {0}, TEST_CONST(512), {0}}}, - {"nor", {{0}, TEST_CONST(1), TEST_CONST(4096), {0}}}, - {"nand", {{0}, TEST_CONST(4096), TEST_CONST(32768), {0}}}, - {NULL, {{0}, {0}, {0}, {0}}}, -}; - -const test_geometry_t *test_geometries = builtin_geometries; -size_t test_geometry_count = 5; - -static void list_geometries(void) { - // at least size so that names fit - unsigned name_width = 23; - for (size_t g = 0; builtin_geometries[g].name; g++) { - size_t len = strlen(builtin_geometries[g].name); - if (len > name_width) { - name_width = len; - } - } - name_width = 4*((name_width+1+4-1)/4)-1; - - // yes we do need to define a suite, this does a bit of bookeeping - // such as setting up the define cache - test_define_suite(&(const struct test_suite){0}); - - printf("%-*s %7s %7s %7s %7s %11s\n", - name_width, "geometry", "read", "prog", "erase", "count", "size"); - for (size_t g = 0; builtin_geometries[g].name; g++) { - test_define_geometry(&builtin_geometries[g]); - test_define_flush(); - printf("%-*s %7ju %7ju %7ju %7ju %11ju\n", - name_width, - builtin_geometries[g].name, - READ_SIZE, - PROG_SIZE, - BLOCK_SIZE, - BLOCK_COUNT, - BLOCK_SIZE*BLOCK_COUNT); - } -} - - // scenarios to run tests under power-loss static void run_powerloss_none( - const lfs_emubd_powercycles_t *cycles, - size_t cycle_count, + const test_powerloss_t *powerloss, const struct test_suite *suite, const struct test_case *case_) { - (void)cycles; - (void)cycle_count; - (void)suite; + (void)powerloss; // create block device and configuration - lfs_emubd_t bd; + lfs3_emubd_t bd; - struct lfs_config cfg = { + struct lfs3_cfg cfg = { .context = &bd, - .read = lfs_emubd_read, - .prog = lfs_emubd_prog, - .erase = lfs_emubd_erase, - .sync = lfs_emubd_sync, - .read_size = READ_SIZE, - .prog_size = PROG_SIZE, - .block_size = BLOCK_SIZE, - .block_count = BLOCK_COUNT, - .block_cycles = BLOCK_CYCLES, - .cache_size = CACHE_SIZE, - .lookahead_size = LOOKAHEAD_SIZE, + .read = lfs3_emubd_read, + .prog = lfs3_emubd_prog, + .erase = lfs3_emubd_erase, + .sync = lfs3_emubd_sync, + TEST_CFG }; - struct lfs_emubd_config bdcfg = { - .erase_value = ERASE_VALUE, - .erase_cycles = ERASE_CYCLES, - .badblock_behavior = BADBLOCK_BEHAVIOR, - .disk_path = test_disk_path, + struct lfs3_emubd_cfg bdcfg = { .read_sleep = test_read_sleep, .prog_sleep = test_prog_sleep, .erase_sleep = test_erase_sleep, + TEST_BDCFG }; - int err = lfs_emubd_createcfg(&cfg, test_disk_path, &bdcfg); + int err = lfs3_emubd_createcfg(&cfg, test_disk_path, &bdcfg); if (err) { fprintf(stderr, "error: could not create block device: %d\n", err); exit(-1); @@ -1369,6 +1336,9 @@ static void run_powerloss_none( perm_printid(suite, case_, NULL, 0); printf("\n"); + // zero pls + TEST_PLS = 0; + case_->run(&cfg); printf("finished "); @@ -1376,7 +1346,7 @@ static void run_powerloss_none( printf("\n"); // cleanup - err = lfs_emubd_destroy(&cfg); + err = lfs3_emubd_destroy(&cfg); if (err) { fprintf(stderr, "error: could not destroy block device: %d\n", err); exit(-1); @@ -1389,49 +1359,38 @@ static void powerloss_longjmp(void *c) { } static void run_powerloss_linear( - const lfs_emubd_powercycles_t *cycles, - size_t cycle_count, + const test_powerloss_t *powerloss, const struct test_suite *suite, const struct test_case *case_) { - (void)cycles; - (void)cycle_count; - (void)suite; + // zero pls + TEST_PLS = 0; // create block device and configuration - lfs_emubd_t bd; + lfs3_emubd_t bd; jmp_buf powerloss_jmp; - volatile lfs_emubd_powercycles_t i = 1; - struct lfs_config cfg = { + struct lfs3_cfg cfg = { .context = &bd, - .read = lfs_emubd_read, - .prog = lfs_emubd_prog, - .erase = lfs_emubd_erase, - .sync = lfs_emubd_sync, - .read_size = READ_SIZE, - .prog_size = PROG_SIZE, - .block_size = BLOCK_SIZE, - .block_count = BLOCK_COUNT, - .block_cycles = BLOCK_CYCLES, - .cache_size = CACHE_SIZE, - .lookahead_size = LOOKAHEAD_SIZE, + .read = lfs3_emubd_read, + .prog = lfs3_emubd_prog, + .erase = lfs3_emubd_erase, + .sync = lfs3_emubd_sync, + TEST_CFG }; - struct lfs_emubd_config bdcfg = { - .erase_value = ERASE_VALUE, - .erase_cycles = ERASE_CYCLES, - .badblock_behavior = BADBLOCK_BEHAVIOR, - .disk_path = test_disk_path, + struct lfs3_emubd_cfg bdcfg = { .read_sleep = test_read_sleep, .prog_sleep = test_prog_sleep, .erase_sleep = test_erase_sleep, - .power_cycles = i, - .powerloss_behavior = POWERLOSS_BEHAVIOR, + .power_cycles = (TEST_PLS < powerloss->cycle_count) + ? TEST_PLS+1 + : 0, .powerloss_cb = powerloss_longjmp, .powerloss_data = &powerloss_jmp, + TEST_BDCFG }; - int err = lfs_emubd_createcfg(&cfg, test_disk_path, &bdcfg); + int err = lfs3_emubd_createcfg(&cfg, test_disk_path, &bdcfg); if (err) { fprintf(stderr, "error: could not create block device: %d\n", err); exit(-1); @@ -1452,14 +1411,15 @@ static void run_powerloss_linear( // power-loss! printf("powerloss "); perm_printid(suite, case_, NULL, 0); - printf(":"); - for (lfs_emubd_powercycles_t j = 1; j <= i; j++) { - leb16_print(j); - } + printf(":x"); + leb16_print(TEST_PLS+1); printf("\n"); - i += 1; - lfs_emubd_setpowercycles(&cfg, i); + // increment pls + TEST_PLS += 1; + lfs3_emubd_setpowercycles(&cfg, (TEST_PLS < powerloss->cycle_count) + ? TEST_PLS+1 + : 0); } printf("finished "); @@ -1467,7 +1427,7 @@ static void run_powerloss_linear( printf("\n"); // cleanup - err = lfs_emubd_destroy(&cfg); + err = lfs3_emubd_destroy(&cfg); if (err) { fprintf(stderr, "error: could not destroy block device: %d\n", err); exit(-1); @@ -1475,49 +1435,38 @@ static void run_powerloss_linear( } static void run_powerloss_log( - const lfs_emubd_powercycles_t *cycles, - size_t cycle_count, + const test_powerloss_t *powerloss, const struct test_suite *suite, const struct test_case *case_) { - (void)cycles; - (void)cycle_count; - (void)suite; + // zero pls + TEST_PLS = 0; // create block device and configuration - lfs_emubd_t bd; + lfs3_emubd_t bd; jmp_buf powerloss_jmp; - volatile lfs_emubd_powercycles_t i = 1; - struct lfs_config cfg = { + struct lfs3_cfg cfg = { .context = &bd, - .read = lfs_emubd_read, - .prog = lfs_emubd_prog, - .erase = lfs_emubd_erase, - .sync = lfs_emubd_sync, - .read_size = READ_SIZE, - .prog_size = PROG_SIZE, - .block_size = BLOCK_SIZE, - .block_count = BLOCK_COUNT, - .block_cycles = BLOCK_CYCLES, - .cache_size = CACHE_SIZE, - .lookahead_size = LOOKAHEAD_SIZE, + .read = lfs3_emubd_read, + .prog = lfs3_emubd_prog, + .erase = lfs3_emubd_erase, + .sync = lfs3_emubd_sync, + TEST_CFG }; - struct lfs_emubd_config bdcfg = { - .erase_value = ERASE_VALUE, - .erase_cycles = ERASE_CYCLES, - .badblock_behavior = BADBLOCK_BEHAVIOR, - .disk_path = test_disk_path, + struct lfs3_emubd_cfg bdcfg = { .read_sleep = test_read_sleep, .prog_sleep = test_prog_sleep, .erase_sleep = test_erase_sleep, - .power_cycles = i, - .powerloss_behavior = POWERLOSS_BEHAVIOR, + .power_cycles = (TEST_PLS < powerloss->cycle_count) + ? 1 << TEST_PLS + : 0, .powerloss_cb = powerloss_longjmp, .powerloss_data = &powerloss_jmp, + TEST_BDCFG }; - int err = lfs_emubd_createcfg(&cfg, test_disk_path, &bdcfg); + int err = lfs3_emubd_createcfg(&cfg, test_disk_path, &bdcfg); if (err) { fprintf(stderr, "error: could not create block device: %d\n", err); exit(-1); @@ -1538,14 +1487,15 @@ static void run_powerloss_log( // power-loss! printf("powerloss "); perm_printid(suite, case_, NULL, 0); - printf(":"); - for (lfs_emubd_powercycles_t j = 1; j <= i; j *= 2) { - leb16_print(j); - } + printf(":y"); + leb16_print(TEST_PLS+1); printf("\n"); - i *= 2; - lfs_emubd_setpowercycles(&cfg, i); + // increment pls + TEST_PLS += 1; + lfs3_emubd_setpowercycles(&cfg, (TEST_PLS < powerloss->cycle_count) + ? 1 << TEST_PLS + : 0); } printf("finished "); @@ -1553,7 +1503,7 @@ static void run_powerloss_log( printf("\n"); // cleanup - err = lfs_emubd_destroy(&cfg); + err = lfs3_emubd_destroy(&cfg); if (err) { fprintf(stderr, "error: could not destroy block device: %d\n", err); exit(-1); @@ -1561,47 +1511,38 @@ static void run_powerloss_log( } static void run_powerloss_cycles( - const lfs_emubd_powercycles_t *cycles, - size_t cycle_count, + const test_powerloss_t *powerloss, const struct test_suite *suite, const struct test_case *case_) { - (void)suite; + // zero pls + TEST_PLS = 0; // create block device and configuration - lfs_emubd_t bd; + lfs3_emubd_t bd; jmp_buf powerloss_jmp; - volatile size_t i = 0; - struct lfs_config cfg = { + struct lfs3_cfg cfg = { .context = &bd, - .read = lfs_emubd_read, - .prog = lfs_emubd_prog, - .erase = lfs_emubd_erase, - .sync = lfs_emubd_sync, - .read_size = READ_SIZE, - .prog_size = PROG_SIZE, - .block_size = BLOCK_SIZE, - .block_count = BLOCK_COUNT, - .block_cycles = BLOCK_CYCLES, - .cache_size = CACHE_SIZE, - .lookahead_size = LOOKAHEAD_SIZE, + .read = lfs3_emubd_read, + .prog = lfs3_emubd_prog, + .erase = lfs3_emubd_erase, + .sync = lfs3_emubd_sync, + TEST_CFG }; - struct lfs_emubd_config bdcfg = { - .erase_value = ERASE_VALUE, - .erase_cycles = ERASE_CYCLES, - .badblock_behavior = BADBLOCK_BEHAVIOR, - .disk_path = test_disk_path, + struct lfs3_emubd_cfg bdcfg = { .read_sleep = test_read_sleep, .prog_sleep = test_prog_sleep, .erase_sleep = test_erase_sleep, - .power_cycles = (i < cycle_count) ? cycles[i] : 0, - .powerloss_behavior = POWERLOSS_BEHAVIOR, + .power_cycles = (TEST_PLS < powerloss->cycle_count) + ? powerloss->cycles[TEST_PLS] + : 0, .powerloss_cb = powerloss_longjmp, .powerloss_data = &powerloss_jmp, + TEST_BDCFG }; - int err = lfs_emubd_createcfg(&cfg, test_disk_path, &bdcfg); + int err = lfs3_emubd_createcfg(&cfg, test_disk_path, &bdcfg); if (err) { fprintf(stderr, "error: could not create block device: %d\n", err); exit(-1); @@ -1620,14 +1561,16 @@ static void run_powerloss_cycles( } // power-loss! - assert(i <= cycle_count); + assert(TEST_PLS <= powerloss->cycle_count); printf("powerloss "); - perm_printid(suite, case_, cycles, i+1); + perm_printid(suite, case_, powerloss->cycles, TEST_PLS+1); printf("\n"); - i += 1; - lfs_emubd_setpowercycles(&cfg, - (i < cycle_count) ? cycles[i] : 0); + // increment pls + TEST_PLS += 1; + lfs3_emubd_setpowercycles(&cfg, (TEST_PLS < powerloss->cycle_count) + ? powerloss->cycles[TEST_PLS] + : 0); } printf("finished "); @@ -1635,7 +1578,7 @@ static void run_powerloss_cycles( printf("\n"); // cleanup - err = lfs_emubd_destroy(&cfg); + err = lfs3_emubd_destroy(&cfg); if (err) { fprintf(stderr, "error: could not destroy block device: %d\n", err); exit(-1); @@ -1643,15 +1586,15 @@ static void run_powerloss_cycles( } struct powerloss_exhaustive_state { - struct lfs_config *cfg; + struct lfs3_cfg *cfg; - lfs_emubd_t *branches; + lfs3_emubd_t *branches; size_t branch_count; size_t branch_capacity; }; struct powerloss_exhaustive_cycles { - lfs_emubd_powercycles_t *cycles; + lfs3_emubd_powercycles_t *cycles; size_t cycle_count; size_t cycle_capacity; }; @@ -1659,9 +1602,9 @@ struct powerloss_exhaustive_cycles { static void powerloss_exhaustive_branch(void *c) { struct powerloss_exhaustive_state *state = c; // append to branches - lfs_emubd_t *branch = mappend( + lfs3_emubd_t *branch = mappend( (void**)&state->branches, - sizeof(lfs_emubd_t), + sizeof(lfs3_emubd_t), &state->branch_count, &state->branch_capacity); if (!branch) { @@ -1670,25 +1613,24 @@ static void powerloss_exhaustive_branch(void *c) { } // create copy-on-write copy - int err = lfs_emubd_copy(state->cfg, branch); + int err = lfs3_emubd_cpy(state->cfg, branch); if (err) { fprintf(stderr, "error: exhaustive: could not create bd copy\n"); exit(-1); } // also trigger on next power cycle - lfs_emubd_setpowercycles(state->cfg, 1); + lfs3_emubd_setpowercycles(state->cfg, 1); } static void run_powerloss_exhaustive_layer( struct powerloss_exhaustive_cycles *cycles, const struct test_suite *suite, const struct test_case *case_, - struct lfs_config *cfg, - struct lfs_emubd_config *bdcfg, - size_t depth) { - (void)suite; - + struct lfs3_cfg *cfg, + struct lfs3_emubd_cfg *bdcfg, + size_t depth, + size_t pls) { struct powerloss_exhaustive_state state = { .cfg = cfg, .branches = NULL, @@ -1696,16 +1638,19 @@ static void run_powerloss_exhaustive_layer( .branch_capacity = 0, }; + // make the number of pls currently seen available to tests/debugging + TEST_PLS = pls; + // run through the test without additional powerlosses, collecting possible // branches as we do so - lfs_emubd_setpowercycles(state.cfg, depth > 0 ? 1 : 0); + lfs3_emubd_setpowercycles(state.cfg, (depth > 0) ? 1 : 0); bdcfg->powerloss_data = &state; // run the tests case_->run(cfg); // aggressively clean up memory here to try to keep our memory usage low - int err = lfs_emubd_destroy(cfg); + int err = lfs3_emubd_destroy(cfg); if (err) { fprintf(stderr, "error: could not destroy block device: %d\n", err); exit(-1); @@ -1714,9 +1659,9 @@ static void run_powerloss_exhaustive_layer( // recurse into each branch for (size_t i = 0; i < state.branch_count; i++) { // first push and print the branch - lfs_emubd_powercycles_t *cycle = mappend( + lfs3_emubd_powercycles_t *cycle = mappend( (void**)&cycles->cycles, - sizeof(lfs_emubd_powercycles_t), + sizeof(lfs3_emubd_powercycles_t), &cycles->cycle_count, &cycles->cycle_capacity); if (!cycle) { @@ -1733,7 +1678,7 @@ static void run_powerloss_exhaustive_layer( cfg->context = &state.branches[i]; run_powerloss_exhaustive_layer(cycles, suite, case_, - cfg, bdcfg, depth-1); + cfg, bdcfg, depth-1, pls+1); // pop the cycle cycles->cycle_count -= 1; @@ -1744,45 +1689,31 @@ static void run_powerloss_exhaustive_layer( } static void run_powerloss_exhaustive( - const lfs_emubd_powercycles_t *cycles, - size_t cycle_count, + const test_powerloss_t *powerloss, const struct test_suite *suite, const struct test_case *case_) { - (void)cycles; - (void)suite; - // create block device and configuration - lfs_emubd_t bd; + lfs3_emubd_t bd; - struct lfs_config cfg = { + struct lfs3_cfg cfg = { .context = &bd, - .read = lfs_emubd_read, - .prog = lfs_emubd_prog, - .erase = lfs_emubd_erase, - .sync = lfs_emubd_sync, - .read_size = READ_SIZE, - .prog_size = PROG_SIZE, - .block_size = BLOCK_SIZE, - .block_count = BLOCK_COUNT, - .block_cycles = BLOCK_CYCLES, - .cache_size = CACHE_SIZE, - .lookahead_size = LOOKAHEAD_SIZE, + .read = lfs3_emubd_read, + .prog = lfs3_emubd_prog, + .erase = lfs3_emubd_erase, + .sync = lfs3_emubd_sync, + TEST_CFG }; - struct lfs_emubd_config bdcfg = { - .erase_value = ERASE_VALUE, - .erase_cycles = ERASE_CYCLES, - .badblock_behavior = BADBLOCK_BEHAVIOR, - .disk_path = test_disk_path, + struct lfs3_emubd_cfg bdcfg = { .read_sleep = test_read_sleep, .prog_sleep = test_prog_sleep, .erase_sleep = test_erase_sleep, - .powerloss_behavior = POWERLOSS_BEHAVIOR, .powerloss_cb = powerloss_exhaustive_branch, .powerloss_data = NULL, + TEST_BDCFG }; - int err = lfs_emubd_createcfg(&cfg, test_disk_path, &bdcfg); + int err = lfs3_emubd_createcfg(&cfg, test_disk_path, &bdcfg); if (err) { fprintf(stderr, "error: could not create block device: %d\n", err); exit(-1); @@ -1797,7 +1728,7 @@ static void run_powerloss_exhaustive( run_powerloss_exhaustive_layer( &(struct powerloss_exhaustive_cycles){NULL, 0, 0}, suite, case_, - &cfg, &bdcfg, cycle_count); + &cfg, &bdcfg, powerloss->cycle_count, 0); printf("finished "); perm_printid(suite, case_, NULL, 0); @@ -1807,8 +1738,8 @@ static void run_powerloss_exhaustive( const test_powerloss_t builtin_powerlosses[] = { {"none", run_powerloss_none, NULL, 0}, - {"log", run_powerloss_log, NULL, 0}, - {"linear", run_powerloss_linear, NULL, 0}, + {"log", run_powerloss_log, NULL, SIZE_MAX}, + {"linear", run_powerloss_linear, NULL, SIZE_MAX}, {"exhaustive", run_powerloss_exhaustive, NULL, SIZE_MAX}, {NULL, NULL, NULL, 0}, }; @@ -1827,7 +1758,7 @@ const char *const builtin_powerlosses_help[] = { // running quickly const test_powerloss_t *test_powerlosses = (const test_powerloss_t[]){ {"none", run_powerloss_none, NULL, 0}, - {"linear", run_powerloss_linear, NULL, 0}, + {"linear", run_powerloss_linear, NULL, SIZE_MAX}, }; size_t test_powerloss_count = 2; @@ -1877,17 +1808,18 @@ void perm_run( } test_step += 1; + // set pls to 1 if running under powerloss so it useful for if predicates + TEST_PLS = (powerloss->run != run_powerloss_none); // filter? - if (case_->filter && !case_->filter()) { + if (!case_->run || !(test_force || !case_->if_ || case_->if_())) { printf("skipped "); perm_printid(suite, case_, NULL, 0); printf("\n"); return; } - powerloss->run( - powerloss->cycles, powerloss->cycle_count, - suite, case_); + // run the test, possibly under powerloss + powerloss->run(powerloss, suite, case_); } static void run(void) { @@ -1895,26 +1827,23 @@ static void run(void) { signal(SIGPIPE, SIG_IGN); for (size_t t = 0; t < test_id_count; t++) { - for (size_t i = 0; i < TEST_SUITE_COUNT; i++) { - test_define_suite(&test_suites[i]); + for (size_t i = 0; i < test_suite_count; i++) { + test_define_suite(&test_ids[t], test_suites[i]); - for (size_t j = 0; j < test_suites[i].case_count; j++) { + for (size_t j = 0; j < test_suites[i]->case_count; j++) { // does neither suite nor case name match? if (test_ids[t].name && !( strcmp(test_ids[t].name, - test_suites[i].name) == 0 + test_suites[i]->name) == 0 || strcmp(test_ids[t].name, - test_suites[i].cases[j].name) == 0)) { + test_suites[i]->cases[j].name) == 0)) { continue; } case_forperm( - &test_suites[i], - &test_suites[i].cases[j], - test_ids[t].defines, - test_ids[t].define_count, - test_ids[t].cycles, - test_ids[t].cycle_count, + &test_ids[t], + test_suites[i], + &test_suites[i]->cases[j], perm_run, NULL); } @@ -1935,23 +1864,23 @@ enum opt_flags { OPT_LIST_DEFINES = 3, OPT_LIST_PERMUTATION_DEFINES = 4, OPT_LIST_IMPLICIT_DEFINES = 5, - OPT_LIST_GEOMETRIES = 6, - OPT_LIST_POWERLOSSES = 7, + OPT_LIST_POWERLOSSES = 6, OPT_DEFINE = 'D', - OPT_GEOMETRY = 'G', + OPT_DEFINE_DEPTH = 7, OPT_POWERLOSS = 'P', OPT_STEP = 's', + OPT_FORCE = 8, OPT_DISK = 'd', OPT_TRACE = 't', - OPT_TRACE_BACKTRACE = 8, - OPT_TRACE_PERIOD = 9, - OPT_TRACE_FREQ = 10, - OPT_READ_SLEEP = 11, - OPT_PROG_SLEEP = 12, - OPT_ERASE_SLEEP = 13, + OPT_TRACE_BACKTRACE = 9, + OPT_TRACE_PERIOD = 10, + OPT_TRACE_FREQ = 11, + OPT_READ_SLEEP = 12, + OPT_PROG_SLEEP = 13, + OPT_ERASE_SLEEP = 14, }; -const char *short_opts = "hYlLD:G:P:s:d:t:"; +const char *short_opts = "hYlLD:P:s:d:t:"; const struct option long_opts[] = { {"help", no_argument, NULL, OPT_HELP}, @@ -1965,12 +1894,12 @@ const struct option long_opts[] = { no_argument, NULL, OPT_LIST_PERMUTATION_DEFINES}, {"list-implicit-defines", no_argument, NULL, OPT_LIST_IMPLICIT_DEFINES}, - {"list-geometries", no_argument, NULL, OPT_LIST_GEOMETRIES}, {"list-powerlosses", no_argument, NULL, OPT_LIST_POWERLOSSES}, {"define", required_argument, NULL, OPT_DEFINE}, - {"geometry", required_argument, NULL, OPT_GEOMETRY}, + {"define-depth", required_argument, NULL, OPT_DEFINE_DEPTH}, {"powerloss", required_argument, NULL, OPT_POWERLOSS}, {"step", required_argument, NULL, OPT_STEP}, + {"force", no_argument, NULL, OPT_FORCE}, {"disk", required_argument, NULL, OPT_DISK}, {"trace", required_argument, NULL, OPT_TRACE}, {"trace-backtrace", no_argument, NULL, OPT_TRACE_BACKTRACE}, @@ -1992,12 +1921,12 @@ const char *const help_text[] = { "List all defines in this test-runner.", "List explicit defines in this test-runner.", "List implicit defines in this test-runner.", - "List the available disk geometries.", "List the available power-loss scenarios.", "Override a test define.", - "Comma-separated list of disk geometries to test.", + "How deep to evaluate recursive defines before erroring.", "Comma-separated list of power-loss scenarios to test.", - "Comma-separated range of test permutations to run (start,stop,step).", + "Comma-separated range of permutations to run.", + "Ignore test filters.", "Direct block device operations to this file.", "Direct trace output to this file.", "Include a backtrace with every trace statement.", @@ -2011,8 +1940,7 @@ const char *const help_text[] = { int main(int argc, char **argv) { void (*op)(void) = run; - size_t test_override_capacity = 0; - size_t test_geometry_capacity = 0; + size_t test_override_define_capacity = 0; size_t test_powerloss_capacity = 0; size_t test_id_capacity = 0; @@ -2020,136 +1948,156 @@ int main(int argc, char **argv) { while (true) { int c = getopt_long(argc, argv, short_opts, long_opts, NULL); switch (c) { - // generate help message - case OPT_HELP: { - printf("usage: %s [options] [test_id]\n", argv[0]); - printf("\n"); - - printf("options:\n"); - size_t i = 0; - while (long_opts[i].name) { - size_t indent; - if (long_opts[i].has_arg == no_argument) { - if (long_opts[i].val >= '0' && long_opts[i].val < 'z') { - indent = printf(" -%c, --%s ", - long_opts[i].val, - long_opts[i].name); - } else { - indent = printf(" --%s ", - long_opts[i].name); - } + // generate help message + case OPT_HELP:; + printf("usage: %s [options] [test_id]\n", argv[0]); + printf("\n"); + + printf("options:\n"); + size_t i = 0; + while (long_opts[i].name) { + size_t indent; + if (long_opts[i].has_arg == no_argument) { + if (long_opts[i].val >= '0' && long_opts[i].val < 'z') { + indent = printf(" -%c, --%s ", + long_opts[i].val, + long_opts[i].name); } else { - if (long_opts[i].val >= '0' && long_opts[i].val < 'z') { - indent = printf(" -%c %s, --%s %s ", - long_opts[i].val, - long_opts[i].name, - long_opts[i].name, - long_opts[i].name); - } else { - indent = printf(" --%s %s ", - long_opts[i].name, - long_opts[i].name); - } + indent = printf(" --%s ", + long_opts[i].name); } - - // a quick, hacky, byte-level method for text wrapping - size_t len = strlen(help_text[i]); - size_t j = 0; - if (indent < 24) { - printf("%*s %.80s\n", - (int)(24-1-indent), - "", - &help_text[i][j]); - j += 80; + } else { + if (long_opts[i].val >= '0' && long_opts[i].val < 'z') { + indent = printf(" -%c %s, --%s %s ", + long_opts[i].val, + long_opts[i].name, + long_opts[i].name, + long_opts[i].name); } else { - printf("\n"); + indent = printf(" --%s %s ", + long_opts[i].name, + long_opts[i].name); } + } - while (j < len) { - printf("%24s%.80s\n", "", &help_text[i][j]); - j += 80; - } + // a quick, hacky, byte-level method for text wrapping + size_t len = strlen(help_text[i]); + size_t j = 0; + if (indent < 24) { + printf("%*s %.80s\n", + (int)(24-1-indent), + "", + &help_text[i][j]); + j += 80; + } else { + printf("\n"); + } - i += 1; + while (j < len) { + printf("%24s%.80s\n", "", &help_text[i][j]); + j += 80; } - printf("\n"); - exit(0); + i += 1; } - // summary/list flags - case OPT_SUMMARY: - op = summary; - break; - case OPT_LIST_SUITES: - op = list_suites; - break; - case OPT_LIST_CASES: - op = list_cases; - break; - case OPT_LIST_SUITE_PATHS: - op = list_suite_paths; - break; - case OPT_LIST_CASE_PATHS: - op = list_case_paths; - break; - case OPT_LIST_DEFINES: - op = list_defines; - break; - case OPT_LIST_PERMUTATION_DEFINES: - op = list_permutation_defines; - break; - case OPT_LIST_IMPLICIT_DEFINES: - op = list_implicit_defines; - break; - case OPT_LIST_GEOMETRIES: - op = list_geometries; - break; - case OPT_LIST_POWERLOSSES: - op = list_powerlosses; - break; - // configuration - case OPT_DEFINE: { - // allocate space - test_override_t *override = mappend( - (void**)&test_overrides, - sizeof(test_override_t), - &test_override_count, - &test_override_capacity); - - // parse into string key/intmax_t value, cannibalizing the - // arg in the process - char *sep = strchr(optarg, '='); - char *parsed = NULL; - if (!sep) { - goto invalid_define; - } - *sep = '\0'; - override->name = optarg; - optarg = sep+1; - // parse comma-separated permutations - { - override->defines = NULL; - override->permutations = 0; - size_t override_capacity = 0; - while (true) { + printf("\n"); + exit(0); + + // summary/list flags + case OPT_SUMMARY:; + op = summary; + break; + + case OPT_LIST_SUITES:; + op = list_suites; + break; + + case OPT_LIST_CASES:; + op = list_cases; + break; + + case OPT_LIST_SUITE_PATHS:; + op = list_suite_paths; + break; + + case OPT_LIST_CASE_PATHS:; + op = list_case_paths; + break; + + case OPT_LIST_DEFINES:; + op = list_defines; + break; + + case OPT_LIST_PERMUTATION_DEFINES:; + op = list_permutation_defines; + break; + + case OPT_LIST_IMPLICIT_DEFINES:; + op = list_implicit_defines; + break; + + case OPT_LIST_POWERLOSSES:; + op = list_powerlosses; + break; + + // configuration + case OPT_DEFINE:; + // allocate space + test_define_t *override = mappend( + (void**)&test_override_defines, + sizeof(test_define_t), + &test_override_define_count, + &test_override_define_capacity); + + // parse into string key/intmax_t value, cannibalizing the + // arg in the process + char *sep = strchr(optarg, '='); + char *parsed = NULL; + if (!sep) { + goto invalid_define; + } + *sep = '\0'; + override->name = optarg; + optarg = sep+1; + + // parse comma-separated permutations + { + test_override_value_t *override_values = NULL; + size_t override_value_count = 0; + size_t override_value_capacity = 0; + size_t override_permutations = 0; + while (true) { + optarg += strspn(optarg, " "); + + if (strncmp(optarg, "range", strlen("range")) == 0) { + // range of values + optarg += strlen("range"); optarg += strspn(optarg, " "); + if (*optarg != '(') { + goto invalid_define; + } + optarg += 1; - if (strncmp(optarg, "range", strlen("range")) == 0) { - // range of values - optarg += strlen("range"); - optarg += strspn(optarg, " "); - if (*optarg != '(') { - goto invalid_define; - } - optarg += 1; + intmax_t start = strtoumax(optarg, &parsed, 0); + intmax_t stop = -1; + intmax_t step = 1; + // allow empty string for start=0 + if (parsed == optarg) { + start = 0; + } + optarg = parsed + strspn(parsed, " "); - intmax_t start = strtoumax(optarg, &parsed, 0); - intmax_t stop = -1; - intmax_t step = 1; - // allow empty string for start=0 + if (*optarg != ',' && *optarg != ')') { + goto invalid_define; + } + + if (*optarg == ',') { + optarg += 1; + stop = strtoumax(optarg, &parsed, 0); + // allow empty string for stop=end if (parsed == optarg) { - start = 0; + stop = -1; } optarg = parsed + strspn(parsed, " "); @@ -2159,371 +2107,282 @@ int main(int argc, char **argv) { if (*optarg == ',') { optarg += 1; - stop = strtoumax(optarg, &parsed, 0); - // allow empty string for stop=end + step = strtoumax(optarg, &parsed, 0); + // allow empty string for stop=1 if (parsed == optarg) { - stop = -1; + step = 1; } optarg = parsed + strspn(parsed, " "); - if (*optarg != ',' && *optarg != ')') { + if (*optarg != ')') { goto invalid_define; } - - if (*optarg == ',') { - optarg += 1; - step = strtoumax(optarg, &parsed, 0); - // allow empty string for stop=1 - if (parsed == optarg) { - step = 1; - } - optarg = parsed + strspn(parsed, " "); - - if (*optarg != ')') { - goto invalid_define; - } - } - } else { - // single value = stop only - stop = start; - start = 0; - } - - if (*optarg != ')') { - goto invalid_define; } - optarg += 1; - - // calculate the range of values - assert(step != 0); - for (intmax_t i = start; - (step < 0) - ? i > stop - : (uintmax_t)i < (uintmax_t)stop; - i += step) { - *(intmax_t*)mappend( - (void**)&override->defines, - sizeof(intmax_t), - &override->permutations, - &override_capacity) = i; - } - } else if (*optarg != '\0') { - // single value - intmax_t define = strtoimax(optarg, &parsed, 0); - if (parsed == optarg) { - goto invalid_define; - } - optarg = parsed + strspn(parsed, " "); - *(intmax_t*)mappend( - (void**)&override->defines, - sizeof(intmax_t), - &override->permutations, - &override_capacity) = define; } else { - break; + // single value = stop only + stop = start; + start = 0; } - if (*optarg == ',') { - optarg += 1; + if (*optarg != ')') { + goto invalid_define; } + optarg += 1; + + // append range + *(test_override_value_t*)mappend( + (void**)&override_values, + sizeof(test_override_value_t), + &override_value_count, + &override_value_capacity) + = (test_override_value_t){ + .start = start, + .stop = stop, + .step = step, + }; + if (step > 0) { + override_permutations += (stop-1 - start) + / step + 1; + } else { + override_permutations += (start-1 - stop) + / -step + 1; + } + } else if (*optarg != '\0') { + // single value + intmax_t define = strtoumax(optarg, &parsed, 0); + if (parsed == optarg) { + goto invalid_define; + } + optarg = parsed + strspn(parsed, " "); + + // append value + *(test_override_value_t*)mappend( + (void**)&override_values, + sizeof(test_override_value_t), + &override_value_count, + &override_value_capacity) + = (test_override_value_t){ + .start = define, + .step = 0, + }; + override_permutations += 1; + } else { + break; + } + + if (*optarg == ',') { + optarg += 1; } } - assert(override->permutations > 0); - break; -invalid_define: - fprintf(stderr, "error: invalid define: %s\n", optarg); - exit(-1); + // define should be patched in test_define_suite + override->define = NULL; + override->cb = test_override_cb; + override->data = malloc(sizeof(test_override_data_t)); + *(test_override_data_t*)override->data + = (test_override_data_t){ + .values = override_values, + .value_count = override_value_count, + }; + override->permutations = override_permutations; } - case OPT_GEOMETRY: { - // reset our geometry scenarios - if (test_geometry_capacity > 0) { - free((test_geometry_t*)test_geometries); - } - test_geometries = NULL; - test_geometry_count = 0; - test_geometry_capacity = 0; - - // parse the comma separated list of disk geometries - while (*optarg) { - // allocate space - test_geometry_t *geometry = mappend( - (void**)&test_geometries, - sizeof(test_geometry_t), - &test_geometry_count, - &test_geometry_capacity); - - // parse the disk geometry - optarg += strspn(optarg, " "); + break; - // named disk geometry - size_t len = strcspn(optarg, " ,"); - for (size_t i = 0; builtin_geometries[i].name; i++) { - if (len == strlen(builtin_geometries[i].name) - && memcmp(optarg, - builtin_geometries[i].name, - len) == 0) { - *geometry = builtin_geometries[i]; - optarg += len; - goto geometry_next; - } - } + invalid_define:; + fprintf(stderr, "error: invalid define: %s\n", optarg); + exit(-1); - // comma-separated read/prog/erase/count - if (*optarg == '{') { - lfs_size_t sizes[4]; - size_t count = 0; + case OPT_DEFINE_DEPTH:; + parsed = NULL; + test_define_depth = strtoumax(optarg, &parsed, 0); + if (parsed == optarg) { + fprintf(stderr, "error: invalid define-depth: %s\n", optarg); + exit(-1); + } + break; - char *s = optarg + 1; - while (count < 4) { - char *parsed = NULL; - sizes[count] = strtoumax(s, &parsed, 0); - count += 1; - - s = parsed + strspn(parsed, " "); - if (*s == ',') { - s += 1; - continue; - } else if (*s == '}') { - s += 1; - break; - } else { - goto geometry_unknown; - } - } + case OPT_POWERLOSS:; + // reset our powerloss scenarios + if (test_powerloss_capacity > 0) { + free((test_powerloss_t*)test_powerlosses); + } + test_powerlosses = NULL; + test_powerloss_count = 0; + test_powerloss_capacity = 0; - // allow implicit r=p and p=e for common geometries - memset(geometry, 0, sizeof(test_geometry_t)); - if (count >= 3) { - geometry->defines[READ_SIZE_i] - = TEST_LIT(sizes[0]); - geometry->defines[PROG_SIZE_i] - = TEST_LIT(sizes[1]); - geometry->defines[BLOCK_SIZE_i] - = TEST_LIT(sizes[2]); - } else if (count >= 2) { - geometry->defines[PROG_SIZE_i] - = TEST_LIT(sizes[0]); - geometry->defines[BLOCK_SIZE_i] - = TEST_LIT(sizes[1]); - } else { - geometry->defines[BLOCK_SIZE_i] - = TEST_LIT(sizes[0]); - } - if (count >= 4) { - geometry->defines[BLOCK_COUNT_i] - = TEST_LIT(sizes[3]); - } - optarg = s; - goto geometry_next; + // parse the comma separated list of power-loss scenarios + while (*optarg) { + // allocate space + test_powerloss_t *powerloss = mappend( + (void**)&test_powerlosses, + sizeof(test_powerloss_t), + &test_powerloss_count, + &test_powerloss_capacity); + + // parse the power-loss scenario + optarg += strspn(optarg, " "); + + // named power-loss scenario + size_t len = strcspn(optarg, " ,"); + for (size_t i = 0; builtin_powerlosses[i].name; i++) { + if (len == strlen(builtin_powerlosses[i].name) + && memcmp(optarg, + builtin_powerlosses[i].name, + len) == 0) { + *powerloss = builtin_powerlosses[i]; + optarg += len; + goto powerloss_next; } + } - // leb16-encoded read/prog/erase/count - if (*optarg == ':') { - lfs_size_t sizes[4]; - size_t count = 0; - - char *s = optarg + 1; - while (true) { - char *parsed = NULL; - uintmax_t x = leb16_parse(s, &parsed); - if (parsed == s || count >= 4) { - break; - } - - sizes[count] = x; - count += 1; - s = parsed; - } + // comma-separated permutation + if (*optarg == '{') { + lfs3_emubd_powercycles_t *cycles = NULL; + size_t cycle_count = 0; + size_t cycle_capacity = 0; - // allow implicit r=p and p=e for common geometries - memset(geometry, 0, sizeof(test_geometry_t)); - if (count >= 3) { - geometry->defines[READ_SIZE_i] - = TEST_LIT(sizes[0]); - geometry->defines[PROG_SIZE_i] - = TEST_LIT(sizes[1]); - geometry->defines[BLOCK_SIZE_i] - = TEST_LIT(sizes[2]); - } else if (count >= 2) { - geometry->defines[PROG_SIZE_i] - = TEST_LIT(sizes[0]); - geometry->defines[BLOCK_SIZE_i] - = TEST_LIT(sizes[1]); + char *s = optarg + 1; + while (true) { + parsed = NULL; + *(lfs3_emubd_powercycles_t*)mappend( + (void**)&cycles, + sizeof(lfs3_emubd_powercycles_t), + &cycle_count, + &cycle_capacity) + = strtoumax(s, &parsed, 0); + + s = parsed + strspn(parsed, " "); + if (*s == ',') { + s += 1; + continue; + } else if (*s == '}') { + s += 1; + break; } else { - geometry->defines[BLOCK_SIZE_i] - = TEST_LIT(sizes[0]); - } - if (count >= 4) { - geometry->defines[BLOCK_COUNT_i] - = TEST_LIT(sizes[3]); + goto powerloss_unknown; } - optarg = s; - goto geometry_next; } -geometry_unknown: - // unknown scenario? - fprintf(stderr, "error: unknown disk geometry: %s\n", - optarg); - exit(-1); - -geometry_next: - optarg += strspn(optarg, " "); - if (*optarg == ',') { - optarg += 1; - } else if (*optarg == '\0') { - break; - } else { - goto geometry_unknown; - } + *powerloss = (test_powerloss_t){ + "explicit", + run_powerloss_cycles, + cycles, + cycle_count}; + optarg = s; + goto powerloss_next; } - break; - } - case OPT_POWERLOSS: { - // reset our powerloss scenarios - if (test_powerloss_capacity > 0) { - free((test_powerloss_t*)test_powerlosses); - } - test_powerlosses = NULL; - test_powerloss_count = 0; - test_powerloss_capacity = 0; - - // parse the comma separated list of power-loss scenarios - while (*optarg) { - // allocate space - test_powerloss_t *powerloss = mappend( - (void**)&test_powerlosses, - sizeof(test_powerloss_t), - &test_powerloss_count, - &test_powerloss_capacity); - - // parse the power-loss scenario - optarg += strspn(optarg, " "); - // named power-loss scenario - size_t len = strcspn(optarg, " ,"); - for (size_t i = 0; builtin_powerlosses[i].name; i++) { - if (len == strlen(builtin_powerlosses[i].name) - && memcmp(optarg, - builtin_powerlosses[i].name, - len) == 0) { - *powerloss = builtin_powerlosses[i]; - optarg += len; - goto powerloss_next; - } - } + // leb16-encoded permutation + if (*optarg == ':') { + // special case for linear power cycles + if (optarg[1] == 'x') { + size_t cycle_count = leb16_parse(optarg+2, &optarg); - // comma-separated permutation - if (*optarg == '{') { - lfs_emubd_powercycles_t *cycles = NULL; - size_t cycle_count = 0; - size_t cycle_capacity = 0; + *powerloss = (test_powerloss_t){ + "linear", + run_powerloss_linear, + NULL, + cycle_count}; + goto powerloss_next; - char *s = optarg + 1; - while (true) { - char *parsed = NULL; - *(lfs_emubd_powercycles_t*)mappend( - (void**)&cycles, - sizeof(lfs_emubd_powercycles_t), - &cycle_count, - &cycle_capacity) - = strtoumax(s, &parsed, 0); - - s = parsed + strspn(parsed, " "); - if (*s == ',') { - s += 1; - continue; - } else if (*s == '}') { - s += 1; - break; - } else { - goto powerloss_unknown; - } - } + // special case for log power cycles + } else if (optarg[1] == 'y') { + size_t cycle_count = leb16_parse(optarg+2, &optarg); *powerloss = (test_powerloss_t){ - .run = run_powerloss_cycles, - .cycles = cycles, - .cycle_count = cycle_count, - }; - optarg = s; + "log", + run_powerloss_log, + NULL, + cycle_count}; goto powerloss_next; - } - // leb16-encoded permutation - if (*optarg == ':') { - lfs_emubd_powercycles_t *cycles = NULL; + // otherwise explicit power cycles + } else { + lfs3_emubd_powercycles_t *cycles = NULL; size_t cycle_count = 0; size_t cycle_capacity = 0; char *s = optarg + 1; while (true) { - char *parsed = NULL; + parsed = NULL; uintmax_t x = leb16_parse(s, &parsed); if (parsed == s) { break; } - *(lfs_emubd_powercycles_t*)mappend( + *(lfs3_emubd_powercycles_t*)mappend( (void**)&cycles, - sizeof(lfs_emubd_powercycles_t), + sizeof(lfs3_emubd_powercycles_t), &cycle_count, &cycle_capacity) = x; s = parsed; } *powerloss = (test_powerloss_t){ - .run = run_powerloss_cycles, - .cycles = cycles, - .cycle_count = cycle_count, - }; + "explicit", + run_powerloss_cycles, + cycles, + cycle_count}; optarg = s; goto powerloss_next; } + } - // exhaustive permutations - { - char *parsed = NULL; - size_t count = strtoumax(optarg, &parsed, 0); - if (parsed == optarg) { - goto powerloss_unknown; - } - *powerloss = (test_powerloss_t){ - .run = run_powerloss_exhaustive, - .cycles = NULL, - .cycle_count = count, - }; - optarg = (char*)parsed; - goto powerloss_next; + // exhaustive permutations + { + parsed = NULL; + size_t count = strtoumax(optarg, &parsed, 0); + if (parsed == optarg) { + goto powerloss_unknown; } + *powerloss = (test_powerloss_t){ + "exhaustive", + run_powerloss_exhaustive, + NULL, + count}; + optarg = (char*)parsed; + goto powerloss_next; + } -powerloss_unknown: - // unknown scenario? - fprintf(stderr, "error: unknown power-loss scenario: %s\n", - optarg); - exit(-1); + powerloss_unknown:; + // unknown scenario? + fprintf(stderr, "error: unknown power-loss scenario: %s\n", + optarg); + exit(-1); -powerloss_next: - optarg += strspn(optarg, " "); - if (*optarg == ',') { - optarg += 1; - } else if (*optarg == '\0') { - break; - } else { - goto powerloss_unknown; - } + powerloss_next:; + optarg += strspn(optarg, " "); + if (*optarg == ',') { + optarg += 1; + } else if (*optarg == '\0') { + break; + } else { + goto powerloss_unknown; } - break; } - case OPT_STEP: { - char *parsed = NULL; - test_step_start = strtoumax(optarg, &parsed, 0); - test_step_stop = -1; - test_step_step = 1; - // allow empty string for start=0 + break; + + case OPT_STEP:; + parsed = NULL; + test_step_start = strtoumax(optarg, &parsed, 0); + test_step_stop = -1; + test_step_step = 1; + // allow empty string for start=0 + if (parsed == optarg) { + test_step_start = 0; + } + optarg = parsed + strspn(parsed, " "); + + if (*optarg != ',' && *optarg != '\0') { + goto step_unknown; + } + + if (*optarg == ',') { + optarg += 1; + test_step_stop = strtoumax(optarg, &parsed, 0); + // allow empty string for stop=end if (parsed == optarg) { - test_step_start = 0; + test_step_stop = -1; } optarg = parsed + strspn(parsed, " "); @@ -2533,107 +2392,103 @@ int main(int argc, char **argv) { if (*optarg == ',') { optarg += 1; - test_step_stop = strtoumax(optarg, &parsed, 0); - // allow empty string for stop=end + test_step_step = strtoumax(optarg, &parsed, 0); + // allow empty string for stop=1 if (parsed == optarg) { - test_step_stop = -1; + test_step_step = 1; } optarg = parsed + strspn(parsed, " "); - if (*optarg != ',' && *optarg != '\0') { + if (*optarg != '\0') { goto step_unknown; } + } + } else { + // single value = stop only + test_step_stop = test_step_start; + test_step_start = 0; + } + break; - if (*optarg == ',') { - optarg += 1; - test_step_step = strtoumax(optarg, &parsed, 0); - // allow empty string for stop=1 - if (parsed == optarg) { - test_step_step = 1; - } - optarg = parsed + strspn(parsed, " "); + step_unknown:; + fprintf(stderr, "error: invalid step: %s\n", optarg); + exit(-1); - if (*optarg != '\0') { - goto step_unknown; - } - } - } else { - // single value = stop only - test_step_stop = test_step_start; - test_step_start = 0; - } + case OPT_FORCE:; + test_force = true; + break; - break; -step_unknown: - fprintf(stderr, "error: invalid step: %s\n", optarg); + case OPT_DISK:; + test_disk_path = optarg; + break; + + case OPT_TRACE:; + test_trace_path = optarg; + break; + + case OPT_TRACE_BACKTRACE:; + test_trace_backtrace = true; + break; + + case OPT_TRACE_PERIOD:; + parsed = NULL; + test_trace_period = strtoumax(optarg, &parsed, 0); + if (parsed == optarg) { + fprintf(stderr, "error: invalid trace-period: %s\n", + optarg); exit(-1); } - case OPT_DISK: - test_disk_path = optarg; - break; - case OPT_TRACE: - test_trace_path = optarg; - break; - case OPT_TRACE_BACKTRACE: - test_trace_backtrace = true; - break; - case OPT_TRACE_PERIOD: { - char *parsed = NULL; - test_trace_period = strtoumax(optarg, &parsed, 0); - if (parsed == optarg) { - fprintf(stderr, "error: invalid trace-period: %s\n", optarg); - exit(-1); - } - break; - } - case OPT_TRACE_FREQ: { - char *parsed = NULL; - test_trace_freq = strtoumax(optarg, &parsed, 0); - if (parsed == optarg) { - fprintf(stderr, "error: invalid trace-freq: %s\n", optarg); - exit(-1); - } - break; - } - case OPT_READ_SLEEP: { - char *parsed = NULL; - double read_sleep = strtod(optarg, &parsed); - if (parsed == optarg) { - fprintf(stderr, "error: invalid read-sleep: %s\n", optarg); - exit(-1); - } - test_read_sleep = read_sleep*1.0e9; - break; + break; + + case OPT_TRACE_FREQ:; + parsed = NULL; + test_trace_freq = strtoumax(optarg, &parsed, 0); + if (parsed == optarg) { + fprintf(stderr, "error: invalid trace-freq: %s\n", optarg); + exit(-1); } - case OPT_PROG_SLEEP: { - char *parsed = NULL; - double prog_sleep = strtod(optarg, &parsed); - if (parsed == optarg) { - fprintf(stderr, "error: invalid prog-sleep: %s\n", optarg); - exit(-1); - } - test_prog_sleep = prog_sleep*1.0e9; - break; + break; + + case OPT_READ_SLEEP:; + parsed = NULL; + double read_sleep = strtod(optarg, &parsed); + if (parsed == optarg) { + fprintf(stderr, "error: invalid read-sleep: %s\n", optarg); + exit(-1); } - case OPT_ERASE_SLEEP: { - char *parsed = NULL; - double erase_sleep = strtod(optarg, &parsed); - if (parsed == optarg) { - fprintf(stderr, "error: invalid erase-sleep: %s\n", optarg); - exit(-1); - } - test_erase_sleep = erase_sleep*1.0e9; - break; + test_read_sleep = read_sleep*1.0e9; + break; + + case OPT_PROG_SLEEP:; + parsed = NULL; + double prog_sleep = strtod(optarg, &parsed); + if (parsed == optarg) { + fprintf(stderr, "error: invalid prog-sleep: %s\n", optarg); + exit(-1); } - // done parsing - case -1: - goto getopt_done; - // unknown arg, getopt prints a message for us - default: + test_prog_sleep = prog_sleep*1.0e9; + break; + + case OPT_ERASE_SLEEP:; + parsed = NULL; + double erase_sleep = strtod(optarg, &parsed); + if (parsed == optarg) { + fprintf(stderr, "error: invalid erase-sleep: %s\n", optarg); exit(-1); + } + test_erase_sleep = erase_sleep*1.0e9; + break; + + // done parsing + case -1:; + goto getopt_done; + + // unknown arg, getopt prints a message for us + default:; + exit(-1); } } -getopt_done: ; +getopt_done:; if (argc > optind) { // reset our test identifier list @@ -2646,8 +2501,7 @@ getopt_done: ; for (; argc > optind; optind++) { test_define_t *defines = NULL; size_t define_count = 0; - lfs_emubd_powercycles_t *cycles = NULL; - size_t cycle_count = 0; + test_powerloss_t powerloss = {NULL, NULL, NULL, 0}; // parse name, can be suite or case char *name = argv[optind]; @@ -2687,24 +2541,64 @@ getopt_done: ; if (d >= define_count) { // align to power of two to avoid any superlinear growth - size_t ncount = 1 << lfs_npw2(d+1); + size_t ncount = 1 << lfs3_nlog2(d+1); defines = realloc(defines, ncount*sizeof(test_define_t)); memset(defines+define_count, 0, (ncount-define_count)*sizeof(test_define_t)); define_count = ncount; } - defines[d] = TEST_LIT(v); + // name/define should be patched in test_define_suite + defines[d] = TEST_LIT(NULL, v); } - if (cycles_) { + // special case for linear power cycles + if (cycles_ && *cycles_ == 'x') { + char *parsed = NULL; + size_t cycle_count = leb16_parse(cycles_+1, &parsed); + if (parsed == cycles_+1) { + fprintf(stderr, "error: " + "could not parse test cycles: %s\n", + cycles_); + exit(-1); + } + cycles_ = parsed; + + powerloss = (test_powerloss_t){ + "linear", + run_powerloss_linear, + NULL, + cycle_count}; + + // special case for log power cycles + } else if (cycles_ && *cycles_ == 'y') { + char *parsed = NULL; + size_t cycle_count = leb16_parse(cycles_+1, &parsed); + if (parsed == cycles_+1) { + fprintf(stderr, "error: " + "could not parse test cycles: %s\n", + cycles_); + exit(-1); + } + cycles_ = parsed; + + powerloss = (test_powerloss_t){ + "log", + run_powerloss_log, + NULL, + cycle_count}; + + // otherwise explicit power cycles + } else if (cycles_) { // parse power cycles + lfs3_emubd_powercycles_t *cycles = NULL; + size_t cycle_count = 0; size_t cycle_capacity = 0; while (*cycles_ != '\0') { char *parsed = NULL; - *(lfs_emubd_powercycles_t*)mappend( + *(lfs3_emubd_powercycles_t*)mappend( (void**)&cycles, - sizeof(lfs_emubd_powercycles_t), + sizeof(lfs3_emubd_powercycles_t), &cycle_count, &cycle_capacity) = leb16_parse(cycles_, &parsed); @@ -2716,6 +2610,12 @@ getopt_done: ; } cycles_ = parsed; } + + powerloss = (test_powerloss_t){ + "explicit", + run_powerloss_cycles, + cycles, + cycle_count}; } } @@ -2728,8 +2628,7 @@ getopt_done: ; .name = name, .defines = defines, .define_count = define_count, - .cycles = cycles, - .cycle_count = cycle_count, + .powerloss = powerloss, }; } @@ -2738,14 +2637,11 @@ getopt_done: ; // cleanup (need to be done for valgrind testing) test_define_cleanup(); - if (test_overrides) { - for (size_t i = 0; i < test_override_count; i++) { - free((void*)test_overrides[i].defines); + if (test_override_defines) { + for (size_t i = 0; i < test_override_define_count; i++) { + free((void*)test_override_defines[i].data); } - free((void*)test_overrides); - } - if (test_geometry_capacity) { - free((void*)test_geometries); + free((void*)test_override_defines); } if (test_powerloss_capacity) { for (size_t i = 0; i < test_powerloss_count; i++) { @@ -2756,7 +2652,7 @@ getopt_done: ; if (test_id_capacity) { for (size_t i = 0; i < test_id_count; i++) { free((void*)test_ids[i].defines); - free((void*)test_ids[i].cycles); + free((void*)test_ids[i].powerloss.cycles); } free((void*)test_ids); } diff --git a/runners/test_runner.h b/runners/test_runner.h index 9ff1f790b..08470101b 100644 --- a/runners/test_runner.h +++ b/runners/test_runner.h @@ -8,20 +8,20 @@ #define TEST_RUNNER_H -// override LFS_TRACE +// override LFS3_TRACE void test_trace(const char *fmt, ...); -#define LFS_TRACE_(fmt, ...) \ +#define LFS3_TRACE_(fmt, ...) \ test_trace("%s:%d:trace: " fmt "%s\n", \ __FILE__, \ __LINE__, \ __VA_ARGS__) -#define LFS_TRACE(...) LFS_TRACE_(__VA_ARGS__, "") -#define LFS_EMUBD_TRACE(...) LFS_TRACE_(__VA_ARGS__, "") +#define LFS3_TRACE(...) LFS3_TRACE_(__VA_ARGS__, "") +#define LFS3_EMUBD_TRACE(...) LFS3_TRACE_(__VA_ARGS__, "") // note these are indirectly included in any generated files -#include "bd/lfs_emubd.h" +#include "bd/lfs3_emubd.h" #include // give source a chance to define feature macros @@ -30,28 +30,33 @@ void test_trace(const char *fmt, ...); // generated test configurations -struct lfs_config; +struct lfs3_cfg; enum test_flags { - TEST_REENTRANT = 0x1, + TEST_INTERNAL = 0x1, + TEST_REENTRANT = 0x2, + TEST_FUZZ = 0x4, }; typedef uint8_t test_flags_t; typedef struct test_define { - intmax_t (*cb)(void *data); + const char *name; + intmax_t *define; + intmax_t (*cb)(void *data, size_t i); void *data; + size_t permutations; } test_define_t; struct test_case { const char *name; const char *path; test_flags_t flags; - size_t permutations; const test_define_t *defines; + size_t permutations; - bool (*filter)(void); - void (*run)(struct lfs_config *cfg); + bool (*if_)(void); + void (*run)(struct lfs3_cfg *cfg); }; struct test_suite { @@ -59,66 +64,110 @@ struct test_suite { const char *path; test_flags_t flags; - const char *const *define_names; + const test_define_t *defines; size_t define_count; const struct test_case *cases; size_t case_count; }; +extern const struct test_suite *const test_suites[]; +extern const size_t test_suite_count; + -// deterministic prng for pseudo-randomness in testes +// this variable tracks the number of powerlosses triggered during the +// current test permutation, this is useful for both tests and debugging +extern volatile size_t TEST_PLS; + +// deterministic prng for pseudo-randomness in tests uint32_t test_prng(uint32_t *state); #define TEST_PRNG(state) test_prng(state) +// generation of specific permutations of an array for exhaustive testing +size_t test_factorial(size_t x); +void test_permutation(size_t i, uint32_t *buffer, size_t size); -// access generated test defines -intmax_t test_define(size_t define); +#define TEST_FACTORIAL(x) test_factorial(x) +#define TEST_PERMUTATION(i, buffer, size) test_permutation(i, buffer, size) -#define TEST_DEFINE(i) test_define(i) // a few preconfigured defines that control how tests run - -#define READ_SIZE_i 0 -#define PROG_SIZE_i 1 -#define BLOCK_SIZE_i 2 -#define BLOCK_COUNT_i 3 -#define CACHE_SIZE_i 4 -#define LOOKAHEAD_SIZE_i 5 -#define BLOCK_CYCLES_i 6 -#define ERASE_VALUE_i 7 -#define ERASE_CYCLES_i 8 -#define BADBLOCK_BEHAVIOR_i 9 -#define POWERLOSS_BEHAVIOR_i 10 - -#define READ_SIZE TEST_DEFINE(READ_SIZE_i) -#define PROG_SIZE TEST_DEFINE(PROG_SIZE_i) -#define BLOCK_SIZE TEST_DEFINE(BLOCK_SIZE_i) -#define BLOCK_COUNT TEST_DEFINE(BLOCK_COUNT_i) -#define CACHE_SIZE TEST_DEFINE(CACHE_SIZE_i) -#define LOOKAHEAD_SIZE TEST_DEFINE(LOOKAHEAD_SIZE_i) -#define BLOCK_CYCLES TEST_DEFINE(BLOCK_CYCLES_i) -#define ERASE_VALUE TEST_DEFINE(ERASE_VALUE_i) -#define ERASE_CYCLES TEST_DEFINE(ERASE_CYCLES_i) -#define BADBLOCK_BEHAVIOR TEST_DEFINE(BADBLOCK_BEHAVIOR_i) -#define POWERLOSS_BEHAVIOR TEST_DEFINE(POWERLOSS_BEHAVIOR_i) - #define TEST_IMPLICIT_DEFINES \ - TEST_DEF(READ_SIZE, PROG_SIZE) \ - TEST_DEF(PROG_SIZE, BLOCK_SIZE) \ - TEST_DEF(BLOCK_SIZE, 0) \ - TEST_DEF(BLOCK_COUNT, (1024*1024)/BLOCK_SIZE) \ - TEST_DEF(CACHE_SIZE, lfs_max(64,lfs_max(READ_SIZE,PROG_SIZE))) \ - TEST_DEF(LOOKAHEAD_SIZE, 16) \ - TEST_DEF(BLOCK_CYCLES, -1) \ - TEST_DEF(ERASE_VALUE, 0xff) \ - TEST_DEF(ERASE_CYCLES, 0) \ - TEST_DEF(BADBLOCK_BEHAVIOR, LFS_EMUBD_BADBLOCK_PROGERROR) \ - TEST_DEF(POWERLOSS_BEHAVIOR, LFS_EMUBD_POWERLOSS_NOOP) - -#define TEST_IMPLICIT_DEFINE_COUNT 11 -#define TEST_GEOMETRY_DEFINE_COUNT 4 + /* name value (overridable) */ \ + TEST_DEFINE(READ_SIZE, 1 ) \ + TEST_DEFINE(PROG_SIZE, 1 ) \ + TEST_DEFINE(BLOCK_SIZE, 4096 ) \ + TEST_DEFINE(BLOCK_COUNT, DISK_SIZE/BLOCK_SIZE ) \ + TEST_DEFINE(DISK_SIZE, 1024*1024 ) \ + TEST_DEFINE(BLOCK_RECYCLES, -1 ) \ + TEST_DEFINE(RCACHE_SIZE, LFS3_MAX(16, READ_SIZE) ) \ + TEST_DEFINE(PCACHE_SIZE, LFS3_MAX(16, PROG_SIZE) ) \ + TEST_DEFINE(FCACHE_SIZE, 16 ) \ + TEST_DEFINE(LOOKAHEAD_SIZE, 16 ) \ + TEST_DEFINE(GC_FLAGS, LFS3_GC_ALL ) \ + TEST_DEFINE(GC_STEPS, 0 ) \ + TEST_DEFINE(GC_LOOKAHEAD_THRESH, -1 ) \ + TEST_DEFINE(GC_LOOKGBMAP_THRESH, -1 ) \ + TEST_DEFINE(GC_COMPACTMETA_THRESH, 0 ) \ + TEST_DEFINE(SHRUB_SIZE, BLOCK_SIZE/4 ) \ + TEST_DEFINE(FRAGMENT_SIZE, LFS3_MIN(BLOCK_SIZE/8, 512) ) \ + TEST_DEFINE(CRYSTAL_THRESH, BLOCK_SIZE/8 ) \ + TEST_DEFINE(LOOKGBMAP_THRESH, BLOCK_COUNT/4 ) \ + TEST_DEFINE(ERASE_VALUE, 0xff ) \ + TEST_DEFINE(ERASE_CYCLES, 0 ) \ + TEST_DEFINE(BADBLOCK_BEHAVIOR, LFS3_EMUBD_BADBLOCK_PROGERROR ) \ + TEST_DEFINE(POWERLOSS_BEHAVIOR, LFS3_EMUBD_POWERLOSS_ATOMIC ) \ + TEST_DEFINE(EMUBD_SEED, 0 ) + +// declare defines as global intmax_ts +#define TEST_DEFINE(k, v) \ + extern intmax_t k; + + TEST_IMPLICIT_DEFINES +#undef TEST_DEFINE + +// map defines to cfg struct fields +#define TEST_CFG \ + .read_size = READ_SIZE, \ + .prog_size = PROG_SIZE, \ + .block_size = BLOCK_SIZE, \ + .block_count = BLOCK_COUNT, \ + .block_recycles = BLOCK_RECYCLES, \ + .rcache_size = RCACHE_SIZE, \ + .pcache_size = PCACHE_SIZE, \ + .fcache_size = FCACHE_SIZE, \ + .lookahead_size = LOOKAHEAD_SIZE, \ + TEST_GBMAP_CFG \ + TEST_GC_CFG \ + .gc_lookahead_thresh = GC_LOOKAHEAD_THRESH, \ + .gc_compactmeta_thresh = GC_COMPACTMETA_THRESH, \ + .shrub_size = SHRUB_SIZE, \ + .fragment_size = FRAGMENT_SIZE, \ + .crystal_thresh = CRYSTAL_THRESH, + +#ifdef LFS3_GBMAP +#define TEST_GBMAP_CFG \ + .gc_lookgbmap_thresh = GC_LOOKGBMAP_THRESH, \ + .lookgbmap_thresh = LOOKGBMAP_THRESH, +#else +#define TEST_GBMAP_CFG +#endif + +#ifdef LFS3_GC +#define TEST_GC_CFG \ + .gc_flags = GC_FLAGS, \ + .gc_steps = GC_STEPS, +#else +#define TEST_GC_CFG +#endif + +#define TEST_BDCFG \ + .erase_value = ERASE_VALUE, \ + .erase_cycles = ERASE_CYCLES, \ + .badblock_behavior = BADBLOCK_BEHAVIOR, \ + .powerloss_behavior = POWERLOSS_BEHAVIOR, \ + .seed = EMUBD_SEED, #endif diff --git a/scripts/bench.py b/scripts/bench.py index f22841eac..7fc3aac2e 100755 --- a/scripts/bench.py +++ b/scripts/bench.py @@ -9,12 +9,16 @@ # SPDX-License-Identifier: BSD-3-Clause # +# prevent local imports +if __name__ == "__main__": + __import__('sys').path.pop(0) + import collections as co import csv import errno -import glob +import fnmatch import itertools as it -import math as m +import functools as ft import os import pty import re @@ -22,29 +26,55 @@ import shutil import signal import subprocess as sp +import sys import threading as th import time -import toml + +try: + import tomllib as toml +except ModuleNotFoundError: + import tomli as toml -RUNNER_PATH = './runners/bench_runner' -HEADER_PATH = 'runners/bench_runner.h' +RUNNER_PATH = ['./runners/bench_runner'] +HEADER_PATHS = ['./runners/bench_runner.h'] GDB_PATH = ['gdb'] +GDB_SCRIPTS = ['./scripts/dbg.gdb.py'] VALGRIND_PATH = ['valgrind'] PERF_SCRIPT = ['./scripts/perf.py'] +# open with '-' for stdin/stdout def openio(path, mode='r', buffering=-1): - # allow '-' for stdin/stdout + import os if path == '-': - if mode == 'r': + if 'r' in mode: return os.fdopen(os.dup(sys.stdin.fileno()), mode, buffering) else: return os.fdopen(os.dup(sys.stdout.fileno()), mode, buffering) else: return open(path, mode, buffering) +# a define range +class DRange: + def __init__(self, start, stop=None, step=None): + if stop is None: + start, stop = None, start + self.start = start if start is not None else 0 + self.stop = stop + self.step = step if step is not None else 1 + + def __len__(self): + if self.step > 0: + return (self.stop-1 - self.start) // self.step + 1 + else: + return (self.start-1 - self.stop) // -self.step + 1 + + def next(self, i): + return '(%s)*%d + %d' % (i, self.step, self.start) + + class BenchCase: # create a BenchCase object from a config def __init__(self, config, args={}): @@ -52,13 +82,21 @@ def __init__(self, config, args={}): self.path = config.pop('path') self.suite = config.pop('suite') self.lineno = config.pop('lineno', None) - self.if_ = config.pop('if', None) - if isinstance(self.if_, bool): - self.if_ = 'true' if self.if_ else 'false' + self.if_ = config.pop('if', []) + if not isinstance(self.if_, list): + self.if_ = [self.if_] + self.ifdef = config.pop('ifdef', []) + if not isinstance(self.ifdef, list): + self.ifdef = [self.ifdef] + self.ifndef = config.pop('ifndef', []) + if not isinstance(self.ifndef, list): + self.ifndef = [self.ifndef] self.code = config.pop('code') self.code_lineno = config.pop('code_lineno', None) self.in_ = config.pop('in', - config.pop('suite_in', None)) + config.pop('suite_in', None)) + + self.internal = bool(self.in_) # figure out defines and build possible permutations self.defines = set() @@ -90,55 +128,56 @@ def csplit(v): def parse_define(v): # a define entry can be a list if isinstance(v, list): - for v_ in v: - yield from parse_define(v_) + return sum((parse_define(v_) for v_ in v), []) # or a string elif isinstance(v, str): # which can be comma-separated values, with optional # range statements. This matches the runtime define parser in # the runner itself. + vs = [] for v_ in csplit(v): - m = re.search(r'\brange\b\s*\(' - '(?P[^,\s]*)' - '\s*(?:,\s*(?P[^,\s]*)' - '\s*(?:,\s*(?P[^,\s]*)\s*)?)?\)', - v_) + m = re.match(r'^\s*range\s*\((.*)\)\s*$', v_) if m: - start = (int(m.group('start'), 0) - if m.group('start') else 0) - stop = (int(m.group('stop'), 0) - if m.group('stop') else None) - step = (int(m.group('step'), 0) - if m.group('step') else 1) - if m.lastindex <= 1: - start, stop = 0, start - for x in range(start, stop, step): - yield from parse_define('%s(%d)%s' % ( - v_[:m.start()], x, v_[m.end():])) + vs.append(DRange(*[ + int(a, 0) for a in csplit(m.group(1))])) else: - yield v_ + vs.append(v_) + return vs # or a literal value elif isinstance(v, bool): - yield 'true' if v else 'false' + return ['true' if v else 'false'] else: - yield v + return [v] # build possible permutations for suite_defines_ in suite_defines: self.defines |= suite_defines_.keys() for defines_ in defines: self.defines |= defines_.keys() - self.permutations.extend(dict(perm) for perm in it.product(*( - [(k, v) for v in parse_define(vs)] - for k, vs in sorted((suite_defines_ | defines_).items())))) + self.permutations.append({ + k: parse_define(v) + for k, v in (suite_defines_ | defines_).items()}) for k in config.keys(): print('%swarning:%s in %s, found unused key %r' % ( - '\x1b[01;33m' if args['color'] else '', - '\x1b[m' if args['color'] else '', - self.name, - k), - file=sys.stderr) + '\x1b[1;33m' if args['color'] else '', + '\x1b[m' if args['color'] else '', + self.name, + k), + file=sys.stderr) + + def __repr__(self): + return '' % self.name + + def __lt__(self, other): + # sort by suite, lineno, and name + return ((self.suite, self.lineno, self.name) + < (other.suite, other.lineno, other.name)) + + def isin(self, path): + return (self.in_ is not None + and os.path.normpath(self.in_) + == os.path.normpath(path)) class BenchSuite: @@ -152,7 +191,7 @@ def __init__(self, path, args={}): # load toml file and parse bench cases with open(self.path) as f: # load benches - config = toml.load(f) + config = toml.load(f.buffer) # find line numbers f.seek(0) @@ -160,9 +199,9 @@ def __init__(self, path, args={}): code_linenos = [] for i, line in enumerate(f): match = re.match( - '(?P\[\s*cases\s*\.\s*(?P\w+)\s*\])' - '|' '(?Pcode\s*=)', - line) + '(?P\[\s*cases\s*\.\s*(?P\w+)\s*\])' + '|' '(?Pcode\s*=)', + line) if match and match.group('case'): case_linenos.append((i+1, match.group('name'))) elif match and match.group('code'): @@ -171,74 +210,121 @@ def __init__(self, path, args={}): # sort in case toml parsing did not retain order case_linenos.sort() - cases = config.pop('cases') + cases = config.pop('cases', {}) for (lineno, name), (nlineno, _) in it.zip_longest( case_linenos, case_linenos[1:], fillvalue=(float('inf'), None)): code_lineno = min( - (l for l in code_linenos if l >= lineno and l < nlineno), - default=None) + (l for l in code_linenos + if l >= lineno and l < nlineno), + default=None) cases[name]['lineno'] = lineno cases[name]['code_lineno'] = code_lineno - self.if_ = config.pop('if', None) - if isinstance(self.if_, bool): - self.if_ = 'true' if self.if_ else 'false' + self.if_ = config.pop('if', []) + if not isinstance(self.if_, list): + self.if_ = [self.if_] + + self.ifdef = config.pop('ifdef', []) + if not isinstance(self.ifdef, list): + self.ifdef = [self.ifdef] + self.ifndef = config.pop('ifndef', []) + if not isinstance(self.ifndef, list): + self.ifndef = [self.ifndef] self.code = config.pop('code', None) self.code_lineno = min( - (l for l in code_linenos - if not case_linenos or l < case_linenos[0][0]), - default=None) + (l for l in code_linenos + if not case_linenos or l < case_linenos[0][0]), + default=None) + self.in_ = config.pop('in', None) + + self.after = config.pop('after', []) + if not isinstance(self.after, list): + self.after = [self.after] # a couple of these we just forward to all cases defines = config.pop('defines', {}) - in_ = config.pop('in', None) self.cases = [] - for name, case in sorted(cases.items(), - key=lambda c: c[1].get('lineno')): - self.cases.append(BenchCase(config={ - 'name': name, - 'path': path + (':%d' % case['lineno'] - if 'lineno' in case else ''), - 'suite': self.name, - 'suite_defines': defines, - 'suite_in': in_, - **case}, - args=args)) + for name, config_ in cases.items(): + case = BenchCase( + config={ + 'name': name, + 'path': path + (':%d' % config_['lineno'] + if 'lineno' in config_ else ''), + 'suite': self.name, + 'suite_defines': defines, + 'suite_in': self.in_, + **config_}, + args=args) + + # skipping internal tests? + if args.get('no_internal') and case.in_ is not None: + continue + + self.cases.append(case) + + # sort for consistency + self.cases.sort() # combine per-case defines - self.defines = set.union(*( - set(case.defines) for case in self.cases)) + self.defines = set.union(set(), *( + set(case.defines) for case in self.cases)) + + # combine other per-case things + self.internal = any(case.internal for case in self.cases) for k in config.keys(): print('%swarning:%s in %s, found unused key %r' % ( - '\x1b[01;33m' if args['color'] else '', - '\x1b[m' if args['color'] else '', - self.name, - k), - file=sys.stderr) - + '\x1b[1;33m' if args['color'] else '', + '\x1b[m' if args['color'] else '', + self.name, + k), + file=sys.stderr) + def __repr__(self): + return '' % self.name -def compile(bench_paths, **args): - # find .toml files - paths = [] - for path in bench_paths: - if os.path.isdir(path): - path = path + '/*.toml' + def __lt__(self, other): + # sort by name + # + # note we override this with a topological sort during compilation + return self.name < other.name - for path in glob.glob(path): - paths.append(path) + def isin(self, path): + return (self.in_ is not None + and os.path.normpath(self.in_) + == os.path.normpath(path)) - if not paths: - print('no bench suites found in %r?' % bench_paths) - sys.exit(-1) +def compile(bench_paths, **args): # load the suites - suites = [BenchSuite(path, args) for path in paths] - suites.sort(key=lambda s: s.name) + suites = [BenchSuite(path, args) for path in bench_paths] + + # sort suites by: + # 1. topologically by "after" dependencies + # 2. lexicographically for consistency + pending = co.OrderedDict((suite.name, suite) + for suite in sorted(suites)) + suites = [] + while pending: + pending_ = co.OrderedDict() + for suite in pending.values(): + if not any(after in pending for after in suite.after): + suites.append(suite) + else: + pending_[suite.name] = suite + + if len(pending_) == len(pending): + print('%serror:%s cycle detected in suite ordering: {%s}' % ( + '\x1b[1;31m' if args['color'] else '', + '\x1b[m' if args['color'] else '', + ', '.join(suite.name for suite in pending.values())), + file=sys.stderr) + sys.exit(-1) + + pending = pending_ # check for name conflicts, these will cause ambiguity problems later # when running benches @@ -246,32 +332,36 @@ def compile(bench_paths, **args): for suite in suites: if suite.name in seen: print('%swarning:%s conflicting suite %r, %s and %s' % ( - '\x1b[01;33m' if args['color'] else '', - '\x1b[m' if args['color'] else '', - suite.name, - suite.path, - seen[suite.name].path), - file=sys.stderr) + '\x1b[1;33m' if args['color'] else '', + '\x1b[m' if args['color'] else '', + suite.name, + suite.path, + seen[suite.name].path), + file=sys.stderr) seen[suite.name] = suite for case in suite.cases: # only allow conflicts if a case and its suite share a name if case.name in seen and not ( isinstance(seen[case.name], BenchSuite) - and seen[case.name].cases == [case]): + and seen[case.name].cases == [case]): print('%swarning:%s conflicting case %r, %s and %s' % ( - '\x1b[01;33m' if args['color'] else '', - '\x1b[m' if args['color'] else '', - case.name, - case.path, - seen[case.name].path), - file=sys.stderr) + '\x1b[1;33m' if args['color'] else '', + '\x1b[m' if args['color'] else '', + case.name, + case.path, + seen[case.name].path), + file=sys.stderr) seen[case.name] = case # we can only compile one bench suite at a time if not args.get('source'): if len(suites) > 1: - print('more than one bench suite for compilation? (%r)' % bench_paths) + print('%serror:%s compiling more than one bench suite? (%r)' % ( + '\x1b[1;31m' if args['color'] else '', + '\x1b[m' if args['color'] else '', + bench_paths), + file=sys.stderr) sys.exit(-1) suite = suites[0] @@ -279,17 +369,18 @@ def compile(bench_paths, **args): # write generated bench source if 'output' in args: with openio(args['output'], 'w') as f: - _write = f.write - def write(s): - f.lineno += s.count('\n') - _write(s) - def writeln(s=''): - f.lineno += s.count('\n') + 1 - _write(s) - _write('\n') + # some helpful file functions f.lineno = 1 - f.write = write - f.writeln = writeln + f.write_ = f.write + def write(self, s): + self.lineno += s.count('\n') + self.write_(s) + f.write = write.__get__(f) + def writeln(self, s=''): + self.lineno += s.count('\n') + 1 + self.write_(s) + self.write_('\n') + f.writeln = writeln.__get__(f) f.writeln("// Generated by %s:" % sys.argv[0]) f.writeln("//") @@ -298,7 +389,8 @@ def writeln(s=''): f.writeln() # include bench_runner.h in every generated file - f.writeln("#include \"%s\"" % args['include']) + for header in (args.get('include') or HEADER_PATHS): + f.writeln("#include \"%s\"" % header) f.writeln() # write out generated functions, this can end up in different @@ -307,81 +399,102 @@ def writeln(s=''): # note it's up to the specific generated file to declare # the bench defines def write_case_functions(f, suite, case): - # create case define functions - if case.defines: - # deduplicate defines by value to try to reduce the - # number of functions we generate - define_cbs = {} - for i, defines in enumerate(case.permutations): - for k, v in sorted(defines.items()): - if v not in define_cbs: - name = ('__bench__%s__%s__%d' - % (case.name, k, i)) - define_cbs[v] = name - f.writeln('intmax_t %s(' - '__attribute__((unused)) ' - 'void *data) {' % name) - f.writeln(4*' '+'return %s;' % v) - f.writeln('}') - f.writeln() - f.writeln('const bench_define_t ' - '__bench__%s__defines[][' - 'BENCH_IMPLICIT_DEFINE_COUNT+%d] = {' - % (case.name, len(suite.defines))) - for defines in case.permutations: - f.writeln(4*' '+'{') - for k, v in sorted(defines.items()): - f.writeln(8*' '+'[%-24s] = {%s, NULL},' % ( - k+'_i', define_cbs[v])) - f.writeln(4*' '+'},') - f.writeln('};') + # write any ifdef prologues + if case.ifdef or case.ifndef: + for ifdef in case.ifdef: + f.writeln('#ifdef %s' % ifdef) + for ifndef in case.ifndef: + f.writeln('#ifndef %s' % ifndef) f.writeln() - # create case filter function - if suite.if_ is not None or case.if_ is not None: - f.writeln('bool __bench__%s__filter(void) {' - % (case.name)) - f.writeln(4*' '+'return %s;' - % ' && '.join('(%s)' % if_ - for if_ in [suite.if_, case.if_] - if if_ is not None)) + # create case define functions + for i, permutation in enumerate(case.permutations): + for k, vs in sorted(permutation.items()): + f.writeln('intmax_t __bench__%s__%s__%d(' + '__attribute__((unused)) void *data, ' + 'size_t i) {' % ( + case.name, k, i)) + j = 0 + for v in vs: + # generate range + if isinstance(v, DRange): + f.writeln(4*' '+'if (i < %d) ' + 'return %s;' % ( + j+len(v), v.next('i-%d' % j))) + j += len(v) + # translate index to define + else: + f.writeln(4*' '+'if (i == %d) ' + 'return %s;' % ( + j, v)) + j += 1 + + f.writeln(4*' '+'__builtin_unreachable();') + f.writeln('}') + f.writeln() + + # create case if function + if suite.if_ or case.if_: + f.writeln('bool __bench__%s__if(void) {' % ( + case.name)) + for if_ in it.chain(suite.if_, case.if_): + f.writeln(4*' '+'if (!(%s)) return false;' % ( + 'true' if if_ is True + else 'false' if if_ is False + else if_)) + f.writeln(4*' '+'return true;') f.writeln('}') f.writeln() # create case run function f.writeln('void __bench__%s__run(' - '__attribute__((unused)) struct lfs_config *cfg) {' - % (case.name)) + '__attribute__((unused)) ' + 'struct lfs3_cfg *CFG) {' % ( + case.name)) f.writeln(4*' '+'// bench case %s' % case.name) if case.code_lineno is not None: - f.writeln(4*' '+'#line %d "%s"' - % (case.code_lineno, suite.path)) + f.writeln(4*' '+'#line %d "%s"' % ( + case.code_lineno, suite.path)) f.write(case.code) if case.code_lineno is not None: - f.writeln(4*' '+'#line %d "%s"' - % (f.lineno+1, args['output'])) + f.writeln(4*' '+'#line %d "%s"' % ( + f.lineno+1, args['output'])) f.writeln('}') f.writeln() + # write any ifdef epilogues + if case.ifdef or case.ifndef: + for ifdef in case.ifdef: + f.writeln('#endif') + for ifndef in case.ifndef: + f.writeln('#endif') + f.writeln() + if not args.get('source'): - if suite.code is not None: - if suite.code_lineno is not None: - f.writeln('#line %d "%s"' - % (suite.code_lineno, suite.path)) - f.write(suite.code) - if suite.code_lineno is not None: - f.writeln('#line %d "%s"' - % (f.lineno+1, args['output'])) + # write any ifdef prologues + if suite.ifdef or suite.ifndef: + for ifdef in suite.ifdef: + f.writeln('#ifdef %s' % ifdef) + for ifndef in suite.ifndef: + f.writeln('#ifndef %s' % ifndef) f.writeln() + # write any suite defines if suite.defines: - for i, define in enumerate(sorted(suite.defines)): - f.writeln('#ifndef %s' % define) - f.writeln('#define %-24s ' - 'BENCH_IMPLICIT_DEFINE_COUNT+%d' % (define+'_i', i)) - f.writeln('#define %-24s ' - 'BENCH_DEFINE(%s)' % (define, define+'_i')) - f.writeln('#endif') + for define in sorted(suite.defines): + f.writeln('__attribute__((weak)) intmax_t %s;' % ( + define)) + f.writeln() + + # write any suite code + if suite.code is not None and suite.in_ is None: + if suite.code_lineno is not None: + f.writeln('#line %d "%s"' % ( + suite.code_lineno, suite.path)) + f.write(suite.code) + if suite.code_lineno is not None: + f.writeln('#line %d "%s"' % ( + f.lineno+1, args['output'])) f.writeln() # create case functions @@ -389,62 +502,104 @@ def write_case_functions(f, suite, case): if case.in_ is None: write_case_functions(f, suite, case) else: - if case.defines: - f.writeln('extern const bench_define_t ' - '__bench__%s__defines[][' - 'BENCH_IMPLICIT_DEFINE_COUNT+%d];' - % (case.name, len(suite.defines))) - if suite.if_ is not None or case.if_ is not None: - f.writeln('extern bool __bench__%s__filter(' - 'void);' - % (case.name)) + for i, permutation in enumerate(case.permutations): + for k, vs in sorted(permutation.items()): + f.writeln('extern intmax_t __bench__%s__%s__%d(' + 'void *data, size_t i);' % ( + case.name, k, i)) + if suite.if_ or case.if_: + f.writeln('extern bool __bench__%s__if(' + 'void);' % ( + case.name)) f.writeln('extern void __bench__%s__run(' - 'struct lfs_config *cfg);' - % (case.name)) + 'struct lfs3_cfg *CFG);' % ( + case.name)) f.writeln() + # write any ifdef epilogues + if suite.ifdef or suite.ifndef: + for ifdef in suite.ifdef: + f.writeln('#endif') + for ifndef in suite.ifndef: + f.writeln('#endif') + f.writeln() + # create suite struct - # - # note we place this in the custom bench_suites section with - # minimum alignment, otherwise GCC ups the alignment to - # 32-bytes for some reason - f.writeln('__attribute__((section("_bench_suites"), ' - 'aligned(1)))') - f.writeln('const struct bench_suite __bench__%s__suite = {' - % suite.name) + f.writeln('const struct bench_suite __bench__%s__suite = {' % ( + suite.name)) f.writeln(4*' '+'.name = "%s",' % suite.name) f.writeln(4*' '+'.path = "%s",' % suite.path) - f.writeln(4*' '+'.flags = 0,') + f.writeln(4*' '+'.flags = %s,' % ( + ' | '.join(filter(None, [ + 'BENCH_INTERNAL' if suite.internal else None])) + or 0)) + for ifdef in suite.ifdef: + f.writeln(4*' '+'#ifdef %s' % ifdef) + for ifndef in suite.ifndef: + f.writeln(4*' '+'#ifndef %s' % ifndef) + # create suite defines if suite.defines: - # create suite define names - f.writeln(4*' '+'.define_names = (const char *const[' - 'BENCH_IMPLICIT_DEFINE_COUNT+%d]){' % ( - len(suite.defines))) + f.writeln(4*' '+'.defines = (const bench_define_t[]){') for k in sorted(suite.defines): - f.writeln(8*' '+'[%-24s] = "%s",' % (k+'_i', k)) + f.writeln(8*' '+'{"%s", &%s, NULL, NULL, 0},' % ( + k, k)) + f.writeln(4*' '+'},') + f.writeln(4*' '+'.define_count = %d,' % len(suite.defines)) + for ifdef in suite.ifdef: + f.writeln(4*' '+'#endif') + for ifndef in suite.ifndef: + f.writeln(4*' '+'#endif') + if suite.cases: + f.writeln(4*' '+'.cases = (const struct bench_case[]){') + for case in suite.cases: + # create case structs + f.writeln(8*' '+'{') + f.writeln(12*' '+'.name = "%s",' % case.name) + f.writeln(12*' '+'.path = "%s",' % case.path) + f.writeln(12*' '+'.flags = %s,' % ( + ' | '.join(filter(None, [ + 'BENCH_INTERNAL' if case.internal + else None])) + or 0)) + for ifdef in it.chain(suite.ifdef, case.ifdef): + f.writeln(12*' '+'#ifdef %s' % ifdef) + for ifndef in it.chain(suite.ifndef, case.ifndef): + f.writeln(12*' '+'#ifndef %s' % ifndef) + # create case defines + if case.defines: + f.writeln(12*' '+'.defines' + ' = (const bench_define_t*)' + '(const bench_define_t[][%d]){' % ( + len(suite.defines))) + for i, permutation in enumerate(case.permutations): + f.writeln(16*' '+'{') + for k, vs in sorted(permutation.items()): + f.writeln(20*' '+'[%d] = {' + '"%s", &%s, ' + '__bench__%s__%s__%d, ' + 'NULL, %d},' % ( + sorted(suite.defines).index(k), + k, k, case.name, k, i, + sum(len(v) + if isinstance( + v, DRange) + else 1 + for v in vs))) + f.writeln(16*' '+'},') + f.writeln(12*' '+'},') + f.writeln(12*' '+'.permutations = %d,' % ( + len(case.permutations))) + if suite.if_ or case.if_: + f.writeln(12*' '+'.if_ = __bench__%s__if,' % ( + case.name)) + f.writeln(12*' '+'.run = __bench__%s__run,' % ( + case.name)) + for ifdef in it.chain(suite.ifdef, case.ifdef): + f.writeln(12*' '+'#endif') + for ifndef in it.chain(suite.ifndef, case.ifndef): + f.writeln(12*' '+'#endif') + f.writeln(8*' '+'},') f.writeln(4*' '+'},') - f.writeln(4*' '+'.define_count = ' - 'BENCH_IMPLICIT_DEFINE_COUNT+%d,' % len(suite.defines)) - f.writeln(4*' '+'.cases = (const struct bench_case[]){') - for case in suite.cases: - # create case structs - f.writeln(8*' '+'{') - f.writeln(12*' '+'.name = "%s",' % case.name) - f.writeln(12*' '+'.path = "%s",' % case.path) - f.writeln(12*' '+'.flags = 0,') - f.writeln(12*' '+'.permutations = %d,' - % len(case.permutations)) - if case.defines: - f.writeln(12*' '+'.defines ' - '= (const bench_define_t*)__bench__%s__defines,' - % (case.name)) - if suite.if_ is not None or case.if_ is not None: - f.writeln(12*' '+'.filter = __bench__%s__filter,' - % (case.name)) - f.writeln(12*' '+'.run = __bench__%s__run,' - % (case.name)) - f.writeln(8*' '+'},') - f.writeln(4*' '+'},') f.writeln(4*' '+'.case_count = %d,' % len(suite.cases)) f.writeln('};') f.writeln() @@ -456,44 +611,81 @@ def write_case_functions(f, suite, case): shutil.copyfileobj(sf, f) f.writeln() + # merge all defines we need, otherwise we will run into + # redefinition errors + defines = ({define + for suite in suites + if suite.isin(args['source']) + for define in suite.defines} + | {define + for suite in suites + for case in suite.cases + if case.isin(args['source']) + for define in case.defines}) + if defines: + for define in sorted(defines): + f.writeln('__attribute__((weak)) intmax_t %s;' % ( + define)) + f.writeln() + # write any internal benches for suite in suites: - for case in suite.cases: - if (case.in_ is not None - and os.path.normpath(case.in_) - == os.path.normpath(args['source'])): - # write defines, but note we need to undef any - # new defines since we're in someone else's file - if suite.defines: - for i, define in enumerate( - sorted(suite.defines)): - f.writeln('#ifndef %s' % define) - f.writeln('#define %-24s ' - 'BENCH_IMPLICIT_DEFINE_COUNT+%d' % ( - define+'_i', i)) - f.writeln('#define %-24s ' - 'BENCH_DEFINE(%s)' % ( - define, define+'_i')) - f.writeln('#define ' - '__BENCH__%s__NEEDS_UNDEF' % ( - define)) - f.writeln('#endif') - f.writeln() + # any ifdef prologues + if suite.ifdef or suite.ifndef: + for ifdef in suite.ifdef: + f.writeln('#ifdef %s' % ifdef) + for ifndef in suite.ifndef: + f.writeln('#ifndef %s' % ifndef) + f.writeln() + # any suite code + if suite.isin(args['source']): + if suite.code_lineno is not None: + f.writeln('#line %d "%s"' % ( + suite.code_lineno, suite.path)) + f.write(suite.code) + if suite.code_lineno is not None: + f.writeln('#line %d "%s"' % ( + f.lineno+1, args['output'])) + f.writeln() + + # any case functions + for case in suite.cases: + if case.isin(args['source']): write_case_functions(f, suite, case) - if suite.defines: - for define in sorted(suite.defines): - f.writeln('#ifdef __BENCH__%s__NEEDS_UNDEF' - % define) - f.writeln('#undef __BENCH__%s__NEEDS_UNDEF' - % define) - f.writeln('#undef %s' % define) - f.writeln('#undef %s' % (define+'_i')) - f.writeln('#endif') - f.writeln() - -def find_runner(runner, **args): + # any ifdef epilogues + if suite.ifdef or suite.ifndef: + for ifdef in suite.ifdef: + f.writeln('#endif') + for ifndef in suite.ifndef: + f.writeln('#endif') + f.writeln() + + # declare our bench suites + # + # by declaring these as weak we can write these to every + # source file without issue, eventually one of these copies + # will be linked + for suite in suites: + f.writeln('extern const struct bench_suite ' + '__bench__%s__suite;' % ( + suite.name)) + f.writeln() + + f.writeln('__attribute__((weak))') + f.writeln('const struct bench_suite *const bench_suites[] = {') + for suite in suites: + f.writeln(4*' '+'&__bench__%s__suite,' % suite.name) + if len(suites) == 0: + f.writeln(4*' '+'0,') + f.writeln('};') + f.writeln('__attribute__((weak))') + f.writeln('const size_t bench_suite_count = %d;' % len(suites)) + f.writeln() + + +def find_runner(runner, id=None, main=True, **args): cmd = runner.copy() # run under some external command? @@ -503,120 +695,113 @@ def find_runner(runner, **args): # run under valgrind? if args.get('valgrind'): cmd[:0] = args['valgrind_path'] + [ - '--leak-check=full', - '--track-origins=yes', - '--error-exitcode=4', - '-q'] + '--leak-check=full', + '--track-origins=yes', + '--error-exitcode=4', + '-q'] # run under perf? if args.get('perf'): cmd[:0] = args['perf_script'] + list(filter(None, [ - '-R', - '--perf-freq=%s' % args['perf_freq'] - if args.get('perf_freq') else None, - '--perf-period=%s' % args['perf_period'] - if args.get('perf_period') else None, - '--perf-events=%s' % args['perf_events'] - if args.get('perf_events') else None, - '--perf-path=%s' % args['perf_path'] - if args.get('perf_path') else None, - '-o%s' % args['perf']])) + '--record', + '--perf-freq=%s' % args['perf_freq'] + if args.get('perf_freq') else None, + '--perf-period=%s' % args['perf_period'] + if args.get('perf_period') else None, + '--perf-events=%s' % args['perf_events'] + if args.get('perf_events') else None, + '--perf-path=%s' % args['perf_path'] + if args.get('perf_path') else None, + '-o%s' % args['perf']])) # other context - if args.get('geometry'): - cmd.append('-G%s' % args['geometry']) - if args.get('disk'): - cmd.append('-d%s' % args['disk']) - if args.get('trace'): - cmd.append('-t%s' % args['trace']) - if args.get('trace_backtrace'): - cmd.append('--trace-backtrace') - if args.get('trace_period'): - cmd.append('--trace-period=%s' % args['trace_period']) - if args.get('trace_freq'): - cmd.append('--trace-freq=%s' % args['trace_freq']) - if args.get('read_sleep'): - cmd.append('--read-sleep=%s' % args['read_sleep']) - if args.get('prog_sleep'): - cmd.append('--prog-sleep=%s' % args['prog_sleep']) - if args.get('erase_sleep'): - cmd.append('--erase-sleep=%s' % args['erase_sleep']) + if args.get('define_depth'): + cmd.append('--define-depth=%s' % args['define_depth']) + if args.get('force'): + cmd.append('--force') + + # only one thread should write to disk/trace, otherwise the output + # ends up clobbered and useless + if main: + if args.get('disk'): + cmd.append('-d%s' % args['disk']) + if args.get('trace'): + cmd.append('-t%s' % args['trace']) + if args.get('trace_backtrace'): + cmd.append('--trace-backtrace') + if args.get('trace_period'): + cmd.append('--trace-period=%s' % args['trace_period']) + if args.get('trace_freq'): + cmd.append('--trace-freq=%s' % args['trace_freq']) + if args.get('read_sleep'): + cmd.append('--read-sleep=%s' % args['read_sleep']) + if args.get('prog_sleep'): + cmd.append('--prog-sleep=%s' % args['prog_sleep']) + if args.get('erase_sleep'): + cmd.append('--erase-sleep=%s' % args['erase_sleep']) # defines? - if args.get('define'): + if args.get('define') and id is None: for define in args.get('define'): cmd.append('-D%s' % define) - return cmd - -def list_(runner, bench_ids=[], **args): - cmd = find_runner(runner, **args) + bench_ids - if args.get('summary'): cmd.append('--summary') - if args.get('list_suites'): cmd.append('--list-suites') - if args.get('list_cases'): cmd.append('--list-cases') - if args.get('list_suite_paths'): cmd.append('--list-suite-paths') - if args.get('list_case_paths'): cmd.append('--list-case-paths') - if args.get('list_defines'): cmd.append('--list-defines') - if args.get('list_permutation_defines'): - cmd.append('--list-permutation-defines') - if args.get('list_implicit_defines'): - cmd.append('--list-implicit-defines') - if args.get('list_geometries'): cmd.append('--list-geometries') - - if args.get('verbose'): - print(' '.join(shlex.quote(c) for c in cmd)) - return sp.call(cmd) + # bench id? + # + # note we disable defines above when id is explicit, defines override id + # in the bench runner, which is not what we want when querying an explicit + # bench id + if id is not None: + cmd.append(id) + return cmd -def find_perms(runner_, ids=[], **args): +def find_perms(runner, bench_ids=[], **args): + runner_ = find_runner(runner, main=False, **args) case_suites = {} - expected_case_perms = co.defaultdict(lambda: 0) + expected_case_perms = co.OrderedDict() expected_perms = 0 total_perms = 0 # query cases from the runner - cmd = runner_ + ['--list-cases'] + ids + cmd = runner_ + ['--list-cases'] + bench_ids if args.get('verbose'): print(' '.join(shlex.quote(c) for c in cmd)) proc = sp.Popen(cmd, - stdout=sp.PIPE, - stderr=sp.PIPE if not args.get('verbose') else None, - universal_newlines=True, - errors='replace', - close_fds=False) + stdout=sp.PIPE, + universal_newlines=True, + errors='replace', + close_fds=False) pattern = re.compile( - '^(?P[^\s]+)' - '\s+(?P[^\s]+)' - '\s+(?P\d+)/(?P\d+)') + '^(?P[^\s]+)' + '\s+(?P[^\s]+)' + '\s+(?P\d+)/(?P\d+)') # skip the first line for line in it.islice(proc.stdout, 1, None): m = pattern.match(line) if m: filtered = int(m.group('filtered')) perms = int(m.group('perms')) - expected_case_perms[m.group('case')] += filtered + expected_case_perms[m.group('case')] = ( + expected_case_perms.get(m.group('case'), 0) + + filtered) expected_perms += filtered total_perms += perms proc.wait() if proc.returncode != 0: - if not args.get('verbose'): - for line in proc.stderr: - sys.stdout.write(line) sys.exit(-1) # get which suite each case belongs to via paths - cmd = runner_ + ['--list-case-paths'] + ids + cmd = runner_ + ['--list-case-paths'] + bench_ids if args.get('verbose'): print(' '.join(shlex.quote(c) for c in cmd)) proc = sp.Popen(cmd, - stdout=sp.PIPE, - stderr=sp.PIPE if not args.get('verbose') else None, - universal_newlines=True, - errors='replace', - close_fds=False) + stdout=sp.PIPE, + universal_newlines=True, + errors='replace', + close_fds=False) pattern = re.compile( - '^(?P[^\s]+)' - '\s+(?P[^:]+):(?P\d+)') + '^(?P[^\s]+)' + '\s+(?P[^:]+):(?P\d+)') # skip the first line for line in it.islice(proc.stdout, 1, None): m = pattern.match(line) @@ -629,38 +814,36 @@ def find_perms(runner_, ids=[], **args): case_suites[m.group('case')] = suite proc.wait() if proc.returncode != 0: - if not args.get('verbose'): - for line in proc.stderr: - sys.stdout.write(line) sys.exit(-1) # figure out expected suite perms - expected_suite_perms = co.defaultdict(lambda: 0) + expected_suite_perms = co.OrderedDict() for case, suite in case_suites.items(): - expected_suite_perms[suite] += expected_case_perms[case] - - return ( - case_suites, - expected_suite_perms, - expected_case_perms, - expected_perms, - total_perms) - -def find_path(runner_, id, **args): + expected_suite_perms[suite] = ( + expected_suite_perms.get(suite, 0) + + expected_case_perms.get(case, 0)) + + return (case_suites, + expected_suite_perms, + expected_case_perms, + expected_perms, + total_perms) + +def find_path(runner, id, **args): + runner_ = find_runner(runner, id, main=False, **args) path = None # query from runner cmd = runner_ + ['--list-case-paths', id] if args.get('verbose'): print(' '.join(shlex.quote(c) for c in cmd)) proc = sp.Popen(cmd, - stdout=sp.PIPE, - stderr=sp.PIPE if not args.get('verbose') else None, - universal_newlines=True, - errors='replace', - close_fds=False) + stdout=sp.PIPE, + universal_newlines=True, + errors='replace', + close_fds=False) pattern = re.compile( - '^(?P[^\s]+)' - '\s+(?P[^:]+):(?P\d+)') + '^(?P[^\s]+)' + '\s+(?P[^:]+):(?P\d+)') # skip the first line for line in it.islice(proc.stdout, 1, None): m = pattern.match(line) @@ -670,24 +853,21 @@ def find_path(runner_, id, **args): path = (path_, lineno) proc.wait() if proc.returncode != 0: - if not args.get('verbose'): - for line in proc.stderr: - sys.stdout.write(line) sys.exit(-1) return path -def find_defines(runner_, id, **args): +def find_defines(runner, id, **args): + runner_ = find_runner(runner, id, main=False, **args) # query permutation defines from runner cmd = runner_ + ['--list-permutation-defines', id] if args.get('verbose'): print(' '.join(shlex.quote(c) for c in cmd)) proc = sp.Popen(cmd, - stdout=sp.PIPE, - stderr=sp.PIPE if not args.get('verbose') else None, - universal_newlines=True, - errors='replace', - close_fds=False) + stdout=sp.PIPE, + universal_newlines=True, + errors='replace', + close_fds=False) defines = co.OrderedDict() pattern = re.compile('^(?P\w+)=(?P.+)') for line in proc.stdout: @@ -698,13 +878,101 @@ def find_defines(runner_, id, **args): defines[define] = value proc.wait() if proc.returncode != 0: - if not args.get('verbose'): - for line in proc.stderr: - sys.stdout.write(line) sys.exit(-1) return defines +def find_ids(runner, bench_ids=[], **args): + # no ids => all ids, we don't need an extra lookup if no special + # behavior is requested + if not (args.get('by_cases') + or args.get('by_suites') + or bench_ids): + return [] + + # lookup suites/cases + (suite_cases, + expected_suite_perms, + expected_case_perms, + _, + _) = find_perms(runner, **args) + + # no ids => all ids, before we evaluate globs + if not bench_ids and args.get('by_cases'): + return [case_ for case_ in expected_case_perms.keys()] + if not bench_ids and args.get('by_suites'): + return [suite for suite in expected_suite_perms.keys()] + + # find suite/case by id + bench_ids_ = [] + for id in bench_ids: + # strip permutation + name, *_ = id.split(':', 1) + bench_ids__ = [] + # resolve globs + if '*' in name: + bench_ids__.extend(suite + for suite in expected_suite_perms.keys() + if fnmatch.fnmatchcase(suite, name)) + if not bench_ids__: + bench_ids__.extend(case_ + for case_ in expected_case_perms.keys() + if fnmatch.fnmatchcase(case_, name)) + # literal suite + elif name in expected_suite_perms: + bench_ids__.append(id) + # literal case + elif name in expected_case_perms: + bench_ids__.append(id) + + # no suite/case found? error + if not bench_ids__: + print('%serror:%s no benches match id %r?' % ( + '\x1b[1;31m' if args['color'] else '', + '\x1b[m' if args['color'] else '', + id), + file=sys.stderr) + sys.exit(-1) + + bench_ids_.extend(bench_ids__) + bench_ids = bench_ids_ + + # expand suites to cases? + if args.get('by_cases'): + bench_ids_ = [] + for id in bench_ids: + if id in expected_suite_perms: + for case_, suite in suite_cases.items(): + if suite == id: + bench_ids_.append(case_) + else: + bench_ids_.append(id) + bench_ids = bench_ids_ + + # no bench ids found? return a garbage id for consistency + return bench_ids if bench_ids else ['?'] + + +def list_(runner, bench_ids=[], **args): + cmd = find_runner(runner, main=False, **args) + cmd.extend(find_ids(runner, bench_ids, **args)) + + if args.get('summary'): cmd.append('--summary') + if args.get('list_suites'): cmd.append('--list-suites') + if args.get('list_cases'): cmd.append('--list-cases') + if args.get('list_suite_paths'): cmd.append('--list-suite-paths') + if args.get('list_case_paths'): cmd.append('--list-case-paths') + if args.get('list_defines'): cmd.append('--list-defines') + if args.get('list_permutation_defines'): + cmd.append('--list-permutation-defines') + if args.get('list_implicit_defines'): + cmd.append('--list-implicit-defines') + + if args.get('verbose'): + print(' '.join(shlex.quote(c) for c in cmd)) + return sp.call(cmd) + + # Thread-safe CSV writer class BenchOutput: @@ -749,17 +1017,19 @@ def __init__(self, id, returncode, stdout, assert_=None): self.stdout = stdout self.assert_ = assert_ -def run_stage(name, runner_, ids, stdout_, trace_, output_, **args): + +def run_stage(name, runner, bench_ids, stdout_, trace_, output_, **args): # get expected suite/case/perm counts (case_suites, - expected_suite_perms, - expected_case_perms, - expected_perms, - total_perms) = find_perms(runner_, ids, **args) + expected_suite_perms, + expected_case_perms, + expected_perms, + total_perms) = find_perms(runner, bench_ids, **args) passed_suite_perms = co.defaultdict(lambda: 0) passed_case_perms = co.defaultdict(lambda: 0) passed_perms = 0 + failed_perms = 0 readed = 0 proged = 0 erased = 0 @@ -767,18 +1037,21 @@ def run_stage(name, runner_, ids, stdout_, trace_, output_, **args): killed = False pattern = re.compile('^(?:' - '(?Prunning|finished|skipped|powerloss)' - ' (?P(?P[^:]+)[^\s]*)' - '(?: (?P\d+))?' - '(?: (?P\d+))?' - '(?: (?P\d+))?' - '|' '(?P[^:]+):(?P\d+):(?Passert):' - ' *(?P.*)' - ')$') + '(?Prunning|finished|skipped)' + ' (?P(?P[^:]+)[^\s]*)' + '|' '(?P[^:]+):(?P\d+):(?Passert):' + ' *(?P.*)' + '|' '(?Pbenched)' + ' (?P[^\s]+)' + ' (?P\d+)' + '(?: (?P[\d\.]+))?' + '(?: (?P[\d\.]+))?' + '(?: (?P[\d\.]+))?' + ')$') locals = th.local() children = set() - def run_runner(runner_, ids=[]): + def run_runner(runner_): nonlocal passed_suite_perms nonlocal passed_case_perms nonlocal passed_perms @@ -788,7 +1061,7 @@ def run_runner(runner_, ids=[]): nonlocal locals # run the benches! - cmd = runner_ + ids + cmd = runner_ if args.get('verbose'): print(' '.join(shlex.quote(c) for c in cmd)) @@ -799,8 +1072,14 @@ def run_runner(runner_, ids=[]): mpty = os.fdopen(mpty, 'r', 1) last_id = None + last_case = None + last_suite = None + last_defines = None # fetched on demand last_stdout = co.deque(maxlen=args.get('context', 5) + 1) last_assert = None + creaded = co.defaultdict(lambda: 0) + cproged = co.defaultdict(lambda: 0) + cerased = co.defaultdict(lambda: 0) try: while True: # parse a line for state changes @@ -822,47 +1101,84 @@ def run_runner(runner_, ids=[]): m = pattern.match(line) if m: - op = m.group('op') or m.group('op_') + op = m.group('op') or m.group('op_') or m.group('op__') if op == 'running': locals.seen_perms += 1 last_id = m.group('id') + last_case = m.group('case') + last_suite = case_suites[last_case] + last_defines = None last_stdout.clear() last_assert = None + creaded.clear() + cproged.clear() + cerased.clear() elif op == 'finished': + # force a failure + if args.get('fail'): + proc.kill() + raise BenchFailure(last_id, 0, list(last_stdout)) + # passed case = m.group('case') suite = case_suites[case] - readed_ = int(m.group('readed')) - proged_ = int(m.group('proged')) - erased_ = int(m.group('erased')) passed_suite_perms[suite] += 1 passed_case_perms[case] += 1 passed_perms += 1 - readed += readed_ - proged += proged_ - erased += erased_ - if output_: - # get defines and write to csv - defines = find_defines( - runner_, m.group('id'), **args) - output_.writerow({ - 'suite': suite, - 'case': case, - 'bench_readed': readed_, - 'bench_proged': proged_, - 'bench_erased': erased_, - **defines}) elif op == 'skipped': locals.seen_perms += 1 elif op == 'assert': last_assert = ( - m.group('path'), - int(m.group('lineno')), - m.group('message')) + m.group('path'), + int(m.group('lineno')), + m.group('message')) # go ahead and kill the process, aborting takes a while if args.get('keep_going'): proc.kill() + elif op == 'benched': + m_ = m.group('m') + n_ = int(m.group('n')) + # parse measurements + def dat(v): + if v is None: + return 0 + elif '.' in v: + return float(v) + else: + return int(v) + readed_ = dat(m.group('readed')) + proged_ = dat(m.group('proged')) + erased_ = dat(m.group('erased')) + # keep track of cumulative measurements + creaded[m_] += readed_ + cproged[m_] += proged_ + cerased[m_] += erased_ + if output_: + # fetch defines if needed, only do this at most + # once per perm + if last_defines is None: + last_defines = find_defines( + runner, last_id, **args) + # write measurements immediately, this allows + # analysis of partial results + output_.writerow({ + 'suite': last_suite, + 'case': last_case, + **last_defines, + 'm': m_, + 'n': n_, + 'bench_readed': readed_, + 'bench_proged': proged_, + 'bench_erased': erased_, + 'bench_creaded': creaded[m_], + 'bench_cproged': cproged[m_], + 'bench_cerased': cerased[m_]}) + # keep track of total for summary + readed += readed_ + proged += proged_ + erased += erased_ except KeyboardInterrupt: - raise BenchFailure(last_id, 1, list(last_stdout)) + proc.kill() + raise BenchFailure(last_id, 0, list(last_stdout)) finally: children.remove(proc) mpty.close() @@ -870,12 +1186,13 @@ def run_runner(runner_, ids=[]): proc.wait() if proc.returncode != 0: raise BenchFailure( - last_id, - proc.returncode, - list(last_stdout), - last_assert) + last_id, + proc.returncode, + list(last_stdout), + last_assert) - def run_job(runner_, ids=[], start=None, step=None): + def run_job(main=True, start=None, step=None): + nonlocal failed_perms nonlocal failures nonlocal killed nonlocal locals @@ -883,36 +1200,32 @@ def run_job(runner_, ids=[], start=None, step=None): start = start or 0 step = step or 1 while start < total_perms: - job_runner = runner_.copy() + runner_ = find_runner(runner, main=main, **args) if args.get('isolate') or args.get('valgrind'): - job_runner.append('-s%s,%s,%s' % (start, start+step, step)) - else: - job_runner.append('-s%s,,%s' % (start, step)) + runner_.append('-s%s,%s,%s' % (start, start+step, step)) + elif start != 0 or step != 1: + runner_.append('-s%s,,%s' % (start, step)) + + runner_.extend(bench_ids) try: # run the benches locals.seen_perms = 0 - run_runner(job_runner, ids) + run_runner(runner_) assert locals.seen_perms > 0 start += locals.seen_perms*step except BenchFailure as failure: - # keep track of failures - if output_: - case, _ = failure.id.split(':', 1) - suite = case_suites[case] - # get defines and write to csv - defines = find_defines(runner_, failure.id, **args) - output_.writerow({ - 'suite': suite, - 'case': case, - **defines}) - # race condition for multiple failures? - if failures and not args.get('keep_going'): - break + if not failures or args.get('keep_going'): + # keep track of how many failed + failed_perms += 1 - failures.append(failure) + # do not store more failures than we need to, otherwise + # we quickly explode RAM when a common bug fails a bunch + # of cases + if len(failures) < args.get('failures', 3): + failures.append(failure) if args.get('keep_going') and not killed: # resume after failed bench @@ -932,42 +1245,45 @@ def run_job(runner_, ids=[], start=None, step=None): if 'jobs' in args: for job in range(args['jobs']): runners.append(th.Thread( - target=run_job, args=(runner_, ids, job, args['jobs']), - daemon=True)) + target=run_job, args=(job == 0, job, args['jobs']), + daemon=True)) else: runners.append(th.Thread( - target=run_job, args=(runner_, ids, None, None), - daemon=True)) + target=run_job, args=(True, None, None), + daemon=True)) def print_update(done): - if not args.get('verbose') and (args['color'] or done): + if (not args.get('quiet') + and not args.get('verbose') + and not args.get('stdout') == '-' + and (args['color'] or done)): sys.stdout.write('%s%srunning %s%s:%s %s%s' % ( - '\r\x1b[K' if args['color'] else '', - '\x1b[?7l' if not done else '', - ('\x1b[34m' if not failures else '\x1b[31m') - if args['color'] else '', - name, - '\x1b[m' if args['color'] else '', - ', '.join(filter(None, [ - '%d/%d suites' % ( - sum(passed_suite_perms[k] == v - for k, v in expected_suite_perms.items()), - len(expected_suite_perms)) - if (not args.get('by_suites') - and not args.get('by_cases')) else None, - '%d/%d cases' % ( - sum(passed_case_perms[k] == v - for k, v in expected_case_perms.items()), - len(expected_case_perms)) - if not args.get('by_cases') else None, - '%d/%d perms' % (passed_perms, expected_perms), - '%s%d/%d failures%s' % ( - '\x1b[31m' if args['color'] else '', - len(failures), - expected_perms, - '\x1b[m' if args['color'] else '') - if failures else None])), - '\x1b[?7h' if not done else '\n')) + '\r\x1b[K' if args['color'] else '', + '\x1b[?7l' if not done else '', + ('\x1b[34m' if not failed_perms else '\x1b[31m') + if args['color'] else '', + name, + '\x1b[m' if args['color'] else '', + ', '.join(filter(None, [ + '%d/%d suites' % ( + sum(passed_suite_perms[k] == v + for k, v in expected_suite_perms.items()), + len(expected_suite_perms)) + if (not args.get('by_suites') + and not args.get('by_cases')) else None, + '%d/%d cases' % ( + sum(passed_case_perms[k] == v + for k, v in expected_case_perms.items()), + len(expected_case_perms)) + if not args.get('by_cases') else None, + '%d/%d perms' % (passed_perms, expected_perms), + '%s%d/%d failures%s' % ( + '\x1b[31m' if args['color'] else '', + failed_perms, + expected_perms, + '\x1b[m' if args['color'] else '') + if failed_perms else None])), + '\x1b[?7h' if not done else '\n')) sys.stdout.flush() for r in runners: @@ -987,31 +1303,36 @@ def print_update(done): for r in runners: r.join() - return ( - expected_perms, - passed_perms, - readed, - proged, - erased, - failures, - killed) + return (expected_perms, + passed_perms, + failed_perms, + readed, + proged, + erased, + failures, + killed) def run(runner, bench_ids=[], **args): # query runner for benches - runner_ = find_runner(runner, **args) - print('using runner: %s' % ' '.join(shlex.quote(c) for c in runner_)) + if not args.get('quiet'): + print('using runner: %s' % ' '.join( + shlex.quote(c) for c in find_runner(runner, **args))) + + # query ids, perms, etc + bench_ids = find_ids(runner, bench_ids, **args) (_, - expected_suite_perms, - expected_case_perms, - expected_perms, - total_perms) = find_perms(runner_, bench_ids, **args) - print('found %d suites, %d cases, %d/%d permutations' % ( - len(expected_suite_perms), - len(expected_case_perms), - expected_perms, - total_perms)) - print() + expected_suite_perms, + expected_case_perms, + expected_perms, + total_perms) = find_perms(runner, bench_ids, **args) + if not args.get('quiet'): + print('found %d suites, %d cases, %d/%d permutations' % ( + len(expected_suite_perms), + len(expected_case_perms), + expected_perms, + total_perms)) + print() # automatic job detection? if args.get('jobs') == 0: @@ -1027,8 +1348,15 @@ def run(runner, bench_ids=[], **args): output = None if args.get('output'): output = BenchOutput(args['output'], - ['suite', 'case'], - ['bench_readed', 'bench_proged', 'bench_erased']) + ['suite', 'case'], + # defines go here + ['m', 'n', + 'bench_readed', + 'bench_proged', + 'bench_erased', + 'bench_creaded', + 'bench_cproged', + 'bench_cerased']) # measure runtime start = time.time() @@ -1036,37 +1364,41 @@ def run(runner, bench_ids=[], **args): # spawn runners expected = 0 passed = 0 + failed = 0 readed = 0 proged = 0 erased = 0 failures = [] - for by in (bench_ids if bench_ids - else expected_case_perms.keys() if args.get('by_cases') - else expected_suite_perms.keys() if args.get('by_suites') - else [None]): + for by in (bench_ids if bench_ids else [None]): # spawn jobs for stage (expected_, - passed_, - readed_, - proged_, - erased_, - failures_, - killed) = run_stage( - by or 'benches', - runner_, - [by] if by is not None else [], - stdout, - trace, - output, - **args) + passed_, + failed_, + readed_, + proged_, + erased_, + failures_, + killed) = run_stage( + by or 'benches', + runner, + [by] if by is not None else [], + stdout, + trace, + output, + **args) # collect passes/failures expected += expected_ passed += passed_ + failed += failed_ readed += readed_ proged += proged_ erased += erased_ - failures.extend(failures_) - if (failures and not args.get('keep_going')) or killed: + # do not store more failures than we need to, otherwise we + # quickly explode RAM when a common bug fails a bunch of cases + failures.extend(failures_[:max( + args.get('failures', 3) - len(failures), + 0)]) + if (failed and not args.get('keep_going')) or killed: break stop = time.time() @@ -1085,53 +1417,55 @@ def run(runner, bench_ids=[], **args): output.close() # show summary - print() - print('%sdone:%s %s' % ( - ('\x1b[34m' if not failures else '\x1b[31m') - if args['color'] else '', - '\x1b[m' if args['color'] else '', - ', '.join(filter(None, [ - '%d readed' % readed, - '%d proged' % proged, - '%d erased' % erased, - 'in %.2fs' % (stop-start)])))) - print() + if not args.get('quiet'): + print() + print('%sdone:%s %s' % ( + ('\x1b[34m' if not failed else '\x1b[31m') + if args['color'] else '', + '\x1b[m' if args['color'] else '', + ', '.join(filter(None, [ + '%d readed' % readed, + '%d proged' % proged, + '%d erased' % erased, + 'in %.2fs' % (stop-start)])))) + print() # print each failure - for failure in failures: + for failure in failures[:args.get('failures', 3)]: assert failure.id is not None, '%s broken? %r' % ( - ' '.join(shlex.quote(c) for c in runner_), - failure) + ' '.join(shlex.quote(c) for c in find_runner(runner, **args)), + failure) # get some extra info from runner - path, lineno = find_path(runner_, failure.id, **args) - defines = find_defines(runner_, failure.id, **args) + path, lineno = find_path(runner, failure.id, **args) + defines = find_defines(runner, failure.id, **args) # show summary of failure print('%s%s:%d:%sfailure:%s %s%s failed' % ( - '\x1b[01m' if args['color'] else '', - path, lineno, - '\x1b[01;31m' if args['color'] else '', - '\x1b[m' if args['color'] else '', - failure.id, - ' (%s)' % ', '.join('%s=%s' % (k,v) for k,v in defines.items()) - if defines else '')) + '\x1b[01m' if args['color'] else '', + path, lineno, + '\x1b[1;31m' if args['color'] else '', + '\x1b[m' if args['color'] else '', + failure.id, + ' (%s)' % ', '.join('%s=%s' % (k,v) + for k,v in defines.items()) + if defines else '')) if failure.stdout: stdout = failure.stdout if failure.assert_ is not None: stdout = stdout[:-1] - for line in stdout[-args.get('context', 5):]: + for line in stdout[max(len(stdout)-args.get('context', 5), 0):]: sys.stdout.write(line) if failure.assert_ is not None: path, lineno, message = failure.assert_ print('%s%s:%d:%sassert:%s %s' % ( - '\x1b[01m' if args['color'] else '', - path, lineno, - '\x1b[01;31m' if args['color'] else '', - '\x1b[m' if args['color'] else '', - message)) + '\x1b[01m' if args['color'] else '', + path, lineno, + '\x1b[1;31m' if args['color'] else '', + '\x1b[m' if args['color'] else '', + message)) with open(path) as f: line = next(it.islice(f, lineno-1, None)).strip('\n') print(line) @@ -1139,43 +1473,48 @@ def run(runner, bench_ids=[], **args): # drop into gdb? if failures and (args.get('gdb') - or args.get('gdb_case') + or args.get('gdb_perm') or args.get('gdb_main')): failure = failures[0] - cmd = runner_ + [failure.id] + cmd = find_runner(runner, failure.id, **args) + gdb_path = args['gdb_path'] + gdb_scripts = (args.get('gdb_script') or GDB_SCRIPTS) if args.get('gdb_main'): # we don't really need the case breakpoint here, but it # can be helpful - path, lineno = find_path(runner_, failure.id, **args) - cmd[:0] = args['gdb_path'] + [ - '-ex', 'break main', - '-ex', 'break %s:%d' % (path, lineno), - '-ex', 'run', - '--args'] - elif args.get('gdb_case'): - path, lineno = find_path(runner_, failure.id, **args) - cmd[:0] = args['gdb_path'] + [ - '-ex', 'break %s:%d' % (path, lineno), - '-ex', 'run', - '--args'] - elif failure.assert_ is not None: - cmd[:0] = args['gdb_path'] + [ - '-ex', 'run', - '-ex', 'frame function raise', - '-ex', 'up 2', - '--args'] + path, lineno = find_path(runner, failure.id, **args) + cmd[:0] = [ + *gdb_path, + *it.chain.from_iterable(['-x', s] for s in gdb_scripts), + '-q', + '-ex', 'break main', + '-ex', 'break %s:%d' % (path, lineno), + '-ex', 'run', + '--args'] + elif args.get('gdb_perm'): + path, lineno = find_path(runner, failure.id, **args) + cmd[:0] = [ + *gdb_path, + *it.chain.from_iterable(['-x', s] for s in gdb_scripts), + '-q', + '-ex', 'break %s:%d' % (path, lineno), + '-ex', 'run', + '--args'] else: - cmd[:0] = args['gdb_path'] + [ - '-ex', 'run', - '--args'] + cmd[:0] = [ + *gdb_path, + *it.chain.from_iterable(['-x', s] for s in gdb_scripts), + '-q', + '-ex', 'run', + '--args'] # exec gdb interactively if args.get('verbose'): print(' '.join(shlex.quote(c) for c in cmd)) os.execvp(cmd[0], cmd) - return 1 if failures else 0 + return 1 if failed else 0 def main(**args): @@ -1196,8 +1535,7 @@ def main(**args): or args.get('list_case_paths') or args.get('list_defines') or args.get('list_permutation_defines') - or args.get('list_implicit_defines') - or args.get('list_geometries')): + or args.get('list_implicit_defines')): return list_(**args) else: return run(**args) @@ -1209,222 +1547,244 @@ def main(**args): argparse.ArgumentParser._handle_conflict_ignore = lambda *_: None argparse._ArgumentGroup._handle_conflict_ignore = lambda *_: None parser = argparse.ArgumentParser( - description="Build and run benches.", - allow_abbrev=False, - conflict_handler='ignore') + description="Build and run benches.", + allow_abbrev=False, + conflict_handler='ignore') + parser.add_argument( + '-v', '--verbose', + action='store_true', + help="Output commands that run behind the scenes.") parser.add_argument( - '-v', '--verbose', - action='store_true', - help="Output commands that run behind the scenes.") + '-q', '--quiet', + action='store_true', + help="Show nothing except for bench failures.") parser.add_argument( - '--color', - choices=['never', 'always', 'auto'], - default='auto', - help="When to use terminal colors. Defaults to 'auto'.") + '--color', + choices=['never', 'always', 'auto'], + default='auto', + help="When to use terminal colors. Defaults to 'auto'.") # bench flags bench_parser = parser.add_argument_group('bench options') bench_parser.add_argument( - 'runner', - nargs='?', - type=lambda x: x.split(), - help="Bench runner to use for benching. Defaults to %r." % RUNNER_PATH) + 'bench_ids', + nargs='*', + help="Description of benches to run.") bench_parser.add_argument( - 'bench_ids', - nargs='*', - help="Description of benches to run.") + '-R', '--runner', + type=lambda x: x.split(), + default=RUNNER_PATH, + help="Bench runner to use for benching. Defaults to " + "%r." % RUNNER_PATH) bench_parser.add_argument( - '-Y', '--summary', - action='store_true', - help="Show quick summary.") + '-Y', '--summary', + action='store_true', + help="Show quick summary.") bench_parser.add_argument( - '-l', '--list-suites', - action='store_true', - help="List bench suites.") + '-l', '--list-suites', + action='store_true', + help="List bench suites.") bench_parser.add_argument( - '-L', '--list-cases', - action='store_true', - help="List bench cases.") + '-L', '--list-cases', + action='store_true', + help="List bench cases.") bench_parser.add_argument( - '--list-suite-paths', - action='store_true', - help="List the path for each bench suite.") + '--list-suite-paths', + action='store_true', + help="List the path for each bench suite.") bench_parser.add_argument( - '--list-case-paths', - action='store_true', - help="List the path and line number for each bench case.") + '--list-case-paths', + action='store_true', + help="List the path and line number for each bench case.") bench_parser.add_argument( - '--list-defines', - action='store_true', - help="List all defines in this bench-runner.") + '--list-defines', + action='store_true', + help="List all defines in this bench-runner.") bench_parser.add_argument( - '--list-permutation-defines', - action='store_true', - help="List explicit defines in this bench-runner.") + '--list-permutation-defines', + action='store_true', + help="List explicit defines in this bench-runner.") bench_parser.add_argument( - '--list-implicit-defines', - action='store_true', - help="List implicit defines in this bench-runner.") + '--list-implicit-defines', + action='store_true', + help="List implicit defines in this bench-runner.") bench_parser.add_argument( - '--list-geometries', - action='store_true', - help="List the available disk geometries.") + '-D', '--define', + action='append', + help="Override a bench define.") bench_parser.add_argument( - '-D', '--define', - action='append', - help="Override a bench define.") + '--define-depth', + help="How deep to evaluate recursive defines before erroring.") bench_parser.add_argument( - '-G', '--geometry', - help="Comma-separated list of disk geometries to bench.") + '--force', + action='store_true', + help="Ignore bench filters.") bench_parser.add_argument( - '-d', '--disk', - help="Direct block device operations to this file.") + '-d', '--disk', + help="Direct block device operations to this file.") bench_parser.add_argument( - '-t', '--trace', - help="Direct trace output to this file.") + '-t', '--trace', + help="Direct trace output to this file.") bench_parser.add_argument( - '--trace-backtrace', - action='store_true', - help="Include a backtrace with every trace statement.") + '--trace-backtrace', + action='store_true', + help="Include a backtrace with every trace statement.") bench_parser.add_argument( - '--trace-period', - help="Sample trace output at this period in cycles.") + '--trace-period', + help="Sample trace output at this period in cycles.") bench_parser.add_argument( - '--trace-freq', - help="Sample trace output at this frequency in hz.") + '--trace-freq', + help="Sample trace output at this frequency in hz.") bench_parser.add_argument( - '-O', '--stdout', - help="Direct stdout to this file. Note stderr is already merged here.") + '-O', '--stdout', + help="Direct stdout to this file. Note stderr is already merged " + "here.") bench_parser.add_argument( - '-o', '--output', - help="CSV file to store results.") + '-o', '--output', + help="CSV file to store results.") bench_parser.add_argument( - '--read-sleep', - help="Artificial read delay in seconds.") + '--read-sleep', + help="Artificial read delay in seconds.") bench_parser.add_argument( - '--prog-sleep', - help="Artificial prog delay in seconds.") + '--prog-sleep', + help="Artificial prog delay in seconds.") bench_parser.add_argument( - '--erase-sleep', - help="Artificial erase delay in seconds.") + '--erase-sleep', + help="Artificial erase delay in seconds.") bench_parser.add_argument( - '-j', '--jobs', - nargs='?', - type=lambda x: int(x, 0), - const=0, - help="Number of parallel runners to run. 0 runs one runner per core.") + '-j', '--jobs', + nargs='?', + type=lambda x: int(x, 0), + const=0, + help="Number of parallel runners to run. 0 runs one runner per " + "core.") bench_parser.add_argument( - '-k', '--keep-going', - action='store_true', - help="Don't stop on first error.") + '-k', '--keep-going', + action='store_true', + help="Don't stop on first failure.") bench_parser.add_argument( - '-i', '--isolate', - action='store_true', - help="Run each bench permutation in a separate process.") + '-f', '--fail', + action='store_true', + help="Force a failure.") bench_parser.add_argument( - '-b', '--by-suites', - action='store_true', - help="Step through benches by suite.") + '-i', '--isolate', + action='store_true', + help="Run each bench permutation in a separate process.") bench_parser.add_argument( - '-B', '--by-cases', - action='store_true', - help="Step through benches by case.") + '-b', '--by-suites', + action='store_true', + help="Step through benches by suite.") bench_parser.add_argument( - '--context', - type=lambda x: int(x, 0), - default=5, - help="Show this many lines of stdout on bench failure. " - "Defaults to 5.") + '-B', '--by-cases', + action='store_true', + help="Step through benches by case.") bench_parser.add_argument( - '--gdb', - action='store_true', - help="Drop into gdb on bench failure.") + '-F', '--failures', + type=lambda x: int(x, 0), + default=3, + help="Show this many bench failures. Defaults to 3.") bench_parser.add_argument( - '--gdb-case', - action='store_true', - help="Drop into gdb on bench failure but stop at the beginning " - "of the failing bench case.") + '-C', '--context', + type=lambda x: int(x, 0), + default=5, + help="Show this many lines of stdout on bench failure. " + "Defaults to 5.") bench_parser.add_argument( - '--gdb-main', - action='store_true', - help="Drop into gdb on bench failure but stop at the beginning " - "of main.") + '--gdb', + action='store_true', + help="Drop into gdb on bench failure.") bench_parser.add_argument( - '--gdb-path', - type=lambda x: x.split(), - default=GDB_PATH, - help="Path to the gdb executable, may include flags. " - "Defaults to %r." % GDB_PATH) + '--gdb-perm', '--gdb-permutation', + action='store_true', + help="Drop into gdb on bench failure but stop at the beginning " + "of the failing bench case.") bench_parser.add_argument( - '--exec', - type=lambda e: e.split(), - help="Run under another executable.") + '--gdb-main', + action='store_true', + help="Drop into gdb on bench failure but stop at the beginning " + "of main.") bench_parser.add_argument( - '--valgrind', - action='store_true', - help="Run under Valgrind to find memory errors. Implicitly sets " - "--isolate.") + '--gdb-path', + type=lambda x: x.split(), + default=GDB_PATH, + help="Path to the gdb executable, may include flags. " + "Defaults to %r." % GDB_PATH) bench_parser.add_argument( - '--valgrind-path', - type=lambda x: x.split(), - default=VALGRIND_PATH, - help="Path to the Valgrind executable, may include flags. " - "Defaults to %r." % VALGRIND_PATH) + '--gdb-script', + action='append', + help="Paths to scripts to execute when dropping into gdb. " + "Defaults to %r." % GDB_SCRIPTS) bench_parser.add_argument( - '-p', '--perf', - help="Run under Linux's perf to sample performance counters, writing " - "samples to this file.") + '--exec', + type=lambda e: e.split(), + help="Run under another executable.") bench_parser.add_argument( - '--perf-freq', - help="perf sampling frequency. This is passed directly to the perf " - "script.") + '--valgrind', + action='store_true', + help="Run under Valgrind to find memory errors. Implicitly sets " + "--isolate.") bench_parser.add_argument( - '--perf-period', - help="perf sampling period. This is passed directly to the perf " - "script.") + '--valgrind-path', + type=lambda x: x.split(), + default=VALGRIND_PATH, + help="Path to the Valgrind executable, may include flags. " + "Defaults to %r." % VALGRIND_PATH) bench_parser.add_argument( - '--perf-events', - help="perf events to record. This is passed directly to the perf " - "script.") + '-p', '--perf', + help="Run under Linux's perf to sample performance counters, " + "writing samples to this file.") bench_parser.add_argument( - '--perf-script', - type=lambda x: x.split(), - default=PERF_SCRIPT, - help="Path to the perf script to use. Defaults to %r." % PERF_SCRIPT) + '--perf-freq', + help="perf sampling frequency. This is passed directly to the " + "perf script.") bench_parser.add_argument( - '--perf-path', - type=lambda x: x.split(), - help="Path to the perf executable, may include flags. This is passed " - "directly to the perf script") + '--perf-period', + help="perf sampling period. This is passed directly to the perf " + "script.") + bench_parser.add_argument( + '--perf-events', + help="perf events to record. This is passed directly to the perf " + "script.") + bench_parser.add_argument( + '--perf-script', + type=lambda x: x.split(), + default=PERF_SCRIPT, + help="Path to the perf script to use. Defaults to " + "%r." % PERF_SCRIPT) + bench_parser.add_argument( + '--perf-path', + type=lambda x: x.split(), + help="Path to the perf executable, may include flags. This is " + "passed directly to the perf script") # compilation flags comp_parser = parser.add_argument_group('compilation options') comp_parser.add_argument( - 'bench_paths', - nargs='*', - help="Description of *.toml files to compile. May be a directory " - "or a list of paths.") + 'bench_paths', + nargs='*', + help="Set of *.toml files to compile.") comp_parser.add_argument( - '-c', '--compile', - action='store_true', - help="Compile a bench suite or source file.") + '-c', '--compile', + action='store_true', + help="Compile a bench suite or source file.") comp_parser.add_argument( - '-s', '--source', - help="Source file to compile, possibly injecting internal benches.") + '-o', '--output', + help="Output file.") comp_parser.add_argument( - '--include', - default=HEADER_PATH, - help="Inject this header file into every compiled bench file. " - "Defaults to %r." % HEADER_PATH) + '-s', '--source', + help="Source file to compile, possibly injecting internal benches.") comp_parser.add_argument( - '-o', '--output', - help="Output file.") + '--include', + help="Inject these header files into every compiled bench file. " + "Defaults to %r." % HEADER_PATHS) + comp_parser.add_argument( + '--no-internal', + action='store_true', + help="Don't build internal tests.") - # runner/bench_paths overlap, so need to do some munging here + # do the thing args = parser.parse_intermixed_args() - args.bench_paths = [' '.join(args.runner or [])] + args.bench_ids - args.runner = args.runner or [RUNNER_PATH] - + args.bench_paths = args.bench_ids sys.exit(main(**{k: v - for k, v in vars(args).items() - if v is not None})) + for k, v in vars(args).items() + if v is not None})) diff --git a/scripts/changeprefix.py b/scripts/changeprefix.py index 51844c054..92a17c36f 100755 --- a/scripts/changeprefix.py +++ b/scripts/changeprefix.py @@ -11,6 +11,10 @@ # SPDX-License-Identifier: BSD-3-Clause # +# prevent local imports +if __name__ == "__main__": + __import__('sys').path.pop(0) + import glob import itertools import os @@ -19,15 +23,18 @@ import shlex import shutil import subprocess +import sys import tempfile + GIT_PATH = ['git'] +# open with '-' for stdin/stdout def openio(path, mode='r', buffering=-1): - # allow '-' for stdin/stdout + import os if path == '-': - if mode == 'r': + if 'r' in mode: return os.fdopen(os.dup(sys.stdin.fileno()), mode, buffering) else: return os.fdopen(os.dup(sys.stdout.fileno()), mode, buffering) @@ -36,17 +43,17 @@ def openio(path, mode='r', buffering=-1): def changeprefix(from_prefix, to_prefix, line): line, count1 = re.subn( - '\\b'+from_prefix, - to_prefix, - line) + '\\b'+from_prefix, + to_prefix, + line) line, count2 = re.subn( - '\\b'+from_prefix.upper(), - to_prefix.upper(), - line) + '\\b'+from_prefix.upper(), + to_prefix.upper(), + line) line, count3 = re.subn( - '\\B-D'+from_prefix.upper(), - '-D'+to_prefix.upper(), - line) + '\\B-D'+from_prefix.upper(), + '-D'+to_prefix.upper(), + line) return line, count1+count2+count3 def changefile(from_prefix, to_prefix, from_path, to_path, *, @@ -79,8 +86,9 @@ def changefile(from_prefix, to_prefix, from_path, to_path, *, # Summary print('%s: %d replacements' % ( - '%s -> %s' % (from_path, to_path) if not to_path_temp else from_path, - count)) + '%s -> %s' % (from_path, to_path) if not to_path_temp + else from_path, + count)) def main(from_prefix, to_prefix, paths=[], *, verbose=False, @@ -111,7 +119,7 @@ def main(from_prefix, to_prefix, paths=[], *, # rename contents changefile(from_prefix, to_prefix, from_path, to_path, - no_replacements=no_replacements) + no_replacements=no_replacements) # stage? if git and not no_stage: @@ -130,49 +138,49 @@ def main(from_prefix, to_prefix, paths=[], *, import argparse import sys parser = argparse.ArgumentParser( - description="Change prefixes in files/filenames. Useful for creating " - "different versions of a codebase that don't conflict at compile " - "time.", - allow_abbrev=False) + description="Change prefixes in files/filenames. Useful for " + "creating different versions of a codebase that don't " + "conflict at compile time.", + allow_abbrev=False) parser.add_argument( - 'from_prefix', - help="Prefix to replace.") + 'from_prefix', + help="Prefix to replace.") parser.add_argument( - 'to_prefix', - help="Prefix to replace with.") + 'to_prefix', + help="Prefix to replace with.") parser.add_argument( - 'paths', - nargs='*', - help="Files to operate on.") + 'paths', + nargs='*', + help="Files to operate on.") parser.add_argument( - '-v', '--verbose', - action='store_true', - help="Output commands that run behind the scenes.") + '-v', '--verbose', + action='store_true', + help="Output commands that run behind the scenes.") parser.add_argument( - '-o', '--output', - help="Output file.") + '-o', '--output', + help="Output file.") parser.add_argument( - '-N', '--no-replacements', - action='store_true', - help="Don't change prefixes in files") + '-N', '--no-replacements', + action='store_true', + help="Don't change prefixes in files") parser.add_argument( - '-R', '--no-renames', - action='store_true', - help="Don't rename files") + '-R', '--no-renames', + action='store_true', + help="Don't rename files") parser.add_argument( - '--git', - action='store_true', - help="Use git to find/update files.") + '--git', + action='store_true', + help="Use git to find/update files.") parser.add_argument( - '--no-stage', - action='store_true', - help="Don't stage changes with git.") + '--no-stage', + action='store_true', + help="Don't stage changes with git.") parser.add_argument( - '--git-path', - type=lambda x: x.split(), - default=GIT_PATH, - help="Path to git executable, may include flags. " - "Defaults to %r." % GIT_PATH) + '--git-path', + type=lambda x: x.split(), + default=GIT_PATH, + help="Path to git executable, may include flags. " + "Defaults to %r." % GIT_PATH) sys.exit(main(**{k: v - for k, v in vars(parser.parse_intermixed_args()).items() - if v is not None})) + for k, v in vars(parser.parse_intermixed_args()).items() + if v is not None})) diff --git a/scripts/code.py b/scripts/code.py index ba8bd1e0d..f9d3e520f 100755 --- a/scripts/code.py +++ b/scripts/code.py @@ -12,321 +12,549 @@ # SPDX-License-Identifier: BSD-3-Clause # +# prevent local imports +if __name__ == "__main__": + __import__('sys').path.pop(0) + import collections as co import csv -import difflib +import fnmatch +import functools as ft +import io import itertools as it -import math as m +import math as mt import os import re import shlex import subprocess as sp +import sys -NM_PATH = ['nm'] -NM_TYPES = 'tTrRdD' OBJDUMP_PATH = ['objdump'] +SECTIONS = ['.text', '.rodata', '.data'] # integer fields -class Int(co.namedtuple('Int', 'x')): +class CsvInt(co.namedtuple('CsvInt', 'a')): __slots__ = () - def __new__(cls, x=0): - if isinstance(x, Int): - return x - if isinstance(x, str): + def __new__(cls, a=0): + if isinstance(a, CsvInt): + return a + if isinstance(a, str): try: - x = int(x, 0) + a = int(a, 0) except ValueError: # also accept +-∞ and +-inf - if re.match('^\s*\+?\s*(?:∞|inf)\s*$', x): - x = m.inf - elif re.match('^\s*-\s*(?:∞|inf)\s*$', x): - x = -m.inf + if re.match('^\s*\+?\s*(?:∞|inf)\s*$', a): + a = mt.inf + elif re.match('^\s*-\s*(?:∞|inf)\s*$', a): + a = -mt.inf else: raise - assert isinstance(x, int) or m.isinf(x), x - return super().__new__(cls, x) + if not (isinstance(a, int) or mt.isinf(a)): + a = int(a) + return super().__new__(cls, a) + + def __repr__(self): + return '%s(%r)' % (self.__class__.__name__, self.a) def __str__(self): - if self.x == m.inf: + if self.a == mt.inf: return '∞' - elif self.x == -m.inf: + elif self.a == -mt.inf: return '-∞' else: - return str(self.x) + return str(self.a) + + def __csv__(self): + if self.a == mt.inf: + return 'inf' + elif self.a == -mt.inf: + return '-inf' + else: + return repr(self.a) + + def __bool__(self): + return bool(self.a) def __int__(self): - assert not m.isinf(self.x) - return self.x + assert not mt.isinf(self.a) + return self.a def __float__(self): - return float(self.x) + return float(self.a) none = '%7s' % '-' def table(self): return '%7s' % (self,) - diff_none = '%7s' % '-' - diff_table = table - - def diff_diff(self, other): - new = self.x if self else 0 - old = other.x if other else 0 + def diff(self, other): + new = self.a if self else 0 + old = other.a if other else 0 diff = new - old - if diff == +m.inf: + if diff == +mt.inf: return '%7s' % '+∞' - elif diff == -m.inf: + elif diff == -mt.inf: return '%7s' % '-∞' else: return '%+7d' % diff def ratio(self, other): - new = self.x if self else 0 - old = other.x if other else 0 - if m.isinf(new) and m.isinf(old): + new = self.a if self else 0 + old = other.a if other else 0 + if mt.isinf(new) and mt.isinf(old): return 0.0 - elif m.isinf(new): - return +m.inf - elif m.isinf(old): - return -m.inf + elif mt.isinf(new): + return +mt.inf + elif mt.isinf(old): + return -mt.inf elif not old and not new: return 0.0 elif not old: - return 1.0 + return +mt.inf else: return (new-old) / old + def __pos__(self): + return self.__class__(+self.a) + + def __neg__(self): + return self.__class__(-self.a) + + def __abs__(self): + return self.__class__(abs(self.a)) + def __add__(self, other): - return self.__class__(self.x + other.x) + return self.__class__(self.a + other.a) def __sub__(self, other): - return self.__class__(self.x - other.x) + return self.__class__(self.a - other.a) def __mul__(self, other): - return self.__class__(self.x * other.x) + return self.__class__(self.a * other.a) + + def __truediv__(self, other): + if not other: + if self >= self.__class__(0): + return self.__class__(+mt.inf) + else: + return self.__class__(-mt.inf) + return self.__class__(self.a // other.a) + + def __mod__(self, other): + return self.__class__(self.a % other.a) # code size results class CodeResult(co.namedtuple('CodeResult', [ 'file', 'function', 'size'])): + _prefix = 'code' _by = ['file', 'function'] _fields = ['size'] _sort = ['size'] - _types = {'size': Int} + _types = {'size': CsvInt} __slots__ = () def __new__(cls, file='', function='', size=0): return super().__new__(cls, file, function, - Int(size)) + CsvInt(size)) def __add__(self, other): return CodeResult(self.file, self.function, - self.size + other.size) + self.size + other.size) +# open with '-' for stdin/stdout def openio(path, mode='r', buffering=-1): - # allow '-' for stdin/stdout + import os if path == '-': - if mode == 'r': + if 'r' in mode: return os.fdopen(os.dup(sys.stdin.fileno()), mode, buffering) else: return os.fdopen(os.dup(sys.stdout.fileno()), mode, buffering) else: return open(path, mode, buffering) -def collect(obj_paths, *, - nm_path=NM_PATH, - nm_types=NM_TYPES, +class Sym(co.namedtuple('Sym', [ + 'name', 'global_', 'section', 'addr', 'size'])): + __slots__ = () + def __new__(cls, name, global_, section, addr, size): + return super().__new__(cls, name, global_, section, addr, size) + + def __repr__(self): + return '%s(%r, %r, %r, 0x%x, 0x%x)' % ( + self.__class__.__name__, + self.name, + self.global_, + self.section, + self.addr, + self.size) + +class SymInfo: + def __init__(self, syms): + self.syms = syms + + def get(self, k, d=None): + # allow lookup by both symbol and address + if isinstance(k, str): + # organize by symbol, note multiple symbols can share a name + if not hasattr(self, '_by_sym'): + by_sym = {} + for sym in self.syms: + if sym.name not in by_sym: + by_sym[sym.name] = [] + if sym not in by_sym[sym.name]: + by_sym[sym.name].append(sym) + self._by_sym = by_sym + + return self._by_sym.get(k, d) + + else: + import bisect + + # organize by address + if not hasattr(self, '_by_addr'): + # sort and keep largest/first when duplicates + syms = self.syms.copy() + syms.sort(key=lambda x: (x.addr, -x.size)) + + by_addr = [] + for sym in syms: + if (len(by_addr) == 0 + or by_addr[-1].addr != sym.addr): + by_addr.append(sym) + self._by_addr = by_addr + + # find sym by range + i = bisect.bisect(self._by_addr, k, + key=lambda x: x.addr) - 1 + # check that we're actually in this sym's size + if i > -1 and k < self._by_addr[i].addr+self._by_addr[i].size: + return self._by_addr[i] + else: + return d + + def __getitem__(self, k): + v = self.get(k) + if v is None: + raise KeyError(k) + return v + + def __contains__(self, k): + return self.get(k) is not None + + def __bool__(self): + return bool(self.syms) + + def __len__(self): + return len(self.syms) + + def __iter__(self): + return iter(self.syms) + + def globals(self): + return SymInfo([sym for sym in self.syms + if sym.global_]) + + def section(self, section): + return SymInfo([sym for sym in self.syms + # note we accept prefixes + if s.startswith(section)]) + +def collect_syms(obj_path, global_=False, sections=None, *, objdump_path=OBJDUMP_PATH, - sources=None, - everything=False, **args): - size_pattern = re.compile( - '^(?P[0-9a-fA-F]+)' + - ' (?P[%s])' % re.escape(nm_types) + - ' (?P.+?)$') - line_pattern = re.compile( - '^\s+(?P[0-9]+)' - '(?:\s+(?P[0-9]+))?' - '\s+.*' - '\s+(?P[^\s]+)$') - info_pattern = re.compile( - '^(?:.*(?PDW_TAG_[a-z_]+).*' - '|.*DW_AT_name.*:\s*(?P[^:\s]+)\s*' - '|.*DW_AT_decl_file.*:\s*(?P[0-9]+)\s*)$') + symbol_pattern = re.compile( + '^(?P[0-9a-fA-F]+)' + ' (?P.).*' + '\s+(?P
[^\s]+)' + '\s+(?P[0-9a-fA-F]+)' + '\s+(?P[^\s]+)\s*$') - results = [] - for path in obj_paths: - # guess the source, if we have debug-info we'll replace this later - file = re.sub('(\.o)?$', '.c', path, 1) - - # find symbol sizes - results_ = [] - # note nm-path may contain extra args - cmd = nm_path + ['--size-sort', path] - if args.get('verbose'): - print(' '.join(shlex.quote(c) for c in cmd)) - proc = sp.Popen(cmd, + # find symbol addresses and sizes + syms = [] + cmd = objdump_path + ['--syms', obj_path] + if args.get('verbose'): + print(' '.join(shlex.quote(c) for c in cmd)) + proc = sp.Popen(cmd, stdout=sp.PIPE, - stderr=sp.PIPE if not args.get('verbose') else None, universal_newlines=True, errors='replace', close_fds=False) - for line in proc.stdout: - m = size_pattern.match(line) - if m: - func = m.group('func') - # discard internal functions - if not everything and func.startswith('__'): - continue - results_.append(CodeResult( - file, func, - int(m.group('size'), 16))) - proc.wait() - if proc.returncode != 0: - if not args.get('verbose'): - for line in proc.stderr: - sys.stdout.write(line) - sys.exit(-1) + for line in proc.stdout: + m = symbol_pattern.match(line) + if m: + name = m.group('name') + scope = m.group('scope') + section = m.group('section') + addr = int(m.group('addr'), 16) + size = int(m.group('size'), 16) + # skip non-globals? + # l => local + # g => global + # u => unique global + # => neither + # ! => local + global + global__ = scope not in 'l ' + if global_ and not global__: + continue + # filter by section? note we accept prefixes + if (sections is not None + and not any(section.startswith(prefix) + for prefix in sections)): + continue + # skip zero sized symbols + if not size: + continue + # note multiple symbols can share a name + syms.append(Sym(name, global__, section, addr, size)) + proc.wait() + if proc.returncode != 0: + raise sp.CalledProcessError(proc.returncode, proc.args) + return SymInfo(syms) - # try to figure out the source file if we have debug-info - dirs = {} - files = {} - # note objdump-path may contain extra args - cmd = objdump_path + ['--dwarf=rawline', path] - if args.get('verbose'): - print(' '.join(shlex.quote(c) for c in cmd)) - proc = sp.Popen(cmd, - stdout=sp.PIPE, - stderr=sp.PIPE if not args.get('verbose') else None, - universal_newlines=True, - errors='replace', - close_fds=False) - for line in proc.stdout: - # note that files contain references to dirs, which we - # dereference as soon as we see them as each file table follows a - # dir table - m = line_pattern.match(line) - if m: - if not m.group('dir'): - # found a directory entry - dirs[int(m.group('no'))] = m.group('path') - else: - # found a file entry - dir = int(m.group('dir')) - if dir in dirs: - files[int(m.group('no'))] = os.path.join( - dirs[dir], - m.group('path')) - else: - files[int(m.group('no'))] = m.group('path') - proc.wait() - if proc.returncode != 0: - if not args.get('verbose'): - for line in proc.stderr: - sys.stdout.write(line) - # do nothing on error, we don't need objdump to work, source files - # may just be inaccurate - pass - - defs = {} - is_func = False - f_name = None - f_file = None - # note objdump-path may contain extra args - cmd = objdump_path + ['--dwarf=info', path] - if args.get('verbose'): - print(' '.join(shlex.quote(c) for c in cmd)) - proc = sp.Popen(cmd, +# each dwarf entry can have attrs and children entries +class DwarfEntry: + def __init__(self, level, off, tag, ats={}, children=[]): + self.level = level + self.off = off + self.tag = tag + self.ats = ats or {} + self.children = children or [] + + def get(self, k, d=None): + return self.ats.get(k, d) + + def __getitem__(self, k): + return self.ats[k] + + def __contains__(self, k): + return k in self.ats + + def __repr__(self): + return '%s(%d, 0x%x, %r, %r)' % ( + self.__class__.__name__, + self.level, + self.off, + self.tag, + self.ats) + + @ft.cached_property + def name(self): + if 'DW_AT_name' in self: + name = self['DW_AT_name'].split(':')[-1].strip() + # prefix with struct/union/enum + if self.tag == 'DW_TAG_structure_type': + name = 'struct ' + name + elif self.tag == 'DW_TAG_union_type': + name = 'union ' + name + elif self.tag == 'DW_TAG_enumeration_type': + name = 'enum ' + name + return name + else: + return None + +# a collection of dwarf entries +class DwarfInfo: + def __init__(self, entries): + self.entries = entries + + def get(self, k, d=None): + # allow lookup by offset or dwarf name + if not isinstance(k, str): + return self.entries.get(k, d) + + else: + # organize entries by name + if not hasattr(self, '_by_name'): + self._by_name = {} + for entry in self.entries.values(): + if entry.name is not None: + self._by_name[entry.name] = entry + + # exact match? do a quick lookup + if k in self._by_name: + return self._by_name[k] + # find the best matching dwarf entry with a simple + # heuristic + # + # this can be different from the actual symbol because + # of optimization passes + else: + def key(entry): + i = k.find(entry.name) + if i == -1: + return None + return (i, len(k)-(i+len(entry.name)), k) + return min( + filter(key, self._by_name.values()), + key=key, + default=d) + + def __getitem__(self, k): + v = self.get(k) + if v is None: + raise KeyError(k) + return v + + def __contains__(self, k): + return self.get(k) is not None + + def __bool__(self): + return bool(self.entries) + + def __len__(self): + return len(self.entries) + + def __iter__(self): + return iter(self.entries.values()) + +def collect_dwarf_info(obj_path, tags=None, *, + objdump_path=OBJDUMP_PATH, + **args): + info_pattern = re.compile( + '^\s*<(?P[^>]*)>' + '\s*<(?P[^>]*)>' + '.*\(\s*(?P[^)]*?)\s*\)\s*$' + '|' '^\s*<(?P[^>]*)>' + '\s*(?P[^>:]*?)' + '\s*:(?P.*)\s*$') + + # collect dwarf entries + info = co.OrderedDict() + entry = None + levels = {} + # note objdump-path may contain extra args + cmd = objdump_path + ['--dwarf=info', obj_path] + if args.get('verbose'): + print(' '.join(shlex.quote(c) for c in cmd)) + proc = sp.Popen(cmd, stdout=sp.PIPE, - stderr=sp.PIPE if not args.get('verbose') else None, universal_newlines=True, errors='replace', close_fds=False) - for line in proc.stdout: - # state machine here to find definitions - m = info_pattern.match(line) - if m: - if m.group('tag'): - if is_func: - defs[f_name] = files.get(f_file, '?') - is_func = (m.group('tag') == 'DW_TAG_subprogram') - elif m.group('name'): - f_name = m.group('name') - elif m.group('file'): - f_file = int(m.group('file')) - if is_func: - defs[f_name] = files.get(f_file, '?') - proc.wait() - if proc.returncode != 0: - if not args.get('verbose'): - for line in proc.stderr: - sys.stdout.write(line) - # do nothing on error, we don't need objdump to work, source files - # may just be inaccurate - pass - - for r in results_: - # find best matching debug symbol, this may be slightly different - # due to optimizations - if defs: - # exact match? avoid difflib if we can for speed - if r.function in defs: - file = defs[r.function] - else: - _, file = max( - defs.items(), - key=lambda d: difflib.SequenceMatcher(None, - d[0], - r.function, False).ratio()) - else: - file = r.file + for line in proc.stdout: + # state machine here to find dwarf entries + m = info_pattern.match(line) + if m: + if m.group('tag'): + entry = DwarfEntry( + level=int(m.group('level'), 0), + off=int(m.group('off'), 16), + tag=m.group('tag').strip(), + ) + # keep track of unfiltered entries + if tags is None or entry.tag in tags: + info[entry.off] = entry + # store entry in parent + levels[entry.level] = entry + if entry.level-1 in levels: + levels[entry.level-1].children.append(entry) + elif m.group('at'): + if entry: + entry.ats[m.group('at').strip()] = ( + m.group('v').strip()) + proc.wait() + if proc.returncode != 0: + raise sp.CalledProcessError(proc.returncode, proc.args) - # ignore filtered sources - if sources is not None: - if not any( - os.path.abspath(file) == os.path.abspath(s) - for s in sources): - continue - else: - # default to only cwd - if not everything and not os.path.commonpath([ - os.getcwd(), - os.path.abspath(file)]) == os.getcwd(): - continue + return DwarfInfo(info) - # simplify path - if os.path.commonpath([ - os.getcwd(), - os.path.abspath(file)]) == os.getcwd(): - file = os.path.relpath(file) - else: - file = os.path.abspath(file) +def collect_code(obj_paths, *, + everything=False, + no_strip=False, + **args): + results = [] + for obj_path in obj_paths: + # find relevant symbols and sizes + syms = collect_syms(obj_path, + sections=SECTIONS, + **args) + + # find dwarf info + info = collect_dwarf_info(obj_path, + tags={'DW_TAG_compile_unit'}, + **args) + + # find source file from dwarf info + for entry in info: + if (entry.tag == 'DW_TAG_compile_unit' + and 'DW_AT_name' in entry + and 'DW_AT_comp_dir' in entry): + file = os.path.join( + entry['DW_AT_comp_dir'].split(':')[-1].strip(), + entry['DW_AT_name'].split(':')[-1].strip()) + break + else: + # guess from obj path + file = re.sub('(\.o)?$', '.c', obj_path, 1) + + # simplify path + if os.path.commonpath([ + os.getcwd(), + os.path.abspath(file)]) == os.getcwd(): + file = os.path.relpath(file) + else: + file = os.path.abspath(file) + + # find function sizes + for sym in syms: + # discard internal functions + if not everything and sym.name.startswith('__'): + continue + + # strip compiler suffixes + name = sym.name + if not no_strip: + name = name.split('.', 1)[0] - results.append(r._replace(file=file)) + results.append(CodeResult(file, name, sym.size)) return results +# common folding/tabling/read/write code + +class Rev(co.namedtuple('Rev', 'a')): + __slots__ = () + # yes we need all of these because we're a namedtuple + def __lt__(self, other): + return self.a > other.a + def __gt__(self, other): + return self.a < other.a + def __le__(self, other): + return self.a >= other.a + def __ge__(self, other): + return self.a <= other.a + def fold(Result, results, *, by=None, - defines=None, + defines=[], + sort=None, + depth=1, **_): + # stop when depth hits zero + if depth == 0: + return [] + + # organize by by if by is None: by = Result._by - for k in it.chain(by or [], (k for k, _ in defines or [])): + for k in it.chain(by or [], (k for k, _ in defines)): if k not in Result._by and k not in Result._fields: - print("error: could not find field %r?" % k) + print("error: could not find field %r?" % k, + file=sys.stderr) sys.exit(-1) # filter by matching defines - if defines is not None: + if defines: results_ = [] for r in results: - if all(getattr(r, k) in vs for k, vs in defines): + if all(any(fnmatch.fnmatchcase(str(getattr(r, k, '')), v) + for v in vs) + for k, vs in defines): results_.append(r) results = results_ @@ -343,17 +571,67 @@ def fold(Result, results, *, for name, rs in folding.items(): folded.append(sum(rs[1:], start=rs[0])) + # sort, note that python's sort is stable + folded.sort(key=lambda r: ( + # sort by explicit sort fields + tuple((Rev + if reverse ^ (not k or k in Result._fields) + else lambda x: x)( + tuple((getattr(r, k_),) + if getattr(r, k_) is not None + else () + for k_ in ([k] if k else Result._sort))) + for k, reverse in (sort or [])), + # sort by result + r)) + + # recurse if we have recursive results + if hasattr(Result, '_children'): + folded = [r._replace(**{ + Result._children: fold( + Result, getattr(r, Result._children), + by=by, + # only filter defines at the top level! + sort=sort, + depth=depth-1)}) + for r in folded] + return folded def table(Result, results, diff_results=None, *, by=None, fields=None, sort=None, - summary=False, - all=False, + labels=None, + depth=1, + hot=None, percent=False, + all=False, + compare=None, + no_header=False, + small_header=False, + no_total=False, + small_total=False, + small_table=False, + summary=False, + total=False, **_): - all_, all = all, __builtins__.all + import builtins + all_, all = all, builtins.all + + # small_table implies small_header + no_total or small_total + if small_table: + small_header = True + small_total = True + no_total = no_total or (not summary and not total) + # summary implies small_header + if summary: + small_header = True + # total implies summary + no_header + small_total + if total: + summary = True + no_header = True + small_total = True if by is None: by = Result._by @@ -361,347 +639,624 @@ def table(Result, results, diff_results=None, *, fields = Result._fields types = Result._types - # fold again - results = fold(Result, results, by=by) - if diff_results is not None: - diff_results = fold(Result, diff_results, by=by) - # organize by name table = { - ','.join(str(getattr(r, k) or '') for k in by): r - for r in results} + ','.join(str(getattr(r, k) + if getattr(r, k) is not None + else '') + for k in by): r + for r in results} diff_table = { - ','.join(str(getattr(r, k) or '') for k in by): r - for r in diff_results or []} - names = list(table.keys() | diff_table.keys()) + ','.join(str(getattr(r, k) + if getattr(r, k) is not None + else '') + for k in by): r + for r in diff_results or []} - # sort again, now with diff info, note that python's sort is stable - names.sort() + # lost results? this only happens if we didn't fold by the same + # by field, which is an error and risks confusing results + assert len(table) == len(results) if diff_results is not None: - names.sort(key=lambda n: tuple( - types[k].ratio( - getattr(table.get(n), k, None), - getattr(diff_table.get(n), k, None)) - for k in fields), - reverse=True) - if sort: - for k, reverse in reversed(sort): - names.sort( - key=lambda n: tuple( - (getattr(table[n], k),) - if getattr(table.get(n), k, None) is not None else () - for k in ([k] if k else [ - k for k in Result._sort if k in fields])), - reverse=reverse ^ (not k or k in Result._fields)) + assert len(diff_table) == len(diff_results) + # find compare entry if there is one + if compare: + compare_ = min( + (n for n in table.keys() + if all(fnmatch.fnmatchcase(k, c) + for k, c in it.zip_longest(n.split(','), compare, + fillvalue=''))), + default=compare) + compare_r = table.get(compare_) # build up our lines lines = [] # header - header = [] - header.append('%s%s' % ( - ','.join(by), - ' (%d added, %d removed)' % ( - sum(1 for n in table if n not in diff_table), - sum(1 for n in diff_table if n not in table)) - if diff_results is not None and not percent else '') - if not summary else '') - if diff_results is None: - for k in fields: - header.append(k) - elif percent: - for k in fields: - header.append(k) - else: - for k in fields: - header.append('o'+k) - for k in fields: - header.append('n'+k) - for k in fields: - header.append('d'+k) - header.append('') - lines.append(header) - - def table_entry(name, r, diff_r=None, ratios=[]): - entry = [] - entry.append(name) - if diff_results is None: + if not no_header: + header = ['%s%s' % ( + ','.join(labels if labels is not None else by), + ' (%d added, %d removed)' % ( + sum(1 for n in table if n not in diff_table), + sum(1 for n in diff_table if n not in table)) + if diff_results is not None and not percent else '') + if not small_header else ''] + if diff_results is None or percent: for k in fields: - entry.append(getattr(r, k).table() - if getattr(r, k, None) is not None - else types[k].none) - elif percent: - for k in fields: - entry.append(getattr(r, k).diff_table() - if getattr(r, k, None) is not None - else types[k].diff_none) + header.append(k) else: for k in fields: - entry.append(getattr(diff_r, k).diff_table() - if getattr(diff_r, k, None) is not None - else types[k].diff_none) + header.append('o'+k) for k in fields: - entry.append(getattr(r, k).diff_table() - if getattr(r, k, None) is not None - else types[k].diff_none) + header.append('n'+k) for k in fields: - entry.append(types[k].diff_diff( - getattr(r, k, None), - getattr(diff_r, k, None))) - if diff_results is None: - entry.append('') + header.append('d'+k) + lines.append(header) + + # delete these to try to catch typos below, we need to rebuild + # these tables at each recursive layer + del table + del diff_table + + # entry helper + def table_entry(name, r, diff_r=None): + # prepend name + entry = [name] + + # normal entry? + if ((compare is None or r == compare_r) + and diff_results is None): + for k in fields: + entry.append( + (getattr(r, k).table(), + getattr(getattr(r, k), 'notes', lambda: [])()) + if getattr(r, k, None) is not None + else types[k].none) + # compare entry? + elif diff_results is None: + for k in fields: + entry.append( + (getattr(r, k).table() + if getattr(r, k, None) is not None + else types[k].none, + (lambda t: ['+∞%'] if t == +mt.inf + else ['-∞%'] if t == -mt.inf + else ['%+.1f%%' % (100*t)])( + types[k].ratio( + getattr(r, k, None), + getattr(compare_r, k, None))))) + # percent entry? elif percent: - entry.append(' (%s)' % ', '.join( - '+∞%' if t == +m.inf - else '-∞%' if t == -m.inf - else '%+.1f%%' % (100*t) - for t in ratios)) + for k in fields: + entry.append( + (getattr(r, k).table() + if getattr(r, k, None) is not None + else types[k].none, + (lambda t: ['+∞%'] if t == +mt.inf + else ['-∞%'] if t == -mt.inf + else ['%+.1f%%' % (100*t)])( + types[k].ratio( + getattr(r, k, None), + getattr(diff_r, k, None))))) + # diff entry? else: - entry.append(' (%s)' % ', '.join( - '+∞%' if t == +m.inf - else '-∞%' if t == -m.inf - else '%+.1f%%' % (100*t) - for t in ratios - if t) - if any(ratios) else '') + for k in fields: + entry.append(getattr(diff_r, k).table() + if getattr(diff_r, k, None) is not None + else types[k].none) + for k in fields: + entry.append(getattr(r, k).table() + if getattr(r, k, None) is not None + else types[k].none) + for k in fields: + entry.append( + (types[k].diff( + getattr(r, k, None), + getattr(diff_r, k, None)), + (lambda t: ['+∞%'] if t == +mt.inf + else ['-∞%'] if t == -mt.inf + else ['%+.1f%%' % (100*t)] if t + else [])( + types[k].ratio( + getattr(r, k, None), + getattr(diff_r, k, None))))) + + # append any notes + if hasattr(Result, '_notes') and r is not None: + notes = sorted(getattr(r, Result._notes)) + if isinstance(entry[-1], tuple): + entry[-1] = (entry[-1][0], entry[-1][1] + notes) + else: + entry[-1] = (entry[-1], notes) + return entry - # entries - if not summary: - for name in names: - r = table.get(name) - if diff_results is None: - diff_r = None - ratios = None + # recursive entry helper + def table_recurse(results_, diff_results_, + depth_, + prefixes=('', '', '', '')): + # build the children table at each layer + table_ = { + ','.join(str(getattr(r, k) + if getattr(r, k) is not None + else '') + for k in by): r + for r in results_} + diff_table_ = { + ','.join(str(getattr(r, k) + if getattr(r, k) is not None + else '') + for k in by): r + for r in diff_results_ or []} + names_ = [n + for n in table_.keys() | diff_table_.keys() + if diff_results is None + or all_ + or any( + types[k].ratio( + getattr(table_.get(n), k, None), + getattr(diff_table_.get(n), k, None)) + for k in fields)] + + # sort again, now with diff info, note that python's sort is stable + names_.sort(key=lambda n: ( + # sort by explicit sort fields + next( + tuple((Rev + if reverse ^ (not k or k in Result._fields) + else lambda x: x)( + tuple((getattr(r_, k_),) + if getattr(r_, k_) is not None + else () + for k_ in ([k] if k else Result._sort))) + for k, reverse in (sort or [])) + for r_ in [table_.get(n), diff_table_.get(n)] + if r_ is not None), + # sort by ratio if diffing + Rev(tuple(types[k].ratio( + getattr(table_.get(n), k, None), + getattr(diff_table_.get(n), k, None)) + for k in fields)) + if diff_results is not None + else (), + # move compare entry to the top, note this can be + # overridden by explicitly sorting by fields + (table_.get(n) != compare_r, + # sort by ratio if comparing + Rev(tuple( + types[k].ratio( + getattr(table_.get(n), k, None), + getattr(compare_r, k, None)) + for k in fields))) + if compare + else (), + # sort by result + (table_[n],) if n in table_ else (), + # and finally by name (diffs may be missing results) + n)) + + for i, name in enumerate(names_): + # find comparable results + r = table_.get(name) + diff_r = diff_table_.get(name) + + # figure out a good label + if labels is not None: + label = next( + ','.join(str(getattr(r_, k) + if getattr(r_, k) is not None + else '') + for k in labels) + for r_ in [r, diff_r] + if r_ is not None) else: - diff_r = diff_table.get(name) - ratios = [ - types[k].ratio( - getattr(r, k, None), - getattr(diff_r, k, None)) - for k in fields] - if not all_ and not any(ratios): - continue - lines.append(table_entry(name, r, diff_r, ratios)) + label = name + + # build line + line = table_entry(label, r, diff_r) + + # add prefixes + line = [x if isinstance(x, tuple) else (x, []) for x in line] + line[0] = (prefixes[0+(i==len(names_)-1)] + line[0][0], line[0][1]) + lines.append(line) + + # recurse? + if name in table_ and depth_ > 1: + table_recurse( + getattr(r, Result._children), + getattr(diff_r, Result._children, None), + depth_-1, + (prefixes[2+(i==len(names_)-1)] + "|-> ", + prefixes[2+(i==len(names_)-1)] + "'-> ", + prefixes[2+(i==len(names_)-1)] + "| ", + prefixes[2+(i==len(names_)-1)] + " ")) + + # build entries + if not summary: + table_recurse(results, diff_results, depth) # total - r = next(iter(fold(Result, results, by=[])), None) - if diff_results is None: - diff_r = None - ratios = None - else: - diff_r = next(iter(fold(Result, diff_results, by=[])), None) - ratios = [ - types[k].ratio( - getattr(r, k, None), - getattr(diff_r, k, None)) - for k in fields] - lines.append(table_entry('TOTAL', r, diff_r, ratios)) - - # find the best widths, note that column 0 contains the names and column -1 - # the ratios, so those are handled a bit differently - widths = [ - ((max(it.chain([w], (len(l[i]) for l in lines)))+1+4-1)//4)*4-1 - for w, i in zip( - it.chain([23], it.repeat(7)), - range(len(lines[0])-1))] + if not no_total: + r = next(iter(fold(Result, results, by=[])), Result()) + if diff_results is None: + diff_r = None + else: + diff_r = next(iter(fold(Result, diff_results, by=[])), Result()) + lines.append(table_entry( + 'TOTAL' if not small_total else '', + r, diff_r)) + + # homogenize + lines = [[x if isinstance(x, tuple) else (x, []) for x in line] + for line in lines] + + # find the best widths, note that column 0 contains the names and is + # handled a bit differently + widths = co.defaultdict(lambda: 7, {0: 7}) + nwidths = co.defaultdict(lambda: 0) + for line in lines: + for i, x in enumerate(line): + widths[i] = max(widths[i], ((len(x[0])+1+4-1)//4)*4-1) + if i != len(line)-1: + nwidths[i] = max(nwidths[i], 1+sum(2+len(n) for n in x[1])) + if not any(line[0][0] for line in lines): + widths[0] = 0 # print our table for line in lines: - print('%-*s %s%s' % ( - widths[0], line[0], - ' '.join('%*s' % (w, x) - for w, x in zip(widths[1:], line[1:-1])), - line[-1])) + print('%-*s %s' % ( + widths[0], line[0][0], + ' '.join('%*s%-*s' % ( + widths[i], x[0], + nwidths[i], ' (%s)' % ', '.join(x[1]) if x[1] else '') + for i, x in enumerate(line[1:], 1)))) + +def read_csv(path, Result, *, + depth=1, + prefix=None, + **_): + # prefix? this only applies to field fields + if prefix is None: + if hasattr(Result, '_prefix'): + prefix = '%s_' % Result._prefix + else: + prefix = '' + by = Result._by + fields = Result._fields -def main(obj_paths, *, - by=None, - fields=None, - defines=None, - sort=None, - **args): - # find sizes - if not args.get('use', None): - results = collect(obj_paths, **args) - else: - results = [] - with openio(args['use']) as f: + with openio(path, 'r') as f: + # csv or json? assume json starts with [ + is_json = (f.buffer.peek(1)[:1] == b'[') + + # read csv? + if not is_json: + results = [] reader = csv.DictReader(f, restval='') for r in reader: - if not any('code_'+k in r and r['code_'+k].strip() - for k in CodeResult._fields): + if not any(prefix+k in r and r[prefix+k].strip() + for k in fields): continue try: - results.append(CodeResult( - **{k: r[k] for k in CodeResult._by - if k in r and r[k].strip()}, - **{k: r['code_'+k] for k in CodeResult._fields - if 'code_'+k in r and r['code_'+k].strip()})) + # note this allows by/fields to overlap + results.append(Result(**( + {k: r[k] for k in by + if k in r + and r[k].strip()} + | {k: r[prefix+k] for k in fields + if prefix+k in r + and r[prefix+k].strip()}))) except TypeError: pass + return results - # fold - results = fold(CodeResult, results, by=by, defines=defines) + # read json? + else: + import json + def unjsonify(results, depth_): + results_ = [] + for r in results: + if not any(prefix+k in r and r[prefix+k].strip() + for k in fields): + continue + try: + # note this allows by/fields to overlap + results_.append(Result(**( + {k: r[k] for k in by + if k in r + and r[k] is not None} + | {k: r[prefix+k] for k in fields + if prefix+k in r + and r[prefix+k] is not None} + | ({Result._children: unjsonify( + r[Result._children], + depth_-1)} + if hasattr(Result, '_children') + and Result._children in r + and r[Result._children] is not None + and depth_ > 1 + else {}) + | ({Result._notes: set(r[Result._notes])} + if hasattr(Result, '_notes') + and Result._notes in r + and r[Result._notes] is not None + else {})))) + except TypeError: + pass + return results_ + return unjsonify(json.load(f), depth) - # sort, note that python's sort is stable - results.sort() - if sort: - for k, reverse in reversed(sort): - results.sort( - key=lambda r: tuple( - (getattr(r, k),) if getattr(r, k) is not None else () - for k in ([k] if k else CodeResult._sort)), - reverse=reverse ^ (not k or k in CodeResult._fields)) +def write_csv(path, Result, results, *, + json=False, + by=None, + fields=None, + depth=1, + prefix=None, + **_): + # prefix? this only applies to field fields + if prefix is None: + if hasattr(Result, '_prefix'): + prefix = '%s_' % Result._prefix + else: + prefix = '' - # write results to CSV - if args.get('output'): - with openio(args['output'], 'w') as f: - writer = csv.DictWriter(f, - (by if by is not None else CodeResult._by) - + ['code_'+k for k in ( - fields if fields is not None else CodeResult._fields)]) + if by is None: + by = Result._by + if fields is None: + fields = Result._fields + + with openio(path, 'w') as f: + # write csv? + if not json: + writer = csv.DictWriter(f, list( + co.OrderedDict.fromkeys(it.chain( + by, + (prefix+k for k in fields))).keys())) writer.writeheader() for r in results: + # note this allows by/fields to overlap writer.writerow( - {k: getattr(r, k) for k in ( - by if by is not None else CodeResult._by)} - | {'code_'+k: getattr(r, k) for k in ( - fields if fields is not None else CodeResult._fields)}) + {k: getattr(r, k) + for k in by + if getattr(r, k) is not None} + | {prefix+k: getattr(r, k).__csv__() + for k in fields + if getattr(r, k) is not None}) + + # write json? + else: + import json + # the neat thing about json is we can include recursive results + def jsonify(results, depth_): + results_ = [] + for r in results: + # note this allows by/fields to overlap + results_.append( + {k: getattr(r, k) + for k in by + if getattr(r, k) is not None} + | {prefix+k: getattr(r, k).__csv__() + for k in fields + if getattr(r, k) is not None} + | ({Result._children: jsonify( + getattr(r, Result._children), + depth_-1)} + if hasattr(Result, '_children') + and getattr(r, Result._children) + and depth_ > 1 + else {}) + | ({Result._notes: list( + getattr(r, Result._notes))} + if hasattr(Result, '_notes') + and getattr(r, Result._notes) + else {})) + return results_ + json.dump(jsonify(results, depth), f, + separators=(',', ':')) + + +def main(obj_paths, *, + by=None, + fields=None, + defines=[], + sort=None, + **args): + # figure out what fields we're interested in + if by is None: + if args.get('output') or args.get('output_json'): + by = CodeResult._by + else: + by = ['function'] + + if fields is None: + fields = CodeResult._fields + + # find sizes + if not args.get('use', None): + # not enough info? + if not obj_paths: + print("error: no *.o files?", + file=sys.stderr) + sys.exit(1) + + # collect info + results = collect_code(obj_paths, + **args) + + else: + results = read_csv(args['use'], CodeResult, + **args) + + # fold + results = fold(CodeResult, results, + by=by, + defines=defines, + sort=sort) # find previous results? + diff_results = None if args.get('diff'): - diff_results = [] try: - with openio(args['diff']) as f: - reader = csv.DictReader(f, restval='') - for r in reader: - if not any('code_'+k in r and r['code_'+k].strip() - for k in CodeResult._fields): - continue - try: - diff_results.append(CodeResult( - **{k: r[k] for k in CodeResult._by - if k in r and r[k].strip()}, - **{k: r['code_'+k] for k in CodeResult._fields - if 'code_'+k in r and r['code_'+k].strip()})) - except TypeError: - pass + diff_results = read_csv( + args.get('diff'), + CodeResult, + **args) except FileNotFoundError: - pass + diff_results = [] # fold - diff_results = fold(CodeResult, diff_results, by=by, defines=defines) + diff_results = fold(CodeResult, diff_results, + by=by, + defines=defines) + # write results to JSON + if args.get('output_json'): + write_csv(args['output_json'], CodeResult, results, json=True, + by=by, + fields=fields, + **args) + # write results to CSV + elif args.get('output'): + write_csv(args['output'], CodeResult, results, + by=by, + fields=fields, + **args) # print table - if not args.get('quiet'): - table(CodeResult, results, - diff_results if args.get('diff') else None, - by=by if by is not None else ['function'], - fields=fields, - sort=sort, - **args) + elif not args.get('quiet'): + table(CodeResult, results, diff_results, + by=by, + fields=fields, + sort=sort, + **args) if __name__ == "__main__": import argparse import sys parser = argparse.ArgumentParser( - description="Find code size at the function level.", - allow_abbrev=False) + description="Find code size at the function level.", + allow_abbrev=False) + parser.add_argument( + 'obj_paths', + nargs='*', + help="Input *.o files.") parser.add_argument( - 'obj_paths', - nargs='*', - help="Input *.o files.") + '-v', '--verbose', + action='store_true', + help="Output commands that run behind the scenes.") parser.add_argument( - '-v', '--verbose', - action='store_true', - help="Output commands that run behind the scenes.") + '-q', '--quiet', + action='store_true', + help="Don't show anything, useful when checking for errors.") parser.add_argument( - '-q', '--quiet', - action='store_true', - help="Don't show anything, useful with -o.") + '-o', '--output', + help="Specify CSV file to store results.") parser.add_argument( - '-o', '--output', - help="Specify CSV file to store results.") + '-O', '--output-json', + help="Specify JSON file to store results. This may contain " + "recursive info.") parser.add_argument( - '-u', '--use', - help="Don't parse anything, use this CSV file.") + '-u', '--use', + help="Don't parse anything, use this CSV/JSON file.") parser.add_argument( - '-d', '--diff', - help="Specify CSV file to diff against.") + '-d', '--diff', + help="Specify CSV/JSON file to diff against.") parser.add_argument( - '-a', '--all', - action='store_true', - help="Show all, not just the ones that changed.") + '-p', '--percent', + action='store_true', + help="Only show percentage change, not a full diff.") parser.add_argument( - '-p', '--percent', - action='store_true', - help="Only show percentage change, not a full diff.") + '-c', '--compare', + type=lambda x: tuple(v.strip() for v in x.split(',')), + help="Compare results to the row matching this by pattern.") parser.add_argument( - '-b', '--by', - action='append', - choices=CodeResult._by, - help="Group by this field.") + '-a', '--all', + action='store_true', + help="Show all, not just the ones that changed.") parser.add_argument( - '-f', '--field', - dest='fields', - action='append', - choices=CodeResult._fields, - help="Show this field.") + '-b', '--by', + action='append', + choices=CodeResult._by, + help="Group by this field.") parser.add_argument( - '-D', '--define', - dest='defines', - action='append', - type=lambda x: (lambda k,v: (k, set(v.split(','))))(*x.split('=', 1)), - help="Only include results where this field is this value.") + '-f', '--field', + dest='fields', + action='append', + choices=CodeResult._fields, + help="Show this field.") + parser.add_argument( + '-D', '--define', + dest='defines', + action='append', + type=lambda x: ( + lambda k, vs: ( + k.strip(), + {v.strip() for v in vs.split(',')}) + )(*x.split('=', 1)), + help="Only include results where this field is this value. May " + "include comma-separated options and globs.") class AppendSort(argparse.Action): def __call__(self, parser, namespace, value, option): if namespace.sort is None: namespace.sort = [] - namespace.sort.append((value, True if option == '-S' else False)) + namespace.sort.append((value, option in {'-S', '--reverse-sort'})) + parser.add_argument( + '-s', '--sort', + nargs='?', + action=AppendSort, + help="Sort by this field.") + parser.add_argument( + '-S', '--reverse-sort', + nargs='?', + action=AppendSort, + help="Sort by this field, but backwards.") + parser.add_argument( + '--no-header', + action='store_true', + help="Don't show the header.") + parser.add_argument( + '--small-header', + action='store_true', + help="Don't show by field names.") + parser.add_argument( + '--no-total', + action='store_true', + help="Don't show the total.") parser.add_argument( - '-s', '--sort', - nargs='?', - action=AppendSort, - help="Sort by this field.") + '--small-total', + action='store_true', + help="Don't show TOTAL name.") parser.add_argument( - '-S', '--reverse-sort', - nargs='?', - action=AppendSort, - help="Sort by this field, but backwards.") + '-Q', '--small-table', + action='store_true', + help="Equivalent to --small-header + --no-total or --small-total.") parser.add_argument( - '-Y', '--summary', - action='store_true', - help="Only show the total.") + '-Y', '--summary', + action='store_true', + help="Only show the total.") parser.add_argument( - '-F', '--source', - dest='sources', - action='append', - help="Only consider definitions in this file. Defaults to anything " - "in the current directory.") + '--total', + action='store_true', + help="Equivalent to --summary + --no-header + --small-total. " + "Useful for scripting.") parser.add_argument( - '--everything', - action='store_true', - help="Include builtin and libc specific symbols.") + '--prefix', + help="Prefix to use for fields in CSV/JSON output. Defaults " + "to %r." % ("%s_" % CodeResult._prefix)) parser.add_argument( - '--nm-types', - default=NM_TYPES, - help="Type of symbols to report, this uses the same single-character " - "type-names emitted by nm. Defaults to %r." % NM_TYPES) + '-!', '--everything', + action='store_true', + help="Include builtin and libc specific symbols.") parser.add_argument( - '--nm-path', - type=lambda x: x.split(), - default=NM_PATH, - help="Path to the nm executable, may include flags. " - "Defaults to %r." % NM_PATH) + '-x', '--no-strip', + action='store_true', + help="Don't strip compiler optimization suffixes from symbols.") parser.add_argument( - '--objdump-path', - type=lambda x: x.split(), - default=OBJDUMP_PATH, - help="Path to the objdump executable, may include flags. " - "Defaults to %r." % OBJDUMP_PATH) + '--objdump-path', + type=lambda x: x.split(), + default=OBJDUMP_PATH, + help="Path to the objdump executable, may include flags. " + "Defaults to %r." % OBJDUMP_PATH) sys.exit(main(**{k: v - for k, v in vars(parser.parse_intermixed_args()).items() - if v is not None})) + for k, v in vars(parser.parse_intermixed_args()).items() + if v is not None})) diff --git a/scripts/codemap.py b/scripts/codemap.py new file mode 100755 index 000000000..2dc9f5460 --- /dev/null +++ b/scripts/codemap.py @@ -0,0 +1,1796 @@ +#!/usr/bin/env python3 +# +# Inspired by d3 and brendangregg's flamegraph svg: +# - https://d3js.org +# - https://github.com/brendangregg/FlameGraph +# + +# prevent local imports +if __name__ == "__main__": + __import__('sys').path.pop(0) + +import bisect +import collections as co +import csv +import fnmatch +import io +import itertools as it +import json +import math as mt +import os +import re +import shlex +import shutil +import subprocess as sp +import time + +try: + import inotify_simple +except ModuleNotFoundError: + inotify_simple = None + + +# we don't actually need that many chars/colors thanks to the +# 4-colorability of all 2d maps +COLORS = [ + '34', # blue + '31', # red + '32', # green + '35', # purple + '33', # yellow + '36', # cyan +] + +CHARS_DOTS = " .':" +CHARS_BRAILLE = ( + '⠀⢀⡀⣀⠠⢠⡠⣠⠄⢄⡄⣄⠤⢤⡤⣤' '⠐⢐⡐⣐⠰⢰⡰⣰⠔⢔⡔⣔⠴⢴⡴⣴' + '⠂⢂⡂⣂⠢⢢⡢⣢⠆⢆⡆⣆⠦⢦⡦⣦' '⠒⢒⡒⣒⠲⢲⡲⣲⠖⢖⡖⣖⠶⢶⡶⣶' + '⠈⢈⡈⣈⠨⢨⡨⣨⠌⢌⡌⣌⠬⢬⡬⣬' '⠘⢘⡘⣘⠸⢸⡸⣸⠜⢜⡜⣜⠼⢼⡼⣼' + '⠊⢊⡊⣊⠪⢪⡪⣪⠎⢎⡎⣎⠮⢮⡮⣮' '⠚⢚⡚⣚⠺⢺⡺⣺⠞⢞⡞⣞⠾⢾⡾⣾' + '⠁⢁⡁⣁⠡⢡⡡⣡⠅⢅⡅⣅⠥⢥⡥⣥' '⠑⢑⡑⣑⠱⢱⡱⣱⠕⢕⡕⣕⠵⢵⡵⣵' + '⠃⢃⡃⣃⠣⢣⡣⣣⠇⢇⡇⣇⠧⢧⡧⣧' '⠓⢓⡓⣓⠳⢳⡳⣳⠗⢗⡗⣗⠷⢷⡷⣷' + '⠉⢉⡉⣉⠩⢩⡩⣩⠍⢍⡍⣍⠭⢭⡭⣭' '⠙⢙⡙⣙⠹⢹⡹⣹⠝⢝⡝⣝⠽⢽⡽⣽' + '⠋⢋⡋⣋⠫⢫⡫⣫⠏⢏⡏⣏⠯⢯⡯⣯' '⠛⢛⡛⣛⠻⢻⡻⣻⠟⢟⡟⣟⠿⢿⡿⣿') + +CODE_PATH = ['./scripts/code.py'] +STACK_PATH = ['./scripts/stack.py'] +CTX_PATH = ['./scripts/ctx.py'] + +SI_PREFIXES = { + 18: 'E', + 15: 'P', + 12: 'T', + 9: 'G', + 6: 'M', + 3: 'K', + 0: '', + -3: 'm', + -6: 'u', + -9: 'n', + -12: 'p', + -15: 'f', + -18: 'a', +} + +SI2_PREFIXES = { + 60: 'Ei', + 50: 'Pi', + 40: 'Ti', + 30: 'Gi', + 20: 'Mi', + 10: 'Ki', + 0: '', + -10: 'mi', + -20: 'ui', + -30: 'ni', + -40: 'pi', + -50: 'fi', + -60: 'ai', +} + + +# open with '-' for stdin/stdout +def openio(path, mode='r', buffering=-1): + import os + if path == '-': + if 'r' in mode: + return os.fdopen(os.dup(sys.stdin.fileno()), mode, buffering) + else: + return os.fdopen(os.dup(sys.stdout.fileno()), mode, buffering) + else: + return open(path, mode, buffering) + +if inotify_simple is None: + Inotify = None +else: + class Inotify(inotify_simple.INotify): + def __init__(self, paths): + super().__init__() + + # wait for interesting events + flags = (inotify_simple.flags.ATTRIB + | inotify_simple.flags.CREATE + | inotify_simple.flags.DELETE + | inotify_simple.flags.DELETE_SELF + | inotify_simple.flags.MODIFY + | inotify_simple.flags.MOVED_FROM + | inotify_simple.flags.MOVED_TO + | inotify_simple.flags.MOVE_SELF) + + # recurse into directories + for path in paths: + if os.path.isdir(path): + for dir, _, files in os.walk(path): + self.add_watch(dir, flags) + for f in files: + self.add_watch(os.path.join(dir, f), flags) + else: + self.add_watch(path, flags) + +# a pseudo-stdout ring buffer +class RingIO: + def __init__(self, maxlen=None, head=False): + self.maxlen = maxlen + self.head = head + self.lines = co.deque( + maxlen=max(maxlen, 0) if maxlen is not None else None) + self.tail = io.StringIO() + + # trigger automatic sizing + self.resize(self.maxlen) + + @property + def width(self): + # just fetch this on demand, we don't actually use width + return shutil.get_terminal_size((80, 5))[0] + + @property + def height(self): + # calculate based on terminal height? + if self.maxlen is None or self.maxlen <= 0: + return max( + shutil.get_terminal_size((80, 5))[1] + + (self.maxlen or 0), + 0) + # limit to maxlen + else: + return self.maxlen + + def resize(self, maxlen): + self.maxlen = maxlen + if maxlen is not None and maxlen <= 0: + maxlen = self.height + if maxlen != self.lines.maxlen: + self.lines = co.deque(self.lines, maxlen=maxlen) + + def __len__(self): + return len(self.lines) + + def write(self, s): + # note using split here ensures the trailing string has no newline + lines = s.split('\n') + + if len(lines) > 1 and self.tail.getvalue(): + self.tail.write(lines[0]) + lines[0] = self.tail.getvalue() + self.tail = io.StringIO() + + self.lines.extend(lines[:-1]) + + if lines[-1]: + self.tail.write(lines[-1]) + + # keep track of maximum drawn canvas + canvas_lines = 1 + + def draw(self): + # did terminal size change? + self.resize(self.maxlen) + + # copy lines + lines = self.lines.copy() + # pad to fill any existing canvas, but truncate to terminal size + h = shutil.get_terminal_size((80, 5))[1] + lines.extend('' for _ in range( + len(lines), + min(RingIO.canvas_lines, h))) + while len(lines) > h: + if self.head: + lines.pop() + else: + lines.popleft() + + # build up the redraw in memory first and render in a single + # write call, this minimizes flickering caused by the cursor + # jumping around + canvas = [] + + # hide the cursor + canvas.append('\x1b[?25l') + + # give ourself a canvas + while RingIO.canvas_lines < len(lines): + canvas.append('\n') + RingIO.canvas_lines += 1 + + # write lines from top to bottom so later lines overwrite earlier + # lines, note xA/xB stop at terminal boundaries + for i, line in enumerate(lines): + # move to col 0 + canvas.append('\r') + # move up to line + if len(lines)-1-i > 0: + canvas.append('\x1b[%dA' % (len(lines)-1-i)) + # clear line + canvas.append('\x1b[K') + # disable line wrap + canvas.append('\x1b[?7l') + # print the line + canvas.append(line) + # enable line wrap + canvas.append('\x1b[?7h') # enable line wrap + # move back down + if len(lines)-1-i > 0: + canvas.append('\x1b[%dB' % (len(lines)-1-i)) + + # show the cursor again + canvas.append('\x1b[?25h') + + # write to stdout and flush + sys.stdout.write(''.join(canvas)) + sys.stdout.flush() + +def iself(path): + # check for an elf file's magic string (\x7fELF) + with open(path, 'rb') as f: + return f.read(4) == b'\x7fELF' + +# parse different data representations +def dat(x, *args): + try: + # allow the first part of an a/b fraction + if '/' in x: + x, _ = x.split('/', 1) + + # first try as int + try: + return int(x, 0) + except ValueError: + pass + + # then try as float + try: + return float(x) + except ValueError: + pass + + # else give up + raise ValueError("invalid dat %r" % x) + + # default on error? + except ValueError as e: + if args: + return args[0] + else: + raise + +# a representation of optionally key-mapped attrs +class CsvAttr: + def __init__(self, attrs, defaults=None): + if attrs is None: + attrs = [] + if isinstance(attrs, dict): + attrs = attrs.items() + + # normalize + self.attrs = [] + self.keyed = co.OrderedDict() + for attr in attrs: + if not isinstance(attr, tuple): + attr = ((), attr) + if attr[0] in {None, (), (None,), ('*',)}: + attr = ((), attr[1]) + if not isinstance(attr[0], tuple): + attr = ((attr[0],), attr[1]) + + self.attrs.append(attr) + if attr[0] not in self.keyed: + self.keyed[attr[0]] = [] + self.keyed[attr[0]].append(attr[1]) + + # create attrs object for defaults + if isinstance(defaults, CsvAttr): + self.defaults = defaults + elif defaults is not None: + self.defaults = CsvAttr(defaults) + else: + self.defaults = None + + def __repr__(self): + if self.defaults is None: + return 'CsvAttr(%r)' % ( + [(','.join(attr[0]), attr[1]) + for attr in self.attrs]) + else: + return 'CsvAttr(%r, %r)' % ( + [(','.join(attr[0]), attr[1]) + for attr in self.attrs], + [(','.join(attr[0]), attr[1]) + for attr in self.defaults.attrs]) + + def __iter__(self): + if () in self.keyed: + return it.cycle(self.keyed[()]) + elif self.defaults is not None: + return iter(self.defaults) + else: + return iter(()) + + def __bool__(self): + return bool(self.attrs) + + def __getitem__(self, key): + if isinstance(key, tuple): + if len(key) > 0 and not isinstance(key[0], str): + i, key = key + if not isinstance(key, tuple): + key = (key,) + else: + i, key = 0, key + elif isinstance(key, str): + i, key = 0, (key,) + else: + i, key = key, () + + # try to lookup by key + best = None + for ks, vs in self.keyed.items(): + prefix = [] + for j, k in enumerate(ks): + if j < len(key) and fnmatch.fnmatchcase(key[j], k): + prefix.append(k) + else: + prefix = None + break + + if prefix is not None and ( + best is None or len(prefix) >= len(best[0])): + best = (prefix, vs) + + if best is not None: + # cycle based on index + return best[1][i % len(best[1])] + + # fallback to defaults? + if self.defaults is not None: + return self.defaults[i, key] + + raise KeyError(i, key) + + def get(self, key, default=None): + try: + return self.__getitem__(key) + except KeyError: + return default + + def __contains__(self, key): + try: + self.__getitem__(key) + return True + except KeyError: + return False + + # get all results for a given key + def getall(self, key, default=None): + if not isinstance(key, tuple): + key = (key,) + + # try to lookup by key + best = None + for ks, vs in self.keyed.items(): + prefix = [] + for j, k in enumerate(ks): + if j < len(key) and fnmatch.fnmatchcase(key[j], k): + prefix.append(k) + else: + prefix = None + break + + if prefix is not None and ( + best is None or len(prefix) >= len(best[0])): + best = (prefix, vs) + + if best is not None: + return best[1] + + # fallback to defaults? + if self.defaults is not None: + return self.defaults.getall(key, default) + + raise default + + # a key function for sorting by key order + def key(self, key): + if not isinstance(key, tuple): + key = (key,) + + best = None + for i, ks in enumerate(self.keyed.keys()): + prefix = [] + for j, k in enumerate(ks): + if j < len(key) and (not k or key[j] == k): + prefix.append(k) + else: + prefix = None + break + + if prefix is not None and ( + best is None or len(prefix) >= len(best[0])): + best = (prefix, i) + + if best is not None: + return best[1] + + # fallback to defaults? + if self.defaults is not None: + return len(self.keyed) + self.defaults.key(key) + + return len(self.keyed) + +# SI-prefix formatter +def si(x): + if x == 0: + return '0' + # figure out prefix and scale + p = 3*mt.floor(mt.log(abs(x), 10**3)) + p = min(18, max(-18, p)) + # format with 3 digits of precision + s = '%.3f' % (abs(x) / (10.0**p)) + s = s[:3+1] + # truncate but only digits that follow the dot + if '.' in s: + s = s.rstrip('0') + s = s.rstrip('.') + return '%s%s%s' % ('-' if x < 0 else '', s, SI_PREFIXES[p]) + +# SI-prefix formatter for powers-of-two +def si2(x): + if x == 0: + return '0' + # figure out prefix and scale + p = 10*mt.floor(mt.log(abs(x), 2**10)) + p = min(30, max(-30, p)) + # format with 3 digits of precision + s = '%.3f' % (abs(x) / (2.0**p)) + s = s[:3+1] + # truncate but only digits that follow the dot + if '.' in s: + s = s.rstrip('0') + s = s.rstrip('.') + return '%s%s%s' % ('-' if x < 0 else '', s, SI2_PREFIXES[p]) + +# parse %-escaped strings +# +# attrs can override __getitem__ for lazy attr generation +def punescape(s, attrs=None): + pattern = re.compile( + '%[%n]' + '|' '%x..' + '|' '%u....' + '|' '%U........' + '|' '%\((?P[^)]*)\)' + '(?P[+\- #0-9\.]*[siIdboxXfFeEgG])') + def unescape(m): + if m.group()[1] == '%': return '%' + elif m.group()[1] == 'n': return '\n' + elif m.group()[1] == 'x': return chr(int(m.group()[2:], 16)) + elif m.group()[1] == 'u': return chr(int(m.group()[2:], 16)) + elif m.group()[1] == 'U': return chr(int(m.group()[2:], 16)) + elif m.group()[1] == '(': + if attrs is not None: + try: + v = attrs[m.group('field')] + except KeyError: + return m.group() + else: + return m.group() + f = m.group('format') + if f[-1] in 'dboxX': + if isinstance(v, str): + v = dat(v, 0) + v = int(v) + elif f[-1] in 'iIfFeEgG': + if isinstance(v, str): + v = dat(v, 0) + v = float(v) + if f[-1] in 'iI': + v = (si if 'i' in f[-1] else si2)(v) + f = f.replace('i', 's').replace('I', 's') + if '+' in f and not v.startswith('-'): + v = '+'+v + f = f.replace('+', '').replace('-', '') + else: + f = ('<' if '-' in f else '>') + f.replace('-', '') + v = str(v) + # note we need Python's new format syntax for binary + return ('{:%s}' % f).format(v) + else: assert False + + return re.sub(pattern, unescape, s) + +# split %-escaped strings into chars +def psplit(s): + pattern = re.compile( + '%[%n]' + '|' '%x..' + '|' '%u....' + '|' '%U........' + '|' '%\((?P[^)]*)\)' + '(?P[+\- #0-9\.]*[siIdboxXfFeEgG])') + return [m.group() for m in re.finditer(pattern.pattern + '|.', s)] + + +# a little ascii renderer +class Canvas: + def __init__(self, width, height, *, + color=False, + dots=False, + braille=False): + # scale if we're printing with dots or braille + if braille: + xscale, yscale = 2, 4 + elif dots: + xscale, yscale = 1, 2 + else: + xscale, yscale = 1, 1 + + self.width_ = width + self.height_ = height + self.width = xscale*width + self.height = yscale*height + self.xscale = xscale + self.yscale = yscale + self.color_ = color + self.dots = dots + self.braille = braille + + # create initial canvas + self.chars = [0] * (width*height) + self.colors = [''] * (width*height) + + def char(self, x, y, char=None): + # ignore out of bounds + if x < 0 or y < 0 or x >= self.width or y >= self.height: + return False + + x_ = x // self.xscale + y_ = y // self.yscale + if char is not None: + c = self.chars[x_ + y_*self.width_] + # mask in sub-char pixel? + if isinstance(char, bool): + if not isinstance(c, int): + c = 0 + self.chars[x_ + y_*self.width_] = (c + | (1 + << ((y%self.yscale)*self.xscale + + (self.xscale-1)-(x%self.xscale)))) + else: + self.chars[x_ + y_*self.width_] = char + else: + c = self.chars[x_ + y_*self.width_] + if isinstance(c, int): + return ((c + >> ((y%self.yscale)*self.xscale + + (self.xscale-1)-(x%self.xscale))) + & 1) == 1 + else: + return c + + def color(self, x, y, color=None): + # ignore out of bounds + if x < 0 or y < 0 or x >= self.width or y >= self.height: + return '' + + x_ = x // self.xscale + y_ = y // self.yscale + if color is not None: + self.colors[x_ + y_*self.width_] = color + else: + return self.colors[x_ + y_*self.width_] + + def __getitem__(self, xy): + x, y = xy + return self.char(x, y) + + def __setitem__(self, xy, char): + x, y = xy + self.char(x, y, char) + + def point(self, x, y, *, + char=True, + color=''): + self.char(x, y, char) + self.color(x, y, color) + + def line(self, x1, y1, x2, y2, *, + char=True, + color=''): + # incremental error line algorithm + ex = abs(x2 - x1) + ey = -abs(y2 - y1) + dx = +1 if x1 < x2 else -1 + dy = +1 if y1 < y2 else -1 + e = ex + ey + + while True: + self.point(x1, y1, char=char, color=color) + e2 = 2*e + + if x1 == x2 and y1 == y2: + break + + if e2 > ey: + e += ey + x1 += dx + + if x1 == x2 and y1 == y2: + break + + if e2 < ex: + e += ex + y1 += dy + + self.point(x2, y2, char=char, color=color) + + def rect(self, x, y, w, h, *, + char=True, + color=''): + for j in range(h): + for i in range(w): + self.point(x+i, y+j, char=char, color=color) + + def label(self, x, y, label, width=None, height=None, *, + color=''): + x_ = x + y_ = y + for char in label: + if char == '\n': + x_ = x + y_ -= self.yscale + else: + if ((width is None or x_ < x+width) + and (height is None or y_ > y-height)): + self.point(x_, y_, char=char, color=color) + x_ += self.xscale + + def draw(self, row): + y_ = self.height_-1 - row + row_ = [] + for x_ in range(self.width_): + # char? + c = self.chars[x_ + y_*self.width_] + if isinstance(c, int): + if self.braille: + assert c < 256 + c = CHARS_BRAILLE[c] + elif self.dots: + assert c < 4 + c = CHARS_DOTS[c] + else: + assert c < 2 + c = '.' if c else ' ' + + # color? + if self.color_: + color = self.colors[x_ + y_*self.width_] + if color: + c = '\x1b[%sm%s\x1b[m' % (color, c) + + row_.append(c) + + return ''.join(row_) + + +# a type to represent tiles +class Tile: + def __init__(self, key, children, *, + x=None, y=None, width=None, height=None, + depth=None, + attrs=None, + label=None, + color=None): + self.key = key + if isinstance(children, list): + self.children = children + self.value = sum(c.value for c in children) + else: + self.children = [] + self.value = children + + self.x = x + self.y = y + self.width = width + self.height = height + self.depth = depth + self.attrs = attrs + self.label = label + self.color = color + + def __repr__(self): + return 'Tile(%r, %r, x=%r, y=%r, width=%r, height=%r)' % ( + ','.join(self.key), self.value, + self.x, self.y, self.width, self.height) + + # recursively build heirarchy + @staticmethod + def merge(tiles, prefix=()): + # organize by 'by' field + tiles_ = co.OrderedDict() + for t in tiles: + if len(prefix)+1 >= len(t.key): + tiles_[t.key] = t + else: + key = prefix + (t.key[len(prefix)],) + if key not in tiles_: + tiles_[key] = [] + tiles_[key].append(t) + + tiles__ = [] + for key, t in tiles_.items(): + if isinstance(t, Tile): + tiles__.append(t) + else: + tiles__.append(Tile.merge(t, key)) + tiles_ = tiles__ + + return Tile(prefix, tiles_, depth=len(prefix)) + + def __lt__(self, other): + return self.value < other.value + + def __le__(self, other): + return self.value <= other.value + + def __gt__(self, other): + return self.value > other.value + + def __ge__(self, other): + return self.value >= other.value + + # recursive traversals + def tiles(self): + yield self + for child in self.children: + yield from child.tiles() + + def leaves(self): + for t in self.tiles(): + if not t.children: + yield t + + # sort recursively + def sort(self): + self.children.sort(reverse=True) + for t in self.children: + t.sort() + + # recursive align to pixel boundaries + def align(self): + # this extra +0.1 and using points instead of width/height is + # to help minimize rounding errors + x0 = int(self.x+0.1) + y0 = int(self.y+0.1) + x1 = int(self.x+self.width+0.1) + y1 = int(self.y+self.height+0.1) + self.x = x0 + self.y = y0 + self.width = x1 - x0 + self.height = y1 - y0 + + # recurse + for t in self.children: + t.align() + + # return some interesting info about these tiles + def stat(self): + leaves = list(self.leaves()) + mean = self.value / max(len(leaves), 1) + stddev = mt.sqrt(sum((t.value - mean)**2 for t in leaves) + / max(len(leaves), 1)) + min_ = min((t.value for t in leaves), default=0) + max_ = max((t.value for t in leaves), default=0) + return { + 'total': self.value, + 'mean': mean, + 'stddev': stddev, + 'min': min_, + 'max': max_, + } + + +# bounded division, limits result to dividend, useful for avoiding +# divide-by-zero issues +def bdiv(a, b): + return a / max(b, 1) + +# our partitioning schemes + +def partition_binary(children, total, x, y, width, height): + sums = [0] + for t in children: + sums.append(sums[-1] + t.value) + + # recursively partition into a roughly weight-balanced binary tree + def partition_(i, j, value, x, y, width, height): + # no child? guess we're done + if i == j: + return + # single child? assign the partition + elif i == j-1: + children[i].x = x + children[i].y = y + children[i].width = width + children[i].height = height + return + + # binary search to find best split index + target = sums[i] + (value / 2) + k = bisect.bisect(sums, target, i+1, j-1) + + # nudge split index if it results in less error + if k > i+1 and (sums[k] - target) > (target - sums[k-1]): + k -= 1 + + l = sums[k] - sums[i] + r = value - l + + # split horizontally? + if width > height: + dx = bdiv(sums[k] - sums[i], value) * width + partition_(i, k, l, x, y, dx, height) + partition_(k, j, r, x+dx, y, width-dx, height) + + # split vertically? + else: + dy = bdiv(sums[k] - sums[i], value) * height + partition_(i, k, l, x, y, width, dy) + partition_(k, j, r, x, y+dy, width, height-dy) + + partition_(0, len(children), total, x, y, width, height) + +def partition_slice(children, total, x, y, width, height): + # give each child a slice + x_ = x + for t in children: + t.x = x_ + t.y = y + t.width = bdiv(t.value, total) * width + t.height = height + + x_ += t.width + +def partition_dice(children, total, x, y, width, height): + # give each child a slice + y_ = y + for t in children: + t.x = x + t.y = y_ + t.width = width + t.height = bdiv(t.value, total) * height + + y_ += t.height + +def partition_squarify(children, total, x, y, width, height, *, + aspect_ratio=1/1): + # this algorithm is described here: + # https://www.win.tue.nl/~vanwijk/stm.pdf + i = 0 + x_ = x + y_ = y + total_ = total + width_ = width + height_ = height + # note we don't really care about width vs height until + # actually slicing + ratio = max(aspect_ratio, 1/aspect_ratio) + + while i < len(children): + # calculate initial aspect ratio + sum_ = children[i].value + min_ = children[i].value + max_ = children[i].value + w = total_ * bdiv(ratio, + max(bdiv(width_, height_), bdiv(height_, width_))) + ratio_ = max(bdiv(max_*w, sum_**2), bdiv(sum_**2, min_*w)) + + # keep adding children to this row/col until it starts to hurt + # our aspect ratio + j = i + 1 + while j < len(children): + sum__ = sum_ + children[j].value + min__ = min(min_, children[j].value) + max__ = max(max_, children[j].value) + ratio__ = max(bdiv(max__*w, sum__**2), bdiv(sum__**2, min__*w)) + if ratio__ > ratio_: + break + + sum_ = sum__ + min_ = min__ + max_ = max__ + ratio_ = ratio__ + j += 1 + + # vertical col? dice horizontally? + if width_ > height_: + dx = bdiv(sum_, total_) * width_ + partition_dice(children[i:j], sum_, x_, y_, dx, height_) + x_ += dx + width_ -= dx + + # horizontal row? slice vertically? + else: + dy = bdiv(sum_, total_) * height_ + partition_slice(children[i:j], sum_, x_, y_, width_, dy) + y_ += dy + height_ -= dy + + # start partitioning the other direction + total_ -= sum_ + i = j + + +def collect_code(obj_paths, *, + code_path=CODE_PATH, + **args): + # note code-path may contain extra args + cmd = code_path + ['-O-'] + obj_paths + if args.get('verbose'): + print(' '.join(shlex.quote(c) for c in cmd)) + proc = sp.Popen(cmd, + stdout=sp.PIPE, + universal_newlines=True, + errors='replace', + close_fds=False) + code = json.load(proc.stdout) + proc.wait() + if proc.returncode != 0: + raise sp.CalledProcessError(proc.returncode, proc.args) + + return code + +def collect_stack(ci_paths, *, + stack_path=STACK_PATH, + **args): + # note stack-path may contain extra args + cmd = stack_path + ['-O-', '--depth=2'] + ci_paths + if args.get('verbose'): + print(' '.join(shlex.quote(c) for c in cmd)) + proc = sp.Popen(cmd, + stdout=sp.PIPE, + universal_newlines=True, + errors='replace', + close_fds=False) + stack = json.load(proc.stdout) + proc.wait() + if proc.returncode != 0: + raise sp.CalledProcessError(proc.returncode, proc.args) + + return stack + +def collect_ctx(obj_paths, *, + ctx_path=CTX_PATH, + **args): + # note stack-path may contain extra args + cmd = ctx_path + ['-O-', '--depth=2', '--internal'] + obj_paths + if args.get('verbose'): + print(' '.join(shlex.quote(c) for c in cmd)) + proc = sp.Popen(cmd, + stdout=sp.PIPE, + universal_newlines=True, + errors='replace', + close_fds=False) + ctx = json.load(proc.stdout) + proc.wait() + if proc.returncode != 0: + raise sp.CalledProcessError(proc.returncode, proc.args) + + return ctx + + +def main_(ring, paths, *, + namespace_depth=2, + labels=[], + chars=[], + colors=[], + color='auto', + dots=False, + braille=False, + width=None, + height=None, + no_header=False, + tile_code=False, + tile_stack=False, + tile_frames=False, + tile_ctx=False, + tile_1=False, + to_scale=None, + to_ratio=1/1, + tiny=False, + title=None, + label=False, + no_label=False, + **args): + # give ring a writeln function + def writeln(self, s=''): + self.write(s) + self.write('\n') + ring.writeln = writeln.__get__(ring) + + # figure out what color should be + if color == 'auto': + color = sys.stdout.isatty() + elif color == 'always': + color = True + else: + color = False + + # tiny mode? + if tiny: + if to_scale is None: + to_scale = 1 + no_header = True + + # default to tiling based on code + if (not tile_code + and not tile_stack + and not tile_frames + and not tile_ctx + and not tile_1): + tile_code = True + + # what chars/colors/labels to use? + chars_ = [] + for char in chars: + if isinstance(char, tuple): + chars_.extend((char[0], c) for c in psplit(char[1])) + else: + chars_.extend(psplit(char)) + chars_ = CsvAttr(chars_) + + colors_ = CsvAttr(colors, defaults=COLORS) + + labels_ = CsvAttr(labels) + + # figure out width/height + if width is None: + width_ = min(80, shutil.get_terminal_size((80, 5))[0]) + elif width > 0: + width_ = width + else: + width_ = max(0, shutil.get_terminal_size((80, 5))[0] + width) + + if height is None: + height_ = 2 if not no_header else 1 + elif height > 0: + height_ = height + else: + height_ = max(0, shutil.get_terminal_size((80, 5))[1] + height) + + # try to parse files as CSV/JSON + results = [] + try: + # if any file starts with elf magic (\x7fELF), assume input is + # elf/callgraph files + fs = [] + for path in paths: + f = openio(path) + if f.buffer.peek(4)[:4] == b'\x7fELF': + for f in fs: + f.close() + raise StopIteration() + fs.append(f) + + for f in fs: + with f: + # csv or json? assume json starts with [ + is_json = (f.buffer.peek(1)[:1] == b'[') + + # read csv? + if not is_json: + results.extend(csv.DictReader(f, restval='')) + + # read json? + else: + results.extend(json.load(f)) + + # fall back to extracting code/stack/ctx info from elf/callgraph files + except StopIteration: + # figure out paths + obj_paths = [] + ci_paths = [] + for path in paths: + if iself(path): + obj_paths.append(path) + else: + ci_paths.append(path) + + # find code/stack/ctx sizes + if obj_paths: + results.extend(collect_code(obj_paths, **args)) + if ci_paths: + results.extend(collect_stack(ci_paths, **args)) + if obj_paths: + results.extend(collect_ctx(obj_paths, **args)) + + # don't render code/stack/ctx results if we don't have any + nil_code = not any('code_size' in r for r in results) + nil_stack = not any('stack_limit' in r for r in results) + nil_frames = not any('stack_frame' in r for r in results) + nil_ctx = not any('ctx_size' in r for r in results) + + # merge code/stack/ctx results + functions = co.OrderedDict() + for r in results: + if 'function' not in r: + continue + if r['function'] not in functions: + functions[r['function']] = {'name': r['function']} + # code things + if 'code_size' in r: + functions[r['function']]['code'] = dat(r['code_size']) + # stack things, including callgraph + if 'stack_frame' in r: + functions[r['function']]['frame'] = dat(r['stack_frame']) + if 'stack_limit' in r: + functions[r['function']]['stack'] = dat(r['stack_limit'], mt.inf) + if 'children' in r: + if 'children' not in functions[r['function']]: + functions[r['function']]['children'] = [] + functions[r['function']]['children'].extend( + r_['function'] + for r_ in r['children'] + if r_.get('stack_frame', '') != '') + # ctx things, including any arguments + if 'ctx_size' in r: + functions[r['function']]['ctx'] = dat(r['ctx_size']) + if 'children' in r: + if 'args' not in functions[r['function']]: + functions[r['function']]['args'] = [] + functions[r['function']]['args'].extend( + {'name': r_['function'], + 'ctx': dat(r_['ctx_size']), + 'attrs': r_} + for r_ in r['children'] + if r_.get('ctx_size', '') != '') + # keep track of other attrs for punescaping + if 'attrs' not in functions[r['function']]: + functions[r['function']]['attrs'] = {} + functions[r['function']]['attrs'].update(r) + + # stack.py returns infinity for recursive functions, so we need to + # recompute a bounded stack limit to show something useful + def limitof(k, f, seen=set()): + # found a cycle? stop here + if k in seen: + return 0 + + limit = 0 + for child in f.get('children', []): + if child not in functions: + continue + limit = max(limit, limitof(child, functions[child], seen | {k})) + + return f['frame'] + limit + + for k, f in functions.items(): + if 'stack' in f: + if mt.isinf(f['stack']): + f['limit'] = limitof(k, f) + else: + f['limit'] = f['stack'] + + # organize into subsystems + namespace_pattern = re.compile('_*[^_]+(?:_*$)?') + namespace_slice = slice(namespace_depth if namespace_depth else None) + subsystems = {} + for k, f in functions.items(): + # ignore leading/trailing underscores + f['subsystem'] = ''.join( + namespace_pattern.findall(k)[ + namespace_slice]) + + if f['subsystem'] not in subsystems: + subsystems[f['subsystem']] = {'name': f['subsystem']} + + # include ctx in subsystems to give them different colors + for _, f in functions.items(): + for a in f.get('args', []): + a['subsystem'] = a['name'] + + if a['subsystem'] not in subsystems: + subsystems[a['subsystem']] = {'name': a['subsystem']} + + # sort to try to keep things reproducible + functions = co.OrderedDict(sorted(functions.items())) + subsystems = co.OrderedDict(sorted(subsystems.items())) + + # sum code/stack/ctx/attrs for punescaping + for k, s in subsystems.items(): + s['code'] = sum( + f.get('code', 0) for f in functions.values() + if f['subsystem'] == k) + s['stack'] = max( + (f.get('stack', 0) for f in functions.values() + if f['subsystem'] == k), + default=0) + s['ctx'] = max( + (f.get('ctx', 0) for f in functions.values() + if f['subsystem'] == k), + default=0) + s['attrs'] = {k_: v_ + for f in functions.values() + if f['subsystem'] == k + for k_, v_ in f['attrs'].items()} + + # also build totals + totals = {} + totals['code'] = sum( + f.get('code', 0) for f in functions.values()) + totals['stack'] = max( + (f.get('stack', 0) for f in functions.values()), + default=0) + totals['ctx'] = max( + (f.get('ctx', 0) for f in functions.values()), + default=0) + totals['count'] = len(functions) + totals['attrs'] = {k: v + for f in functions.values() + for k, v in f['attrs'].items()} + + # assign colors to subsystems, note this is after sorting, but + # before tile generation, we want code and stack tiles to have the + # same color if they're in the same subsystem + for i, (k, s) in enumerate(subsystems.items()): + color__ = colors_[i, k] + # don't punescape unless we have to + if '%' in color__: + color__ = punescape(color__, s['attrs'] | s) + s['color'] = color__ + + + # build code heirarchy + code = Tile.merge( + Tile( (f['subsystem'], f['name']), + f.get('code', 0) if tile_code and not nil_code + else f.get('stack', 0) if tile_stack and not nil_stack + else f.get('frame', 0) if tile_frames and not nil_frames + else f.get('ctx', 0) if tile_ctx and not nil_ctx + else 1, + attrs=f) + for f in functions.values()) + + # assign colors/chars/labels to code tiles + for i, t in enumerate(code.leaves()): + # skip the top tile, yes this can happen if we have no code + if t.depth == 0: + continue + + t.color = subsystems[t.attrs['subsystem']]['color'] + + if (i, t.attrs['name']) in chars_: + char__ = chars_[i, t.attrs['name']] + if isinstance(char__, str): + # don't punescape unless we have to + if '%' in char__: + char__ = punescape(char__, t.attrs['attrs'] | t.attrs) + char__ = char__[0] # limit to 1 char + t.char = char__ + elif braille or dots: + t.char = True + elif len(t.attrs['subsystem']) < len(t.attrs['name']): + t.char = (t.attrs['name'][len(t.attrs['subsystem']):].lstrip('_') + or '')[0] + else: + t.char = (t.attrs['subsystem'].rstrip('_').rsplit('_', 1)[-1] + or '')[0] + + if (i, t.attrs['name']) in labels_: + label__ = labels_[i, t.attrs['name']] + # don't punescape unless we have to + if '%' in label__: + label__ = punescape(label__, t.attrs['attrs'] | t.attrs) + t.label = label__ + else: + t.label = t.attrs['name'] + + # scale width/height if requested now that we have our data + if (to_scale is not None + and (width is None or height is None)): + total_value = (totals.get('code', 0) if tile_code + else totals.get('stack', 0) if tile_stack + else totals.get('frame', 0) if tile_frames + else totals.get('ctx', 0) if tile_ctx + else totals.get('count', 0)) + if total_value: + # don't include header in scale + width__ = width_ + height__ = height_ - (1 if not no_header else 0) + + # scale width only + if height is not None: + width__ = mt.ceil((total_value * to_scale) / max(height__, 1)) + # scale height only + elif width is not None: + height__ = mt.ceil((total_value * to_scale) / max(width__, 1)) + # scale based on aspect-ratio + else: + width__ = mt.ceil(mt.sqrt(total_value * to_scale * to_ratio)) + height__ = mt.ceil((total_value * to_scale) / max(width__, 1)) + + width_ = width__ + height_ = height__ + (1 if not no_header else 0) + + # as a special case, if height is implicit and we have nothing to + # show, don't print anything + if height is None and nil_code and nil_frames and nil_ctx: + height_ = 1 if not no_header else 0 + + # our general purpose partition function + def partition(tile, **args): + x__ = tile.x + y__ = tile.y + width__ = tile.width + height__ = tile.height + + # partition via requested scheme + if tile.children: + if args.get('binary'): + partition_binary(tile.children, tile.value, + x__, y__, width__, height__) + elif (args.get('slice') + or (args.get('slice_and_dice') and (tile.depth & 1) == 0) + or (args.get('dice_and_slice') and (tile.depth & 1) == 1)): + partition_slice(tile.children, tile.value, + x__, y__, width__, height__) + elif (args.get('dice') + or (args.get('slice_and_dice') and (tile.depth & 1) == 1) + or (args.get('dice_and_slice') and (tile.depth & 1) == 0)): + partition_dice(tile.children, tile.value, + x__, y__, width__, height__) + elif (args.get('squarify') + or args.get('squarify_ratio') + or args.get('rectify')): + partition_squarify(tile.children, tile.value, + x__, y__, width__, height__, + aspect_ratio=( + args['squarify_ratio'] + if args.get('squarify_ratio') + else width_/height_ + if args.get('rectify') + else 1/1)) + else: + # default to binary partitioning + partition_binary(tile.children, tile.value, + x__, y__, width__, height__) + + # recursively partition + for t in tile.children: + partition(t, **args) + + # create a canvas + canvas = Canvas( + width_, + height_ - (1 if not no_header else 0), + color=color, + dots=dots, + braille=braille) + + # sort and partition code + code.sort() + code.x = 0 + code.y = 0 + code.width = canvas.width + code.height = canvas.height + partition(code, **args) + # align to pixel boundaries + code.align() + + + # render to canvas + labels__ = [] + for t in code.leaves(): + x__ = t.x + y__ = t.y + width__ = t.width + height__ = t.height + # skip anything with zero weight/height after aligning things + if width__ == 0 or height__ == 0: + continue + + # flip y + y__ = canvas.height - (y__+height__) + + canvas.rect(x__, y__, width__, height__, + # default to first letter of the last part of the key + char=(t.char if getattr(t, 'char', None) is not None + else True if braille or dots + else t.key[len(by)-1][0] if t.key and t.key[len(by)-1] + else chars_.get(0)), + color=t.color if t.color is not None else colors_.get(0)) + + if label or (labels and not no_label): + if t.label is not None: + label__ = t.label + else: + label__ = ','.join(t.key) + + # render these later so they get priority + labels__.append((x__, y__+height__-1, label__, + width__, height__)) + + for label__ in labels__: + canvas.label(*label__) + + # print some summary info + if not no_header: + if title: + ring.writeln(punescape(title, totals['attrs'] | totals)) + else: + ring.writeln('code %d stack %s ctx %d' % ( + totals.get('code', 0), + (lambda s: '∞' if mt.isinf(s) else s)( + totals.get('stack', 0)), + totals.get('ctx', 0))) + + # draw canvas + for row in range(canvas.height//canvas.yscale): + line = canvas.draw(row) + ring.writeln(line) + + if (args.get('error_on_recursion') + and mt.isinf(totals.get('stack', 0))): + sys.exit(2) + + +def main(paths, *, + width=None, + height=None, + no_header=None, + keep_open=False, + lines=None, + head=False, + cat=False, + wait=False, + **args): + # keep-open? + if keep_open: + try: + # keep track of history if lines specified + if lines is not None: + ring = RingIO(lines+1 + if not no_header and lines > 0 + else lines) + while True: + # register inotify before running the command, this avoids + # modification race conditions + if Inotify: + inotify = Inotify(paths) + + # cat? write directly to stdout + if cat: + main_(sys.stdout, paths, + width=width, + # make space for shell prompt + height=-1 if height is ... else height, + no_header=no_header, + **args) + # not cat? write to a bounded ring + else: + ring_ = RingIO(head=head) + main_(ring_, paths, + width=width, + height=0 if height is ... else height, + no_header=no_header, + **args) + # no history? draw immediately + if lines is None: + ring_.draw() + # history? merge with previous lines + else: + # write header separately? + if not no_header: + if not ring.lines: + ring.lines.append('') + ring.lines.extend(it.islice(ring_.lines, 1, None)) + ring.lines[0] = ring_.lines[0] + else: + ring.lines.extend(ring_.lines) + ring.draw() + + # try to inotifywait + if Inotify: + inotify.read() + inotify.close() + # sleep a minimum amount of time to avoid flickering + time.sleep(wait if wait is not None + else 2 if not Inotify + else 0.01) + except KeyboardInterrupt: + pass + + if not cat: + sys.stdout.write('\n') + + # single-pass? + else: + main_(sys.stdout, paths, + width=width, + # make space for shell prompt + height=-1 if height is ... else height, + no_header=no_header, + **args) + + +if __name__ == "__main__": + import argparse + import sys + parser = argparse.ArgumentParser( + description="Render code info as a treemap.", + allow_abbrev=False) + class AppendPath(argparse.Action): + def __call__(self, parser, namespace, value, option): + if getattr(namespace, 'paths', None) is None: + namespace.paths = [] + if value is None: + pass + elif isinstance(value, str): + namespace.paths.append(value) + else: + namespace.paths.extend(value) + parser.add_argument( + 'obj_paths', + nargs='*', + action=AppendPath, + help="Input *.o files.") + parser.add_argument( + 'ci_paths', + nargs='*', + action=AppendPath, + help="Input *.ci files.") + parser.add_argument( + 'csv_paths', + nargs='*', + action=AppendPath, + help="Input *.csv files.") + parser.add_argument( + 'json_paths', + nargs='*', + action=AppendPath, + help="Input *.json files.") + parser.add_argument( + '-_', '--namespace-depth', + nargs='?', + type=lambda x: int(x, 0), + const=0, + help="Number of underscore-separated namespaces to partition by. " + "0 treats every function as its own subsystem, while -1 uses " + "the longest matching prefix. Defaults to 2, which is " + "probably a good level of detail for most standalone " + "libraries.") + parser.add_argument( + '-v', '--verbose', + action='store_true', + help="Output commands that run behind the scenes.") + parser.add_argument( + '-L', '--add-label', + dest='labels', + action='append', + type=lambda x: ( + lambda ks, v: ( + tuple(k.strip() for k in ks.split(',')), + v.strip()) + )(*x.split('=', 1)) + if '=' in x else x.strip(), + help="Add a label to use. Can be assigned to a specific " + "function/subsystem. Accepts %% modifiers.") + parser.add_argument( + '-.', '--add-char', '--chars', + dest='chars', + action='append', + type=lambda x: ( + lambda ks, v: ( + tuple(k.strip() for k in ks.split(',')), + v.strip()) + )(*x.split('=', 1)) + if '=' in x else x.strip(), + help="Add characters to use. Can be assigned to a specific " + "function/subsystem. Accepts %% modifiers.") + parser.add_argument( + '-C', '--add-color', + dest='colors', + action='append', + type=lambda x: ( + lambda ks, v: ( + tuple(k.strip() for k in ks.split(',')), + v.strip()) + )(*x.split('=', 1)) + if '=' in x else x.strip(), + help="Add a color to use. Can be assigned to a specific " + "function/subsystem. Accepts %% modifiers.") + parser.add_argument( + '--color', + choices=['never', 'always', 'auto'], + default='auto', + help="When to use terminal colors. Defaults to 'auto'.") + parser.add_argument( + '-:', '--dots', + action='store_true', + help="Use 1x2 ascii dot characters.") + parser.add_argument( + '-⣿', '--braille', + action='store_true', + help="Use 2x4 unicode braille characters. Note that braille " + "characters sometimes suffer from inconsistent widths.") + parser.add_argument( + '-W', '--width', + nargs='?', + type=lambda x: int(x, 0), + const=0, + help="Width in columns. <=0 uses the terminal width. Defaults " + "to min(terminal, 80).") + parser.add_argument( + '-H', '--height', + nargs='?', + type=lambda x: int(x, 0), + const=..., # handles shell prompt spacing, which is a bit subtle + help="Height in rows. <=0 uses the terminal height. Defaults " + "to 1.") + parser.add_argument( + '--no-header', + action='store_true', + help="Don't show the header.") + parser.add_argument( + '--tile-code', + action='store_true', + help="Tile based on code size. This is the default.") + parser.add_argument( + '--tile-stack', + action='store_true', + help="Tile based on stack limits.") + parser.add_argument( + '--tile-frames', + action='store_true', + help="Tile based on stack frames.") + parser.add_argument( + '--tile-ctx', + action='store_true', + help="Tile based on function context.") + parser.add_argument( + '--tile-1', + action='store_true', + help="Tile functions evenly.") + parser.add_argument( + '--binary', + action='store_true', + help="Use the binary partitioning scheme. This attempts to " + "recursively subdivide the tiles into a roughly " + "weight-balanced binary tree. This is the default.") + parser.add_argument( + '--slice', + action='store_true', + help="Use the slice partitioning scheme. This simply slices " + "tiles vertically.") + parser.add_argument( + '--dice', + action='store_true', + help="Use the dice partitioning scheme. This simply slices " + "tiles horizontally.") + parser.add_argument( + '--slice-and-dice', + action='store_true', + help="Use the slice-and-dice partitioning scheme. This " + "alternates between slicing and dicing each layer.") + parser.add_argument( + '--dice-and-slice', + action='store_true', + help="Use the dice-and-slice partitioning scheme. This is like " + "slice-and-dice, but flipped.") + parser.add_argument( + '--squarify', + action='store_true', + help="Use the squarify partitioning scheme. This is a greedy " + "algorithm created by Mark Bruls et al that tries to " + "minimize tile aspect ratios.") + parser.add_argument( + '--rectify', + action='store_true', + help="Use the rectify partitioning scheme. This is like " + "squarify, but tries to match the aspect ratio of the " + "window.") + parser.add_argument( + '--squarify-ratio', + type=lambda x: ( + (lambda a, b: a / b)(*(float(v) for v in x.split(':', 1))) + if ':' in x else float(x)), + help="Specify an explicit aspect ratio for the squarify " + "algorithm. Implies --squarify.") + parser.add_argument( + '--to-scale', + nargs='?', + type=lambda x: ( + (lambda a, b: a / b)(*(float(v) for v in x.split(':', 1))) + if ':' in x else float(x)), + const=1, + help="Scale the resulting treemap such that 1 char ~= 1/scale " + "units. Defaults to scale=1. ") + parser.add_argument( + '--to-ratio', + type=lambda x: ( + (lambda a, b: a / b)(*(float(v) for v in x.split(':', 1))) + if ':' in x else float(x)), + help="Aspect ratio to use with --to-scale. Defaults to 1:1.") + parser.add_argument( + '--tiny', + action='store_true', + help="Tiny mode, alias for --to-scale=1 and --no-header.") + parser.add_argument( + '--title', + help="Add a title. Accepts %% modifiers.") + parser.add_argument( + '-l', '--label', + action='store_true', + help="Render labels.") + parser.add_argument( + '--no-label', + action='store_true', + help="Don't render any labels.") + parser.add_argument( + '-e', '--error-on-recursion', + action='store_true', + help="Error if any functions are recursive.") + parser.add_argument( + '--code-path', + type=lambda x: x.split(), + default=CODE_PATH, + help="Path to the code.py script, may include flags. " + "Defaults to %r." % CODE_PATH) + parser.add_argument( + '--stack-path', + type=lambda x: x.split(), + default=STACK_PATH, + help="Path to the stack.py script, may include flags. " + "Defaults to %r." % STACK_PATH) + parser.add_argument( + '--ctx-path', + type=lambda x: x.split(), + default=CTX_PATH, + help="Path to the ctx.py script, may include flags. " + "Defaults to %r." % CTX_PATH) + parser.add_argument( + '-k', '--keep-open', + action='store_true', + help="Continue to open and redraw the CSV files in a loop.") + parser.add_argument( + '-n', '--lines', + nargs='?', + type=lambda x: int(x, 0), + const=0, + help="Show this many lines of history. <=0 uses the terminal " + "height. Defaults to 1.") + parser.add_argument( + '-^', '--head', + action='store_true', + help="Show the first n lines.") + parser.add_argument( + '-c', '--cat', + action='store_true', + help="Pipe directly to stdout.") + parser.add_argument( + '-w', '--wait', + type=float, + help="Time in seconds to sleep between redraws when running " + "with -k. Defaults to 2 seconds.") + sys.exit(main(**{k: v + for k, v in vars(parser.parse_intermixed_args()).items() + if v is not None})) diff --git a/scripts/codemapsvg.py b/scripts/codemapsvg.py new file mode 100755 index 000000000..6e4516403 --- /dev/null +++ b/scripts/codemapsvg.py @@ -0,0 +1,2457 @@ +#!/usr/bin/env python3 +# +# Inspired by d3 and brendangregg's flamegraph svg: +# - https://d3js.org +# - https://github.com/brendangregg/FlameGraph +# + +# prevent local imports +if __name__ == "__main__": + __import__('sys').path.pop(0) + +import bisect +import collections as co +import csv +import fnmatch +import itertools as it +import json +import math as mt +import re +import shlex +import shutil +import subprocess as sp + + +# some nicer colors borrowed from Seaborn +# note these include a non-opaque alpha +COLORS = [ + '#7995c4', # was '#4c72b0bf', # blue + '#e6a37d', # was '#dd8452bf', # orange + '#80be8e', # was '#55a868bf', # green + '#d37a7d', # was '#c44e52bf', # red + '#a195c6', # was '#8172b3bf', # purple + '#ae9a88', # was '#937860bf', # brown + '#e3a8d2', # was '#da8bc3bf', # pink + '#a9a9a9', # was '#8c8c8cbf', # gray + '#d9cb97', # was '#ccb974bf', # yellow + '#8bc8da', # was '#64b5cdbf', # cyan +] +COLORS_DARK = [ + '#7997b7', # was '#a1c9f4bf', # blue + '#bf8761', # was '#ffb482bf', # orange + '#6aac79', # was '#8de5a1bf', # green + '#bf7774', # was '#ff9f9bbf', # red + '#9c8cbf', # was '#d0bbffbf', # purple + '#a68c74', # was '#debb9bbf', # brown + '#bb84ab', # was '#fab0e4bf', # pink + '#9b9b9b', # was '#cfcfcfbf', # gray + '#bfbe7a', # was '#fffea3bf', # yellow + '#8bb5b4', # was '#b9f2f0bf', # cyan +] + +WIDTH = 750 +HEIGHT = 350 +FONT = ['sans-serif'] +FONT_SIZE = 10 + +CODE_PATH = ['./scripts/code.py'] +STACK_PATH = ['./scripts/stack.py'] +CTX_PATH = ['./scripts/ctx.py'] + +SI_PREFIXES = { + 18: 'E', + 15: 'P', + 12: 'T', + 9: 'G', + 6: 'M', + 3: 'K', + 0: '', + -3: 'm', + -6: 'u', + -9: 'n', + -12: 'p', + -15: 'f', + -18: 'a', +} + +SI2_PREFIXES = { + 60: 'Ei', + 50: 'Pi', + 40: 'Ti', + 30: 'Gi', + 20: 'Mi', + 10: 'Ki', + 0: '', + -10: 'mi', + -20: 'ui', + -30: 'ni', + -40: 'pi', + -50: 'fi', + -60: 'ai', +} + + +# open with '-' for stdin/stdout +def openio(path, mode='r', buffering=-1): + import os + if path == '-': + if 'r' in mode: + return os.fdopen(os.dup(sys.stdin.fileno()), mode, buffering) + else: + return os.fdopen(os.dup(sys.stdout.fileno()), mode, buffering) + else: + return open(path, mode, buffering) + +def iself(path): + # check for an elf file's magic string (\x7fELF) + with open(path, 'rb') as f: + return f.read(4) == b'\x7fELF' + +# parse different data representations +def dat(x, *args): + try: + # allow the first part of an a/b fraction + if '/' in x: + x, _ = x.split('/', 1) + + # first try as int + try: + return int(x, 0) + except ValueError: + pass + + # then try as float + try: + return float(x) + except ValueError: + pass + + # else give up + raise ValueError("invalid dat %r" % x) + + # default on error? + except ValueError as e: + if args: + return args[0] + else: + raise + +# a representation of optionally key-mapped attrs +class CsvAttr: + def __init__(self, attrs, defaults=None): + if attrs is None: + attrs = [] + if isinstance(attrs, dict): + attrs = attrs.items() + + # normalize + self.attrs = [] + self.keyed = co.OrderedDict() + for attr in attrs: + if not isinstance(attr, tuple): + attr = ((), attr) + if attr[0] in {None, (), (None,), ('*',)}: + attr = ((), attr[1]) + if not isinstance(attr[0], tuple): + attr = ((attr[0],), attr[1]) + + self.attrs.append(attr) + if attr[0] not in self.keyed: + self.keyed[attr[0]] = [] + self.keyed[attr[0]].append(attr[1]) + + # create attrs object for defaults + if isinstance(defaults, CsvAttr): + self.defaults = defaults + elif defaults is not None: + self.defaults = CsvAttr(defaults) + else: + self.defaults = None + + def __repr__(self): + if self.defaults is None: + return 'CsvAttr(%r)' % ( + [(','.join(attr[0]), attr[1]) + for attr in self.attrs]) + else: + return 'CsvAttr(%r, %r)' % ( + [(','.join(attr[0]), attr[1]) + for attr in self.attrs], + [(','.join(attr[0]), attr[1]) + for attr in self.defaults.attrs]) + + def __iter__(self): + if () in self.keyed: + return it.cycle(self.keyed[()]) + elif self.defaults is not None: + return iter(self.defaults) + else: + return iter(()) + + def __bool__(self): + return bool(self.attrs) + + def __getitem__(self, key): + if isinstance(key, tuple): + if len(key) > 0 and not isinstance(key[0], str): + i, key = key + if not isinstance(key, tuple): + key = (key,) + else: + i, key = 0, key + elif isinstance(key, str): + i, key = 0, (key,) + else: + i, key = key, () + + # try to lookup by key + best = None + for ks, vs in self.keyed.items(): + prefix = [] + for j, k in enumerate(ks): + if j < len(key) and fnmatch.fnmatchcase(key[j], k): + prefix.append(k) + else: + prefix = None + break + + if prefix is not None and ( + best is None or len(prefix) >= len(best[0])): + best = (prefix, vs) + + if best is not None: + # cycle based on index + return best[1][i % len(best[1])] + + # fallback to defaults? + if self.defaults is not None: + return self.defaults[i, key] + + raise KeyError(i, key) + + def get(self, key, default=None): + try: + return self.__getitem__(key) + except KeyError: + return default + + def __contains__(self, key): + try: + self.__getitem__(key) + return True + except KeyError: + return False + + # get all results for a given key + def getall(self, key, default=None): + if not isinstance(key, tuple): + key = (key,) + + # try to lookup by key + best = None + for ks, vs in self.keyed.items(): + prefix = [] + for j, k in enumerate(ks): + if j < len(key) and fnmatch.fnmatchcase(key[j], k): + prefix.append(k) + else: + prefix = None + break + + if prefix is not None and ( + best is None or len(prefix) >= len(best[0])): + best = (prefix, vs) + + if best is not None: + return best[1] + + # fallback to defaults? + if self.defaults is not None: + return self.defaults.getall(key, default) + + raise default + + # a key function for sorting by key order + def key(self, key): + if not isinstance(key, tuple): + key = (key,) + + best = None + for i, ks in enumerate(self.keyed.keys()): + prefix = [] + for j, k in enumerate(ks): + if j < len(key) and (not k or key[j] == k): + prefix.append(k) + else: + prefix = None + break + + if prefix is not None and ( + best is None or len(prefix) >= len(best[0])): + best = (prefix, i) + + if best is not None: + return best[1] + + # fallback to defaults? + if self.defaults is not None: + return len(self.keyed) + self.defaults.key(key) + + return len(self.keyed) + +# SI-prefix formatter +def si(x): + if x == 0: + return '0' + # figure out prefix and scale + p = 3*mt.floor(mt.log(abs(x), 10**3)) + p = min(18, max(-18, p)) + # format with 3 digits of precision + s = '%.3f' % (abs(x) / (10.0**p)) + s = s[:3+1] + # truncate but only digits that follow the dot + if '.' in s: + s = s.rstrip('0') + s = s.rstrip('.') + return '%s%s%s' % ('-' if x < 0 else '', s, SI_PREFIXES[p]) + +# SI-prefix formatter for powers-of-two +def si2(x): + if x == 0: + return '0' + # figure out prefix and scale + p = 10*mt.floor(mt.log(abs(x), 2**10)) + p = min(30, max(-30, p)) + # format with 3 digits of precision + s = '%.3f' % (abs(x) / (2.0**p)) + s = s[:3+1] + # truncate but only digits that follow the dot + if '.' in s: + s = s.rstrip('0') + s = s.rstrip('.') + return '%s%s%s' % ('-' if x < 0 else '', s, SI2_PREFIXES[p]) + +# parse %-escaped strings +# +# attrs can override __getitem__ for lazy attr generation +def punescape(s, attrs=None): + pattern = re.compile( + '%[%n]' + '|' '%x..' + '|' '%u....' + '|' '%U........' + '|' '%\((?P[^)]*)\)' + '(?P[+\- #0-9\.]*[siIdboxXfFeEgG])') + def unescape(m): + if m.group()[1] == '%': return '%' + elif m.group()[1] == 'n': return '\n' + elif m.group()[1] == 'x': return chr(int(m.group()[2:], 16)) + elif m.group()[1] == 'u': return chr(int(m.group()[2:], 16)) + elif m.group()[1] == 'U': return chr(int(m.group()[2:], 16)) + elif m.group()[1] == '(': + if attrs is not None: + try: + v = attrs[m.group('field')] + except KeyError: + return m.group() + else: + return m.group() + f = m.group('format') + if f[-1] in 'dboxX': + if isinstance(v, str): + v = dat(v, 0) + v = int(v) + elif f[-1] in 'iIfFeEgG': + if isinstance(v, str): + v = dat(v, 0) + v = float(v) + if f[-1] in 'iI': + v = (si if 'i' in f[-1] else si2)(v) + f = f.replace('i', 's').replace('I', 's') + if '+' in f and not v.startswith('-'): + v = '+'+v + f = f.replace('+', '').replace('-', '') + else: + f = ('<' if '-' in f else '>') + f.replace('-', '') + v = str(v) + # note we need Python's new format syntax for binary + return ('{:%s}' % f).format(v) + else: assert False + + return re.sub(pattern, unescape, s) + + + +# a type to represent tiles +class Tile: + def __init__(self, key, children, *, + x=None, y=None, width=None, height=None, + depth=None, + attrs=None, + label=None, + color=None): + self.key = key + if isinstance(children, list): + self.children = children + self.value = sum(c.value for c in children) + else: + self.children = [] + self.value = children + + self.x = x + self.y = y + self.width = width + self.height = height + self.depth = depth + self.attrs = attrs + self.label = label + self.color = color + + def __repr__(self): + return 'Tile(%r, %r, x=%r, y=%r, width=%r, height=%r)' % ( + ','.join(self.key), self.value, + self.x, self.y, self.width, self.height) + + # recursively build heirarchy + @staticmethod + def merge(tiles, prefix=()): + # organize by 'by' field + tiles_ = co.OrderedDict() + for t in tiles: + if len(prefix)+1 >= len(t.key): + tiles_[t.key] = t + else: + key = prefix + (t.key[len(prefix)],) + if key not in tiles_: + tiles_[key] = [] + tiles_[key].append(t) + + tiles__ = [] + for key, t in tiles_.items(): + if isinstance(t, Tile): + tiles__.append(t) + else: + tiles__.append(Tile.merge(t, key)) + tiles_ = tiles__ + + return Tile(prefix, tiles_, depth=len(prefix)) + + def __lt__(self, other): + return self.value < other.value + + def __le__(self, other): + return self.value <= other.value + + def __gt__(self, other): + return self.value > other.value + + def __ge__(self, other): + return self.value >= other.value + + # recursive traversals + def tiles(self): + yield self + for child in self.children: + yield from child.tiles() + + def leaves(self): + for t in self.tiles(): + if not t.children: + yield t + + # sort recursively + def sort(self): + self.children.sort(reverse=True) + for t in self.children: + t.sort() + + # recursive align to pixel boundaries + def align(self): + # this extra +0.1 and using points instead of width/height is + # to help minimize rounding errors + x0 = int(self.x+0.1) + y0 = int(self.y+0.1) + x1 = int(self.x+self.width+0.1) + y1 = int(self.y+self.height+0.1) + self.x = x0 + self.y = y0 + self.width = x1 - x0 + self.height = y1 - y0 + + # recurse + for t in self.children: + t.align() + + # return some interesting info about these tiles + def stat(self): + leaves = list(self.leaves()) + mean = self.value / max(len(leaves), 1) + stddev = mt.sqrt(sum((t.value - mean)**2 for t in leaves) + / max(len(leaves), 1)) + min_ = min((t.value for t in leaves), default=0) + max_ = max((t.value for t in leaves), default=0) + return { + 'total': self.value, + 'mean': mean, + 'stddev': stddev, + 'min': min_, + 'max': max_, + } + + +# bounded division, limits result to dividend, useful for avoiding +# divide-by-zero issues +def bdiv(a, b): + return a / max(b, 1) + +# our partitioning schemes + +def partition_binary(children, total, x, y, width, height): + sums = [0] + for t in children: + sums.append(sums[-1] + t.value) + + # recursively partition into a roughly weight-balanced binary tree + def partition_(i, j, value, x, y, width, height): + # no child? guess we're done + if i == j: + return + # single child? assign the partition + elif i == j-1: + children[i].x = x + children[i].y = y + children[i].width = width + children[i].height = height + return + + # binary search to find best split index + target = sums[i] + (value / 2) + k = bisect.bisect(sums, target, i+1, j-1) + + # nudge split index if it results in less error + if k > i+1 and (sums[k] - target) > (target - sums[k-1]): + k -= 1 + + l = sums[k] - sums[i] + r = value - l + + # split horizontally? + if width > height: + dx = bdiv(sums[k] - sums[i], value) * width + partition_(i, k, l, x, y, dx, height) + partition_(k, j, r, x+dx, y, width-dx, height) + + # split vertically? + else: + dy = bdiv(sums[k] - sums[i], value) * height + partition_(i, k, l, x, y, width, dy) + partition_(k, j, r, x, y+dy, width, height-dy) + + partition_(0, len(children), total, x, y, width, height) + +def partition_slice(children, total, x, y, width, height): + # give each child a slice + x_ = x + for t in children: + t.x = x_ + t.y = y + t.width = bdiv(t.value, total) * width + t.height = height + + x_ += t.width + +def partition_dice(children, total, x, y, width, height): + # give each child a slice + y_ = y + for t in children: + t.x = x + t.y = y_ + t.width = width + t.height = bdiv(t.value, total) * height + + y_ += t.height + +def partition_squarify(children, total, x, y, width, height, *, + aspect_ratio=1/1): + # this algorithm is described here: + # https://www.win.tue.nl/~vanwijk/stm.pdf + i = 0 + x_ = x + y_ = y + total_ = total + width_ = width + height_ = height + # note we don't really care about width vs height until + # actually slicing + ratio = max(aspect_ratio, 1/aspect_ratio) + + while i < len(children): + # calculate initial aspect ratio + sum_ = children[i].value + min_ = children[i].value + max_ = children[i].value + w = total_ * bdiv(ratio, + max(bdiv(width_, height_), bdiv(height_, width_))) + ratio_ = max(bdiv(max_*w, sum_**2), bdiv(sum_**2, min_*w)) + + # keep adding children to this row/col until it starts to hurt + # our aspect ratio + j = i + 1 + while j < len(children): + sum__ = sum_ + children[j].value + min__ = min(min_, children[j].value) + max__ = max(max_, children[j].value) + ratio__ = max(bdiv(max__*w, sum__**2), bdiv(sum__**2, min__*w)) + if ratio__ > ratio_: + break + + sum_ = sum__ + min_ = min__ + max_ = max__ + ratio_ = ratio__ + j += 1 + + # vertical col? dice horizontally? + if width_ > height_: + dx = bdiv(sum_, total_) * width_ + partition_dice(children[i:j], sum_, x_, y_, dx, height_) + x_ += dx + width_ -= dx + + # horizontal row? slice vertically? + else: + dy = bdiv(sum_, total_) * height_ + partition_slice(children[i:j], sum_, x_, y_, width_, dy) + y_ += dy + height_ -= dy + + # start partitioning the other direction + total_ -= sum_ + i = j + + +def collect_code(obj_paths, *, + code_path=CODE_PATH, + **args): + # note code-path may contain extra args + cmd = code_path + ['-O-'] + obj_paths + if args.get('verbose'): + print(' '.join(shlex.quote(c) for c in cmd)) + proc = sp.Popen(cmd, + stdout=sp.PIPE, + universal_newlines=True, + errors='replace', + close_fds=False) + code = json.load(proc.stdout) + proc.wait() + if proc.returncode != 0: + raise sp.CalledProcessError(proc.returncode, proc.args) + + return code + +def collect_stack(ci_paths, *, + stack_path=STACK_PATH, + **args): + # note stack-path may contain extra args + cmd = stack_path + ['-O-', '--depth=2'] + ci_paths + if args.get('verbose'): + print(' '.join(shlex.quote(c) for c in cmd)) + proc = sp.Popen(cmd, + stdout=sp.PIPE, + universal_newlines=True, + errors='replace', + close_fds=False) + stack = json.load(proc.stdout) + proc.wait() + if proc.returncode != 0: + raise sp.CalledProcessError(proc.returncode, proc.args) + + return stack + +def collect_ctx(obj_paths, *, + ctx_path=CTX_PATH, + **args): + # note stack-path may contain extra args + cmd = ctx_path + ['-O-', '--depth=2', '--internal'] + obj_paths + if args.get('verbose'): + print(' '.join(shlex.quote(c) for c in cmd)) + proc = sp.Popen(cmd, + stdout=sp.PIPE, + universal_newlines=True, + errors='replace', + close_fds=False) + ctx = json.load(proc.stdout) + proc.wait() + if proc.returncode != 0: + raise sp.CalledProcessError(proc.returncode, proc.args) + + return ctx + + +def main(paths, output, *, + namespace_depth=2, + quiet=False, + labels=[], + colors=[], + width=None, + height=None, + no_header=False, + no_mode=False, + no_stack=False, + stack_ratio=1/5, + no_ctx=False, + no_frames=False, + tile_code=False, + tile_stack=False, + tile_frames=False, + tile_ctx=False, + tile_1=False, + no_javascript=False, + mode_callgraph=False, + mode_deepest=False, + mode_callees=False, + mode_callers=False, + to_scale=None, + to_ratio=1/1, + title=None, + padding=1, + no_label=False, + tiny=False, + nested=False, + dark=False, + font=FONT, + font_size=FONT_SIZE, + background=None, + **args): + # tiny mode? + if tiny: + if to_scale is None: + to_scale = 1 + no_header = True + no_label = True + no_stack = True + no_javascript = True + + # default to tiling based on code + if (not tile_code + and not tile_stack + and not tile_frames + and not tile_ctx + and not tile_1): + tile_code = True + + # default to all modes + if (not mode_callgraph + and not mode_deepest + and not mode_callees + and not mode_callers): + mode_callgraph = True + mode_deepest = True + mode_callees = True + mode_callers = True + + # what colors/labels to use? + colors_ = CsvAttr(colors, defaults=COLORS_DARK if dark else COLORS) + + labels_ = CsvAttr(labels) + + if background is not None: + background_ = background + elif dark: + background_ = '#000000' + else: + background_ = '#ffffff' + + # figure out width/height + if width is not None: + width_ = width + else: + width_ = WIDTH + + if height is not None: + height_ = height + else: + height_ = HEIGHT + + # try to parse files as CSV/JSON + results = [] + try: + # if any file starts with elf magic (\x7fELF), assume input is + # elf/callgraph files + fs = [] + for path in paths: + f = openio(path) + if f.buffer.peek(4)[:4] == b'\x7fELF': + for f_ in fs: + f_.close() + raise StopIteration() + fs.append(f) + + for f in fs: + with f: + # csv or json? assume json starts with [ + is_json = (f.buffer.peek(1)[:1] == b'[') + + # read csv? + if not is_json: + results.extend(csv.DictReader(f, restval='')) + + # read json? + else: + results.extend(json.load(f)) + + # fall back to extracting code/stack/ctx info from elf/callgraph files + except StopIteration: + # figure out paths + obj_paths = [] + ci_paths = [] + for path in paths: + if iself(path): + obj_paths.append(path) + else: + ci_paths.append(path) + + # find code/stack/ctx sizes + if obj_paths: + results.extend(collect_code(obj_paths, **args)) + if ci_paths: + results.extend(collect_stack(ci_paths, **args)) + if obj_paths: + results.extend(collect_ctx(obj_paths, **args)) + + # don't render code/stack/ctx results if we don't have any + nil_code = not any('code_size' in r for r in results) + nil_stack = not any('stack_limit' in r for r in results) + nil_frames = not any('stack_frame' in r for r in results) + nil_ctx = not any('ctx_size' in r for r in results) + + if nil_frames: + no_frames = True + if nil_ctx: + no_ctx = True + + if no_frames and no_ctx: + no_stack = True + + # merge code/stack/ctx results + functions = co.OrderedDict() + for r in results: + if 'function' not in r: + continue + if r['function'] not in functions: + functions[r['function']] = {'name': r['function']} + # code things + if 'code_size' in r: + functions[r['function']]['code'] = dat(r['code_size']) + # stack things, including callgraph + if 'stack_frame' in r: + functions[r['function']]['frame'] = dat(r['stack_frame']) + if 'stack_limit' in r: + functions[r['function']]['stack'] = dat(r['stack_limit'], mt.inf) + if 'children' in r: + if 'children' not in functions[r['function']]: + functions[r['function']]['children'] = [] + functions[r['function']]['children'].extend( + r_['function'] + for r_ in r['children'] + if r_.get('stack_frame', '') != '') + # ctx things, including any arguments + if 'ctx_size' in r: + functions[r['function']]['ctx'] = dat(r['ctx_size']) + if 'children' in r: + if 'args' not in functions[r['function']]: + functions[r['function']]['args'] = [] + functions[r['function']]['args'].extend( + {'name': r_['function'], + 'ctx': dat(r_['ctx_size']), + 'attrs': r_} + for r_ in r['children'] + if r_.get('ctx_size', '') != '') + # keep track of other attrs for punescaping + if 'attrs' not in functions[r['function']]: + functions[r['function']]['attrs'] = {} + functions[r['function']]['attrs'].update(r) + + # stack.py returns infinity for recursive functions, so we need to + # recompute a bounded stack limit to show something useful + def limitof(k, f, seen=set()): + # found a cycle? stop here + if k in seen: + return 0 + + limit = 0 + for child in f.get('children', []): + if child not in functions: + continue + limit = max(limit, limitof(child, functions[child], seen | {k})) + + return f['frame'] + limit + + for k, f in functions.items(): + if 'stack' in f: + if mt.isinf(f['stack']): + f['limit'] = limitof(k, f) + else: + f['limit'] = f['stack'] + + # organize into subsystems + namespace_pattern = re.compile('_*[^_]+(?:_*$)?') + namespace_slice = slice(namespace_depth if namespace_depth else None) + subsystems = {} + for k, f in functions.items(): + # ignore leading/trailing underscores + f['subsystem'] = ''.join( + namespace_pattern.findall(k)[ + namespace_slice]) + + if f['subsystem'] not in subsystems: + subsystems[f['subsystem']] = {'name': f['subsystem']} + + # include ctx in subsystems to give them different colors + for _, f in functions.items(): + for a in f.get('args', []): + a['subsystem'] = a['name'] + + if a['subsystem'] not in subsystems: + subsystems[a['subsystem']] = {'name': a['subsystem']} + + # sort to try to keep things reproducible + functions = co.OrderedDict(sorted(functions.items())) + subsystems = co.OrderedDict(sorted(subsystems.items())) + + # sum code/stack/ctx/attrs for punescaping + for k, s in subsystems.items(): + s['code'] = sum( + f.get('code', 0) for f in functions.values() + if f['subsystem'] == k) + s['stack'] = max( + (f.get('stack', 0) for f in functions.values() + if f['subsystem'] == k), + default=0) + s['ctx'] = max( + (f.get('ctx', 0) for f in functions.values() + if f['subsystem'] == k), + default=0) + s['attrs'] = {k_: v_ + for f in functions.values() + if f['subsystem'] == k + for k_, v_ in f['attrs'].items()} + + # also build totals + totals = {} + totals['code'] = sum( + f.get('code', 0) for f in functions.values()) + totals['stack'] = max( + (f.get('stack', 0) for f in functions.values()), + default=0) + totals['ctx'] = max( + (f.get('ctx', 0) for f in functions.values()), + default=0) + totals['count'] = len(functions) + totals['attrs'] = {k: v + for f in functions.values() + for k, v in f['attrs'].items()} + + # assign colors to subsystems, note this is after sorting, but + # before tile generation, we want code and stack tiles to have the + # same color if they're in the same subsystem + for i, (k, s) in enumerate(subsystems.items()): + color__ = colors_[i, k] + # don't punescape unless we have to + if '%' in color__: + color__ = punescape(color__, s['attrs'] | s) + s['color'] = color__ + + + # build code heirarchy + code = Tile.merge( + Tile( (f['subsystem'], f['name']), + f.get('code', 0) if tile_code and not nil_code + else f.get('stack', 0) if tile_stack and not nil_stack + else f.get('frame', 0) if tile_frames and not nil_frames + else f.get('ctx', 0) if tile_ctx and not nil_ctx + else 1, + attrs=f) + for f in functions.values()) + + # assign colors/labels to code tiles + for i, t in enumerate(code.leaves()): + # skip the top tile, yes this can happen if we have no code + if t.depth == 0: + continue + + t.color = subsystems[t.attrs['subsystem']]['color'] + + if (i, t.attrs['name']) in labels_: + label__ = labels_[i, t.attrs['name']] + # don't punescape unless we have to + if '%' in label__: + label__ = punescape(label__, t.attrs['attrs'] | t.attrs) + t.label = label__ + else: + t.label = '%s%s%s%s' % ( + t.attrs['name'], + '\ncode %d' % t.attrs.get('code', 0) + if not nil_code else '', + '\nstack %s' % (lambda s: '∞' if mt.isinf(s) else s)( + t.attrs.get('stack', 0)) + if not nil_frames else '', + '\nctx %d' % t.attrs.get('ctx', 0) + if not nil_ctx else '') + + # build stack heirarchies + if not no_stack and not no_frames: + stacks = co.OrderedDict() + for k, f in functions.items(): + stack = [] + def rec(f, seen=set()): + if f['name'] in seen: + stack.append(f) + return + seen.add(f['name']) + + stack.append(f) + + if f.get('children'): + hot = max(f['children'], key=lambda k: + functions[k].get('limit', 0) + if k not in seen else -1) + rec(functions[hot], seen) + rec(f) + + stacks[k] = Tile.merge( + Tile( (f['name'],), + f.get('frame', 0), + attrs=f) + for f in stack) + + # assign colors/labels to stack tiles + for i, t in enumerate(stacks[k].leaves()): + t.color = subsystems[t.attrs['subsystem']]['color'] + if (i, t.attrs['name']) in labels_: + label__ = labels_[i, t.attrs['name']] + # don't punescape unless we have to + if '%' in label__: + label__ = punescape(label__, + t.attrs['attrs'] | t.attrs) + t.label = label__ + else: + t.label = '%s\nframe %d' % ( + t.attrs['name'], + t.attrs.get('frame', 0)) + + # build ctx heirarchies + if not no_stack and not no_ctx: + ctxs = co.OrderedDict() + for k, f in functions.items(): + if f.get('args'): + args_ = f['args'] + else: + args_ = [{ + 'name': k, + 'subsystem': f['subsystem'], + 'ctx': f.get('ctx', 0), + 'attrs': f}] + + ctxs[k] = Tile.merge( + Tile( (a['name'],), + a.get('ctx', 0), + attrs=a) + for a in args_) + + # assign colors/labels to ctx tiles + for i, t in enumerate(ctxs[k].leaves()): + t.color = subsystems[t.attrs['subsystem']]['color'] + if (i, t.attrs['name']) in labels_: + label__ = labels_[i, t.attrs['name']] + # don't punescape unless we have to + if '%' in label__: + label__ = punescape(label__, + t.attrs['attrs'] | t.attrs) + t.label = label__ + else: + t.label = '%s\nctx %d' % ( + t.attrs['name'], + t.attrs.get('ctx', 0)) + + # scale width/height if requested now that we have our data + if (to_scale is not None + and (width is None or height is None)): + total_value = (totals.get('code', 0) if tile_code + else totals.get('stack', 0) if tile_stack + else totals.get('frame', 0) if tile_frames + else totals.get('ctx', 0) if tile_ctx + else totals.get('count', 0)) + if total_value: + # don't include header/stack in scale + width__ = width_ + height__ = height_ + if not no_header: + height__ -= mt.ceil(FONT_SIZE * 1.3) + if not no_stack: + width__ *= (1 - stack_ratio) + + # scale width only + if height is not None: + width__ = mt.ceil((total_value * to_scale) / max(height__, 1)) + # scale height only + elif width is not None: + height__ = mt.ceil((total_value * to_scale) / max(width__, 1)) + # scale based on aspect-ratio + else: + width__ = mt.ceil(mt.sqrt(total_value * to_scale * to_ratio)) + height__ = mt.ceil((total_value * to_scale) / max(width__, 1)) + + if not no_stack: + width__ /= (1 - stack_ratio) + if not no_header: + height__ += mt.ceil(FONT_SIZE * 1.3) + width_ = width__ + height_ = height__ + + # our general purpose partition function + def partition(tile, **args): + if tile.depth == 0: + # apply top padding + tile.x += padding + tile.y += padding + tile.width -= min(padding, tile.width) + tile.height -= min(padding, tile.height) + # apply bottom padding + if not tile.children: + tile.width -= min(padding, tile.width) + tile.height -= min(padding, tile.height) + + x__ = tile.x + y__ = tile.y + width__ = tile.width + height__ = tile.height + + else: + # apply bottom padding + if not tile.children: + tile.width -= min(padding, tile.width) + tile.height -= min(padding, tile.height) + + x__ = tile.x + y__ = tile.y + width__ = tile.width + height__ = tile.height + + # partition via requested scheme + if tile.children: + if args.get('binary'): + partition_binary(tile.children, tile.value, + x__, y__, width__, height__) + elif (args.get('slice') + or (args.get('slice_and_dice') and (tile.depth & 1) == 0) + or (args.get('dice_and_slice') and (tile.depth & 1) == 1)): + partition_slice(tile.children, tile.value, + x__, y__, width__, height__) + elif (args.get('dice') + or (args.get('slice_and_dice') and (tile.depth & 1) == 1) + or (args.get('dice_and_slice') and (tile.depth & 1) == 0)): + partition_dice(tile.children, tile.value, + x__, y__, width__, height__) + elif (args.get('squarify') + or args.get('squarify_ratio') + or args.get('rectify')): + partition_squarify(tile.children, tile.value, + x__, y__, width__, height__, + aspect_ratio=( + args['squarify_ratio'] + if args.get('squarify_ratio') + else width_/height_ + if args.get('rectify') + else 1/1)) + else: + # default to binary partitioning + partition_binary(tile.children, tile.value, + x__, y__, width__, height__) + + # recursively partition + for t in tile.children: + partition(t, **args) + + # create space for header + x__ = 0 + y__ = 0 + width__ = width_ + height__ = height_ + if not no_header: + y__ += mt.ceil(FONT_SIZE * 1.3) + height__ -= min(mt.ceil(FONT_SIZE * 1.3), height__) + + # split code/stack + if not no_stack: + code_split = width__ * (1 - stack_ratio) + else: + code_split = width__ + + # sort and partition code + code.sort() + code.x = x__ + code.y = y__ + code.width = code_split + code.height = height__ + partition(code, **args) + # align to pixel boundaries + code.align() + + # partition stacks/ctxs + if not no_stack: + deepest = max(functions.values(), + key=lambda f: + (f.get('limit', 0) if not no_frames else 0) + + (f.get('ctx', 0) if not no_ctx else 0)) + + for k, f in functions.items(): + # scale to deepest stack/ctx + height___ = height__ * bdiv( + (f.get('limit', 0) if not no_frames else 0) + + (f.get('ctx', 0) if not no_ctx else 0), + (deepest.get('limit', 0) if not no_frames else 0) + + (deepest.get('ctx', 0) if not no_ctx else 0)) + + # split stack/ctx + ctx_split = height___ * bdiv( + (f.get('ctx', 0) if not no_ctx else 0), + (f.get('limit', 0) if not no_frames else 0) + + (f.get('ctx', 0) if not no_ctx else 0)) + + # partition ctx + if not no_ctx: + ctx = ctxs[k] + ctx.x = code.x + code.width + 1 + ctx.y = y__ + ctx.width = width__ - ctx.x + ctx.height = ctx_split + partition(ctx, slice=True) + # align to pixel boundaries + ctx.align() + + # partition stack + if not no_frames: + stack = stacks[k] + stack.x = code.x + code.width + 1 + stack.y = ctx.y + ctx.height + 1 if ctx_split > 0 else y__ + stack.width = width__ - stack.x + stack.height = height___ - (stack.y - y__) + partition(stack, dice=True) + # align to pixel boundaries + stack.align() + + + # create svg file + with openio(output, 'w') as f: + def writeln(self, s=''): + self.write(s) + self.write('\n') + f.writeln = writeln.__get__(f) + + # yes this is svg + f.write('' % dict( + width=width_, + height=height_, + font=','.join(font), + font_size=font_size, + background=background_, + user_select='none' if not no_javascript else 'auto')) + + # create header + if not no_header: + f.write('' % dict( + js= 'cursor="pointer" ' + 'onclick="click_header(this,event)">' + if not no_javascript else '')) + # add an invisible rect to make things more clickable + f.write('' % dict( + x=0, + y=0, + width=width_, + height=y__)) + f.write('') + f.write('' % dict( + color='#ffffff' if dark else '#000000')) + f.write('') + if title: + f.write(punescape(title, totals['attrs'] | totals)) + else: + f.write('code %d stack %s ctx %d' % ( + totals.get('code', 0), + (lambda s: '∞' if mt.isinf(s) else s)( + totals.get('stack', 0)), + totals.get('ctx', 0))) + f.write('') + if not no_mode and not no_javascript: + f.write('' % dict( + x=width_-3)) + f.write('mode: %s' % ( + 'callgraph' if mode_callgraph + else 'deepest' if mode_deepest + else 'callees' if mode_callees + else 'callers')) + f.write('') + f.write('') + f.write('') + + # create code tiles + for i, t in enumerate(code.leaves()): + # skip the top tile, yes this can happen if we have no code + if t.depth == 0: + continue + # skip anything with zero weight/height after aligning things + if t.width == 0 or t.height == 0: + continue + + f.write('' % dict( + name=t.attrs['name'], + x=t.x, + y=t.y, + js= 'data-name="%(name)s" ' + # precompute x/y for javascript, svg makes this + # weirdly difficult to figure out post-transform + 'data-x="%(x)d" ' + 'data-y="%(y)d" ' + 'data-width="%(width)d" ' + 'data-height="%(height)d" ' + 'onmouseenter="enter_tile(this,event)" ' + 'onmouseleave="leave_tile(this,event)" ' + 'onclick="click_tile(this,event)">' % dict( + name=t.attrs['name'], + x=t.x, + y=t.y, + width=t.width, + height=t.height) + if not no_javascript else '')) + # add an invisible rect to make things more clickable + f.write('' % dict( + width=t.width + padding, + height=t.height + padding)) + f.write('') + f.write('') + f.write(t.label) + f.write('') + f.write('' % dict( + id=i, + color=t.color, + width=t.width, + height=t.height)) + f.write('') + if not no_label: + f.write('' % i) + f.write('' % i) + f.write('') + f.write('') + f.write('' % i) + for j, l in enumerate(t.label.split('\n')): + if j == 0: + f.write('') + f.write(l) + f.write('') + else: + if t.children: + f.write('') + f.write(l) + f.write('') + else: + f.write('') + f.write(l) + f.write('') + f.write('') + f.write('') + + # create stack/ctx tiles + if not no_stack and (not no_ctx or not no_frames): + for i, k in enumerate(functions.keys()): + # only include the deepest stack if no_javascript, no reason to + # include a bunch of tiles we will never render + if no_javascript and functions[k]['name'] != deepest['name']: + continue + + # create stack group + # + # note we conveniently don't need unique ids for each ctx/frame + # tile, just for the entire stack group + f.write('' % dict( + name=k, + js= 'visibility="%(visibility)s">' % dict( + visibility="visible" + if functions[k]['name'] + == deepest['name'] + else "hidden") + if not no_javascript else '')) + + # add a separator between code/stack + f.write('' % dict( + x=code.x + code.width, + y=code.y, + width=1, + height=max( + stacks[k].y + stacks[k].height + if not no_frames else 0, + ctxs[k].y + ctxs[k].height + if not no_ctx else 0) + - code.y - padding, + color='#7f7f7f' if dark else '#555555')) + f.write('') + + # create ctx tiles + if not no_ctx: + for j, t in enumerate(ctxs[k].leaves()): + # skip anything with zero weight/height after aligning things + if t.width == 0 or t.height == 0: + continue + + f.write('' % dict( + id='%s-%s' % (i, j), + x=t.x, + y=t.y, + js= 'data-name="%(name)s" ' + 'data-func="%(func)s" ' + # precompute x/y for javascript, svg makes + # this weirdly difficult to figure out + # post-transform + 'data-x="%(x)d" ' + 'data-y="%(y)d" ' + 'data-width="%(width)d" ' + 'data-height="%(height)d" ' + 'onmouseenter="enter_tile(this,event)" ' + 'onmouseleave="leave_tile(this,event)" ' + 'onclick="click_tile(this,event)">' % dict( + name=t.attrs['name'], + func=k, + x=t.x, + y=t.y, + width=t.width, + height=t.height) + if not no_javascript else '')) + # add an invisible rect to make things more clickable + f.write('' % dict( + width=t.width + padding, + height=t.height + padding)) + f.write('') + f.write('') + f.write(t.label) + f.write('') + f.write('' % dict( + id='%s-%s' % (i, j), + color=t.color, + width=t.width, + height=t.height)) + f.write('') + if not no_label: + f.write('' % ('%s-%s' % (i, j))) + f.write('' % ('%s-%s' % (i, j))) + f.write('') + f.write('') + f.write('' % ( + '%s-%s' % (i, j))) + for j, l in enumerate(t.label.split('\n')): + if j == 0: + f.write('') + f.write(l) + f.write('') + else: + if t.children: + f.write('') + f.write(l) + f.write('') + else: + f.write('') + f.write(l) + f.write('') + f.write('') + f.write('') + + # add a separator between ctx/stack + if not no_ctx and not no_frames: + f.write('' % dict( + x=ctxs[k].x, + y=ctxs[k].y + ctxs[k].height, + width=ctxs[k].width - padding, + height=1, + color='#7f7f7f' if dark else '#555555')) + f.write('') + + # create stack tiles + if not no_frames: + for j, t in enumerate(stacks[k].leaves()): + # skip anything with zero weight/height after aligning things + if t.width == 0 or t.height == 0: + continue + + f.write('' % dict( + id='%s-%s' % (i, j), + x=t.x, + y=t.y, + js= 'data-name="%(name)s" ' + 'data-func="%(func)s" ' + # precompute x/y for javascript, svg makes + # this weirdly difficult to figure out + # post-transform + 'data-x="%(x)d" ' + 'data-y="%(y)d" ' + 'data-width="%(width)d" ' + 'data-height="%(height)d" ' + 'onmouseenter="enter_tile(this,event)" ' + 'onmouseleave="leave_tile(this,event)" ' + 'onclick="click_tile(this,event)"' % dict( + name=t.attrs['name'], + func=k, + x=t.x, + y=t.y, + width=t.width, + height=t.height) + if not no_javascript else '')) + # add an invisible rect to make things more clickable + f.write('' % dict( + width=t.width + padding, + height=t.height + padding)) + f.write('') + f.write('') + f.write(t.label) + f.write('') + f.write('' % dict( + id='%s-%s' % (i, j), + color=t.color, + width=t.width, + height=t.height)) + f.write('') + if not no_label: + f.write('' % ('%s-%s' % (i, j))) + f.write('' % ('%s-%s' % (i, j))) + f.write('') + f.write('') + f.write('' % ( + '%s-%s' % (i, j))) + for j, l in enumerate(t.label.split('\n')): + if j == 0: + f.write('') + f.write(l) + f.write('') + else: + if t.children: + f.write('') + f.write(l) + f.write('') + else: + f.write('') + f.write(l) + f.write('') + f.write('') + f.write('') + + f.write('') + + if not no_javascript: + # arrowhead for arrows + f.write('') + f.write('' % dict( + color='#000000' if dark else '#555555')) + f.write('') + f.write('') + f.write('') + + # javascript for arrows + # + # why tf does svg support javascript? + f.write('') + + f.write('') + + + # print some summary info + if not quiet: + stat = code.stat() + print('updated %s, code %d stack %s ctx %d' % ( + output, + totals.get('code', 0), + (lambda s: '∞' if mt.isinf(s) else s)( + totals.get('stack', 0)), + totals.get('ctx', 0))) + + if (args.get('error_on_recursion') + and mt.isinf(totals.get('stack', 0))): + sys.exit(2) + + +if __name__ == "__main__": + import argparse + import sys + parser = argparse.ArgumentParser( + description="Render code info as an interactive SVG treemap.", + allow_abbrev=False) + class AppendPath(argparse.Action): + def __call__(self, parser, namespace, value, option): + if getattr(namespace, 'paths', None) is None: + namespace.paths = [] + if value is None: + pass + elif isinstance(value, str): + namespace.paths.append(value) + else: + namespace.paths.extend(value) + parser.add_argument( + 'obj_paths', + nargs='*', + action=AppendPath, + help="Input *.o files.") + parser.add_argument( + 'ci_paths', + nargs='*', + action=AppendPath, + help="Input *.ci files.") + parser.add_argument( + 'csv_paths', + nargs='*', + action=AppendPath, + help="Input *.csv files.") + parser.add_argument( + 'json_paths', + nargs='*', + action=AppendPath, + help="Input *.json files.") + parser.add_argument( + '-o', '--output', + required=True, + help="Output *.svg file.") + parser.add_argument( + '-_', '--namespace-depth', + nargs='?', + type=lambda x: int(x, 0), + const=0, + help="Number of underscore-separated namespaces to partition by. " + "0 treats every function as its own subsystem, while -1 uses " + "the longest matching prefix. Defaults to 2, which is " + "probably a good level of detail for most standalone " + "libraries.") + parser.add_argument( + '-v', '--verbose', + action='store_true', + help="Output commands that run behind the scenes.") + parser.add_argument( + '-q', '--quiet', + action='store_true', + help="Don't print info.") + parser.add_argument( + '-L', '--add-label', + dest='labels', + action='append', + type=lambda x: ( + lambda ks, v: ( + tuple(k.strip() for k in ks.split(',')), + v.strip()) + )(*x.split('=', 1)) + if '=' in x else x.strip(), + help="Add a label to use. Can be assigned to a specific " + "function/subsystem. Accepts %% modifiers.") + parser.add_argument( + '-C', '--add-color', + dest='colors', + action='append', + type=lambda x: ( + lambda ks, v: ( + tuple(k.strip() for k in ks.split(',')), + v.strip()) + )(*x.split('=', 1)) + if '=' in x else x.strip(), + help="Add a color to use. Can be assigned to a specific " + "function/subsystem. Accepts %% modifiers.") + parser.add_argument( + '-W', '--width', + type=lambda x: int(x, 0), + help="Width in pixels. Defaults to %r." % WIDTH) + parser.add_argument( + '-H', '--height', + type=lambda x: int(x, 0), + help="Height in pixels. Defaults to %r." % HEIGHT) + parser.add_argument( + '--no-header', + action='store_true', + help="Don't show the header.") + parser.add_argument( + '--no-mode', + action='store_true', + help="Don't show the mode state.") + parser.add_argument( + '-S', '--no-stack', + action='store_true', + help="Don't render any stack info.") + parser.add_argument( + '-s', '--stack-ratio', + type=lambda x: ( + (lambda a, b: a / b)(*(float(v) for v in x.split(':', 1))) + if ':' in x else float(x)), + help="Ratio of width to use for stack info. Defaults to 1:5.") + parser.add_argument( + '--no-ctx', + action='store_true', + help="Don't render function context.") + parser.add_argument( + '--no-frames', + action='store_true', + help="Don't render function stack frame info.") + parser.add_argument( + '--tile-code', + action='store_true', + help="Tile based on code size. This is the default.") + parser.add_argument( + '--tile-stack', + action='store_true', + help="Tile based on stack limits.") + parser.add_argument( + '--tile-frames', + action='store_true', + help="Tile based on stack frames.") + parser.add_argument( + '--tile-ctx', + action='store_true', + help="Tile based on function context.") + parser.add_argument( + '--tile-1', + action='store_true', + help="Tile functions evenly.") + parser.add_argument( + '-J', '--no-javascript', + action='store_true', + help="Don't add javascript for interactability.") + parser.add_argument( + '--mode-callgraph', + action='store_true', + help="Include the callgraph rendering mode.") + parser.add_argument( + '--mode-deepest', + action='store_true', + help="Include the deepest rendering mode.") + parser.add_argument( + '--mode-callees', + action='store_true', + help="Include the callees rendering mode.") + parser.add_argument( + '--mode-callers', + action='store_true', + help="Include the callers rendering mode.") + parser.add_argument( + '--binary', + action='store_true', + help="Use the binary partitioning scheme. This attempts to " + "recursively subdivide the tiles into a roughly " + "weight-balanced binary tree. This is the default.") + parser.add_argument( + '--slice', + action='store_true', + help="Use the slice partitioning scheme. This simply slices " + "tiles vertically.") + parser.add_argument( + '--dice', + action='store_true', + help="Use the dice partitioning scheme. This simply slices " + "tiles horizontally.") + parser.add_argument( + '--slice-and-dice', + action='store_true', + help="Use the slice-and-dice partitioning scheme. This " + "alternates between slicing and dicing each layer.") + parser.add_argument( + '--dice-and-slice', + action='store_true', + help="Use the dice-and-slice partitioning scheme. This is like " + "slice-and-dice, but flipped.") + parser.add_argument( + '--squarify', + action='store_true', + help="Use the squarify partitioning scheme. This is a greedy " + "algorithm created by Mark Bruls et al that tries to " + "minimize tile aspect ratios.") + parser.add_argument( + '--rectify', + action='store_true', + help="Use the rectify partitioning scheme. This is like " + "squarify, but tries to match the aspect ratio of the " + "window.") + parser.add_argument( + '--squarify-ratio', + type=lambda x: ( + (lambda a, b: a / b)(*(float(v) for v in x.split(':', 1))) + if ':' in x else float(x)), + help="Specify an explicit aspect ratio for the squarify " + "algorithm. Implies --squarify.") + parser.add_argument( + '--to-scale', + nargs='?', + type=lambda x: ( + (lambda a, b: a / b)(*(float(v) for v in x.split(':', 1))) + if ':' in x else float(x)), + const=1, + help="Scale the resulting treemap such that 1 pixel ~= 1/scale " + "units. Defaults to scale=1. ") + parser.add_argument( + '--to-ratio', + type=lambda x: ( + (lambda a, b: a / b)(*(float(v) for v in x.split(':', 1))) + if ':' in x else float(x)), + help="Aspect ratio to use with --to-scale. Defaults to 1:1.") + parser.add_argument( + '--tiny', + action='store_true', + help="Tiny mode, alias for --to-scale=1, --no-header, " + "--no-label, --no-stack, and --no-javascript.") + parser.add_argument( + '--title', + help="Add a title. Accepts %% modifiers.") + parser.add_argument( + '--padding', + type=float, + help="Padding to add to each level of the treemap. Defaults to 1.") + parser.add_argument( + '--no-label', + action='store_true', + help="Don't render any labels.") + parser.add_argument( + '--dark', + action='store_true', + help="Use the dark style.") + parser.add_argument( + '--font', + type=lambda x: [x.strip() for x in x.split(',')], + help="Font family to use.") + parser.add_argument( + '--font-size', + help="Font size to use. Defaults to %r." % FONT_SIZE) + parser.add_argument( + '--background', + help="Background color to use. Note #00000000 can make the " + "background transparent.") + parser.add_argument( + '-e', '--error-on-recursion', + action='store_true', + help="Error if any functions are recursive.") + parser.add_argument( + '--code-path', + type=lambda x: x.split(), + default=CODE_PATH, + help="Path to the code.py script, may include flags. " + "Defaults to %r." % CODE_PATH) + parser.add_argument( + '--stack-path', + type=lambda x: x.split(), + default=STACK_PATH, + help="Path to the stack.py script, may include flags. " + "Defaults to %r." % STACK_PATH) + parser.add_argument( + '--ctx-path', + type=lambda x: x.split(), + default=CTX_PATH, + help="Path to the ctx.py script, may include flags. " + "Defaults to %r." % CTX_PATH) + sys.exit(main(**{k: v + for k, v in vars(parser.parse_intermixed_args()).items() + if v is not None})) diff --git a/scripts/cov.py b/scripts/cov.py index b61b2e524..4cb300f8f 100755 --- a/scripts/cov.py +++ b/scripts/cov.py @@ -4,23 +4,31 @@ # # Example: # ./scripts/cov.py \ -# lfs.t.a.gcda lfs_util.t.a.gcda \ -# -Flfs.c -Flfs_util.c -slines +# lfs.t.a.gcda lfs_util.t.a.gcda \ +# -Flfs.c -Flfs_util.c -slines # # Copyright (c) 2022, The littlefs authors. # Copyright (c) 2020, Arm Limited. All rights reserved. # SPDX-License-Identifier: BSD-3-Clause # +# prevent local imports +if __name__ == "__main__": + __import__('sys').path.pop(0) + import collections as co import csv +import fnmatch +import io import itertools as it import json -import math as m +import math as mt import os import re import shlex import subprocess as sp +import sys + # TODO use explode_asserts to avoid counting assert branches? # TODO use dwarf=info to find functions for inline functions? @@ -29,128 +37,183 @@ # integer fields -class Int(co.namedtuple('Int', 'x')): +class CsvInt(co.namedtuple('CsvInt', 'a')): __slots__ = () - def __new__(cls, x=0): - if isinstance(x, Int): - return x - if isinstance(x, str): + def __new__(cls, a=0): + if isinstance(a, CsvInt): + return a + if isinstance(a, str): try: - x = int(x, 0) + a = int(a, 0) except ValueError: # also accept +-∞ and +-inf - if re.match('^\s*\+?\s*(?:∞|inf)\s*$', x): - x = m.inf - elif re.match('^\s*-\s*(?:∞|inf)\s*$', x): - x = -m.inf + if re.match('^\s*\+?\s*(?:∞|inf)\s*$', a): + a = mt.inf + elif re.match('^\s*-\s*(?:∞|inf)\s*$', a): + a = -mt.inf else: raise - assert isinstance(x, int) or m.isinf(x), x - return super().__new__(cls, x) + if not (isinstance(a, int) or mt.isinf(a)): + a = int(a) + return super().__new__(cls, a) + + def __repr__(self): + return '%s(%r)' % (self.__class__.__name__, self.a) def __str__(self): - if self.x == m.inf: + if self.a == mt.inf: return '∞' - elif self.x == -m.inf: + elif self.a == -mt.inf: return '-∞' else: - return str(self.x) + return str(self.a) + + def __csv__(self): + if self.a == mt.inf: + return 'inf' + elif self.a == -mt.inf: + return '-inf' + else: + return repr(self.a) + + def __bool__(self): + return bool(self.a) def __int__(self): - assert not m.isinf(self.x) - return self.x + assert not mt.isinf(self.a) + return self.a def __float__(self): - return float(self.x) + return float(self.a) none = '%7s' % '-' def table(self): return '%7s' % (self,) - diff_none = '%7s' % '-' - diff_table = table - - def diff_diff(self, other): - new = self.x if self else 0 - old = other.x if other else 0 + def diff(self, other): + new = self.a if self else 0 + old = other.a if other else 0 diff = new - old - if diff == +m.inf: + if diff == +mt.inf: return '%7s' % '+∞' - elif diff == -m.inf: + elif diff == -mt.inf: return '%7s' % '-∞' else: return '%+7d' % diff def ratio(self, other): - new = self.x if self else 0 - old = other.x if other else 0 - if m.isinf(new) and m.isinf(old): + new = self.a if self else 0 + old = other.a if other else 0 + if mt.isinf(new) and mt.isinf(old): return 0.0 - elif m.isinf(new): - return +m.inf - elif m.isinf(old): - return -m.inf + elif mt.isinf(new): + return +mt.inf + elif mt.isinf(old): + return -mt.inf elif not old and not new: return 0.0 elif not old: - return 1.0 + return +mt.inf else: return (new-old) / old + def __pos__(self): + return self.__class__(+self.a) + + def __neg__(self): + return self.__class__(-self.a) + + def __abs__(self): + return self.__class__(abs(self.a)) + def __add__(self, other): - return self.__class__(self.x + other.x) + return self.__class__(self.a + other.a) def __sub__(self, other): - return self.__class__(self.x - other.x) + return self.__class__(self.a - other.a) def __mul__(self, other): - return self.__class__(self.x * other.x) + return self.__class__(self.a * other.a) + + def __truediv__(self, other): + if not other: + if self >= self.__class__(0): + return self.__class__(+mt.inf) + else: + return self.__class__(-mt.inf) + return self.__class__(self.a // other.a) + + def __mod__(self, other): + return self.__class__(self.a % other.a) # fractional fields, a/b -class Frac(co.namedtuple('Frac', 'a,b')): +class CsvFrac(co.namedtuple('CsvFrac', 'a,b')): __slots__ = () def __new__(cls, a=0, b=None): - if isinstance(a, Frac) and b is None: + if isinstance(a, CsvFrac) and b is None: return a if isinstance(a, str) and b is None: a, b = a.split('/', 1) if b is None: b = a - return super().__new__(cls, Int(a), Int(b)) + return super().__new__(cls, CsvInt(a), CsvInt(b)) + + def __repr__(self): + return '%s(%r, %r)' % (self.__class__.__name__, self.a.a, self.b.a) def __str__(self): return '%s/%s' % (self.a, self.b) + def __csv__(self): + return '%s/%s' % (self.a.__csv__(), self.b.__csv__()) + + def __bool__(self): + return bool(self.a) + + def __int__(self): + return int(self.a) + def __float__(self): return float(self.a) - none = '%11s %7s' % ('-', '-') + none = '%11s' % '-' def table(self): - t = self.a.x/self.b.x if self.b.x else 1.0 - return '%11s %7s' % ( - self, - '∞%' if t == +m.inf - else '-∞%' if t == -m.inf - else '%.1f%%' % (100*t)) - - diff_none = '%11s' % '-' - def diff_table(self): return '%11s' % (self,) - def diff_diff(self, other): - new_a, new_b = self if self else (Int(0), Int(0)) - old_a, old_b = other if other else (Int(0), Int(0)) + def notes(self): + if self.b.a == 0 and self.a.a == 0: + t = 1.0 + elif self.b.a == 0: + t = mt.copysign(mt.inf, self.a.a) + else: + t = self.a.a / self.b.a + return ['∞%' if t == +mt.inf + else '-∞%' if t == -mt.inf + else '%.1f%%' % (100*t)] + + def diff(self, other): + new_a, new_b = self if self else (CsvInt(0), CsvInt(0)) + old_a, old_b = other if other else (CsvInt(0), CsvInt(0)) return '%11s' % ('%s/%s' % ( - new_a.diff_diff(old_a).strip(), - new_b.diff_diff(old_b).strip())) + new_a.diff(old_a).strip(), + new_b.diff(old_b).strip())) def ratio(self, other): - new_a, new_b = self if self else (Int(0), Int(0)) - old_a, old_b = other if other else (Int(0), Int(0)) - new = new_a.x/new_b.x if new_b.x else 1.0 - old = old_a.x/old_b.x if old_b.x else 1.0 + new_a, new_b = self if self else (CsvInt(0), CsvInt(0)) + old_a, old_b = other if other else (CsvInt(0), CsvInt(0)) + new = new_a.a/new_b.a if new_b.a else 1.0 + old = old_a.a/old_b.a if old_b.a else 1.0 return new - old + def __pos__(self): + return self.__class__(+self.a, +self.b) + + def __neg__(self): + return self.__class__(-self.a, -self.b) + + def __abs__(self): + return self.__class__(abs(self.a), abs(self.b)) + def __add__(self, other): return self.__class__(self.a + other.a, self.b + other.b) @@ -158,12 +221,26 @@ def __sub__(self, other): return self.__class__(self.a - other.a, self.b - other.b) def __mul__(self, other): - return self.__class__(self.a * other.a, self.b + other.b) + return self.__class__(self.a * other.a, self.b * other.b) + + def __truediv__(self, other): + return self.__class__(self.a / other.a, self.b / other.b) + + def __mod__(self, other): + return self.__class__(self.a % other.a, self.b % other.b) + + def __eq__(self, other): + self_a, self_b = self if self.b.a else (CsvInt(1), CsvInt(1)) + other_a, other_b = other if other.b.a else (CsvInt(1), CsvInt(1)) + return self_a * other_b == other_a * self_b + + def __ne__(self, other): + return not self.__eq__(other) def __lt__(self, other): - self_t = self.a.x/self.b.x if self.b.x else 1.0 - other_t = other.a.x/other.b.x if other.b.x else 1.0 - return (self_t, self.a.x) < (other_t, other.a.x) + self_a, self_b = self if self.b.a else (CsvInt(1), CsvInt(1)) + other_a, other_b = other if other.b.a else (CsvInt(1), CsvInt(1)) + return self_a * other_b < other_a * self_b def __gt__(self, other): return self.__class__.__lt__(other, self) @@ -178,70 +255,75 @@ def __ge__(self, other): class CovResult(co.namedtuple('CovResult', [ 'file', 'function', 'line', 'calls', 'hits', 'funcs', 'lines', 'branches'])): + _prefix = 'cov' _by = ['file', 'function', 'line'] _fields = ['calls', 'hits', 'funcs', 'lines', 'branches'] _sort = ['funcs', 'lines', 'branches', 'hits', 'calls'] _types = { - 'calls': Int, 'hits': Int, - 'funcs': Frac, 'lines': Frac, 'branches': Frac} + 'calls': CsvInt, 'hits': CsvInt, + 'funcs': CsvFrac, 'lines': CsvFrac, 'branches': CsvFrac} __slots__ = () def __new__(cls, file='', function='', line=0, calls=0, hits=0, funcs=0, lines=0, branches=0): - return super().__new__(cls, file, function, int(Int(line)), - Int(calls), Int(hits), Frac(funcs), Frac(lines), Frac(branches)) + return super().__new__(cls, file, function, int(CsvInt(line)), + CsvInt(calls), CsvInt(hits), + CsvFrac(funcs), CsvFrac(lines), CsvFrac(branches)) def __add__(self, other): return CovResult(self.file, self.function, self.line, - max(self.calls, other.calls), - max(self.hits, other.hits), - self.funcs + other.funcs, - self.lines + other.lines, - self.branches + other.branches) + max(self.calls, other.calls), + max(self.hits, other.hits), + self.funcs + other.funcs, + self.lines + other.lines, + self.branches + other.branches) +# open with '-' for stdin/stdout def openio(path, mode='r', buffering=-1): - # allow '-' for stdin/stdout + import os if path == '-': - if mode == 'r': + if 'r' in mode: return os.fdopen(os.dup(sys.stdin.fileno()), mode, buffering) else: return os.fdopen(os.dup(sys.stdout.fileno()), mode, buffering) else: return open(path, mode, buffering) -def collect(gcda_paths, *, +def collect_gcov(gcda_path, *, gcov_path=GCOV_PATH, - sources=None, - everything=False, **args): - results = [] - for path in gcda_paths: - # get coverage info through gcov's json output - # note, gcov-path may contain extra args - cmd = GCOV_PATH + ['-b', '-t', '--json-format', path] - if args.get('verbose'): - print(' '.join(shlex.quote(c) for c in cmd)) - proc = sp.Popen(cmd, + # get coverage info through gcov's json output + # note, gcov-path may contain extra args + cmd = GCOV_PATH + ['-b', '-t', '--json-format', gcda_path] + if args.get('verbose'): + print(' '.join(shlex.quote(c) for c in cmd)) + proc = sp.Popen(cmd, stdout=sp.PIPE, - stderr=sp.PIPE if not args.get('verbose') else None, universal_newlines=True, errors='replace', close_fds=False) - data = json.load(proc.stdout) - proc.wait() - if proc.returncode != 0: - if not args.get('verbose'): - for line in proc.stderr: - sys.stdout.write(line) - sys.exit(-1) + cov = json.load(proc.stdout) + proc.wait() + if proc.returncode != 0: + raise sp.CalledProcessError(proc.returncode, proc.args) + + return cov + +def collect_cov(gcda_paths, *, + sources=None, + everything=False, + **args): + results = [] + for gcda_path in gcda_paths: + # find coverage info + cov = collect_gcov(gcda_path, **args) # collect line/branch coverage - for file in data['files']: + for file in cov['files']: # ignore filtered sources if sources is not None: - if not any( - os.path.abspath(file['file']) == os.path.abspath(s) + if not any(os.path.abspath(file['file']) == os.path.abspath(s) for s in sources): continue else: @@ -262,58 +344,80 @@ def collect(gcda_paths, *, for func in file['functions']: func_name = func.get('name', '(inlined)') # discard internal functions (this includes injected test cases) - if not everything: - if func_name.startswith('__'): - continue + if not everything and func_name.startswith('__'): + continue # go ahead and add functions, later folding will merge this if # there are other hits on this line results.append(CovResult( - file_name, func_name, func['start_line'], - func['execution_count'], 0, - Frac(1 if func['execution_count'] > 0 else 0, 1), - 0, - 0)) + file_name, func_name, func['start_line'], + func['execution_count'], 0, + CsvFrac(1 if func['execution_count'] > 0 else 0, 1), + 0, + 0)) for line in file['lines']: func_name = line.get('function_name', '(inlined)') # discard internal function (this includes injected test cases) - if not everything: - if func_name.startswith('__'): - continue + if not everything and func_name.startswith('__'): + continue # go ahead and add lines, later folding will merge this if # there are other hits on this line results.append(CovResult( - file_name, func_name, line['line_number'], - 0, line['count'], - 0, - Frac(1 if line['count'] > 0 else 0, 1), - Frac( - sum(1 if branch['count'] > 0 else 0 - for branch in line['branches']), - len(line['branches'])))) + file_name, func_name, line['line_number'], + 0, line['count'], + 0, + CsvFrac(1 if line['count'] > 0 else 0, 1), + CsvFrac( + sum(1 if branch['count'] > 0 else 0 + for branch in line['branches']), + len(line['branches'])))) return results +# common folding/tabling/read/write code + +class Rev(co.namedtuple('Rev', 'a')): + __slots__ = () + # yes we need all of these because we're a namedtuple + def __lt__(self, other): + return self.a > other.a + def __gt__(self, other): + return self.a < other.a + def __le__(self, other): + return self.a >= other.a + def __ge__(self, other): + return self.a <= other.a + def fold(Result, results, *, by=None, - defines=None, + defines=[], + sort=None, + depth=1, **_): + # stop when depth hits zero + if depth == 0: + return [] + + # organize by by if by is None: by = Result._by - for k in it.chain(by or [], (k for k, _ in defines or [])): + for k in it.chain(by or [], (k for k, _ in defines)): if k not in Result._by and k not in Result._fields: - print("error: could not find field %r?" % k) + print("error: could not find field %r?" % k, + file=sys.stderr) sys.exit(-1) # filter by matching defines - if defines is not None: + if defines: results_ = [] for r in results: - if all(getattr(r, k) in vs for k, vs in defines): + if all(any(fnmatch.fnmatchcase(str(getattr(r, k, '')), v) + for v in vs) + for k, vs in defines): results_.append(r) results = results_ @@ -330,17 +434,67 @@ def fold(Result, results, *, for name, rs in folding.items(): folded.append(sum(rs[1:], start=rs[0])) + # sort, note that python's sort is stable + folded.sort(key=lambda r: ( + # sort by explicit sort fields + tuple((Rev + if reverse ^ (not k or k in Result._fields) + else lambda x: x)( + tuple((getattr(r, k_),) + if getattr(r, k_) is not None + else () + for k_ in ([k] if k else Result._sort))) + for k, reverse in (sort or [])), + # sort by result + r)) + + # recurse if we have recursive results + if hasattr(Result, '_children'): + folded = [r._replace(**{ + Result._children: fold( + Result, getattr(r, Result._children), + by=by, + # only filter defines at the top level! + sort=sort, + depth=depth-1)}) + for r in folded] + return folded def table(Result, results, diff_results=None, *, by=None, fields=None, sort=None, - summary=False, - all=False, + labels=None, + depth=1, + hot=None, percent=False, + all=False, + compare=None, + no_header=False, + small_header=False, + no_total=False, + small_total=False, + small_table=False, + summary=False, + total=False, **_): - all_, all = all, __builtins__.all + import builtins + all_, all = all, builtins.all + + # small_table implies small_header + no_total or small_total + if small_table: + small_header = True + small_total = True + no_total = no_total or (not summary and not total) + # summary implies small_header + if summary: + small_header = True + # total implies summary + no_header + small_total + if total: + summary = True + no_header = True + small_total = True if by is None: by = Result._by @@ -348,159 +502,420 @@ def table(Result, results, diff_results=None, *, fields = Result._fields types = Result._types - # fold again - results = fold(Result, results, by=by) - if diff_results is not None: - diff_results = fold(Result, diff_results, by=by) - # organize by name table = { - ','.join(str(getattr(r, k) or '') for k in by): r - for r in results} + ','.join(str(getattr(r, k) + if getattr(r, k) is not None + else '') + for k in by): r + for r in results} diff_table = { - ','.join(str(getattr(r, k) or '') for k in by): r - for r in diff_results or []} - names = list(table.keys() | diff_table.keys()) - - # sort again, now with diff info, note that python's sort is stable - names.sort() + ','.join(str(getattr(r, k) + if getattr(r, k) is not None + else '') + for k in by): r + for r in diff_results or []} + + # lost results? this only happens if we didn't fold by the same + # by field, which is an error and risks confusing results + assert len(table) == len(results) if diff_results is not None: - names.sort(key=lambda n: tuple( - types[k].ratio( - getattr(table.get(n), k, None), - getattr(diff_table.get(n), k, None)) - for k in fields), - reverse=True) - if sort: - for k, reverse in reversed(sort): - names.sort( - key=lambda n: tuple( - (getattr(table[n], k),) - if getattr(table.get(n), k, None) is not None else () - for k in ([k] if k else [ - k for k in Result._sort if k in fields])), - reverse=reverse ^ (not k or k in Result._fields)) - + assert len(diff_table) == len(diff_results) + + # find compare entry if there is one + if compare: + compare_ = min( + (n for n in table.keys() + if all(fnmatch.fnmatchcase(k, c) + for k, c in it.zip_longest(n.split(','), compare, + fillvalue=''))), + default=compare) + compare_r = table.get(compare_) # build up our lines lines = [] # header - header = [] - header.append('%s%s' % ( - ','.join(by), - ' (%d added, %d removed)' % ( - sum(1 for n in table if n not in diff_table), - sum(1 for n in diff_table if n not in table)) - if diff_results is not None and not percent else '') - if not summary else '') - if diff_results is None: - for k in fields: - header.append(k) - elif percent: - for k in fields: - header.append(k) - else: - for k in fields: - header.append('o'+k) - for k in fields: - header.append('n'+k) - for k in fields: - header.append('d'+k) - header.append('') - lines.append(header) - - def table_entry(name, r, diff_r=None, ratios=[]): - entry = [] - entry.append(name) - if diff_results is None: - for k in fields: - entry.append(getattr(r, k).table() - if getattr(r, k, None) is not None - else types[k].none) - elif percent: + if not no_header: + header = ['%s%s' % ( + ','.join(labels if labels is not None else by), + ' (%d added, %d removed)' % ( + sum(1 for n in table if n not in diff_table), + sum(1 for n in diff_table if n not in table)) + if diff_results is not None and not percent else '') + if not small_header else ''] + if diff_results is None or percent: for k in fields: - entry.append(getattr(r, k).diff_table() - if getattr(r, k, None) is not None - else types[k].diff_none) + header.append(k) else: for k in fields: - entry.append(getattr(diff_r, k).diff_table() - if getattr(diff_r, k, None) is not None - else types[k].diff_none) + header.append('o'+k) for k in fields: - entry.append(getattr(r, k).diff_table() - if getattr(r, k, None) is not None - else types[k].diff_none) + header.append('n'+k) for k in fields: - entry.append(types[k].diff_diff( - getattr(r, k, None), - getattr(diff_r, k, None))) - if diff_results is None: - entry.append('') + header.append('d'+k) + lines.append(header) + + # delete these to try to catch typos below, we need to rebuild + # these tables at each recursive layer + del table + del diff_table + + # entry helper + def table_entry(name, r, diff_r=None): + # prepend name + entry = [name] + + # normal entry? + if ((compare is None or r == compare_r) + and diff_results is None): + for k in fields: + entry.append( + (getattr(r, k).table(), + getattr(getattr(r, k), 'notes', lambda: [])()) + if getattr(r, k, None) is not None + else types[k].none) + # compare entry? + elif diff_results is None: + for k in fields: + entry.append( + (getattr(r, k).table() + if getattr(r, k, None) is not None + else types[k].none, + (lambda t: ['+∞%'] if t == +mt.inf + else ['-∞%'] if t == -mt.inf + else ['%+.1f%%' % (100*t)])( + types[k].ratio( + getattr(r, k, None), + getattr(compare_r, k, None))))) + # percent entry? elif percent: - entry.append(' (%s)' % ', '.join( - '+∞%' if t == +m.inf - else '-∞%' if t == -m.inf - else '%+.1f%%' % (100*t) - for t in ratios)) + for k in fields: + entry.append( + (getattr(r, k).table() + if getattr(r, k, None) is not None + else types[k].none, + (lambda t: ['+∞%'] if t == +mt.inf + else ['-∞%'] if t == -mt.inf + else ['%+.1f%%' % (100*t)])( + types[k].ratio( + getattr(r, k, None), + getattr(diff_r, k, None))))) + # diff entry? else: - entry.append(' (%s)' % ', '.join( - '+∞%' if t == +m.inf - else '-∞%' if t == -m.inf - else '%+.1f%%' % (100*t) - for t in ratios - if t) - if any(ratios) else '') + for k in fields: + entry.append(getattr(diff_r, k).table() + if getattr(diff_r, k, None) is not None + else types[k].none) + for k in fields: + entry.append(getattr(r, k).table() + if getattr(r, k, None) is not None + else types[k].none) + for k in fields: + entry.append( + (types[k].diff( + getattr(r, k, None), + getattr(diff_r, k, None)), + (lambda t: ['+∞%'] if t == +mt.inf + else ['-∞%'] if t == -mt.inf + else ['%+.1f%%' % (100*t)] if t + else [])( + types[k].ratio( + getattr(r, k, None), + getattr(diff_r, k, None))))) + + # append any notes + if hasattr(Result, '_notes') and r is not None: + notes = sorted(getattr(r, Result._notes)) + if isinstance(entry[-1], tuple): + entry[-1] = (entry[-1][0], entry[-1][1] + notes) + else: + entry[-1] = (entry[-1], notes) + return entry - # entries - if not summary: - for name in names: - r = table.get(name) - if diff_results is None: - diff_r = None - ratios = None + # recursive entry helper + def table_recurse(results_, diff_results_, + depth_, + prefixes=('', '', '', '')): + # build the children table at each layer + table_ = { + ','.join(str(getattr(r, k) + if getattr(r, k) is not None + else '') + for k in by): r + for r in results_} + diff_table_ = { + ','.join(str(getattr(r, k) + if getattr(r, k) is not None + else '') + for k in by): r + for r in diff_results_ or []} + names_ = [n + for n in table_.keys() | diff_table_.keys() + if diff_results is None + or all_ + or any( + types[k].ratio( + getattr(table_.get(n), k, None), + getattr(diff_table_.get(n), k, None)) + for k in fields)] + + # sort again, now with diff info, note that python's sort is stable + names_.sort(key=lambda n: ( + # sort by explicit sort fields + next( + tuple((Rev + if reverse ^ (not k or k in Result._fields) + else lambda x: x)( + tuple((getattr(r_, k_),) + if getattr(r_, k_) is not None + else () + for k_ in ([k] if k else Result._sort))) + for k, reverse in (sort or [])) + for r_ in [table_.get(n), diff_table_.get(n)] + if r_ is not None), + # sort by ratio if diffing + Rev(tuple(types[k].ratio( + getattr(table_.get(n), k, None), + getattr(diff_table_.get(n), k, None)) + for k in fields)) + if diff_results is not None + else (), + # move compare entry to the top, note this can be + # overridden by explicitly sorting by fields + (table_.get(n) != compare_r, + # sort by ratio if comparing + Rev(tuple( + types[k].ratio( + getattr(table_.get(n), k, None), + getattr(compare_r, k, None)) + for k in fields))) + if compare + else (), + # sort by result + (table_[n],) if n in table_ else (), + # and finally by name (diffs may be missing results) + n)) + + for i, name in enumerate(names_): + # find comparable results + r = table_.get(name) + diff_r = diff_table_.get(name) + + # figure out a good label + if labels is not None: + label = next( + ','.join(str(getattr(r_, k) + if getattr(r_, k) is not None + else '') + for k in labels) + for r_ in [r, diff_r] + if r_ is not None) else: - diff_r = diff_table.get(name) - ratios = [ - types[k].ratio( - getattr(r, k, None), - getattr(diff_r, k, None)) - for k in fields] - if not all_ and not any(ratios): - continue - lines.append(table_entry(name, r, diff_r, ratios)) + label = name + + # build line + line = table_entry(label, r, diff_r) + + # add prefixes + line = [x if isinstance(x, tuple) else (x, []) for x in line] + line[0] = (prefixes[0+(i==len(names_)-1)] + line[0][0], line[0][1]) + lines.append(line) + + # recurse? + if name in table_ and depth_ > 1: + table_recurse( + getattr(r, Result._children), + getattr(diff_r, Result._children, None), + depth_-1, + (prefixes[2+(i==len(names_)-1)] + "|-> ", + prefixes[2+(i==len(names_)-1)] + "'-> ", + prefixes[2+(i==len(names_)-1)] + "| ", + prefixes[2+(i==len(names_)-1)] + " ")) + + # build entries + if not summary: + table_recurse(results, diff_results, depth) # total - r = next(iter(fold(Result, results, by=[])), None) - if diff_results is None: - diff_r = None - ratios = None - else: - diff_r = next(iter(fold(Result, diff_results, by=[])), None) - ratios = [ - types[k].ratio( - getattr(r, k, None), - getattr(diff_r, k, None)) - for k in fields] - lines.append(table_entry('TOTAL', r, diff_r, ratios)) - - # find the best widths, note that column 0 contains the names and column -1 - # the ratios, so those are handled a bit differently - widths = [ - ((max(it.chain([w], (len(l[i]) for l in lines)))+1+4-1)//4)*4-1 - for w, i in zip( - it.chain([23], it.repeat(7)), - range(len(lines[0])-1))] + if not no_total: + r = next(iter(fold(Result, results, by=[])), Result()) + if diff_results is None: + diff_r = None + else: + diff_r = next(iter(fold(Result, diff_results, by=[])), Result()) + lines.append(table_entry( + 'TOTAL' if not small_total else '', + r, diff_r)) + + # homogenize + lines = [[x if isinstance(x, tuple) else (x, []) for x in line] + for line in lines] + + # find the best widths, note that column 0 contains the names and is + # handled a bit differently + widths = co.defaultdict(lambda: 7, {0: 7}) + nwidths = co.defaultdict(lambda: 0) + for line in lines: + for i, x in enumerate(line): + widths[i] = max(widths[i], ((len(x[0])+1+4-1)//4)*4-1) + if i != len(line)-1: + nwidths[i] = max(nwidths[i], 1+sum(2+len(n) for n in x[1])) + if not any(line[0][0] for line in lines): + widths[0] = 0 # print our table for line in lines: - print('%-*s %s%s' % ( - widths[0], line[0], - ' '.join('%*s' % (w, x) - for w, x in zip(widths[1:], line[1:-1])), - line[-1])) + print('%-*s %s' % ( + widths[0], line[0][0], + ' '.join('%*s%-*s' % ( + widths[i], x[0], + nwidths[i], ' (%s)' % ', '.join(x[1]) if x[1] else '') + for i, x in enumerate(line[1:], 1)))) + +def read_csv(path, Result, *, + depth=1, + prefix=None, + **_): + # prefix? this only applies to field fields + if prefix is None: + if hasattr(Result, '_prefix'): + prefix = '%s_' % Result._prefix + else: + prefix = '' + + by = Result._by + fields = Result._fields + + with openio(path, 'r') as f: + # csv or json? assume json starts with [ + is_json = (f.buffer.peek(1)[:1] == b'[') + + # read csv? + if not is_json: + results = [] + reader = csv.DictReader(f, restval='') + for r in reader: + if not any(prefix+k in r and r[prefix+k].strip() + for k in fields): + continue + try: + # note this allows by/fields to overlap + results.append(Result(**( + {k: r[k] for k in by + if k in r + and r[k].strip()} + | {k: r[prefix+k] for k in fields + if prefix+k in r + and r[prefix+k].strip()}))) + except TypeError: + pass + return results + + # read json? + else: + import json + def unjsonify(results, depth_): + results_ = [] + for r in results: + if not any(prefix+k in r and r[prefix+k].strip() + for k in fields): + continue + try: + # note this allows by/fields to overlap + results_.append(Result(**( + {k: r[k] for k in by + if k in r + and r[k] is not None} + | {k: r[prefix+k] for k in fields + if prefix+k in r + and r[prefix+k] is not None} + | ({Result._children: unjsonify( + r[Result._children], + depth_-1)} + if hasattr(Result, '_children') + and Result._children in r + and r[Result._children] is not None + and depth_ > 1 + else {}) + | ({Result._notes: set(r[Result._notes])} + if hasattr(Result, '_notes') + and Result._notes in r + and r[Result._notes] is not None + else {})))) + except TypeError: + pass + return results_ + return unjsonify(json.load(f), depth) + +def write_csv(path, Result, results, *, + json=False, + by=None, + fields=None, + depth=1, + prefix=None, + **_): + # prefix? this only applies to field fields + if prefix is None: + if hasattr(Result, '_prefix'): + prefix = '%s_' % Result._prefix + else: + prefix = '' + + if by is None: + by = Result._by + if fields is None: + fields = Result._fields + + with openio(path, 'w') as f: + # write csv? + if not json: + writer = csv.DictWriter(f, list( + co.OrderedDict.fromkeys(it.chain( + by, + (prefix+k for k in fields))).keys())) + writer.writeheader() + for r in results: + # note this allows by/fields to overlap + writer.writerow( + {k: getattr(r, k) + for k in by + if getattr(r, k) is not None} + | {prefix+k: getattr(r, k).__csv__() + for k in fields + if getattr(r, k) is not None}) + + # write json? + else: + import json + # the neat thing about json is we can include recursive results + def jsonify(results, depth_): + results_ = [] + for r in results: + # note this allows by/fields to overlap + results_.append( + {k: getattr(r, k) + for k in by + if getattr(r, k) is not None} + | {prefix+k: getattr(r, k).__csv__() + for k in fields + if getattr(r, k) is not None} + | ({Result._children: jsonify( + getattr(r, Result._children), + depth_-1)} + if hasattr(Result, '_children') + and getattr(r, Result._children) + and depth_ > 1 + else {}) + | ({Result._notes: list( + getattr(r, Result._notes))} + if hasattr(Result, '_notes') + and getattr(r, Result._notes) + else {})) + return results_ + json.dump(jsonify(results, depth), f, + separators=(',', ':')) def annotate(Result, results, *, @@ -527,14 +942,14 @@ def annotate(Result, results, *, or (branches and r.branches.a < r.branches.b)): if last is not None and line - last.stop <= args['context']: last = range( - last.start, - line+1+args['context']) + last.start, + line+1+args['context']) else: if last is not None: spans.append((last, func)) last = range( - line-args['context'], - line+1+args['context']) + line-args['context'], + line+1+args['context']) func = r.function if last is not None: spans.append((last, func)) @@ -550,11 +965,11 @@ def annotate(Result, results, *, if skipped: skipped = False print('%s@@ %s:%d: %s @@%s' % ( - '\x1b[36m' if args['color'] else '', - path, - i+1, - next(iter(f for _, f in spans)), - '\x1b[m' if args['color'] else '')) + '\x1b[36m' if args['color'] else '', + path, + i+1, + next(iter(f for _, f in spans)), + '\x1b[m' if args['color'] else '')) # build line if line.endswith('\n'): @@ -563,11 +978,11 @@ def annotate(Result, results, *, if i+1 in table: r = table[i+1] line = '%-*s // %s hits%s' % ( - args['width'], - line, - r.hits, - ', %s branches' % (r.branches,) - if int(r.branches.b) else '') + args['width'], + line, + r.hits, + ', %s branches' % (r.branches,) + if int(r.branches.b) else '') if args['color']: if lines and int(r.hits) == 0: @@ -581,7 +996,7 @@ def annotate(Result, results, *, def main(gcda_paths, *, by=None, fields=None, - defines=None, + defines=[], sort=None, hits=False, **args): @@ -593,98 +1008,89 @@ def main(gcda_paths, *, else: args['color'] = False + # figure out what fields we're interested in + if by is None: + if (args.get('annotate') + or args.get('lines') + or args.get('branches') + or args.get('output') + or args.get('output_json')): + by = CovResult._by + else: + by = ['function'] + + if fields is None: + if (args.get('annotate') + or args.get('lines') + or args.get('branches') + or args.get('output') + or args.get('output_json')): + fields = CovResult._fields + elif not hits: + fields = ['lines', 'branches'] + else: + fields = ['calls', 'hits'] + # find sizes if not args.get('use', None): - results = collect(gcda_paths, **args) + # not enough info? + if not gcda_paths: + print("error: no *.gcda files?", + file=sys.stderr) + sys.exit(1) + + # collect info + results = collect_cov(gcda_paths, + **args) + else: - results = [] - with openio(args['use']) as f: - reader = csv.DictReader(f, restval='') - for r in reader: - if not any('cov_'+k in r and r['cov_'+k].strip() - for k in CovResult._fields): - continue - try: - results.append(CovResult( - **{k: r[k] for k in CovResult._by - if k in r and r[k].strip()}, - **{k: r['cov_'+k] - for k in CovResult._fields - if 'cov_'+k in r - and r['cov_'+k].strip()})) - except TypeError: - pass + results = read_csv(args['use'], CovResult, + **args) # fold - results = fold(CovResult, results, by=by, defines=defines) - - # sort, note that python's sort is stable - results.sort() - if sort: - for k, reverse in reversed(sort): - results.sort( - key=lambda r: tuple( - (getattr(r, k),) if getattr(r, k) is not None else () - for k in ([k] if k else CovResult._sort)), - reverse=reverse ^ (not k or k in CovResult._fields)) - - # write results to CSV - if args.get('output'): - with openio(args['output'], 'w') as f: - writer = csv.DictWriter(f, - (by if by is not None else CovResult._by) - + ['cov_'+k for k in ( - fields if fields is not None else CovResult._fields)]) - writer.writeheader() - for r in results: - writer.writerow( - {k: getattr(r, k) for k in ( - by if by is not None else CovResult._by)} - | {'cov_'+k: getattr(r, k) for k in ( - fields if fields is not None else CovResult._fields)}) + results = fold(CovResult, results, + by=by, + defines=defines, + sort=sort) # find previous results? + diff_results = None if args.get('diff'): - diff_results = [] try: - with openio(args['diff']) as f: - reader = csv.DictReader(f, restval='') - for r in reader: - if not any('cov_'+k in r and r['cov_'+k].strip() - for k in CovResult._fields): - continue - try: - diff_results.append(CovResult( - **{k: r[k] for k in CovResult._by - if k in r and r[k].strip()}, - **{k: r['cov_'+k] - for k in CovResult._fields - if 'cov_'+k in r - and r['cov_'+k].strip()})) - except TypeError: - pass + diff_results = read_csv( + args.get('diff'), + CovResult, + **args) except FileNotFoundError: - pass + diff_results = [] # fold diff_results = fold(CovResult, diff_results, - by=by, defines=defines) - + by=by, + defines=defines) + + # annotate sources + if (args.get('annotate') + or args.get('lines') + or args.get('branches')): + annotate(CovResult, results, **args) + # write results to JSON + elif args.get('output_json'): + write_csv(args['output_json'], CovResult, results, json=True, + by=by, + fields=fields, + **args) + # write results to CSV + elif args.get('output'): + write_csv(args['output'], CovResult, results, + by=by, + fields=fields, + **args) # print table - if not args.get('quiet'): - if (args.get('annotate') - or args.get('lines') - or args.get('branches')): - # annotate sources - annotate(CovResult, results, **args) - else: - # print table - table(CovResult, results, - diff_results if args.get('diff') else None, - by=by if by is not None else ['function'], - fields=fields if fields is not None - else ['lines', 'branches'] if not hits - else ['calls', 'hits'], + elif not args.get('quiet'): + table(CovResult, results, diff_results, + by=by, + fields=fields, sort=sort, **args) @@ -701,128 +1107,171 @@ def main(gcda_paths, *, import argparse import sys parser = argparse.ArgumentParser( - description="Find coverage info after running tests.", - allow_abbrev=False) - parser.add_argument( - 'gcda_paths', - nargs='*', - help="Input *.gcda files.") - parser.add_argument( - '-v', '--verbose', - action='store_true', - help="Output commands that run behind the scenes.") - parser.add_argument( - '-q', '--quiet', - action='store_true', - help="Don't show anything, useful with -o.") - parser.add_argument( - '-o', '--output', - help="Specify CSV file to store results.") - parser.add_argument( - '-u', '--use', - help="Don't parse anything, use this CSV file.") - parser.add_argument( - '-d', '--diff', - help="Specify CSV file to diff against.") - parser.add_argument( - '-a', '--all', - action='store_true', - help="Show all, not just the ones that changed.") - parser.add_argument( - '-p', '--percent', - action='store_true', - help="Only show percentage change, not a full diff.") - parser.add_argument( - '-b', '--by', - action='append', - choices=CovResult._by, - help="Group by this field.") - parser.add_argument( - '-f', '--field', - dest='fields', - action='append', - choices=CovResult._fields, - help="Show this field.") - parser.add_argument( - '-D', '--define', - dest='defines', - action='append', - type=lambda x: (lambda k,v: (k, set(v.split(','))))(*x.split('=', 1)), - help="Only include results where this field is this value.") + description="Find coverage info after running tests.", + allow_abbrev=False) + parser.add_argument( + 'gcda_paths', + nargs='*', + help="Input *.gcda files.") + parser.add_argument( + '-v', '--verbose', + action='store_true', + help="Output commands that run behind the scenes.") + parser.add_argument( + '-q', '--quiet', + action='store_true', + help="Don't show anything, useful when checking for errors.") + parser.add_argument( + '-o', '--output', + help="Specify CSV file to store results.") + parser.add_argument( + '-O', '--output-json', + help="Specify JSON file to store results. This may contain " + "recursive info.") + parser.add_argument( + '-u', '--use', + help="Don't parse anything, use this CSV/JSON file.") + parser.add_argument( + '-d', '--diff', + help="Specify CSV/JSON file to diff against.") + parser.add_argument( + '-p', '--percent', + action='store_true', + help="Only show percentage change, not a full diff.") + parser.add_argument( + '-c', '--compare', + type=lambda x: tuple(v.strip() for v in x.split(',')), + help="Compare results to the row matching this by pattern.") + parser.add_argument( + '-a', '--all', + action='store_true', + help="Show all, not just the ones that changed.") + parser.add_argument( + '-b', '--by', + action='append', + choices=CovResult._by, + help="Group by this field.") + parser.add_argument( + '-f', '--field', + dest='fields', + action='append', + choices=CovResult._fields, + help="Show this field.") + parser.add_argument( + '-D', '--define', + dest='defines', + action='append', + type=lambda x: ( + lambda k, vs: ( + k.strip(), + {v.strip() for v in vs.split(',')}) + )(*x.split('=', 1)), + help="Only include results where this field is this value. May " + "include comma-separated options and globs.") class AppendSort(argparse.Action): def __call__(self, parser, namespace, value, option): if namespace.sort is None: namespace.sort = [] - namespace.sort.append((value, True if option == '-S' else False)) - parser.add_argument( - '-s', '--sort', - nargs='?', - action=AppendSort, - help="Sort by this field.") - parser.add_argument( - '-S', '--reverse-sort', - nargs='?', - action=AppendSort, - help="Sort by this field, but backwards.") - parser.add_argument( - '-Y', '--summary', - action='store_true', - help="Only show the total.") - parser.add_argument( - '-F', '--source', - dest='sources', - action='append', - help="Only consider definitions in this file. Defaults to anything " - "in the current directory.") - parser.add_argument( - '--everything', - action='store_true', - help="Include builtin and libc specific symbols.") - parser.add_argument( - '--hits', - action='store_true', - help="Show total hits instead of coverage.") - parser.add_argument( - '-A', '--annotate', - action='store_true', - help="Show source files annotated with coverage info.") - parser.add_argument( - '-L', '--lines', - action='store_true', - help="Show uncovered lines.") - parser.add_argument( - '-B', '--branches', - action='store_true', - help="Show uncovered branches.") - parser.add_argument( - '-c', '--context', - type=lambda x: int(x, 0), - default=3, - help="Show n additional lines of context. Defaults to 3.") - parser.add_argument( - '-W', '--width', - type=lambda x: int(x, 0), - default=80, - help="Assume source is styled with this many columns. Defaults to 80.") - parser.add_argument( - '--color', - choices=['never', 'always', 'auto'], - default='auto', - help="When to use terminal colors. Defaults to 'auto'.") - parser.add_argument( - '-e', '--error-on-lines', - action='store_true', - help="Error if any lines are not covered.") - parser.add_argument( - '-E', '--error-on-branches', - action='store_true', - help="Error if any branches are not covered.") - parser.add_argument( - '--gcov-path', - default=GCOV_PATH, - type=lambda x: x.split(), - help="Path to the gcov executable, may include paths. " - "Defaults to %r." % GCOV_PATH) + namespace.sort.append((value, option in {'-S', '--reverse-sort'})) + parser.add_argument( + '-s', '--sort', + nargs='?', + action=AppendSort, + help="Sort by this field.") + parser.add_argument( + '-S', '--reverse-sort', + nargs='?', + action=AppendSort, + help="Sort by this field, but backwards.") + parser.add_argument( + '--no-header', + action='store_true', + help="Don't show the header.") + parser.add_argument( + '--small-header', + action='store_true', + help="Don't show by field names.") + parser.add_argument( + '--no-total', + action='store_true', + help="Don't show the total.") + parser.add_argument( + '--small-total', + action='store_true', + help="Don't show TOTAL name.") + parser.add_argument( + '-Q', '--small-table', + action='store_true', + help="Equivalent to --small-header + --no-total or --small-total.") + parser.add_argument( + '-Y', '--summary', + action='store_true', + help="Only show the total.") + parser.add_argument( + '--total', + action='store_true', + help="Equivalent to --summary + --no-header + --small-total. " + "Useful for scripting.") + parser.add_argument( + '--prefix', + help="Prefix to use for fields in CSV/JSON output. Defaults " + "to %r." % ("%s_" % CovResult._prefix)) + parser.add_argument( + '-F', '--source', + dest='sources', + action='append', + help="Only consider definitions in this file. Defaults to " + "anything in the current directory.") + parser.add_argument( + '-!', '--everything', + action='store_true', + help="Include builtin and libc specific symbols.") + parser.add_argument( + '--hits', + action='store_true', + help="Show total hits instead of coverage.") + parser.add_argument( + '-A', '--annotate', + action='store_true', + help="Show source files annotated with coverage info.") + parser.add_argument( + '-L', '--lines', + action='store_true', + help="Show uncovered lines.") + parser.add_argument( + '-B', '--branches', + action='store_true', + help="Show uncovered branches.") + parser.add_argument( + '-C', '--context', + type=lambda x: int(x, 0), + default=3, + help="Show n additional lines of context. Defaults to 3.") + parser.add_argument( + '-W', '--width', + type=lambda x: int(x, 0), + default=80, + help="Assume source is styled with this many columns. Defaults " + "to 80.") + parser.add_argument( + '--color', + choices=['never', 'always', 'auto'], + default='auto', + help="When to use terminal colors. Defaults to 'auto'.") + parser.add_argument( + '-e', '--error-on-lines', + action='store_true', + help="Error if any lines are not covered.") + parser.add_argument( + '-E', '--error-on-branches', + action='store_true', + help="Error if any branches are not covered.") + parser.add_argument( + '--gcov-path', + default=GCOV_PATH, + type=lambda x: x.split(), + help="Path to the gcov executable, may include paths. " + "Defaults to %r." % GCOV_PATH) sys.exit(main(**{k: v - for k, v in vars(parser.parse_intermixed_args()).items() - if v is not None})) + for k, v in vars(parser.parse_intermixed_args()).items() + if v is not None})) diff --git a/scripts/crc32c.py b/scripts/crc32c.py new file mode 100755 index 000000000..6693117f9 --- /dev/null +++ b/scripts/crc32c.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 + +# prevent local imports +if __name__ == "__main__": + __import__('sys').path.pop(0) + +import io +import os +import struct +import sys + +try: + import crc32c as crc32c_lib +except ModuleNotFoundError: + crc32c_lib = None + + +# open with '-' for stdin/stdout +def openio(path, mode='r', buffering=-1): + import os + if path == '-': + if 'r' in mode: + return os.fdopen(os.dup(sys.stdin.fileno()), mode, buffering) + else: + return os.fdopen(os.dup(sys.stdout.fileno()), mode, buffering) + else: + return open(path, mode, buffering) + +def crc32c(data, crc=0): + if crc32c_lib is not None: + return crc32c_lib.crc32c(data, crc) + else: + crc ^= 0xffffffff + for b in data: + crc ^= b + for j in range(8): + crc = (crc >> 1) ^ ((crc & 1) * 0x82f63b78) + return 0xffffffff ^ crc + + +def main(paths, *, + hex=False, + string=False): + import builtins + hex_, hex = hex, builtins.hex + + # interpret as sequence of hex bytes + if hex_: + bytes_ = [b for path in paths for b in path.split()] + print('%08x' % crc32c(bytes(int(b, 16) for b in bytes_))) + + # interpret as strings + elif string: + for path in paths: + print('%08x %s' % (crc32c(path.encode('utf8')), path)) + + # default to interpreting as paths + else: + if not paths: + paths = [None] + + for path in paths: + with openio(path or '-', 'rb') as f: + # calculate crc + crc = 0 + while True: + block = f.read(io.DEFAULT_BUFFER_SIZE) + if not block: + break + + crc = crc32c(block, crc) + + # print what we found + if path is not None: + print('%08x %s' % (crc, path)) + else: + print('%08x' % crc) + + +if __name__ == "__main__": + import argparse + import sys + parser = argparse.ArgumentParser( + description="Calculates crc32cs.", + allow_abbrev=False) + parser.add_argument( + 'paths', + nargs='*', + help="Paths to read. Reads stdin by default.") + parser.add_argument( + '-x', '--hex', + action='store_true', + help="Interpret as a sequence of hex bytes.") + parser.add_argument( + '-s', '--string', + action='store_true', + help="Interpret as strings.") + sys.exit(main(**{k: v + for k, v in vars(parser.parse_intermixed_args()).items() + if v is not None})) diff --git a/scripts/csv.py b/scripts/csv.py new file mode 100755 index 000000000..c3a7b27f9 --- /dev/null +++ b/scripts/csv.py @@ -0,0 +1,2753 @@ +#!/usr/bin/env python3 +# +# Script to manipulate CSV files. +# +# Example: +# ./scripts/csv.py lfs.code.csv lfs.stack.csv \ +# -bfunction -fcode -fstack='max(stack)' +# +# Copyright (c) 2022, The littlefs authors. +# SPDX-License-Identifier: BSD-3-Clause +# + +# prevent local imports +if __name__ == "__main__": + __import__('sys').path.pop(0) + +import collections as co +import csv +import fnmatch +import functools as ft +import itertools as it +import math as mt +import os +import re +import sys + +SI_PREFIXES = { + 18: 'E', + 15: 'P', + 12: 'T', + 9: 'G', + 6: 'M', + 3: 'K', + 0: '', + -3: 'm', + -6: 'u', + -9: 'n', + -12: 'p', + -15: 'f', + -18: 'a', +} + +SI2_PREFIXES = { + 60: 'Ei', + 50: 'Pi', + 40: 'Ti', + 30: 'Gi', + 20: 'Mi', + 10: 'Ki', + 0: '', + -10: 'mi', + -20: 'ui', + -30: 'ni', + -40: 'pi', + -50: 'fi', + -60: 'ai', +} + + +# various field types + +# integer fields +class CsvInt(co.namedtuple('CsvInt', 'a')): + __slots__ = () + def __new__(cls, a=0): + if isinstance(a, CsvInt): + return a + if isinstance(a, str): + try: + a = int(a, 0) + except ValueError: + # also accept +-∞ and +-inf + if re.match('^\s*\+?\s*(?:∞|inf)\s*$', a): + a = mt.inf + elif re.match('^\s*-\s*(?:∞|inf)\s*$', a): + a = -mt.inf + else: + raise + if not (isinstance(a, int) or mt.isinf(a)): + a = int(a) + return super().__new__(cls, a) + + def __repr__(self): + return '%s(%r)' % (self.__class__.__name__, self.a) + + def __str__(self): + if self.a == mt.inf: + return '∞' + elif self.a == -mt.inf: + return '-∞' + else: + return str(self.a) + + def __csv__(self): + if self.a == mt.inf: + return 'inf' + elif self.a == -mt.inf: + return '-inf' + else: + return repr(self.a) + + def __bool__(self): + return bool(self.a) + + def __int__(self): + assert not mt.isinf(self.a) + return self.a + + def __float__(self): + return float(self.a) + + none = '%7s' % '-' + def table(self): + return '%7s' % (self,) + + def diff(self, other): + new = self.a if self else 0 + old = other.a if other else 0 + diff = new - old + if diff == +mt.inf: + return '%7s' % '+∞' + elif diff == -mt.inf: + return '%7s' % '-∞' + else: + return '%+7d' % diff + + def ratio(self, other): + new = self.a if self else 0 + old = other.a if other else 0 + if mt.isinf(new) and mt.isinf(old): + return 0.0 + elif mt.isinf(new): + return +mt.inf + elif mt.isinf(old): + return -mt.inf + elif not old and not new: + return 0.0 + elif not old: + return +mt.inf + else: + return (new-old) / old + + def __pos__(self): + return self.__class__(+self.a) + + def __neg__(self): + return self.__class__(-self.a) + + def __abs__(self): + return self.__class__(abs(self.a)) + + def __add__(self, other): + return self.__class__(self.a + other.a) + + def __sub__(self, other): + return self.__class__(self.a - other.a) + + def __mul__(self, other): + return self.__class__(self.a * other.a) + + def __truediv__(self, other): + if not other: + if self >= self.__class__(0): + return self.__class__(+mt.inf) + else: + return self.__class__(-mt.inf) + return self.__class__(self.a // other.a) + + def __mod__(self, other): + return self.__class__(self.a % other.a) + +# float fields +class CsvFloat(co.namedtuple('CsvFloat', 'a')): + __slots__ = () + def __new__(cls, a=0.0): + if isinstance(a, CsvFloat): + return a + if isinstance(a, str): + try: + a = float(a) + except ValueError: + # also accept +-∞ and +-inf + if re.match('^\s*\+?\s*(?:∞|inf)\s*$', a): + a = mt.inf + elif re.match('^\s*-\s*(?:∞|inf)\s*$', a): + a = -mt.inf + else: + raise + if not isinstance(a, float): + a = float(a) + return super().__new__(cls, a) + + def __repr__(self): + return '%s(%r)' % (self.__class__.__name__, self.a) + + def __str__(self): + if self.a == mt.inf: + return '∞' + elif self.a == -mt.inf: + return '-∞' + else: + return '%.1f' % self.a + + def __csv__(self): + if self.a == mt.inf: + return 'inf' + elif self.a == -mt.inf: + return '-inf' + else: + return repr(self.a) + + def __bool__(self): + return bool(self.a) + + def __int__(self): + return int(self.a) + + def __float__(self): + return float(self.a) + + none = '%7s' % '-' + def table(self): + return '%7s' % (self,) + + def diff(self, other): + new = self.a if self else 0 + old = other.a if other else 0 + diff = new - old + if diff == +mt.inf: + return '%7s' % '+∞' + elif diff == -mt.inf: + return '%7s' % '-∞' + else: + return '%+7.1f' % diff + + def ratio(self, other): + new = self.a if self else 0 + old = other.a if other else 0 + if mt.isinf(new) and mt.isinf(old): + return 0.0 + elif mt.isinf(new): + return +mt.inf + elif mt.isinf(old): + return -mt.inf + elif not old and not new: + return 0.0 + elif not old: + return +mt.inf + else: + return (new-old) / old + + def __pos__(self): + return self.__class__(+self.a) + + def __neg__(self): + return self.__class__(-self.a) + + def __abs__(self): + return self.__class__(abs(self.a)) + + def __add__(self, other): + return self.__class__(self.a + other.a) + + def __sub__(self, other): + return self.__class__(self.a - other.a) + + def __mul__(self, other): + return self.__class__(self.a * other.a) + + def __truediv__(self, other): + if not other: + if self >= self.__class__(0): + return self.__class__(+mt.inf) + else: + return self.__class__(-mt.inf) + return self.__class__(self.a / other.a) + + def __mod__(self, other): + return self.__class__(self.a % other.a) + +# fractional fields, a/b +class CsvFrac(co.namedtuple('CsvFrac', 'a,b')): + __slots__ = () + def __new__(cls, a=0, b=None): + if isinstance(a, CsvFrac) and b is None: + return a + if isinstance(a, str) and b is None: + a, b = a.split('/', 1) + if b is None: + b = a + return super().__new__(cls, CsvInt(a), CsvInt(b)) + + def __repr__(self): + return '%s(%r, %r)' % (self.__class__.__name__, self.a.a, self.b.a) + + def __str__(self): + return '%s/%s' % (self.a, self.b) + + def __csv__(self): + return '%s/%s' % (self.a.__csv__(), self.b.__csv__()) + + def __bool__(self): + return bool(self.a) + + def __int__(self): + return int(self.a) + + def __float__(self): + return float(self.a) + + none = '%11s' % '-' + def table(self): + return '%11s' % (self,) + + def notes(self): + if self.b.a == 0 and self.a.a == 0: + t = 1.0 + elif self.b.a == 0: + t = mt.copysign(mt.inf, self.a.a) + else: + t = self.a.a / self.b.a + return ['∞%' if t == +mt.inf + else '-∞%' if t == -mt.inf + else '%.1f%%' % (100*t)] + + def diff(self, other): + new_a, new_b = self if self else (CsvInt(0), CsvInt(0)) + old_a, old_b = other if other else (CsvInt(0), CsvInt(0)) + return '%11s' % ('%s/%s' % ( + new_a.diff(old_a).strip(), + new_b.diff(old_b).strip())) + + def ratio(self, other): + new_a, new_b = self if self else (CsvInt(0), CsvInt(0)) + old_a, old_b = other if other else (CsvInt(0), CsvInt(0)) + new = new_a.a/new_b.a if new_b.a else 1.0 + old = old_a.a/old_b.a if old_b.a else 1.0 + return new - old + + def __pos__(self): + return self.__class__(+self.a, +self.b) + + def __neg__(self): + return self.__class__(-self.a, -self.b) + + def __abs__(self): + return self.__class__(abs(self.a), abs(self.b)) + + def __add__(self, other): + return self.__class__(self.a + other.a, self.b + other.b) + + def __sub__(self, other): + return self.__class__(self.a - other.a, self.b - other.b) + + def __mul__(self, other): + return self.__class__(self.a * other.a, self.b * other.b) + + def __truediv__(self, other): + return self.__class__(self.a / other.a, self.b / other.b) + + def __mod__(self, other): + return self.__class__(self.a % other.a, self.b % other.b) + + def __eq__(self, other): + self_a, self_b = self if self.b.a else (CsvInt(1), CsvInt(1)) + other_a, other_b = other if other.b.a else (CsvInt(1), CsvInt(1)) + return self_a * other_b == other_a * self_b + + def __ne__(self, other): + return not self.__eq__(other) + + def __lt__(self, other): + self_a, self_b = self if self.b.a else (CsvInt(1), CsvInt(1)) + other_a, other_b = other if other.b.a else (CsvInt(1), CsvInt(1)) + return self_a * other_b < other_a * self_b + + def __gt__(self, other): + return self.__class__.__lt__(other, self) + + def __le__(self, other): + return not self.__gt__(other) + + def __ge__(self, other): + return not self.__lt__(other) + + +# various fold operations +class CsvSum: + def __call__(self, xs): + return sum(xs[1:], start=xs[0]) + +class CsvProd: + def __call__(self, xs): + return mt.prod(xs[1:], start=xs[0]) + +class CsvMin: + def __call__(self, xs): + return min(xs) + +class CsvMax: + def __call__(self, xs): + return max(xs) + +class CsvAvg: + def __call__(self, xs): + return CsvFloat(sum(float(x) for x in xs) / len(xs)) + +class CsvStddev: + def __call__(self, xs): + avg = sum(float(x) for x in xs) / len(xs) + return CsvFloat(mt.sqrt( + sum((float(x) - avg)**2 for x in xs) / len(xs))) + +class CsvGMean: + def __call__(self, xs): + return CsvFloat(mt.prod(float(x) for x in xs)**(1/len(xs))) + +class CsvGStddev: + def __call__(self, xs): + gmean = mt.prod(float(x) for x in xs)**(1/len(xs)) + return CsvFloat( + mt.exp(mt.sqrt( + sum(mt.log(float(x)/gmean)**2 for x in xs) / len(xs))) + if gmean else mt.inf) + + +# a simple general-purpose parser class +# +# basically just because memoryview doesn't support strs +class Parser: + def __init__(self, data, ws='\s*', ws_flags=0): + self.data = data + self.i = 0 + self.m = None + # also consume whitespace + self.ws = re.compile(ws, ws_flags) + self.i = self.ws.match(self.data, self.i).end() + + def __repr__(self): + if len(self.data) - self.i <= 32: + return repr(self.data[self.i:]) + else: + return "%s..." % repr(self.data[self.i:self.i+32])[:32] + + def __str__(self): + return self.data[self.i:] + + def __len__(self): + return len(self.data) - self.i + + def __bool__(self): + return self.i != len(self.data) + + def match(self, pattern, flags=0): + # compile so we can use the pos arg, this is still cached + self.m = re.compile(pattern, flags).match(self.data, self.i) + return self.m + + def group(self, *groups): + return self.m.group(*groups) + + def chomp(self, *groups): + g = self.group(*groups) + self.i = self.m.end() + # also consume whitespace + self.i = self.ws.match(self.data, self.i).end() + return g + + class Error(Exception): + pass + + def chompmatch(self, pattern, flags=0, *groups): + if not self.match(pattern, flags): + raise Parser.Error("expected %r, found %r" % (pattern, self)) + return self.chomp(*groups) + + def unexpected(self): + raise Parser.Error("unexpected %r" % self) + + def lookahead(self): + # push state on the stack + if not hasattr(self, 'stack'): + self.stack = [] + self.stack.append((self.i, self.m)) + return self + + def consume(self): + # pop and use new state + self.stack.pop() + + def discard(self): + # pop and discard new state + self.i, self.m = self.stack.pop() + + def __enter__(self): + return self + + def __exit__(self, et, ev, tb): + # keep new state if no exception occured + if et is None: + self.consume() + else: + self.discard() + +# a lazily-evaluated field expression +class CsvExpr: + # expr parsing/typechecking/etc errors + class Error(Exception): + pass + + # expr node base class + class Expr: + def __init__(self, *args): + for k, v in zip('abcdefghijklmnopqrstuvwxyz', args): + setattr(self, k, v) + + def __iter__(self): + return (getattr(self, k) + for k in it.takewhile( + lambda k: hasattr(self, k), + 'abcdefghijklmnopqrstuvwxyz')) + + def __len__(self): + return sum(1 for _ in self) + + def __repr__(self): + return '%s(%s)' % ( + self.__class__.__name__, + ','.join(repr(v) for v in self)) + + def fields(self): + return set(it.chain.from_iterable(v.fields() for v in self)) + + def type(self, types={}): + t = self.a.type(types) + if not all(t == v.type(types) for v in it.islice(self, 1, None)): + raise CsvExpr.Error("mismatched types? %r" % self) + return t + + def fold(self, types={}): + return self.a.fold(types) + + def eval(self, fields={}): + return self.a.eval(fields) + + # expr nodes + + # literal exprs + class IntLit(Expr): + def fields(self): + return set() + + def type(self, types={}): + return CsvInt + + def fold(self, types={}): + return CsvSum, CsvInt + + def eval(self, fields={}): + return self.a + + class FloatLit(Expr): + def fields(self): + return set() + + def type(self, types={}): + return CsvFloat + + def fold(self, types={}): + return CsvSum, CsvFloat + + def eval(self, fields={}): + return self.a + + # field expr + class Field(Expr): + def fields(self): + return {self.a} + + def type(self, types={}): + if self.a not in types: + raise CsvExpr.Error("untyped field? %s" % self.a) + return types[self.a] + + def fold(self, types={}): + if self.a not in types: + raise CsvExpr.Error("unfoldable field? %s" % self.a) + return CsvSum, types[self.a] + + def eval(self, fields={}): + if self.a not in fields: + raise CsvExpr.Error("unknown field? %s" % self.a) + return fields[self.a] + + # func expr helper + def func(funcs): + def func(name, args="a"): + def func(f): + f._func = name + f._fargs = args + funcs[f._func] = f + return f + return func + return func + + funcs = {} + func = func(funcs) + + # type exprs + @func('int', 'a') + class Int(Expr): + """Convert to an integer""" + def type(self, types={}): + return CsvInt + + def eval(self, fields={}): + return CsvInt(self.a.eval(fields)) + + @func('float', 'a') + class Float(Expr): + """Convert to a float""" + def type(self, types={}): + return CsvFloat + + def eval(self, fields={}): + return CsvFloat(self.a.eval(fields)) + + @func('frac', 'a[, b]') + class Frac(Expr): + """Convert to a fraction""" + def type(self, types={}): + return CsvFrac + + def eval(self, fields={}): + if len(self) == 1: + return CsvFrac(self.a.eval(fields)) + else: + return CsvFrac(self.a.eval(fields), self.b.eval(fields)) + + # fold exprs + @func('sum', 'a[, ...]') + class Sum(Expr): + """Find the sum of this column or fields""" + def fold(self, types={}): + if len(self) == 1: + return CsvSum, self.a.type(types) + else: + return self.a.fold(types) + + def eval(self, fields={}): + if len(self) == 1: + return self.a.eval(fields) + else: + return CsvSum()([v.eval(fields) for v in self]) + + @func('prod', 'a[, ...]') + class Prod(Expr): + """Find the product of this column or fields""" + def fold(self, types={}): + if len(self) == 1: + return Prod, self.a.type(types) + else: + return self.a.fold(types) + + def eval(self, fields={}): + if len(self) == 1: + return self.a.eval(fields) + else: + return Prod()([v.eval(fields) for v in self]) + + @func('min', 'a[, ...]') + class Min(Expr): + """Find the minimum of this column or fields""" + def fold(self, types={}): + if len(self) == 1: + return CsvMin, self.a.type(types) + else: + return self.a.fold(types) + + def eval(self, fields={}): + if len(self) == 1: + return self.a.eval(fields) + else: + return CsvMin()([v.eval(fields) for v in self]) + + @func('max', 'a[, ...]') + class Max(Expr): + """Find the maximum of this column or fields""" + def fold(self, types={}): + if len(self) == 1: + return CsvMax, self.a.type(types) + else: + return self.a.fold(types) + + def eval(self, fields={}): + if len(self) == 1: + return self.a.eval(fields) + else: + return CsvMax()([v.eval(fields) for v in self]) + + @func('avg', 'a[, ...]') + class Avg(Expr): + """Find the average of this column or fields""" + def type(self, types={}): + if len(self) == 1: + return self.a.type(types) + else: + return CsvFloat + + def fold(self, types={}): + if len(self) == 1: + return CsvAvg, CsvFloat + else: + return self.a.fold(types) + + def eval(self, fields={}): + if len(self) == 1: + return self.a.eval(fields) + else: + return CsvAvg()([v.eval(fields) for v in self]) + + @func('stddev', 'a[, ...]') + class Stddev(Expr): + """Find the standard deviation of this column or fields""" + def type(self, types={}): + if len(self) == 1: + return self.a.type(types) + else: + return CsvFloat + + def fold(self, types={}): + if len(self) == 1: + return CsvStddev, CsvFloat + else: + return self.a.fold(types) + + def eval(self, fields={}): + if len(self) == 1: + return self.a.eval(fields) + else: + return CsvStddev()([v.eval(fields) for v in self]) + + @func('gmean', 'a[, ...]') + class GMean(Expr): + """Find the geometric mean of this column or fields""" + def type(self, types={}): + if len(self) == 1: + return self.a.type(types) + else: + return CsvFloat + + def fold(self, types={}): + if len(self) == 1: + return CsvGMean, CsvFloat + else: + return self.a.fold(types) + + def eval(self, fields={}): + if len(self) == 1: + return self.a.eval(fields) + else: + return CsvGMean()([v.eval(fields) for v in self]) + + @func('gstddev', 'a[, ...]') + class GStddev(Expr): + """Find the geometric stddev of this column or fields""" + def type(self, types={}): + if len(self) == 1: + return self.a.type(types) + else: + return CsvFloat + + def fold(self, types={}): + if len(self) == 1: + return CsvGStddev, CsvFloat + else: + return self.a.fold(types) + + def eval(self, fields={}): + if len(self) == 1: + return self.a.eval(fields) + else: + return CsvGStddev()([v.eval(fields) for v in self]) + + # functions + @func('ratio', 'a') + class Ratio(Expr): + """Ratio of a fraction as a float""" + def type(self, types={}): + return CsvFloat + + def eval(self, fields={}): + v = CsvFrac(self.a.eval(fields)) + if not float(v.b) and not float(v.a): + return CsvFloat(1) + elif not float(v.b): + return CsvFloat(mt.copysign(mt.inf, float(v.a))) + else: + return CsvFloat(float(v.a) / float(v.b)) + + @func('total', 'a') + class Total(Expr): + """Total part of a fraction""" + def type(self, types={}): + return CsvInt + + def eval(self, fields={}): + return CsvFrac(self.a.eval(fields)).b + + @func('abs', 'a') + class Abs(Expr): + """Absolute value""" + def eval(self, fields={}): + return abs(self.a.eval(fields)) + + @func('ceil', 'a') + class Ceil(Expr): + """Round up to nearest integer""" + def type(self, types={}): + return CsvFloat + + def eval(self, fields={}): + return CsvFloat(mt.ceil(float(self.a.eval(fields)))) + + @func('floor', 'a') + class Floor(Expr): + """Round down to nearest integer""" + def type(self, types={}): + return CsvFloat + + def eval(self, fields={}): + return CsvFloat(mt.floor(float(self.a.eval(fields)))) + + @func('log', 'a[, b]') + class Log(Expr): + """Log of a with base e, or log of a with base b""" + def type(self, types={}): + return CsvFloat + + def eval(self, fields={}): + if len(self) == 1: + return CsvFloat(mt.log( + float(self.a.eval(fields)))) + else: + return CsvFloat(mt.log( + float(self.a.eval(fields)), + float(self.b.eval(fields)))) + + @func('pow', 'a[, b]') + class Pow(Expr): + """e to the power of a, or a to the power of b""" + def type(self, types={}): + return CsvFloat + + def eval(self, fields={}): + if len(self) == 1: + return CsvFloat(mt.exp( + float(self.a.eval(fields)))) + else: + return CsvFloat(mt.pow( + float(self.a.eval(fields)), + float(self.b.eval(fields)))) + + @func('sqrt', 'a') + class Sqrt(Expr): + """Square root""" + def type(self, types={}): + return CsvFloat + + def eval(self, fields={}): + return CsvFloat(mt.sqrt(float(self.a.eval(fields)))) + + @func('isint', 'a') + class IsInt(Expr): + """1 if a is an integer, otherwise 0""" + def type(self, types={}): + return CsvInt + + def eval(self, fields={}): + if isinstance(self.a.eval(fields), CsvInt): + return CsvInt(1) + else: + return CsvInt(0) + + @func('isfloat', 'a') + class IsFloat(Expr): + """1 if a is a float, otherwise 0""" + def type(self, types={}): + return CsvInt + + def eval(self, fields={}): + if isinstance(self.a.eval(fields), CsvFloat): + return CsvInt(1) + else: + return CsvInt(0) + + @func('isfrac', 'a') + class IsFrac(Expr): + """1 if a is a fraction, otherwise 0""" + def type(self, types={}): + return CsvInt + + def eval(self, fields={}): + if isinstance(self.a.eval(fields), CsvFrac): + return CsvInt(1) + else: + return CsvInt(0) + + @func('isinf', 'a') + class IsInf(Expr): + """1 if a is infinite, otherwise 0""" + def type(self, types={}): + return CsvInt + + def eval(self, fields={}): + if mt.isinf(self.a.eval(fields)): + return CsvInt(1) + else: + return CsvInt(0) + + @func('isnan') + class IsNan(Expr): + """1 if a is a NAN, otherwise 0""" + def type(self, types={}): + return CsvInt + + def eval(self, fields={}): + if mt.isnan(self.a.eval(fields)): + return CsvInt(1) + else: + return CsvInt(0) + + # unary expr helper + def uop(uops): + def uop(op): + def uop(f): + f._uop = op + uops[f._uop] = f + return f + return uop + return uop + + uops = {} + uop = uop(uops) + + # unary ops + @uop('+') + class Pos(Expr): + """Non-negation""" + def eval(self, fields={}): + return +self.a.eval(fields) + + @uop('-') + class Neg(Expr): + """Negation""" + def eval(self, fields={}): + return -self.a.eval(fields) + + @uop('!') + class NotNot(Expr): + """1 if a is zero, otherwise 0""" + def type(self, types={}): + return CsvInt + + def eval(self, fields={}): + if self.a.eval(fields): + return CsvInt(0) + else: + return CsvInt(1) + + # binary expr help + def bop(bops, bprecs): + def bop(op, prec): + def bop(f): + f._bop = op + f._bprec = prec + bops[f._bop] = f + bprecs[f._bop] = f._bprec + return f + return bop + return bop + + bops = {} + bprecs = {} + bop = bop(bops, bprecs) + + # binary ops + @bop('*', 10) + class Mul(Expr): + """Multiplication""" + def eval(self, fields={}): + return self.a.eval(fields) * self.b.eval(fields) + + @bop('/', 10) + class Div(Expr): + """Division""" + def eval(self, fields={}): + return self.a.eval(fields) / self.b.eval(fields) + + @bop('%', 10) + class Mod(Expr): + """Modulo""" + def eval(self, fields={}): + return self.a.eval(fields) % self.b.eval(fields) + + @bop('+', 9) + class Add(Expr): + """Addition""" + def eval(self, fields={}): + a = self.a.eval(fields) + b = self.b.eval(fields) + return a + b + + @bop('-', 9) + class Sub(Expr): + """Subtraction""" + def eval(self, fields={}): + return self.a.eval(fields) - self.b.eval(fields) + + @bop('==', 4) + class Eq(Expr): + """1 if a equals b, otherwise 0""" + def eval(self, fields={}): + if self.a.eval(fields) == self.b.eval(fields): + return CsvInt(1) + else: + return CsvInt(0) + + @bop('!=', 4) + class Ne(Expr): + """1 if a does not equal b, otherwise 0""" + def eval(self, fields={}): + if self.a.eval(fields) != self.b.eval(fields): + return CsvInt(1) + else: + return CsvInt(0) + + @bop('<', 4) + class Lt(Expr): + """1 if a is less than b""" + def eval(self, fields={}): + if self.a.eval(fields) < self.b.eval(fields): + return CsvInt(1) + else: + return CsvInt(0) + + @bop('<=', 4) + class Le(Expr): + """1 if a is less than or equal to b""" + def eval(self, fields={}): + if self.a.eval(fields) <= self.b.eval(fields): + return CsvInt(1) + else: + return CsvInt(0) + + @bop('>', 4) + class Gt(Expr): + """1 if a is greater than b""" + def eval(self, fields={}): + if self.a.eval(fields) > self.b.eval(fields): + return CsvInt(1) + else: + return CsvInt(0) + + @bop('>=', 4) + class Ge(Expr): + """1 if a is greater than or equal to b""" + def eval(self, fields={}): + if self.a.eval(fields) >= self.b.eval(fields): + return CsvInt(1) + else: + return CsvInt(0) + + @bop('&&', 3) + class AndAnd(Expr): + """b if a is non-zero, otherwise a""" + def eval(self, fields={}): + a = self.a.eval(fields) + if a: + return self.b.eval(fields) + else: + return a + + @bop('||', 2) + class OrOr(Expr): + """a if a is non-zero, otherwise b""" + def eval(self, fields={}): + a = self.a.eval(fields) + if a: + return a + else: + return self.b.eval(fields) + + # ternary expr help + def top(tops, tprecs): + def top(op_a, op_b, prec): + def top(f): + f._top = (op_a, op_b) + f._tprec = prec + tops[f._top] = f + tprecs[f._top] = f._tprec + return f + return top + return top + + tops = {} + tprecs = {} + top = top(tops, tprecs) + + # ternary ops + @top('?', ':', 1) + class IfElse(Expr): + """b if a is non-zero, otherwise c""" + def type(self, types={}): + t = self.b.type(types) + u = self.c.type(types) + if t != u: + raise CsvExpr.Error("mismatched types? %r" % self) + return t + + def fold(self, types={}): + return self.b.fold(types) + + def eval(self, fields={}): + a = self.a.eval(fields) + if a: + return self.b.eval(fields) + else: + return self.c.eval(fields) + + # show expr help text + @classmethod + def help(cls): + print('uops:') + for op in cls.uops.keys(): + print(' %-21s %s' % ('%sa' % op, CsvExpr.uops[op].__doc__)) + print('bops:') + for op in cls.bops.keys(): + print(' %-21s %s' % ('a %s b' % op, CsvExpr.bops[op].__doc__)) + print('tops:') + for op in cls.tops.keys(): + print(' %-21s %s' % ('a %s b %s c' % op, CsvExpr.tops[op].__doc__)) + print('funcs:') + for func in cls.funcs.keys(): + print(' %-21s %s' % ( + '%s(%s)' % (func, CsvExpr.funcs[func]._fargs), + CsvExpr.funcs[func].__doc__)) + + # parse an expr + def __init__(self, expr): + self.expr = expr.strip() + + # parse the expression into a tree + def p_expr(p, prec=0): + # parens + if p.match('\('): + p.chomp() + a = p_expr(p) + if not p.match('\)'): + raise CsvExpr.Error("mismatched parens? %s" % p) + p.chomp() + + # floats + elif p.match('[+-]?(?:[_0-9]*\.(?:[_0-9]|[eE][+-]?)*|nan)'): + a = CsvExpr.FloatLit(CsvFloat(p.chomp())) + + # ints + elif p.match('[+-]?(?:[0-9][bBoOxX]?[_0-9a-fA-F]*|∞|inf)'): + a = CsvExpr.IntLit(CsvInt(p.chomp())) + + # fields/functions + elif p.match('[_a-zA-Z][_a-zA-Z0-9]*'): + a = p.chomp() + + if p.match('\('): + p.chomp() + if a not in CsvExpr.funcs: + raise CsvExpr.Error("unknown function? %s" % a) + args = [] + while True: + b = p_expr(p) + args.append(b) + if p.match(','): + p.chomp() + continue + else: + if not p.match('\)'): + raise CsvExpr.Error("mismatched parens? %s" % p) + p.chomp() + a = CsvExpr.funcs[a](*args) + break + else: + a = CsvExpr.Field(a) + + # unary ops + elif any(p.match(re.escape(op)) for op in CsvExpr.uops.keys()): + # sort by len to avoid ambiguities + for op in sorted(CsvExpr.uops.keys(), reverse=True): + if p.match(re.escape(op)): + p.chomp() + a = p_expr(p, mt.inf) + a = CsvExpr.uops[op](a) + break + else: + assert False + + # unknown expr? + else: + raise CsvExpr.Error("unknown expr? %s" % p) + + # parse tail + while True: + # binary ops + if any(p.match(re.escape(op)) + and prec < CsvExpr.bprecs[op] + for op in CsvExpr.bops.keys()): + # sort by len to avoid ambiguities + for op in sorted(CsvExpr.bops.keys(), reverse=True): + if (p.match(re.escape(op)) + and prec < CsvExpr.bprecs[op]): + p.chomp() + b = p_expr(p, CsvExpr.bprecs[op]) + a = CsvExpr.bops[op](a, b) + break + else: + assert False + + # ternary ops, these are intentionally right associative + elif any(p.match(re.escape(op[0])) + and prec <= CsvExpr.tprecs[op] + for op in CsvExpr.tops.keys()): + # sort by len to avoid ambiguities + for op in sorted(CsvExpr.tops.keys(), reverse=True): + if (p.match(re.escape(op[0])) + and prec <= CsvExpr.tprecs[op]): + p.chomp() + b = p_expr(p, CsvExpr.tprecs[op]) + if not p.match(re.escape(op[1])): + raise CsvExpr.Error( + 'mismatched ternary op? %s %s' % op) + p.chomp() + c = p_expr(p, CsvExpr.tprecs[op]) + a = CsvExpr.tops[op](a, b, c) + break + else: + assert False + + # no tail + else: + return a + + try: + p = Parser(self.expr) + self.tree = p_expr(p) + if p: + raise CsvExpr.Error("trailing expr? %s" % p) + + except (CsvExpr.Error, ValueError) as e: + print('error: in expr: %s' % self.expr, + file=sys.stderr) + print('error: %s' % e, + file=sys.stderr) + sys.exit(3) + + # recursively find all fields + def fields(self): + try: + return self.tree.fields() + except CsvExpr.Error as e: + print('error: in expr: %s' % self.expr, + file=sys.stderr) + print('error: %s' % e, + file=sys.stderr) + sys.exit(3) + + # recursively find the type + def type(self, types={}): + try: + return self.tree.type(types) + except CsvExpr.Error as e: + print('error: in expr: %s' % self.expr, + file=sys.stderr) + print('error: %s' % e, + file=sys.stderr) + sys.exit(3) + + # recursively find the fold operation + def fold(self, types={}): + try: + return self.tree.fold(types) + except CsvExpr.Error as e: + print('error: in expr: %s' % self.expr, + file=sys.stderr) + print('error: %s' % e, + file=sys.stderr) + sys.exit(3) + + # recursive evaluate the expr + def eval(self, fields={}): + try: + return self.tree.eval(fields) + except CsvExpr.Error as e: + print('error: in expr: %s' % self.expr, + file=sys.stderr) + print('error: %s' % e, + file=sys.stderr) + sys.exit(3) + +# SI-prefix formatter +def si(x): + if x == 0: + return '0' + # figure out prefix and scale + p = 3*mt.floor(mt.log(abs(x), 10**3)) + p = min(18, max(-18, p)) + # format with 3 digits of precision + s = '%.3f' % (abs(x) / (10.0**p)) + s = s[:3+1] + # truncate but only digits that follow the dot + if '.' in s: + s = s.rstrip('0') + s = s.rstrip('.') + return '%s%s%s' % ('-' if x < 0 else '', s, SI_PREFIXES[p]) + +# SI-prefix formatter for powers-of-two +def si2(x): + if x == 0: + return '0' + # figure out prefix and scale + p = 10*mt.floor(mt.log(abs(x), 2**10)) + p = min(30, max(-30, p)) + # format with 3 digits of precision + s = '%.3f' % (abs(x) / (2.0**p)) + s = s[:3+1] + # truncate but only digits that follow the dot + if '.' in s: + s = s.rstrip('0') + s = s.rstrip('.') + return '%s%s%s' % ('-' if x < 0 else '', s, SI2_PREFIXES[p]) + +# parse %-escaped strings +# +# attrs can override __getitem__ for lazy attr generation +def punescape(s, attrs=None): + pattern = re.compile( + '%[%n]' + '|' '%x..' + '|' '%u....' + '|' '%U........' + '|' '%\((?P[^)]*)\)' + '(?P[+\- #0-9\.]*[siIdboxXfFeEgG])') + def unescape(m): + if m.group()[1] == '%': return '%' + elif m.group()[1] == 'n': return '\n' + elif m.group()[1] == 'x': return chr(int(m.group()[2:], 16)) + elif m.group()[1] == 'u': return chr(int(m.group()[2:], 16)) + elif m.group()[1] == 'U': return chr(int(m.group()[2:], 16)) + elif m.group()[1] == '(': + if attrs is not None: + try: + v = attrs[m.group('field')] + except KeyError: + return m.group() + else: + return m.group() + f = m.group('format') + if f[-1] in 'dboxX': + if isinstance(v, str): + v = dat(v, 0) + v = int(v) + elif f[-1] in 'iIfFeEgG': + if isinstance(v, str): + v = dat(v, 0) + v = float(v) + if f[-1] in 'iI': + v = (si if 'i' in f[-1] else si2)(v) + f = f.replace('i', 's').replace('I', 's') + if '+' in f and not v.startswith('-'): + v = '+'+v + f = f.replace('+', '').replace('-', '') + else: + f = ('<' if '-' in f else '>') + f.replace('-', '') + v = str(v) + # note we need Python's new format syntax for binary + return ('{:%s}' % f).format(v) + else: assert False + + return re.sub(pattern, unescape, s) + +def punescape_help(): + print('mods:') + print(' %-21s %s' % ('%%', 'A literal % character')) + print(' %-21s %s' % ('%n', 'A newline')) + print(' %-21s %s' % ( + '%xaa', 'A character with the hex value aa')) + print(' %-21s %s' % ( + '%uaaaa', 'A character with the hex value aaaa')) + print(' %-21s %s' % ( + '%Uaaaaaaaa', 'A character with the hex value aaaaaaaa')) + print(' %-21s %s' % ( + '%(field)s', 'An existing field formatted as a string')) + print(' %-21s %s' % ( + '%(field)i', 'An field formatted with a base-10 SI prefix')) + print(' %-21s %s' % ( + '%(field)I', 'An field formatted with a base-2 SI prefix')) + print(' %-21s %s' % ( + '%(field)[dboxX]', 'An existing field formatted as an integer')) + print(' %-21s %s' % ( + '%(field)[fFeEgG]', 'An existing field formatted as a float')) + + +# open with '-' for stdin/stdout +def openio(path, mode='r', buffering=-1): + import os + if path == '-': + if 'r' in mode: + return os.fdopen(os.dup(sys.stdin.fileno()), mode, buffering) + else: + return os.fdopen(os.dup(sys.stdout.fileno()), mode, buffering) + else: + return open(path, mode, buffering) + +def collect_csv(csv_paths, *, + depth=1, + children=None, + notes=None, + **_): + # collect both results and fields from CSV files + fields = co.OrderedDict() + results = [] + for path in csv_paths: + try: + with openio(path) as f: + # csv or json? assume json starts with [ + is_json = (f.buffer.peek(1)[:1] == b'[') + + # read csv? + if not is_json: + reader = csv.DictReader(f, restval='') + # collect fields + fields.update((k, True) for k in reader.fieldnames or []) + for r in reader: + # strip and drop empty fields + r_ = {k: v.strip() + for k, v in r.items() + if k not in {'notes'} + and v.strip()} + # special handling for notes field + if notes is not None and notes in r: + r_[notes] = set(r[notes].split(',')) + results.append(r_) + + # read json? + else: + import json + def unjsonify(results, depth_): + results_ = [] + for r in results: + # collect fields + fields.update((k, True) for k in r.keys()) + # convert to strings, we'll reparse these later + # + # this may seem a bit backwards, but it keeps + # the rest of the script simpler if we pretend + # everything came from a csv + r_ = {k: str(v).strip() + for k, v in r.items() + if k not in {'children', 'notes'} + and str(v).strip()} + # special handling for children field + if (children is not None + and children in r + and r[children] is not None + and depth_ > 1): + r_[children] = unjsonify( + r[children], + depth_-1) + # special handling for notes field + if (notes is not None + and notes in r + and r[notes] is not None): + r_[notes] = set(r[notes]) + results_.append(r_) + return results_ + results.extend(unjsonify(json.load(f), depth)) + + except FileNotFoundError: + pass + + return list(fields.keys()), results + +def compile(fields_, results, + by=None, + fields=None, + mods=[], + exprs=[], + sort=None, + children=None, + hot=None, + notes=None, + prefix=None, + **_): + # default to no prefix + if prefix is None: + prefix = '' + + by = by.copy() + fields = fields.copy() + + # make sure sort/hot fields are included + for k, reverse in it.chain(sort or [], hot or []): + # this defaults to typechecking sort/hot fields, which is + # probably safer, if you really want to sort by strings you + # can use --by + --label to create hidden by fields + if k and k not in by and k not in fields: + fields.append(k) + # make sure all expr targets are in fields so they get typechecked + # correctly + for k, _ in exprs: + if k not in fields: + fields.append(k) + + # we only really care about the last mod/expr for each field + mods = {k: mod for k, mod in mods} + exprs = {k: expr for k, expr in exprs} + + # find best type for all fields used by field exprs + fields__ = set(it.chain.from_iterable( + exprs[k].fields() if k in exprs else [k] + for k in fields)) + types__ = {} + for k in fields__: + # check if dependency is in original fields + # + # it's tempting to also allow enumerate fields here, but this + # currently doesn't work when hotifying + if prefix+k not in fields_: + print("error: no field %r?" % k, + file=sys.stderr) + sys.exit(2) + + for t in [CsvInt, CsvFloat, CsvFrac]: + for r in results: + if prefix+k in r and r[prefix+k].strip(): + try: + t(r[prefix+k]) + except ValueError: + break + else: + types__[k] = t + break + else: + print("error: no type matches field %r?" % k, + file=sys.stderr) + sys.exit(2) + + # typecheck exprs, note these may reference input fields with + # the same name, which is why we only do a single eval pass + types___ = types__.copy() + for k, expr in exprs.items(): + types___[k] = expr.type(types__) + + # foldcheck field exprs + folds___ = {k: (CsvSum, t) for k, v in types__.items()} + for k, expr in exprs.items(): + folds___[k] = expr.fold(types__) + folds___ = {k: (f(), t) for k, (f, t) in folds___.items()} + + # create result class + def __new__(cls, **r): + r_ = r.copy() + # evaluate types, strip prefix + for k, t in types__.items(): + r_[k] = t(r[prefix+k]) if prefix+k in r else t() + + r__ = r_.copy() + # evaluate exprs + for k, expr in exprs.items(): + r__[k] = expr.eval(r_) + # evaluate mods + for k, m in mods.items(): + r__[k] = punescape(m, r_) + + # return result + return cls.__mro__[1].__new__(cls, **( + {k: r__.get(k, '') for k in by} + | {k: ([r__[k]], 1) if k in r__ else ([], 0) + for k in fields} + | ({children: r[children] if children in r else []} + if children is not None else {}) + | ({notes: r[notes] if notes in r else set()} + if notes is not None else {}))) + + def __add__(self, other): + # reuse lists if possible + def extend(a, b): + if len(a[0]) == a[1]: + a[0].extend(b[0][:b[1]]) + return (a[0], a[1] + b[1]) + else: + return (a[0][:a[1]] + b[0][:b[1]], a[1] + b[1]) + + # lazily fold results + return self.__class__.__mro__[1].__new__(self.__class__, **( + {k: getattr(self, k) for k in by} + | {k: extend( + object.__getattribute__(self, k), + object.__getattribute__(other, k)) + for k in fields} + | ({children: self.children + other.children} + if children is not None else {}) + | ({notes: self.notes | other.notes} + if notes is not None else {}))) + + def __getattribute__(self, k): + # lazily fold results on demand, this avoids issues with fold + # operations that depend on the number of results + if k in fields: + v = object.__getattribute__(self, k) + if v[1]: + return folds___[k][0](v[0][:v[1]]) + else: + return None + return object.__getattribute__(self, k) + + return type( + 'Result', + (co.namedtuple('Result', list(co.OrderedDict.fromkeys(it.chain( + by, + fields, + [children] if children is not None else [], + [notes] if notes is not None else [])).keys())),), + dict( + __slots__=(), + __new__=__new__, + __add__=__add__, + __getattribute__=__getattribute__, + _by=by, + _fields=fields, + _sort=fields, + _types={k: t for k, (_, t) in folds___.items()}, + _mods=mods, + _exprs=exprs, + **{'_children': children} if children is not None else {}, + **{'_notes': notes} if notes is not None else {})) + +def homogenize(Result, results, *, + enumerates=None, + defines=[], + depth=1, + **_): + # this just converts all (possibly recursive) results to our + # result type + results_ = [] + for r in results: + # filter by matching defines + # + # we do this here instead of in fold to be consistent with + # evaluation order of exprs/mods/etc, note this isn't really + # inconsistent with the other scripts, since they don't really + # evaluate anything + if not all(any(fnmatch.fnmatchcase(str(r.get(k, '')), v) + for v in vs) + for k, vs in defines): + continue + + # append a result + results_.append(Result(**( + r + # enumerate? + | ({e: len(results_) for e in enumerates} + if enumerates is not None + else {}) + # recurse? + | ({Result._children: homogenize( + Result, r[Result._children], + # only filter defines at the top level! + enumerates=enumerates, + depth=depth-1)} + if hasattr(Result, '_children') + and Result._children in r + and r[Result._children] is not None + and depth > 1 + else {})))) + return results_ + + +# common folding/tabling/read/write code + +class Rev(co.namedtuple('Rev', 'a')): + __slots__ = () + # yes we need all of these because we're a namedtuple + def __lt__(self, other): + return self.a > other.a + def __gt__(self, other): + return self.a < other.a + def __le__(self, other): + return self.a >= other.a + def __ge__(self, other): + return self.a <= other.a + +def fold(Result, results, *, + by=None, + defines=[], + sort=None, + depth=1, + **_): + # stop when depth hits zero + if depth == 0: + return [] + + # organize by by + if by is None: + by = Result._by + + for k in it.chain(by or [], (k for k, _ in defines)): + if k not in Result._by and k not in Result._fields: + print("error: could not find field %r?" % k, + file=sys.stderr) + sys.exit(-1) + + # filter by matching defines + if defines: + results_ = [] + for r in results: + if all(any(fnmatch.fnmatchcase(str(getattr(r, k, '')), v) + for v in vs) + for k, vs in defines): + results_.append(r) + results = results_ + + # organize results into conflicts + folding = co.OrderedDict() + for r in results: + name = tuple(getattr(r, k) for k in by) + if name not in folding: + folding[name] = [] + folding[name].append(r) + + # merge conflicts + folded = [] + for name, rs in folding.items(): + folded.append(sum(rs[1:], start=rs[0])) + + # sort, note that python's sort is stable + folded.sort(key=lambda r: ( + # sort by explicit sort fields + tuple((Rev + if reverse ^ (not k or k in Result._fields) + else lambda x: x)( + tuple((getattr(r, k_),) + if getattr(r, k_) is not None + else () + for k_ in ([k] if k else Result._sort))) + for k, reverse in (sort or [])), + # sort by result + r)) + + # recurse if we have recursive results + if hasattr(Result, '_children'): + folded = [r._replace(**{ + Result._children: fold( + Result, getattr(r, Result._children), + by=by, + # only filter defines at the top level! + sort=sort, + depth=depth-1)}) + for r in folded] + + return folded + +def hotify(Result, results, *, + enumerates=None, + depth=1, + hot=None, + **_): + # note! hotifying risks confusion if you don't enumerate/have a + # z field, since it will allow folding across recursive boundaries + + # hotify only makes sense for recursive results + assert hasattr(Result, '_children') + + results_ = [] + for r in results: + hot_ = [] + def recurse(results_, depth_): + nonlocal hot_ + if not results_: + return + + # find the hottest result + r = min(results_, key=lambda r: + tuple((Rev + if reverse ^ (not k or k in Result._fields) + else lambda x: x)( + tuple((getattr(r, k_),) + if getattr(r, k_) is not None + else () + for k_ in ([k] if k else Result._sort))) + for k, reverse in it.chain(hot, [(None, False)]))) + + hot_.append(r._replace(**( + # enumerate? + ({e: len(hot_) for e in enumerates} + if enumerates is not None + else {}) + | {Result._children: []}))) + + # recurse? + if depth_ > 1: + recurse(getattr(r, Result._children), + depth_-1) + + recurse(getattr(r, Result._children), depth-1) + results_.append(r._replace(**{Result._children: hot_})) + + return results_ + +def table(Result, results, diff_results=None, *, + by=None, + fields=None, + sort=None, + labels=None, + depth=1, + hot=None, + percent=False, + all=False, + compare=None, + no_header=False, + small_header=False, + no_total=False, + small_total=False, + small_table=False, + summary=False, + total=False, + **_): + import builtins + all_, all = all, builtins.all + + # small_table implies small_header + no_total or small_total + if small_table: + small_header = True + small_total = True + no_total = no_total or (not summary and not total) + # summary implies small_header + if summary: + small_header = True + # total implies summary + no_header + small_total + if total: + summary = True + no_header = True + small_total = True + + if by is None: + by = Result._by + if fields is None: + fields = Result._fields + types = Result._types + + # organize by name + table = { + ','.join(str(getattr(r, k) + if getattr(r, k) is not None + else '') + for k in by): r + for r in results} + diff_table = { + ','.join(str(getattr(r, k) + if getattr(r, k) is not None + else '') + for k in by): r + for r in diff_results or []} + + # lost results? this only happens if we didn't fold by the same + # by field, which is an error and risks confusing results + assert len(table) == len(results) + if diff_results is not None: + assert len(diff_table) == len(diff_results) + + # find compare entry if there is one + if compare: + compare_ = min( + (n for n in table.keys() + if all(fnmatch.fnmatchcase(k, c) + for k, c in it.zip_longest(n.split(','), compare, + fillvalue=''))), + default=compare) + compare_r = table.get(compare_) + + # build up our lines + lines = [] + + # header + if not no_header: + header = ['%s%s' % ( + ','.join(labels if labels is not None else by), + ' (%d added, %d removed)' % ( + sum(1 for n in table if n not in diff_table), + sum(1 for n in diff_table if n not in table)) + if diff_results is not None and not percent else '') + if not small_header else ''] + if diff_results is None or percent: + for k in fields: + header.append(k) + else: + for k in fields: + header.append('o'+k) + for k in fields: + header.append('n'+k) + for k in fields: + header.append('d'+k) + lines.append(header) + + # delete these to try to catch typos below, we need to rebuild + # these tables at each recursive layer + del table + del diff_table + + # entry helper + def table_entry(name, r, diff_r=None): + # prepend name + entry = [name] + + # normal entry? + if ((compare is None or r == compare_r) + and diff_results is None): + for k in fields: + entry.append( + (getattr(r, k).table(), + getattr(getattr(r, k), 'notes', lambda: [])()) + if getattr(r, k, None) is not None + else types[k].none) + # compare entry? + elif diff_results is None: + for k in fields: + entry.append( + (getattr(r, k).table() + if getattr(r, k, None) is not None + else types[k].none, + (lambda t: ['+∞%'] if t == +mt.inf + else ['-∞%'] if t == -mt.inf + else ['%+.1f%%' % (100*t)])( + types[k].ratio( + getattr(r, k, None), + getattr(compare_r, k, None))))) + # percent entry? + elif percent: + for k in fields: + entry.append( + (getattr(r, k).table() + if getattr(r, k, None) is not None + else types[k].none, + (lambda t: ['+∞%'] if t == +mt.inf + else ['-∞%'] if t == -mt.inf + else ['%+.1f%%' % (100*t)])( + types[k].ratio( + getattr(r, k, None), + getattr(diff_r, k, None))))) + # diff entry? + else: + for k in fields: + entry.append(getattr(diff_r, k).table() + if getattr(diff_r, k, None) is not None + else types[k].none) + for k in fields: + entry.append(getattr(r, k).table() + if getattr(r, k, None) is not None + else types[k].none) + for k in fields: + entry.append( + (types[k].diff( + getattr(r, k, None), + getattr(diff_r, k, None)), + (lambda t: ['+∞%'] if t == +mt.inf + else ['-∞%'] if t == -mt.inf + else ['%+.1f%%' % (100*t)] if t + else [])( + types[k].ratio( + getattr(r, k, None), + getattr(diff_r, k, None))))) + + # append any notes + if hasattr(Result, '_notes') and r is not None: + notes = sorted(getattr(r, Result._notes)) + if isinstance(entry[-1], tuple): + entry[-1] = (entry[-1][0], entry[-1][1] + notes) + else: + entry[-1] = (entry[-1], notes) + + return entry + + # recursive entry helper + def table_recurse(results_, diff_results_, + depth_, + prefixes=('', '', '', '')): + # build the children table at each layer + table_ = { + ','.join(str(getattr(r, k) + if getattr(r, k) is not None + else '') + for k in by): r + for r in results_} + diff_table_ = { + ','.join(str(getattr(r, k) + if getattr(r, k) is not None + else '') + for k in by): r + for r in diff_results_ or []} + names_ = [n + for n in table_.keys() | diff_table_.keys() + if diff_results is None + or all_ + or any( + types[k].ratio( + getattr(table_.get(n), k, None), + getattr(diff_table_.get(n), k, None)) + for k in fields)] + + # sort again, now with diff info, note that python's sort is stable + names_.sort(key=lambda n: ( + # sort by explicit sort fields + next( + tuple((Rev + if reverse ^ (not k or k in Result._fields) + else lambda x: x)( + tuple((getattr(r_, k_),) + if getattr(r_, k_) is not None + else () + for k_ in ([k] if k else Result._sort))) + for k, reverse in (sort or [])) + for r_ in [table_.get(n), diff_table_.get(n)] + if r_ is not None), + # sort by ratio if diffing + Rev(tuple(types[k].ratio( + getattr(table_.get(n), k, None), + getattr(diff_table_.get(n), k, None)) + for k in fields)) + if diff_results is not None + else (), + # move compare entry to the top, note this can be + # overridden by explicitly sorting by fields + (table_.get(n) != compare_r, + # sort by ratio if comparing + Rev(tuple( + types[k].ratio( + getattr(table_.get(n), k, None), + getattr(compare_r, k, None)) + for k in fields))) + if compare + else (), + # sort by result + (table_[n],) if n in table_ else (), + # and finally by name (diffs may be missing results) + n)) + + for i, name in enumerate(names_): + # find comparable results + r = table_.get(name) + diff_r = diff_table_.get(name) + + # figure out a good label + if labels is not None: + label = next( + ','.join(str(getattr(r_, k) + if getattr(r_, k) is not None + else '') + for k in labels) + for r_ in [r, diff_r] + if r_ is not None) + else: + label = name + + # build line + line = table_entry(label, r, diff_r) + + # add prefixes + line = [x if isinstance(x, tuple) else (x, []) for x in line] + line[0] = (prefixes[0+(i==len(names_)-1)] + line[0][0], line[0][1]) + lines.append(line) + + # recurse? + if name in table_ and depth_ > 1: + table_recurse( + getattr(r, Result._children), + getattr(diff_r, Result._children, None), + depth_-1, + (prefixes[2+(i==len(names_)-1)] + "|-> ", + prefixes[2+(i==len(names_)-1)] + "'-> ", + prefixes[2+(i==len(names_)-1)] + "| ", + prefixes[2+(i==len(names_)-1)] + " ")) + + # build entries + if not summary: + table_recurse(results, diff_results, depth) + + # total + if not no_total: + r = next(iter(fold(Result, results, by=[])), Result()) + if diff_results is None: + diff_r = None + else: + diff_r = next(iter(fold(Result, diff_results, by=[])), Result()) + lines.append(table_entry( + 'TOTAL' if not small_total else '', + r, diff_r)) + + # homogenize + lines = [[x if isinstance(x, tuple) else (x, []) for x in line] + for line in lines] + + # find the best widths, note that column 0 contains the names and is + # handled a bit differently + widths = co.defaultdict(lambda: 7, {0: 7}) + nwidths = co.defaultdict(lambda: 0) + for line in lines: + for i, x in enumerate(line): + widths[i] = max(widths[i], ((len(x[0])+1+4-1)//4)*4-1) + if i != len(line)-1: + nwidths[i] = max(nwidths[i], 1+sum(2+len(n) for n in x[1])) + if not any(line[0][0] for line in lines): + widths[0] = 0 + + # print our table + for line in lines: + print('%-*s %s' % ( + widths[0], line[0][0], + ' '.join('%*s%-*s' % ( + widths[i], x[0], + nwidths[i], ' (%s)' % ', '.join(x[1]) if x[1] else '') + for i, x in enumerate(line[1:], 1)))) + +def read_csv(path, Result, *, + depth=1, + prefix=None, + **_): + # prefix? this only applies to field fields + if prefix is None: + if hasattr(Result, '_prefix'): + prefix = '%s_' % Result._prefix + else: + prefix = '' + + by = Result._by + fields = Result._fields + + with openio(path, 'r') as f: + # csv or json? assume json starts with [ + json = (f.buffer.peek(1)[:1] == b'[') + + # read csv? + if not json: + results = [] + reader = csv.DictReader(f, restval='') + for r in reader: + if not any(prefix+k in r and r[prefix+k].strip() + for k in fields): + continue + try: + # note this allows by/fields to overlap + results.append(Result(**( + {k: r[k] for k in by + if k in r + and r[k].strip()} + | {k: r[prefix+k] for k in fields + if prefix+k in r + and r[prefix+k].strip()}))) + except TypeError: + pass + return results + + # read json? + else: + import json + def unjsonify(results, depth_): + results_ = [] + for r in results: + if not any(prefix+k in r and r[prefix+k].strip() + for k in fields): + continue + try: + # note this allows by/fields to overlap + results_.append(Result(**( + {k: r[k] for k in by + if k in r + and r[k] is not None} + | {k: r[prefix+k] for k in fields + if prefix+k in r + and r[prefix+k] is not None} + | ({Result._children: unjsonify( + r[Result._children], + depth_-1)} + if hasattr(Result, '_children') + and Result._children in r + and r[Result._children] is not None + and depth_ > 1 + else {}) + | ({Result._notes: set(r[Result._notes])} + if hasattr(Result, '_notes') + and Result._notes in r + and r[Result._notes] is not None + else {})))) + except TypeError: + pass + return results_ + return unjsonify(json.load(f), depth) + +def write_csv(path, Result, results, *, + json=False, + by=None, + fields=None, + depth=1, + prefix=None, + **_): + # prefix? this only applies to field fields + if prefix is None: + if hasattr(Result, '_prefix'): + prefix = '%s_' % Result._prefix + else: + prefix = '' + + if by is None: + by = Result._by + if fields is None: + fields = Result._fields + + with openio(path, 'w') as f: + # write csv? + if not json: + writer = csv.DictWriter(f, list( + co.OrderedDict.fromkeys(it.chain( + by, + (prefix+k for k in fields))).keys())) + writer.writeheader() + for r in results: + # note this allows by/fields to overlap + writer.writerow( + {k: getattr(r, k) + for k in by + if getattr(r, k) is not None} + | {prefix+k: getattr(r, k).__csv__() + for k in fields + if getattr(r, k) is not None}) + + # write json? + else: + import json + # the neat thing about json is we can include recursive results + def jsonify(results, depth_): + results_ = [] + for r in results: + # note this allows by/fields to overlap + results_.append( + {k: getattr(r, k) + for k in by + if getattr(r, k) is not None} + | {prefix+k: getattr(r, k).__csv__() + for k in fields + if getattr(r, k) is not None} + | ({Result._children: jsonify( + getattr(r, Result._children), + depth_-1)} + if hasattr(Result, '_children') + and getattr(r, Result._children) + and depth_ > 1 + else {}) + | ({Result._notes: list( + getattr(r, Result._notes))} + if hasattr(Result, '_notes') + and getattr(r, Result._notes) + else {})) + return results_ + json.dump(jsonify(results, depth), f, + separators=(',', ':')) + + +def main(csv_paths, *, + by=None, + fields=None, + defines=[], + sort=None, + depth=None, + children=None, + hot=None, + notes=None, + **args): + # show mod help text? + if args.get('help_mods'): + return punescape_help() + # show expr help text? + if args.get('help_exprs'): + return CsvExpr.help() + + if ((by is None or all(hidden for (k, v), hidden in by)) + and (fields is None or all(hidden for (k, v), hidden in fields))): + print("error: needs --by or --fields to figure out fields", + file=sys.stderr) + sys.exit(-1) + + if children is not None: + if len(children) > 1: + print("error: multiple --children fields currently not supported", + file=sys.stderr) + sys.exit(-1) + children = children[0] + + if notes is not None: + if len(notes) > 1: + print("error: multiple --notes fields currently not supported", + file=sys.stderr) + sys.exit(-1) + notes = notes[0] + + # recursive results imply --children + if (depth is not None or hot is not None) and children is None: + children = 'children' + + # figure out depth + if depth is None: + depth = mt.inf if hot else 1 + elif depth == 0: + depth = mt.inf + + # find results + if not args.get('use', None): + # not enough info? + if not csv_paths: + print("error: no *.csv files?", + file=sys.stderr) + sys.exit(1) + + # collect info + fields_, results = collect_csv(csv_paths, + depth=depth, + children=children, + notes=notes, + **args) + + else: + # use is just an alias but takes priority + fields_, results = collect_csv([args['use']], + depth=depth, + children=children, + notes=notes, + **args) + + # separate out enumerates/mods/exprs + # + # enumerate enumerates: -ia + # by supports mods: -ba=%(b)s + # fields/sort/etc supports exprs: -fa=b+c + # + enumerates = [k + for (k, v), hidden in (by or []) + if v == enumerate] + mods = [(k, v) + for k, v in it.chain( + ((k, v) for (k, v), hidden in (by or []) + if v != enumerate)) + if v is not None] + exprs = [(k, v) + for k, v in it.chain( + ((k, v) for (k, v), hidden in (fields or [])), + ((k, v) for (k, v), reverse in (sort or [])), + ((k, v) for (k, v), reverse in (hot or []))) + if v is not None] + + # figure out labels/by/fields + labels__ = None + by__ = [] + fields__ = [] + if by is not None and any(not hidden for (k, v), hidden in by): + labels__ = [k for (k, v), hidden in by if not hidden] + if by is not None: + by__ = [k for (k, v), hidden in by] + if fields is not None: + fields__ = [k for (k, v), hidden in fields + if not hidden + or args.get('output') + or args.get('output_json')] + + # if by not specified, guess it's anything not in fields/defines/exprs/etc + if by is None or all(hidden for (k, v), hidden in by): + by__.extend(k for k in fields_ + if not any(k == k_ for (k_, _), _ in (by or [])) + and not any(k == k_ for (k_, _), _ in (fields or [])) + and not any(k == k_ for k_, _ in defines) + and not any(k == k_ for (k_, _), _ in (sort or [])) + and k != children + and not any(k == k_ for (k_, _), _ in (hot or [])) + and k != notes + and not any(k == k_ + for _, expr in exprs + for k_ in expr.fields())) + + # if fields not specified, guess it's anything not in by/defines/exprs/etc + if fields is None or all(hidden for (k, v), hidden in fields): + fields__.extend(k for k in fields_ + if not any(k == k_ for (k_, _), _ in (by or [])) + and not any(k == k_ for (k_, _), _ in (fields or [])) + and not any(k == k_ for k_, _ in defines) + and not any(k == k_ for (k_, _), _ in (sort or [])) + and k != children + and not any(k == k_ for (k_, _), _ in (hot or [])) + and k != notes + and not any(k == k_ + for _, expr in exprs + for k_ in expr.fields())) + labels = labels__ + by = by__ + fields = fields__ + + # filter exprs from sort/hot + if sort is not None: + sort = [(k, reverse) for (k, v), reverse in sort] + if hot is not None: + hot = [(k, reverse) for (k, v), reverse in hot] + + # ok ok, now that by/fields/bla/bla/bla is all figured out + # + # build result type + Result = compile(fields_, results, + by=by, + fields=fields, + mods=mods, + exprs=exprs, + sort=sort, + children=children, + hot=hot, + notes=notes, + **args) + + # homogenize + results = homogenize(Result, results, + enumerates=enumerates, + defines=defines, + depth=depth) + + # fold + results = fold(Result, results, + by=by, + sort=sort, + depth=depth) + + # hotify? + if hot: + results = hotify(Result, results, + enumerates=enumerates, + depth=depth, + hot=hot) + + # find previous results? + diff_results = None + if args.get('diff'): + # note! don't use read_csv here + # + # it's tempting now that we have a Result type, but we want to + # make sure all the defines/exprs/mods/etc are evaluated in the + # same order + try: + _, diff_results = collect_csv( + [args.get('diff')], + depth=depth, + children=children, + notes=notes, + **args) + except FileNotFoundError: + diff_results = [] + + # homogenize + diff_results = homogenize(Result, diff_results, + enumerates=enumerates, + defines=defines, + depth=depth) + + # fold + diff_results = fold(Result, diff_results, + by=by, + depth=depth) + + # hotify? + if hot: + diff_results = hotify(Result, diff_results, + enumerates=enumerates, + depth=depth, + hot=hot) + + # write results to JSON + if args.get('output_json'): + write_csv(args['output_json'], Result, results, json=True, + by=by, + fields=fields, + depth=depth, + **args) + # write results to CSV + elif args.get('output'): + write_csv(args['output'], Result, results, + by=by, + fields=fields, + depth=depth, + **args) + # print table + elif not args.get('quiet'): + table(Result, results, diff_results, + by=by, + fields=fields, + sort=sort, + labels=labels, + depth=depth, + **args) + + +if __name__ == "__main__": + import argparse + import sys + parser = argparse.ArgumentParser( + description="Script to manipulate CSV files.", + allow_abbrev=False) + parser.add_argument( + 'csv_paths', + nargs='*', + help="Input *.csv files.") + parser.add_argument( + '--help-mods', + action='store_true', + help="Show what %% modifiers are available.") + parser.add_argument( + '--help-exprs', + action='store_true', + help="Show what field exprs are available.") + parser.add_argument( + '-q', '--quiet', + action='store_true', + help="Don't show anything, useful when checking for errors.") + parser.add_argument( + '-o', '--output', + help="Specify CSV file to store results.") + parser.add_argument( + '-O', '--output-json', + help="Specify JSON file to store results. This may contain " + "recursive info.") + parser.add_argument( + '-u', '--use', + help="Don't parse anything, use this CSV/JSON file.") + parser.add_argument( + '-d', '--diff', + help="Specify CSV/JSON file to diff against.") + parser.add_argument( + '-p', '--percent', + action='store_true', + help="Only show percentage change, not a full diff.") + parser.add_argument( + '-c', '--compare', + type=lambda x: tuple(v.strip() for v in x.split(',')), + help="Compare results to the row matching this by pattern.") + parser.add_argument( + '-a', '--all', + action='store_true', + help="Show all, not just the ones that changed.") + class AppendBy(argparse.Action): + def __call__(self, parser, namespace, value, option): + if namespace.by is None: + namespace.by = [] + namespace.by.append((value, option in { + '-B', '--hidden-by', + '-I', '--hidden-enumerate'})) + parser.add_argument( + '-i', '--enumerate', + action=AppendBy, + nargs='?', + type=lambda x: (x, enumerate), + const=('i', enumerate), + help="Enumerate results with this field. This will prevent " + "result folding.") + parser.add_argument( + '-I', '--hidden-enumerate', + action=AppendBy, + nargs='?', + type=lambda x: (x, enumerate), + const=('i', enumerate), + help="Like -i/--enumerate, but hidden from the table renderer, " + "and doesn't affect -b/--by defaults.") + parser.add_argument( + '-b', '--by', + action=AppendBy, + type=lambda x: ( + lambda k, v=None: ( + k.strip(), + v.strip() if v is not None else None) + )(*x.split('=', 1)), + help="Group by this field. This does _not_ support expressions, " + "but can be assigned a string with %% modifiers.") + parser.add_argument( + '-B', '--hidden-by', + action=AppendBy, + type=lambda x: ( + lambda k, v=None: ( + k.strip(), + v.strip() if v is not None else None) + )(*x.split('=', 1)), + help="Like -b/--by, but hidden from the table renderer, " + "and doesn't affect -b/--by defaults.") + class AppendField(argparse.Action): + def __call__(self, parser, namespace, value, option): + if namespace.fields is None: + namespace.fields = [] + namespace.fields.append((value, option in { + '-F', '--hidden-field'})) + parser.add_argument( + '-f', '--field', + dest='fields', + action=AppendField, + type=lambda x: ( + lambda k, v=None: ( + k.strip(), + CsvExpr(v) if v is not None else None) + )(*x.split('=', 1)), + help="Show this field. Can include an expression of the form " + "field=expr.") + parser.add_argument( + '-F', '--hidden-field', + dest='fields', + action=AppendField, + type=lambda x: ( + lambda k, v=None: ( + k.strip(), + v.strip() if v is not None else None) + )(*x.split('=', 1)), + help="Like -f/--field, but hidden from the table renderer, " + "and doesn't affect -f/--field defaults.") + parser.add_argument( + '-D', '--define', + dest='defines', + action='append', + type=lambda x: ( + lambda k, vs: ( + k.strip(), + {v.strip() for v in vs.split(',')}) + )(*x.split('=', 1)), + help="Only include results where this field is this value. May " + "include comma-separated options and globs.") + class AppendSort(argparse.Action): + def __call__(self, parser, namespace, value, option): + if namespace.sort is None: + namespace.sort = [] + namespace.sort.append((value, option in {'-S', '--reverse-sort'})) + parser.add_argument( + '-s', '--sort', + nargs='?', + action=AppendSort, + type=lambda x: ( + lambda k, v=None: ( + k.strip(), + CsvExpr(v) if v is not None else None) + )(*x.split('=', 1)), + const=(None, None), + help="Sort by this field. Can include an expression of the form " + "field=expr.") + parser.add_argument( + '-S', '--reverse-sort', + nargs='?', + action=AppendSort, + type=lambda x: ( + lambda k, v=None: ( + k.strip(), + CsvExpr(v) if v is not None else None) + )(*x.split('=', 1)), + const=(None, None), + help="Sort by this field, but backwards. Can include an expression " + "of the form field=expr.") + parser.add_argument( + '-z', '--depth', + nargs='?', + type=lambda x: int(x, 0), + const=0, + help="Depth of function calls to show. 0 shows all calls unless " + "we find a cycle. Defaults to 0.") + parser.add_argument( + '-Z', '--children', + nargs='?', + const='children', + action='append', + help="Field to use for recursive results. This expects a list " + "and really only works with JSON input.") + class AppendHot(argparse.Action): + def __call__(self, parser, namespace, value, option): + if namespace.hot is None: + namespace.hot = [] + namespace.hot.append((value, option in {'-R', '--reverse-hot'})) + parser.add_argument( + '-r', '--hot', + nargs='?', + action=AppendHot, + type=lambda x: ( + lambda k, v=None: ( + k.strip(), + CsvExpr(v) if v is not None else None) + )(*x.split('=', 1)), + const=(None, None), + help="Show only the hot path for each function call. Can " + "optionally provide fields like sort. Can include an " + "expression in the form of field=expr.") + parser.add_argument( + '-R', '--reverse-hot', + nargs='?', + action=AppendHot, + type=lambda x: ( + lambda k, v=None: ( + k.strip(), + CsvExpr(v) if v is not None else None) + )(*x.split('=', 1)), + const=(None, None), + help="Like -r/--hot, but backwards.") + parser.add_argument( + '-N', '--notes', + nargs='?', + const='notes', + action='append', + help="Field to use for notes.") + parser.add_argument( + '--no-header', + action='store_true', + help="Don't show the header.") + parser.add_argument( + '--small-header', + action='store_true', + help="Don't show by field names.") + parser.add_argument( + '--no-total', + action='store_true', + help="Don't show the total.") + parser.add_argument( + '--small-total', + action='store_true', + help="Don't show TOTAL name.") + parser.add_argument( + '-Q', '--small-table', + action='store_true', + help="Equivalent to --small-header + --no-total or --small-total.") + parser.add_argument( + '-Y', '--summary', + action='store_true', + help="Only show the total.") + parser.add_argument( + '--total', + action='store_true', + help="Equivalent to --summary + --no-header + --small-total. " + "Useful for scripting.") + parser.add_argument( + '--prefix', + help="Prefix to use for fields in CSV/JSON output.") + sys.exit(main(**{k: v + for k, v in vars(parser.parse_intermixed_args()).items() + if v is not None})) diff --git a/scripts/ctx.py b/scripts/ctx.py new file mode 100755 index 000000000..05ea31b04 --- /dev/null +++ b/scripts/ctx.py @@ -0,0 +1,1588 @@ +#!/usr/bin/env python3 +# +# Script to find function context (params and relevant structs). +# +# Example: +# ./scripts/ctx.py lfs.o lfs_util.o -Ssize +# +# Copyright (c) 2024, The littlefs authors. +# SPDX-License-Identifier: BSD-3-Clause +# + +# prevent local imports +if __name__ == "__main__": + __import__('sys').path.pop(0) + +import collections as co +import csv +import fnmatch +import functools as ft +import io +import itertools as it +import math as mt +import os +import re +import shlex +import subprocess as sp +import sys + + +OBJDUMP_PATH = ['objdump'] + + +# integer fields +class CsvInt(co.namedtuple('CsvInt', 'a')): + __slots__ = () + def __new__(cls, a=0): + if isinstance(a, CsvInt): + return a + if isinstance(a, str): + try: + a = int(a, 0) + except ValueError: + # also accept +-∞ and +-inf + if re.match('^\s*\+?\s*(?:∞|inf)\s*$', a): + a = mt.inf + elif re.match('^\s*-\s*(?:∞|inf)\s*$', a): + a = -mt.inf + else: + raise + if not (isinstance(a, int) or mt.isinf(a)): + a = int(a) + return super().__new__(cls, a) + + def __repr__(self): + return '%s(%r)' % (self.__class__.__name__, self.a) + + def __str__(self): + if self.a == mt.inf: + return '∞' + elif self.a == -mt.inf: + return '-∞' + else: + return str(self.a) + + def __csv__(self): + if self.a == mt.inf: + return 'inf' + elif self.a == -mt.inf: + return '-inf' + else: + return repr(self.a) + + def __bool__(self): + return bool(self.a) + + def __int__(self): + assert not mt.isinf(self.a) + return self.a + + def __float__(self): + return float(self.a) + + none = '%7s' % '-' + def table(self): + return '%7s' % (self,) + + def diff(self, other): + new = self.a if self else 0 + old = other.a if other else 0 + diff = new - old + if diff == +mt.inf: + return '%7s' % '+∞' + elif diff == -mt.inf: + return '%7s' % '-∞' + else: + return '%+7d' % diff + + def ratio(self, other): + new = self.a if self else 0 + old = other.a if other else 0 + if mt.isinf(new) and mt.isinf(old): + return 0.0 + elif mt.isinf(new): + return +mt.inf + elif mt.isinf(old): + return -mt.inf + elif not old and not new: + return 0.0 + elif not old: + return +mt.inf + else: + return (new-old) / old + + def __pos__(self): + return self.__class__(+self.a) + + def __neg__(self): + return self.__class__(-self.a) + + def __abs__(self): + return self.__class__(abs(self.a)) + + def __add__(self, other): + return self.__class__(self.a + other.a) + + def __sub__(self, other): + return self.__class__(self.a - other.a) + + def __mul__(self, other): + return self.__class__(self.a * other.a) + + def __truediv__(self, other): + if not other: + if self >= self.__class__(0): + return self.__class__(+mt.inf) + else: + return self.__class__(-mt.inf) + return self.__class__(self.a // other.a) + + def __mod__(self, other): + return self.__class__(self.a % other.a) + +# ctx size results +class CtxResult(co.namedtuple('CtxResult', [ + 'z', 'i', 'file', 'function', + 'off', 'size', + 'children', 'notes'])): + _prefix = 'ctx' + _by = ['z', 'i', 'file', 'function'] + _fields = ['off', 'size'] + _sort = ['size'] + _types = {'off': CsvInt, 'size': CsvInt} + _children = 'children' + _notes = 'notes' + + __slots__ = () + def __new__(cls, z=0, i=0, file='', function='', off=0, size=0, + children=None, notes=None): + return super().__new__(cls, z, i, file, function, + CsvInt(off), CsvInt(size), + children if children is not None else [], + notes if notes is not None else set()) + + def __add__(self, other): + return CtxResult(self.z, self.i, self.file, self.function, + min(self.off, other.off), + max(self.size, other.size), + self.children + other.children, + self.notes | other.notes) + + +# open with '-' for stdin/stdout +def openio(path, mode='r', buffering=-1): + import os + if path == '-': + if 'r' in mode: + return os.fdopen(os.dup(sys.stdin.fileno()), mode, buffering) + else: + return os.fdopen(os.dup(sys.stdout.fileno()), mode, buffering) + else: + return open(path, mode, buffering) + +class Sym(co.namedtuple('Sym', [ + 'name', 'global_', 'section', 'addr', 'size'])): + __slots__ = () + def __new__(cls, name, global_, section, addr, size): + return super().__new__(cls, name, global_, section, addr, size) + + def __repr__(self): + return '%s(%r, %r, %r, 0x%x, 0x%x)' % ( + self.__class__.__name__, + self.name, + self.global_, + self.section, + self.addr, + self.size) + +class SymInfo: + def __init__(self, syms): + self.syms = syms + + def get(self, k, d=None): + # allow lookup by both symbol and address + if isinstance(k, str): + # organize by symbol, note multiple symbols can share a name + if not hasattr(self, '_by_sym'): + by_sym = {} + for sym in self.syms: + if sym.name not in by_sym: + by_sym[sym.name] = [] + if sym not in by_sym[sym.name]: + by_sym[sym.name].append(sym) + self._by_sym = by_sym + + return self._by_sym.get(k, d) + + else: + import bisect + + # organize by address + if not hasattr(self, '_by_addr'): + # sort and keep largest/first when duplicates + syms = self.syms.copy() + syms.sort(key=lambda x: (x.addr, -x.size)) + + by_addr = [] + for sym in syms: + if (len(by_addr) == 0 + or by_addr[-1].addr != sym.addr): + by_addr.append(sym) + self._by_addr = by_addr + + # find sym by range + i = bisect.bisect(self._by_addr, k, + key=lambda x: x.addr) - 1 + # check that we're actually in this sym's size + if i > -1 and k < self._by_addr[i].addr+self._by_addr[i].size: + return self._by_addr[i] + else: + return d + + def __getitem__(self, k): + v = self.get(k) + if v is None: + raise KeyError(k) + return v + + def __contains__(self, k): + return self.get(k) is not None + + def __bool__(self): + return bool(self.syms) + + def __len__(self): + return len(self.syms) + + def __iter__(self): + return iter(self.syms) + + def globals(self): + return SymInfo([sym for sym in self.syms + if sym.global_]) + + def section(self, section): + return SymInfo([sym for sym in self.syms + # note we accept prefixes + if s.startswith(section)]) + +def collect_syms(obj_path, global_=False, sections=None, *, + objdump_path=OBJDUMP_PATH, + **args): + symbol_pattern = re.compile( + '^(?P[0-9a-fA-F]+)' + ' (?P.).*' + '\s+(?P
[^\s]+)' + '\s+(?P[0-9a-fA-F]+)' + '\s+(?P[^\s]+)\s*$') + + # find symbol addresses and sizes + syms = [] + cmd = objdump_path + ['--syms', obj_path] + if args.get('verbose'): + print(' '.join(shlex.quote(c) for c in cmd)) + proc = sp.Popen(cmd, + stdout=sp.PIPE, + universal_newlines=True, + errors='replace', + close_fds=False) + for line in proc.stdout: + m = symbol_pattern.match(line) + if m: + name = m.group('name') + scope = m.group('scope') + section = m.group('section') + addr = int(m.group('addr'), 16) + size = int(m.group('size'), 16) + # skip non-globals? + # l => local + # g => global + # u => unique global + # => neither + # ! => local + global + global__ = scope not in 'l ' + if global_ and not global__: + continue + # filter by section? note we accept prefixes + if (sections is not None + and not any(section.startswith(prefix) + for prefix in sections)): + continue + # skip zero sized symbols + if not size: + continue + # note multiple symbols can share a name + syms.append(Sym(name, global__, section, addr, size)) + proc.wait() + if proc.returncode != 0: + raise sp.CalledProcessError(proc.returncode, proc.args) + + return SymInfo(syms) + +# each dwarf entry can have attrs and children entries +class DwarfEntry: + def __init__(self, level, off, tag, ats={}, children=[]): + self.level = level + self.off = off + self.tag = tag + self.ats = ats or {} + self.children = children or [] + + def get(self, k, d=None): + return self.ats.get(k, d) + + def __getitem__(self, k): + return self.ats[k] + + def __contains__(self, k): + return k in self.ats + + def __repr__(self): + return '%s(%d, 0x%x, %r, %r)' % ( + self.__class__.__name__, + self.level, + self.off, + self.tag, + self.ats) + + @ft.cached_property + def name(self): + if 'DW_AT_name' in self: + name = self['DW_AT_name'].split(':')[-1].strip() + # prefix with struct/union/enum + if self.tag == 'DW_TAG_structure_type': + name = 'struct ' + name + elif self.tag == 'DW_TAG_union_type': + name = 'union ' + name + elif self.tag == 'DW_TAG_enumeration_type': + name = 'enum ' + name + return name + else: + return None + +# a collection of dwarf entries +class DwarfInfo: + def __init__(self, entries): + self.entries = entries + + def get(self, k, d=None): + # allow lookup by offset or dwarf name + if not isinstance(k, str): + return self.entries.get(k, d) + + else: + # organize entries by name + if not hasattr(self, '_by_name'): + self._by_name = {} + for entry in self.entries.values(): + if entry.name is not None: + self._by_name[entry.name] = entry + + # exact match? do a quick lookup + if k in self._by_name: + return self._by_name[k] + # find the best matching dwarf entry with a simple + # heuristic + # + # this can be different from the actual symbol because + # of optimization passes + else: + def key(entry): + i = k.find(entry.name) + if i == -1: + return None + return (i, len(k)-(i+len(entry.name)), k) + return min( + filter(key, self._by_name.values()), + key=key, + default=d) + + def __getitem__(self, k): + v = self.get(k) + if v is None: + raise KeyError(k) + return v + + def __contains__(self, k): + return self.get(k) is not None + + def __bool__(self): + return bool(self.entries) + + def __len__(self): + return len(self.entries) + + def __iter__(self): + return iter(self.entries.values()) + +def collect_dwarf_info(obj_path, tags=None, *, + objdump_path=OBJDUMP_PATH, + **args): + info_pattern = re.compile( + '^\s*<(?P[^>]*)>' + '\s*<(?P[^>]*)>' + '.*\(\s*(?P[^)]*?)\s*\)\s*$' + '|' '^\s*<(?P[^>]*)>' + '\s*(?P[^>:]*?)' + '\s*:(?P.*)\s*$') + + # collect dwarf entries + info = co.OrderedDict() + entry = None + levels = {} + # note objdump-path may contain extra args + cmd = objdump_path + ['--dwarf=info', obj_path] + if args.get('verbose'): + print(' '.join(shlex.quote(c) for c in cmd)) + proc = sp.Popen(cmd, + stdout=sp.PIPE, + universal_newlines=True, + errors='replace', + close_fds=False) + for line in proc.stdout: + # state machine here to find dwarf entries + m = info_pattern.match(line) + if m: + if m.group('tag'): + entry = DwarfEntry( + level=int(m.group('level'), 0), + off=int(m.group('off'), 16), + tag=m.group('tag').strip(), + ) + # keep track of unfiltered entries + if tags is None or entry.tag in tags: + info[entry.off] = entry + # store entry in parent + levels[entry.level] = entry + if entry.level-1 in levels: + levels[entry.level-1].children.append(entry) + elif m.group('at'): + if entry: + entry.ats[m.group('at').strip()] = ( + m.group('v').strip()) + proc.wait() + if proc.returncode != 0: + raise sp.CalledProcessError(proc.returncode, proc.args) + + return DwarfInfo(info) + +def collect_ctx(obj_paths, *, + internal=False, + everything=False, + no_strip=False, + depth=1, + **args): + results = [] + for obj_path in obj_paths: + # find global symbols + syms = collect_syms(obj_path, + sections=['.text'], + # only include internal symbols if explicitly requested + global_=not internal and not everything, + **args) + + # find dwarf info + info = collect_dwarf_info(obj_path, **args) + + # find source file from dwarf info + for entry in info: + if (entry.tag == 'DW_TAG_compile_unit' + and 'DW_AT_name' in entry + and 'DW_AT_comp_dir' in entry): + file = os.path.join( + entry['DW_AT_comp_dir'].split(':')[-1].strip(), + entry['DW_AT_name'].split(':')[-1].strip()) + break + else: + # guess from obj path + file = re.sub('(\.o)?$', '.c', obj_path, 1) + + # simplify path + if os.path.commonpath([ + os.getcwd(), + os.path.abspath(file)]) == os.getcwd(): + file = os.path.relpath(file) + else: + file = os.path.abspath(file) + + # recursive+cached size finder + def sizeof(entry, seen=set()): + # found a cycle? stop here + if entry.off in seen: + return 0 + # cached? + if not hasattr(sizeof, 'cache'): + sizeof.cache = {} + if entry.off in sizeof.cache: + return sizeof.cache[entry.off] + + # pointer? deref and include size + if entry.tag == 'DW_TAG_pointer_type': + size = int(entry['DW_AT_byte_size']) + if 'DW_AT_type' in entry: + type = info[int(entry['DW_AT_type'].strip('<>'), 0)] + size += sizeof(type, seen | {entry.off}) + # base type? + elif entry.tag == 'DW_TAG_base_type': + size = int(entry['DW_AT_byte_size']) + # function pointer? + elif entry.tag == 'DW_TAG_subroutine_type': + size = 0 + # struct? include any nested pointers + elif entry.tag == 'DW_TAG_structure_type': + # note structs/unions can be incomplete + size = int(entry.get('DW_AT_byte_size', 0)) + for child in entry.children: + if child.tag != 'DW_TAG_member': + continue + type_ = info[int(child['DW_AT_type'].strip('<>'), 0)] + if (type_.tag != 'DW_TAG_pointer_type' + or 'DW_AT_type' not in type_): + continue + type__ = info[int(type_['DW_AT_type'].strip('<>'), 0)] + size += sizeof(type__, seen | {entry.off}) + # union? include any nested pointers + elif entry.tag == 'DW_TAG_union_type': + # note structs/unions can be incomplete + size = int(entry.get('DW_AT_byte_size', 0)) + size_ = 0 + for child in entry.children: + if child.tag != 'DW_TAG_member': + continue + type_ = info[int(child['DW_AT_type'].strip('<>'), 0)] + if (type_.tag != 'DW_TAG_pointer_type' + or 'DW_AT_type' not in type_): + continue + type__ = info[int(type_['DW_AT_type'].strip('<>'), 0)] + size_ = max(size_, sizeof(type__, seen | {entry.off})) + size += size_ + # array? multiply by size + elif entry.tag == 'DW_TAG_array_type': + type = info[int(entry['DW_AT_type'].strip('<>'), 0)] + size = sizeof(type, seen | {entry.off}) + for child in entry.children: + if child.tag == 'DW_TAG_subrange_type': + size *= int(child['DW_AT_upper_bound']) + 1 + # a modifier? + elif (entry.tag in { + 'DW_TAG_typedef', + 'DW_TAG_array_type', + 'DW_TAG_enumeration_type', + 'DW_TAG_formal_parameter', + 'DW_TAG_member', + 'DW_TAG_const_type', + 'DW_TAG_volatile_type', + 'DW_TAG_restrict_type'} + and 'DW_AT_type' in entry): + type = info[int(entry['DW_AT_type'].strip('<>'), 0)] + size = sizeof(type, seen | {entry.off}) + # void? + elif ('DW_AT_type' not in entry + and 'DW_AT_byte_size' not in entry): + size = 0 + else: + assert False, "Unknown dwarf entry? %r" % entry.tag + + sizeof.cache[entry.off] = size + return size + + # recursive+cached children finder + def childrenof(entry, depth, seen=set()): + # found a cycle? stop here + if entry.off in seen: + return [], {'cycle detected'}, True + # stop here? + if depth < 1: + return [], set(), False + # cached? + if not hasattr(childrenof, 'cache'): + childrenof.cache = {} + if entry.off in childrenof.cache: + return childrenof.cache[entry.off] + + # pointer? deref and include size + if entry.tag == 'DW_TAG_pointer_type': + children, notes, dirty = [], set(), False + if 'DW_AT_type' in entry: + type = info[int(entry['DW_AT_type'].strip('<>'), 0)] + # skip modifiers to try to find name + while (type.name is None + and 'DW_AT_type' in type + and type.tag != 'DW_TAG_subroutine_type'): + type = info[int(type['DW_AT_type'].strip('<>'), 0)] + if (type.name is not None + and type.tag != 'DW_TAG_subroutine_type'): + # find size, etc + name_ = type.name + size_ = sizeof(type, seen | {entry.off}) + children_, notes_, dirty_ = childrenof( + type, depth-1, seen | {entry.off}) + children.append(CtxResult( + 0, 0, file, name_, 0, size_, + children=children_, + notes=notes_)) + dirty = dirty or dirty_ + # struct? union? + elif entry.tag in { + 'DW_TAG_structure_type', + 'DW_TAG_union_type'}: + # iterate over children in struct/union + children, notes, dirty = [], set(), False + for child in entry.children: + if child.tag != 'DW_TAG_member': + continue + # find name + name_ = child.name + # try to find offset for struct members, note this + # is _not_ the same as the dwarf entry offset + off_ = int(child.get('DW_AT_data_member_location', 0)) + # find size, children, etc + size_ = sizeof(child, seen | {entry.off}) + children_, notes_, dirty_ = childrenof( + child, depth-1, seen | {entry.off}) + children.append(CtxResult( + 0, len(children), file, name_, off_, size_, + children=children_, + notes=notes_)) + dirty = dirty or dirty_ + # base type? function pointer? + elif entry.tag in { + 'DW_TAG_base_type', + 'DW_TAG_subroutine_type'}: + children, notes, dirty = [], set(), False + # a modifier? + elif (entry.tag in { + 'DW_TAG_typedef', + 'DW_TAG_array_type', + 'DW_TAG_enumeration_type', + 'DW_TAG_formal_parameter', + 'DW_TAG_member', + 'DW_TAG_const_type', + 'DW_TAG_volatile_type', + 'DW_TAG_restrict_type'} + and 'DW_AT_type' in entry): + type = int(entry['DW_AT_type'].strip('<>'), 0) + children, notes, dirty = childrenof( + info[type], depth, seen | {entry.off}) + # void? + elif ('DW_AT_type' not in entry + and 'DW_AT_byte_size' not in entry): + children, notes = [], set(), False + else: + assert False, "Unknown dwarf entry? %r" % entry.tag + + if not dirty: + childrenof.cache[entry.off] = children, notes, dirty + return children, notes, dirty + + # find each function's context + for sym in syms: + # discard internal functions + if not everything and sym.name.startswith('__'): + continue + + # find best matching dwarf entry + entry = info.get(sym.name) + + # skip non-functions + if entry is None or entry.tag != 'DW_TAG_subprogram': + continue + + # find all parameters + params = [] + for param in entry.children: + if param.tag != 'DW_TAG_formal_parameter': + continue + + # find name, if there is one + name_ = param.name if param.name is not None else '(unnamed)' + + # find size, recursing if necessary + size_ = sizeof(param) + + # find children, recursing if necessary + children_, notes_, _ = childrenof(param, depth-2) + + params.append(CtxResult( + 0, len(params), file, name_, 0, size_, + children=children_, + notes=notes_)) + + # strip compiler suffixes + name = sym.name + if not no_strip: + name = name.split('.', 1)[0] + + # context = sum of params + size = sum((param.size for param in params), start=CsvInt(0)) + + results.append(CtxResult( + 0, 0, file, name, 0, size, + children=params)) + + # assign z at the end to avoid issues with caching + def zed(results, z): + return [r._replace(z=z, children=zed(r.children, z+1)) + for r in results] + results = zed(results, 0) + + return results + + +# common folding/tabling/read/write code + +class Rev(co.namedtuple('Rev', 'a')): + __slots__ = () + # yes we need all of these because we're a namedtuple + def __lt__(self, other): + return self.a > other.a + def __gt__(self, other): + return self.a < other.a + def __le__(self, other): + return self.a >= other.a + def __ge__(self, other): + return self.a <= other.a + +def fold(Result, results, *, + by=None, + defines=[], + sort=None, + depth=1, + **_): + # stop when depth hits zero + if depth == 0: + return [] + + # organize by by + if by is None: + by = Result._by + + for k in it.chain(by or [], (k for k, _ in defines)): + if k not in Result._by and k not in Result._fields: + print("error: could not find field %r?" % k, + file=sys.stderr) + sys.exit(-1) + + # filter by matching defines + if defines: + results_ = [] + for r in results: + if all(any(fnmatch.fnmatchcase(str(getattr(r, k, '')), v) + for v in vs) + for k, vs in defines): + results_.append(r) + results = results_ + + # organize results into conflicts + folding = co.OrderedDict() + for r in results: + name = tuple(getattr(r, k) for k in by) + if name not in folding: + folding[name] = [] + folding[name].append(r) + + # merge conflicts + folded = [] + for name, rs in folding.items(): + folded.append(sum(rs[1:], start=rs[0])) + + # sort, note that python's sort is stable + folded.sort(key=lambda r: ( + # sort by explicit sort fields + tuple((Rev + if reverse ^ (not k or k in Result._fields) + else lambda x: x)( + tuple((getattr(r, k_),) + if getattr(r, k_) is not None + else () + for k_ in ([k] if k else Result._sort))) + for k, reverse in (sort or [])), + # sort by result + r)) + + # recurse if we have recursive results + if hasattr(Result, '_children'): + folded = [r._replace(**{ + Result._children: fold( + Result, getattr(r, Result._children), + by=by, + # only filter defines at the top level! + sort=sort, + depth=depth-1)}) + for r in folded] + + return folded + +def hotify(Result, results, *, + enumerates=None, + depth=1, + hot=None, + **_): + # note! hotifying risks confusion if you don't enumerate/have a + # z field, since it will allow folding across recursive boundaries + + # hotify only makes sense for recursive results + assert hasattr(Result, '_children') + + results_ = [] + for r in results: + hot_ = [] + def recurse(results_, depth_): + nonlocal hot_ + if not results_: + return + + # find the hottest result + r = min(results_, key=lambda r: + tuple((Rev + if reverse ^ (not k or k in Result._fields) + else lambda x: x)( + tuple((getattr(r, k_),) + if getattr(r, k_) is not None + else () + for k_ in ([k] if k else Result._sort))) + for k, reverse in it.chain(hot, [(None, False)]))) + + hot_.append(r._replace(**( + # enumerate? + ({e: len(hot_) for e in enumerates} + if enumerates is not None + else {}) + | {Result._children: []}))) + + # recurse? + if depth_ > 1: + recurse(getattr(r, Result._children), + depth_-1) + + recurse(getattr(r, Result._children), depth-1) + results_.append(r._replace(**{Result._children: hot_})) + + return results_ + +def table(Result, results, diff_results=None, *, + by=None, + fields=None, + sort=None, + labels=None, + depth=1, + hot=None, + percent=False, + all=False, + compare=None, + no_header=False, + small_header=False, + no_total=False, + small_total=False, + small_table=False, + summary=False, + total=False, + **_): + import builtins + all_, all = all, builtins.all + + # small_table implies small_header + no_total or small_total + if small_table: + small_header = True + small_total = True + no_total = no_total or (not summary and not total) + # summary implies small_header + if summary: + small_header = True + # total implies summary + no_header + small_total + if total: + summary = True + no_header = True + small_total = True + + if by is None: + by = Result._by + if fields is None: + fields = Result._fields + types = Result._types + + # organize by name + table = { + ','.join(str(getattr(r, k) + if getattr(r, k) is not None + else '') + for k in by): r + for r in results} + diff_table = { + ','.join(str(getattr(r, k) + if getattr(r, k) is not None + else '') + for k in by): r + for r in diff_results or []} + + # lost results? this only happens if we didn't fold by the same + # by field, which is an error and risks confusing results + assert len(table) == len(results) + if diff_results is not None: + assert len(diff_table) == len(diff_results) + + # find compare entry if there is one + if compare: + compare_ = min( + (n for n in table.keys() + if all(fnmatch.fnmatchcase(k, c) + for k, c in it.zip_longest(n.split(','), compare, + fillvalue=''))), + default=compare) + compare_r = table.get(compare_) + + # build up our lines + lines = [] + + # header + if not no_header: + header = ['%s%s' % ( + ','.join(labels if labels is not None else by), + ' (%d added, %d removed)' % ( + sum(1 for n in table if n not in diff_table), + sum(1 for n in diff_table if n not in table)) + if diff_results is not None and not percent else '') + if not small_header else ''] + if diff_results is None or percent: + for k in fields: + header.append(k) + else: + for k in fields: + header.append('o'+k) + for k in fields: + header.append('n'+k) + for k in fields: + header.append('d'+k) + lines.append(header) + + # delete these to try to catch typos below, we need to rebuild + # these tables at each recursive layer + del table + del diff_table + + # entry helper + def table_entry(name, r, diff_r=None): + # prepend name + entry = [name] + + # normal entry? + if ((compare is None or r == compare_r) + and diff_results is None): + for k in fields: + entry.append( + (getattr(r, k).table(), + getattr(getattr(r, k), 'notes', lambda: [])()) + if getattr(r, k, None) is not None + else types[k].none) + # compare entry? + elif diff_results is None: + for k in fields: + entry.append( + (getattr(r, k).table() + if getattr(r, k, None) is not None + else types[k].none, + (lambda t: ['+∞%'] if t == +mt.inf + else ['-∞%'] if t == -mt.inf + else ['%+.1f%%' % (100*t)])( + types[k].ratio( + getattr(r, k, None), + getattr(compare_r, k, None))))) + # percent entry? + elif percent: + for k in fields: + entry.append( + (getattr(r, k).table() + if getattr(r, k, None) is not None + else types[k].none, + (lambda t: ['+∞%'] if t == +mt.inf + else ['-∞%'] if t == -mt.inf + else ['%+.1f%%' % (100*t)])( + types[k].ratio( + getattr(r, k, None), + getattr(diff_r, k, None))))) + # diff entry? + else: + for k in fields: + entry.append(getattr(diff_r, k).table() + if getattr(diff_r, k, None) is not None + else types[k].none) + for k in fields: + entry.append(getattr(r, k).table() + if getattr(r, k, None) is not None + else types[k].none) + for k in fields: + entry.append( + (types[k].diff( + getattr(r, k, None), + getattr(diff_r, k, None)), + (lambda t: ['+∞%'] if t == +mt.inf + else ['-∞%'] if t == -mt.inf + else ['%+.1f%%' % (100*t)] if t + else [])( + types[k].ratio( + getattr(r, k, None), + getattr(diff_r, k, None))))) + + # append any notes + if hasattr(Result, '_notes') and r is not None: + notes = sorted(getattr(r, Result._notes)) + if isinstance(entry[-1], tuple): + entry[-1] = (entry[-1][0], entry[-1][1] + notes) + else: + entry[-1] = (entry[-1], notes) + + return entry + + # recursive entry helper + def table_recurse(results_, diff_results_, + depth_, + prefixes=('', '', '', '')): + # build the children table at each layer + table_ = { + ','.join(str(getattr(r, k) + if getattr(r, k) is not None + else '') + for k in by): r + for r in results_} + diff_table_ = { + ','.join(str(getattr(r, k) + if getattr(r, k) is not None + else '') + for k in by): r + for r in diff_results_ or []} + names_ = [n + for n in table_.keys() | diff_table_.keys() + if diff_results is None + or all_ + or any( + types[k].ratio( + getattr(table_.get(n), k, None), + getattr(diff_table_.get(n), k, None)) + for k in fields)] + + # sort again, now with diff info, note that python's sort is stable + names_.sort(key=lambda n: ( + # sort by explicit sort fields + next( + tuple((Rev + if reverse ^ (not k or k in Result._fields) + else lambda x: x)( + tuple((getattr(r_, k_),) + if getattr(r_, k_) is not None + else () + for k_ in ([k] if k else Result._sort))) + for k, reverse in (sort or [])) + for r_ in [table_.get(n), diff_table_.get(n)] + if r_ is not None), + # sort by ratio if diffing + Rev(tuple(types[k].ratio( + getattr(table_.get(n), k, None), + getattr(diff_table_.get(n), k, None)) + for k in fields)) + if diff_results is not None + else (), + # move compare entry to the top, note this can be + # overridden by explicitly sorting by fields + (table_.get(n) != compare_r, + # sort by ratio if comparing + Rev(tuple( + types[k].ratio( + getattr(table_.get(n), k, None), + getattr(compare_r, k, None)) + for k in fields))) + if compare + else (), + # sort by result + (table_[n],) if n in table_ else (), + # and finally by name (diffs may be missing results) + n)) + + for i, name in enumerate(names_): + # find comparable results + r = table_.get(name) + diff_r = diff_table_.get(name) + + # figure out a good label + if labels is not None: + label = next( + ','.join(str(getattr(r_, k) + if getattr(r_, k) is not None + else '') + for k in labels) + for r_ in [r, diff_r] + if r_ is not None) + else: + label = name + + # build line + line = table_entry(label, r, diff_r) + + # add prefixes + line = [x if isinstance(x, tuple) else (x, []) for x in line] + line[0] = (prefixes[0+(i==len(names_)-1)] + line[0][0], line[0][1]) + lines.append(line) + + # recurse? + if name in table_ and depth_ > 1: + table_recurse( + getattr(r, Result._children), + getattr(diff_r, Result._children, None), + depth_-1, + (prefixes[2+(i==len(names_)-1)] + "|-> ", + prefixes[2+(i==len(names_)-1)] + "'-> ", + prefixes[2+(i==len(names_)-1)] + "| ", + prefixes[2+(i==len(names_)-1)] + " ")) + + # build entries + if not summary: + table_recurse(results, diff_results, depth) + + # total + if not no_total: + r = next(iter(fold(Result, results, by=[])), Result()) + if diff_results is None: + diff_r = None + else: + diff_r = next(iter(fold(Result, diff_results, by=[])), Result()) + lines.append(table_entry( + 'TOTAL' if not small_total else '', + r, diff_r)) + + # homogenize + lines = [[x if isinstance(x, tuple) else (x, []) for x in line] + for line in lines] + + # find the best widths, note that column 0 contains the names and is + # handled a bit differently + widths = co.defaultdict(lambda: 7, {0: 7}) + nwidths = co.defaultdict(lambda: 0) + for line in lines: + for i, x in enumerate(line): + widths[i] = max(widths[i], ((len(x[0])+1+4-1)//4)*4-1) + if i != len(line)-1: + nwidths[i] = max(nwidths[i], 1+sum(2+len(n) for n in x[1])) + if not any(line[0][0] for line in lines): + widths[0] = 0 + + # print our table + for line in lines: + print('%-*s %s' % ( + widths[0], line[0][0], + ' '.join('%*s%-*s' % ( + widths[i], x[0], + nwidths[i], ' (%s)' % ', '.join(x[1]) if x[1] else '') + for i, x in enumerate(line[1:], 1)))) + +def read_csv(path, Result, *, + depth=1, + prefix=None, + **_): + # prefix? this only applies to field fields + if prefix is None: + if hasattr(Result, '_prefix'): + prefix = '%s_' % Result._prefix + else: + prefix = '' + + by = Result._by + fields = Result._fields + + with openio(path, 'r') as f: + # csv or json? assume json starts with [ + is_json = (f.buffer.peek(1)[:1] == b'[') + + # read csv? + if not is_json: + results = [] + reader = csv.DictReader(f, restval='') + for r in reader: + if not any(prefix+k in r and r[prefix+k].strip() + for k in fields): + continue + try: + # note this allows by/fields to overlap + results.append(Result(**( + {k: r[k] for k in by + if k in r + and r[k].strip()} + | {k: r[prefix+k] for k in fields + if prefix+k in r + and r[prefix+k].strip()}))) + except TypeError: + pass + return results + + # read json? + else: + import json + def unjsonify(results, depth_): + results_ = [] + for r in results: + if not any(prefix+k in r and r[prefix+k].strip() + for k in fields): + continue + try: + # note this allows by/fields to overlap + results_.append(Result(**( + {k: r[k] for k in by + if k in r + and r[k] is not None} + | {k: r[prefix+k] for k in fields + if prefix+k in r + and r[prefix+k] is not None} + | ({Result._children: unjsonify( + r[Result._children], + depth_-1)} + if hasattr(Result, '_children') + and Result._children in r + and r[Result._children] is not None + and depth_ > 1 + else {}) + | ({Result._notes: set(r[Result._notes])} + if hasattr(Result, '_notes') + and Result._notes in r + and r[Result._notes] is not None + else {})))) + except TypeError: + pass + return results_ + return unjsonify(json.load(f), depth) + +def write_csv(path, Result, results, *, + json=False, + by=None, + fields=None, + depth=1, + prefix=None, + **_): + # prefix? this only applies to field fields + if prefix is None: + if hasattr(Result, '_prefix'): + prefix = '%s_' % Result._prefix + else: + prefix = '' + + if by is None: + by = Result._by + if fields is None: + fields = Result._fields + + with openio(path, 'w') as f: + # write csv? + if not json: + writer = csv.DictWriter(f, list( + co.OrderedDict.fromkeys(it.chain( + by, + (prefix+k for k in fields))).keys())) + writer.writeheader() + for r in results: + # note this allows by/fields to overlap + writer.writerow( + {k: getattr(r, k) + for k in by + if getattr(r, k) is not None} + | {prefix+k: getattr(r, k).__csv__() + for k in fields + if getattr(r, k) is not None}) + + # write json? + else: + import json + # the neat thing about json is we can include recursive results + def jsonify(results, depth_): + results_ = [] + for r in results: + # note this allows by/fields to overlap + results_.append( + {k: getattr(r, k) + for k in by + if getattr(r, k) is not None} + | {prefix+k: getattr(r, k).__csv__() + for k in fields + if getattr(r, k) is not None} + | ({Result._children: jsonify( + getattr(r, Result._children), + depth_-1)} + if hasattr(Result, '_children') + and getattr(r, Result._children) + and depth_ > 1 + else {}) + | ({Result._notes: list( + getattr(r, Result._notes))} + if hasattr(Result, '_notes') + and getattr(r, Result._notes) + else {})) + return results_ + json.dump(jsonify(results, depth), f, + separators=(',', ':')) + + +def main(obj_paths, *, + by=None, + fields=None, + defines=[], + sort=None, + depth=None, + hot=None, + **args): + # figure out what fields we're interested in + labels = None + if by is None: + if args.get('output') or args.get('output_json'): + by = CtxResult._by + elif depth is not None or hot is not None: + by = ['z', 'i', 'function'] + labels = ['function'] + else: + by = ['function'] + + if fields is None: + if args.get('output') or args.get('output_json'): + fields = CtxResult._fields + else: + fields = ['size'] + + # figure out depth + if depth is None: + depth = mt.inf if hot else 1 + elif depth == 0: + depth = mt.inf + + # find sizes + if not args.get('use', None): + # not enough info? + if not obj_paths: + print("error: no *.o files?", + file=sys.stderr) + sys.exit(1) + + # collect info + results = collect_ctx(obj_paths, + depth=depth, + **args) + + else: + results = read_csv(args['use'], CtxResult, + depth=depth, + **args) + + # fold + results = fold(CtxResult, results, + by=by, + defines=defines, + sort=sort, + depth=depth) + + # hotify? + if hot: + results = hotify(CtxResult, results, + depth=depth, + hot=hot) + + # find previous results? + diff_results = None + if args.get('diff'): + try: + diff_results = read_csv( + args.get('diff'), + CtxResult, + depth=depth, + **args) + except FileNotFoundError: + diff_results = [] + + # fold + diff_results = fold(CtxResult, diff_results, + by=by, + defines=defines, + depth=depth) + + # hotify? + if hot: + diff_results = hotify(CtxResult, diff_results, + depth=depth, + hot=hot) + + # write results to JSON + if args.get('output_json'): + write_csv(args['output_json'], CtxResult, results, json=True, + by=by, + fields=fields, + depth=depth, + **args) + # write results to CSV + elif args.get('output'): + write_csv(args['output'], CtxResult, results, + by=by, + fields=fields, + depth=depth, + **args) + # print table + elif not args.get('quiet'): + table(CtxResult, results, diff_results, + by=by, + fields=fields, + sort=sort, + labels=labels, + depth=depth, + **args) + + +if __name__ == "__main__": + import argparse + import sys + parser = argparse.ArgumentParser( + description="Find the overhead of function contexts.", + allow_abbrev=False) + parser.add_argument( + 'obj_paths', + nargs='*', + help="Input *.o files.") + parser.add_argument( + '-v', '--verbose', + action='store_true', + help="Output commands that run behind the scenes.") + parser.add_argument( + '-q', '--quiet', + action='store_true', + help="Don't show anything, useful when checking for errors.") + parser.add_argument( + '-o', '--output', + help="Specify CSV file to store results.") + parser.add_argument( + '-O', '--output-json', + help="Specify JSON file to store results. This may contain " + "recursive info.") + parser.add_argument( + '-u', '--use', + help="Don't parse anything, use this CSV/JSON file.") + parser.add_argument( + '-d', '--diff', + help="Specify CSV/JSON file to diff against.") + parser.add_argument( + '-p', '--percent', + action='store_true', + help="Only show percentage change, not a full diff.") + parser.add_argument( + '-c', '--compare', + type=lambda x: tuple(v.strip() for v in x.split(',')), + help="Compare results to the row matching this by pattern.") + parser.add_argument( + '-a', '--all', + action='store_true', + help="Show all, not just the ones that changed.") + parser.add_argument( + '-b', '--by', + action='append', + choices=CtxResult._by, + help="Group by this field.") + parser.add_argument( + '-f', '--field', + dest='fields', + action='append', + choices=CtxResult._fields, + help="Show this field.") + parser.add_argument( + '-D', '--define', + dest='defines', + action='append', + type=lambda x: ( + lambda k, vs: ( + k.strip(), + {v.strip() for v in vs.split(',')}) + )(*x.split('=', 1)), + help="Only include results where this field is this value. May " + "include comma-separated options and globs.") + class AppendSort(argparse.Action): + def __call__(self, parser, namespace, value, option): + if namespace.sort is None: + namespace.sort = [] + namespace.sort.append((value, option in {'-S', '--reverse-sort'})) + parser.add_argument( + '-s', '--sort', + nargs='?', + action=AppendSort, + help="Sort by this field.") + parser.add_argument( + '-S', '--reverse-sort', + nargs='?', + action=AppendSort, + help="Sort by this field, but backwards.") + parser.add_argument( + '-z', '--depth', + nargs='?', + type=lambda x: int(x, 0), + const=0, + help="Depth of function calls to show. 0 shows all calls unless " + "we find a cycle. Defaults to 0.") + class AppendHot(argparse.Action): + def __call__(self, parser, namespace, value, option): + if namespace.hot is None: + namespace.hot = [] + namespace.hot.append((value, option in {'-R', '--reverse-hot'})) + parser.add_argument( + '-r', '--hot', + nargs='?', + action=AppendHot, + help="Show only the hot path for each function call. Can " + "optionally provide fields like sort.") + parser.add_argument( + '-R', '--reverse-hot', + nargs='?', + action=AppendHot, + help="Like -r/--hot, but backwards.") + parser.add_argument( + '--no-header', + action='store_true', + help="Don't show the header.") + parser.add_argument( + '--small-header', + action='store_true', + help="Don't show by field names.") + parser.add_argument( + '--no-total', + action='store_true', + help="Don't show the total.") + parser.add_argument( + '--small-total', + action='store_true', + help="Don't show TOTAL name.") + parser.add_argument( + '-Q', '--small-table', + action='store_true', + help="Equivalent to --small-header + --no-total or --small-total.") + parser.add_argument( + '-Y', '--summary', + action='store_true', + help="Only show the total.") + parser.add_argument( + '--total', + action='store_true', + help="Equivalent to --summary + --no-header + --small-total. " + "Useful for scripting.") + parser.add_argument( + '--prefix', + help="Prefix to use for fields in CSV/JSON output. Defaults " + "to %r." % ("%s_" % CtxResult._prefix)) + parser.add_argument( + '-i', '--internal', + action='store_true', + help="Include internal symbols. Useful for introspection, but " + "usually you don't care about these.") + parser.add_argument( + '-!', '--everything', + action='store_true', + help="Include builtin and libc specific symbols.") + parser.add_argument( + '-x', '--no-strip', + action='store_true', + help="Don't strip compiler optimization suffixes from symbols.") + parser.add_argument( + '--objdump-path', + type=lambda x: x.split(), + default=OBJDUMP_PATH, + help="Path to the objdump executable, may include flags. " + "Defaults to %r." % OBJDUMP_PATH) + sys.exit(main(**{k: v + for k, v in vars(parser.parse_intermixed_args()).items() + if v is not None})) diff --git a/scripts/data.py b/scripts/data.py index e9770aa1e..c27a8c733 100755 --- a/scripts/data.py +++ b/scripts/data.py @@ -12,321 +12,549 @@ # SPDX-License-Identifier: BSD-3-Clause # +# prevent local imports +if __name__ == "__main__": + __import__('sys').path.pop(0) + import collections as co import csv -import difflib +import fnmatch +import functools as ft +import io import itertools as it -import math as m +import math as mt import os import re import shlex import subprocess as sp +import sys -NM_PATH = ['nm'] -NM_TYPES = 'dDbB' OBJDUMP_PATH = ['objdump'] +SECTIONS = ['.data', '.bss'] # integer fields -class Int(co.namedtuple('Int', 'x')): +class CsvInt(co.namedtuple('CsvInt', 'a')): __slots__ = () - def __new__(cls, x=0): - if isinstance(x, Int): - return x - if isinstance(x, str): + def __new__(cls, a=0): + if isinstance(a, CsvInt): + return a + if isinstance(a, str): try: - x = int(x, 0) + a = int(a, 0) except ValueError: # also accept +-∞ and +-inf - if re.match('^\s*\+?\s*(?:∞|inf)\s*$', x): - x = m.inf - elif re.match('^\s*-\s*(?:∞|inf)\s*$', x): - x = -m.inf + if re.match('^\s*\+?\s*(?:∞|inf)\s*$', a): + a = mt.inf + elif re.match('^\s*-\s*(?:∞|inf)\s*$', a): + a = -mt.inf else: raise - assert isinstance(x, int) or m.isinf(x), x - return super().__new__(cls, x) + if not (isinstance(a, int) or mt.isinf(a)): + a = int(a) + return super().__new__(cls, a) + + def __repr__(self): + return '%s(%r)' % (self.__class__.__name__, self.a) def __str__(self): - if self.x == m.inf: + if self.a == mt.inf: return '∞' - elif self.x == -m.inf: + elif self.a == -mt.inf: return '-∞' else: - return str(self.x) + return str(self.a) + + def __csv__(self): + if self.a == mt.inf: + return 'inf' + elif self.a == -mt.inf: + return '-inf' + else: + return repr(self.a) + + def __bool__(self): + return bool(self.a) def __int__(self): - assert not m.isinf(self.x) - return self.x + assert not mt.isinf(self.a) + return self.a def __float__(self): - return float(self.x) + return float(self.a) none = '%7s' % '-' def table(self): return '%7s' % (self,) - diff_none = '%7s' % '-' - diff_table = table - - def diff_diff(self, other): - new = self.x if self else 0 - old = other.x if other else 0 + def diff(self, other): + new = self.a if self else 0 + old = other.a if other else 0 diff = new - old - if diff == +m.inf: + if diff == +mt.inf: return '%7s' % '+∞' - elif diff == -m.inf: + elif diff == -mt.inf: return '%7s' % '-∞' else: return '%+7d' % diff def ratio(self, other): - new = self.x if self else 0 - old = other.x if other else 0 - if m.isinf(new) and m.isinf(old): + new = self.a if self else 0 + old = other.a if other else 0 + if mt.isinf(new) and mt.isinf(old): return 0.0 - elif m.isinf(new): - return +m.inf - elif m.isinf(old): - return -m.inf + elif mt.isinf(new): + return +mt.inf + elif mt.isinf(old): + return -mt.inf elif not old and not new: return 0.0 elif not old: - return 1.0 + return +mt.inf else: return (new-old) / old + def __pos__(self): + return self.__class__(+self.a) + + def __neg__(self): + return self.__class__(-self.a) + + def __abs__(self): + return self.__class__(abs(self.a)) + def __add__(self, other): - return self.__class__(self.x + other.x) + return self.__class__(self.a + other.a) def __sub__(self, other): - return self.__class__(self.x - other.x) + return self.__class__(self.a - other.a) def __mul__(self, other): - return self.__class__(self.x * other.x) + return self.__class__(self.a * other.a) + + def __truediv__(self, other): + if not other: + if self >= self.__class__(0): + return self.__class__(+mt.inf) + else: + return self.__class__(-mt.inf) + return self.__class__(self.a // other.a) + + def __mod__(self, other): + return self.__class__(self.a % other.a) # data size results class DataResult(co.namedtuple('DataResult', [ 'file', 'function', 'size'])): + _prefix = 'data' _by = ['file', 'function'] _fields = ['size'] _sort = ['size'] - _types = {'size': Int} + _types = {'size': CsvInt} __slots__ = () def __new__(cls, file='', function='', size=0): return super().__new__(cls, file, function, - Int(size)) + CsvInt(size)) def __add__(self, other): return DataResult(self.file, self.function, - self.size + other.size) + self.size + other.size) +# open with '-' for stdin/stdout def openio(path, mode='r', buffering=-1): - # allow '-' for stdin/stdout + import os if path == '-': - if mode == 'r': + if 'r' in mode: return os.fdopen(os.dup(sys.stdin.fileno()), mode, buffering) else: return os.fdopen(os.dup(sys.stdout.fileno()), mode, buffering) else: return open(path, mode, buffering) -def collect(obj_paths, *, - nm_path=NM_PATH, - nm_types=NM_TYPES, +class Sym(co.namedtuple('Sym', [ + 'name', 'global_', 'section', 'addr', 'size'])): + __slots__ = () + def __new__(cls, name, global_, section, addr, size): + return super().__new__(cls, name, global_, section, addr, size) + + def __repr__(self): + return '%s(%r, %r, %r, 0x%x, 0x%x)' % ( + self.__class__.__name__, + self.name, + self.global_, + self.section, + self.addr, + self.size) + +class SymInfo: + def __init__(self, syms): + self.syms = syms + + def get(self, k, d=None): + # allow lookup by both symbol and address + if isinstance(k, str): + # organize by symbol, note multiple symbols can share a name + if not hasattr(self, '_by_sym'): + by_sym = {} + for sym in self.syms: + if sym.name not in by_sym: + by_sym[sym.name] = [] + if sym not in by_sym[sym.name]: + by_sym[sym.name].append(sym) + self._by_sym = by_sym + + return self._by_sym.get(k, d) + + else: + import bisect + + # organize by address + if not hasattr(self, '_by_addr'): + # sort and keep largest/first when duplicates + syms = self.syms.copy() + syms.sort(key=lambda x: (x.addr, -x.size)) + + by_addr = [] + for sym in syms: + if (len(by_addr) == 0 + or by_addr[-1].addr != sym.addr): + by_addr.append(sym) + self._by_addr = by_addr + + # find sym by range + i = bisect.bisect(self._by_addr, k, + key=lambda x: x.addr) - 1 + # check that we're actually in this sym's size + if i > -1 and k < self._by_addr[i].addr+self._by_addr[i].size: + return self._by_addr[i] + else: + return d + + def __getitem__(self, k): + v = self.get(k) + if v is None: + raise KeyError(k) + return v + + def __contains__(self, k): + return self.get(k) is not None + + def __bool__(self): + return bool(self.syms) + + def __len__(self): + return len(self.syms) + + def __iter__(self): + return iter(self.syms) + + def globals(self): + return SymInfo([sym for sym in self.syms + if sym.global_]) + + def section(self, section): + return SymInfo([sym for sym in self.syms + # note we accept prefixes + if s.startswith(section)]) + +def collect_syms(obj_path, global_=False, sections=None, *, objdump_path=OBJDUMP_PATH, - sources=None, - everything=False, **args): - size_pattern = re.compile( - '^(?P[0-9a-fA-F]+)' + - ' (?P[%s])' % re.escape(nm_types) + - ' (?P.+?)$') - line_pattern = re.compile( - '^\s+(?P[0-9]+)' - '(?:\s+(?P[0-9]+))?' - '\s+.*' - '\s+(?P[^\s]+)$') - info_pattern = re.compile( - '^(?:.*(?PDW_TAG_[a-z_]+).*' - '|.*DW_AT_name.*:\s*(?P[^:\s]+)\s*' - '|.*DW_AT_decl_file.*:\s*(?P[0-9]+)\s*)$') + symbol_pattern = re.compile( + '^(?P[0-9a-fA-F]+)' + ' (?P.).*' + '\s+(?P
[^\s]+)' + '\s+(?P[0-9a-fA-F]+)' + '\s+(?P[^\s]+)\s*$') - results = [] - for path in obj_paths: - # guess the source, if we have debug-info we'll replace this later - file = re.sub('(\.o)?$', '.c', path, 1) - - # find symbol sizes - results_ = [] - # note nm-path may contain extra args - cmd = nm_path + ['--size-sort', path] - if args.get('verbose'): - print(' '.join(shlex.quote(c) for c in cmd)) - proc = sp.Popen(cmd, + # find symbol addresses and sizes + syms = [] + cmd = objdump_path + ['--syms', obj_path] + if args.get('verbose'): + print(' '.join(shlex.quote(c) for c in cmd)) + proc = sp.Popen(cmd, stdout=sp.PIPE, - stderr=sp.PIPE if not args.get('verbose') else None, universal_newlines=True, errors='replace', close_fds=False) - for line in proc.stdout: - m = size_pattern.match(line) - if m: - func = m.group('func') - # discard internal functions - if not everything and func.startswith('__'): - continue - results_.append(DataResult( - file, func, - int(m.group('size'), 16))) - proc.wait() - if proc.returncode != 0: - if not args.get('verbose'): - for line in proc.stderr: - sys.stdout.write(line) - sys.exit(-1) + for line in proc.stdout: + m = symbol_pattern.match(line) + if m: + name = m.group('name') + scope = m.group('scope') + section = m.group('section') + addr = int(m.group('addr'), 16) + size = int(m.group('size'), 16) + # skip non-globals? + # l => local + # g => global + # u => unique global + # => neither + # ! => local + global + global__ = scope not in 'l ' + if global_ and not global__: + continue + # filter by section? note we accept prefixes + if (sections is not None + and not any(section.startswith(prefix) + for prefix in sections)): + continue + # skip zero sized symbols + if not size: + continue + # note multiple symbols can share a name + syms.append(Sym(name, global__, section, addr, size)) + proc.wait() + if proc.returncode != 0: + raise sp.CalledProcessError(proc.returncode, proc.args) + return SymInfo(syms) - # try to figure out the source file if we have debug-info - dirs = {} - files = {} - # note objdump-path may contain extra args - cmd = objdump_path + ['--dwarf=rawline', path] - if args.get('verbose'): - print(' '.join(shlex.quote(c) for c in cmd)) - proc = sp.Popen(cmd, - stdout=sp.PIPE, - stderr=sp.PIPE if not args.get('verbose') else None, - universal_newlines=True, - errors='replace', - close_fds=False) - for line in proc.stdout: - # note that files contain references to dirs, which we - # dereference as soon as we see them as each file table follows a - # dir table - m = line_pattern.match(line) - if m: - if not m.group('dir'): - # found a directory entry - dirs[int(m.group('no'))] = m.group('path') - else: - # found a file entry - dir = int(m.group('dir')) - if dir in dirs: - files[int(m.group('no'))] = os.path.join( - dirs[dir], - m.group('path')) - else: - files[int(m.group('no'))] = m.group('path') - proc.wait() - if proc.returncode != 0: - if not args.get('verbose'): - for line in proc.stderr: - sys.stdout.write(line) - # do nothing on error, we don't need objdump to work, source files - # may just be inaccurate - pass - - defs = {} - is_func = False - f_name = None - f_file = None - # note objdump-path may contain extra args - cmd = objdump_path + ['--dwarf=info', path] - if args.get('verbose'): - print(' '.join(shlex.quote(c) for c in cmd)) - proc = sp.Popen(cmd, +# each dwarf entry can have attrs and children entries +class DwarfEntry: + def __init__(self, level, off, tag, ats={}, children=[]): + self.level = level + self.off = off + self.tag = tag + self.ats = ats or {} + self.children = children or [] + + def get(self, k, d=None): + return self.ats.get(k, d) + + def __getitem__(self, k): + return self.ats[k] + + def __contains__(self, k): + return k in self.ats + + def __repr__(self): + return '%s(%d, 0x%x, %r, %r)' % ( + self.__class__.__name__, + self.level, + self.off, + self.tag, + self.ats) + + @ft.cached_property + def name(self): + if 'DW_AT_name' in self: + name = self['DW_AT_name'].split(':')[-1].strip() + # prefix with struct/union/enum + if self.tag == 'DW_TAG_structure_type': + name = 'struct ' + name + elif self.tag == 'DW_TAG_union_type': + name = 'union ' + name + elif self.tag == 'DW_TAG_enumeration_type': + name = 'enum ' + name + return name + else: + return None + +# a collection of dwarf entries +class DwarfInfo: + def __init__(self, entries): + self.entries = entries + + def get(self, k, d=None): + # allow lookup by offset or dwarf name + if not isinstance(k, str): + return self.entries.get(k, d) + + else: + # organize entries by name + if not hasattr(self, '_by_name'): + self._by_name = {} + for entry in self.entries.values(): + if entry.name is not None: + self._by_name[entry.name] = entry + + # exact match? do a quick lookup + if k in self._by_name: + return self._by_name[k] + # find the best matching dwarf entry with a simple + # heuristic + # + # this can be different from the actual symbol because + # of optimization passes + else: + def key(entry): + i = k.find(entry.name) + if i == -1: + return None + return (i, len(k)-(i+len(entry.name)), k) + return min( + filter(key, self._by_name.values()), + key=key, + default=d) + + def __getitem__(self, k): + v = self.get(k) + if v is None: + raise KeyError(k) + return v + + def __contains__(self, k): + return self.get(k) is not None + + def __bool__(self): + return bool(self.entries) + + def __len__(self): + return len(self.entries) + + def __iter__(self): + return iter(self.entries.values()) + +def collect_dwarf_info(obj_path, tags=None, *, + objdump_path=OBJDUMP_PATH, + **args): + info_pattern = re.compile( + '^\s*<(?P[^>]*)>' + '\s*<(?P[^>]*)>' + '.*\(\s*(?P[^)]*?)\s*\)\s*$' + '|' '^\s*<(?P[^>]*)>' + '\s*(?P[^>:]*?)' + '\s*:(?P.*)\s*$') + + # collect dwarf entries + info = co.OrderedDict() + entry = None + levels = {} + # note objdump-path may contain extra args + cmd = objdump_path + ['--dwarf=info', obj_path] + if args.get('verbose'): + print(' '.join(shlex.quote(c) for c in cmd)) + proc = sp.Popen(cmd, stdout=sp.PIPE, - stderr=sp.PIPE if not args.get('verbose') else None, universal_newlines=True, errors='replace', close_fds=False) - for line in proc.stdout: - # state machine here to find definitions - m = info_pattern.match(line) - if m: - if m.group('tag'): - if is_func: - defs[f_name] = files.get(f_file, '?') - is_func = (m.group('tag') == 'DW_TAG_subprogram') - elif m.group('name'): - f_name = m.group('name') - elif m.group('file'): - f_file = int(m.group('file')) - if is_func: - defs[f_name] = files.get(f_file, '?') - proc.wait() - if proc.returncode != 0: - if not args.get('verbose'): - for line in proc.stderr: - sys.stdout.write(line) - # do nothing on error, we don't need objdump to work, source files - # may just be inaccurate - pass - - for r in results_: - # find best matching debug symbol, this may be slightly different - # due to optimizations - if defs: - # exact match? avoid difflib if we can for speed - if r.function in defs: - file = defs[r.function] - else: - _, file = max( - defs.items(), - key=lambda d: difflib.SequenceMatcher(None, - d[0], - r.function, False).ratio()) - else: - file = r.file + for line in proc.stdout: + # state machine here to find dwarf entries + m = info_pattern.match(line) + if m: + if m.group('tag'): + entry = DwarfEntry( + level=int(m.group('level'), 0), + off=int(m.group('off'), 16), + tag=m.group('tag').strip(), + ) + # keep track of unfiltered entries + if tags is None or entry.tag in tags: + info[entry.off] = entry + # store entry in parent + levels[entry.level] = entry + if entry.level-1 in levels: + levels[entry.level-1].children.append(entry) + elif m.group('at'): + if entry: + entry.ats[m.group('at').strip()] = ( + m.group('v').strip()) + proc.wait() + if proc.returncode != 0: + raise sp.CalledProcessError(proc.returncode, proc.args) - # ignore filtered sources - if sources is not None: - if not any( - os.path.abspath(file) == os.path.abspath(s) - for s in sources): - continue - else: - # default to only cwd - if not everything and not os.path.commonpath([ - os.getcwd(), - os.path.abspath(file)]) == os.getcwd(): - continue + return DwarfInfo(info) - # simplify path - if os.path.commonpath([ - os.getcwd(), - os.path.abspath(file)]) == os.getcwd(): - file = os.path.relpath(file) - else: - file = os.path.abspath(file) +def collect_data(obj_paths, *, + everything=False, + no_strip=False, + **args): + results = [] + for obj_path in obj_paths: + # find relevant symbols and sizes + syms = collect_syms(obj_path, + sections=SECTIONS, + **args) + + # find dwarf info + info = collect_dwarf_info(obj_path, + tags={'DW_TAG_compile_unit'}, + **args) + + # find source file from dwarf info + for entry in info: + if (entry.tag == 'DW_TAG_compile_unit' + and 'DW_AT_name' in entry + and 'DW_AT_comp_dir' in entry): + file = os.path.join( + entry['DW_AT_comp_dir'].split(':')[-1].strip(), + entry['DW_AT_name'].split(':')[-1].strip()) + break + else: + # guess from obj path + file = re.sub('(\.o)?$', '.c', obj_path, 1) + + # simplify path + if os.path.commonpath([ + os.getcwd(), + os.path.abspath(file)]) == os.getcwd(): + file = os.path.relpath(file) + else: + file = os.path.abspath(file) - results.append(r._replace(file=file)) + # find function sizes + for sym in syms: + # discard internal functions + if not everything and sym.name.startswith('__'): + continue + + # strip compiler suffixes + name = sym.name + if not no_strip: + name = name.split('.', 1)[0] + + results.append(DataResult(file, name, sym.size)) return results +# common folding/tabling/read/write code + +class Rev(co.namedtuple('Rev', 'a')): + __slots__ = () + # yes we need all of these because we're a namedtuple + def __lt__(self, other): + return self.a > other.a + def __gt__(self, other): + return self.a < other.a + def __le__(self, other): + return self.a >= other.a + def __ge__(self, other): + return self.a <= other.a + def fold(Result, results, *, by=None, - defines=None, + defines=[], + sort=None, + depth=1, **_): + # stop when depth hits zero + if depth == 0: + return [] + + # organize by by if by is None: by = Result._by - for k in it.chain(by or [], (k for k, _ in defines or [])): + for k in it.chain(by or [], (k for k, _ in defines)): if k not in Result._by and k not in Result._fields: - print("error: could not find field %r?" % k) + print("error: could not find field %r?" % k, + file=sys.stderr) sys.exit(-1) # filter by matching defines - if defines is not None: + if defines: results_ = [] for r in results: - if all(getattr(r, k) in vs for k, vs in defines): + if all(any(fnmatch.fnmatchcase(str(getattr(r, k, '')), v) + for v in vs) + for k, vs in defines): results_.append(r) results = results_ @@ -343,17 +571,67 @@ def fold(Result, results, *, for name, rs in folding.items(): folded.append(sum(rs[1:], start=rs[0])) + # sort, note that python's sort is stable + folded.sort(key=lambda r: ( + # sort by explicit sort fields + tuple((Rev + if reverse ^ (not k or k in Result._fields) + else lambda x: x)( + tuple((getattr(r, k_),) + if getattr(r, k_) is not None + else () + for k_ in ([k] if k else Result._sort))) + for k, reverse in (sort or [])), + # sort by result + r)) + + # recurse if we have recursive results + if hasattr(Result, '_children'): + folded = [r._replace(**{ + Result._children: fold( + Result, getattr(r, Result._children), + by=by, + # only filter defines at the top level! + sort=sort, + depth=depth-1)}) + for r in folded] + return folded def table(Result, results, diff_results=None, *, by=None, fields=None, sort=None, - summary=False, - all=False, + labels=None, + depth=1, + hot=None, percent=False, + all=False, + compare=None, + no_header=False, + small_header=False, + no_total=False, + small_total=False, + small_table=False, + summary=False, + total=False, **_): - all_, all = all, __builtins__.all + import builtins + all_, all = all, builtins.all + + # small_table implies small_header + no_total or small_total + if small_table: + small_header = True + small_total = True + no_total = no_total or (not summary and not total) + # summary implies small_header + if summary: + small_header = True + # total implies summary + no_header + small_total + if total: + summary = True + no_header = True + small_total = True if by is None: by = Result._by @@ -361,344 +639,624 @@ def table(Result, results, diff_results=None, *, fields = Result._fields types = Result._types - # fold again - results = fold(Result, results, by=by) - if diff_results is not None: - diff_results = fold(Result, diff_results, by=by) - # organize by name table = { - ','.join(str(getattr(r, k) or '') for k in by): r - for r in results} + ','.join(str(getattr(r, k) + if getattr(r, k) is not None + else '') + for k in by): r + for r in results} diff_table = { - ','.join(str(getattr(r, k) or '') for k in by): r - for r in diff_results or []} - names = list(table.keys() | diff_table.keys()) + ','.join(str(getattr(r, k) + if getattr(r, k) is not None + else '') + for k in by): r + for r in diff_results or []} - # sort again, now with diff info, note that python's sort is stable - names.sort() + # lost results? this only happens if we didn't fold by the same + # by field, which is an error and risks confusing results + assert len(table) == len(results) if diff_results is not None: - names.sort(key=lambda n: tuple( - types[k].ratio( - getattr(table.get(n), k, None), - getattr(diff_table.get(n), k, None)) - for k in fields), - reverse=True) - if sort: - for k, reverse in reversed(sort): - names.sort( - key=lambda n: tuple( - (getattr(table[n], k),) - if getattr(table.get(n), k, None) is not None else () - for k in ([k] if k else [ - k for k in Result._sort if k in fields])), - reverse=reverse ^ (not k or k in Result._fields)) + assert len(diff_table) == len(diff_results) + # find compare entry if there is one + if compare: + compare_ = min( + (n for n in table.keys() + if all(fnmatch.fnmatchcase(k, c) + for k, c in it.zip_longest(n.split(','), compare, + fillvalue=''))), + default=compare) + compare_r = table.get(compare_) # build up our lines lines = [] # header - header = [] - header.append('%s%s' % ( - ','.join(by), - ' (%d added, %d removed)' % ( - sum(1 for n in table if n not in diff_table), - sum(1 for n in diff_table if n not in table)) - if diff_results is not None and not percent else '') - if not summary else '') - if diff_results is None: - for k in fields: - header.append(k) - elif percent: - for k in fields: - header.append(k) - else: - for k in fields: - header.append('o'+k) - for k in fields: - header.append('n'+k) - for k in fields: - header.append('d'+k) - header.append('') - lines.append(header) - - def table_entry(name, r, diff_r=None, ratios=[]): - entry = [] - entry.append(name) - if diff_results is None: - for k in fields: - entry.append(getattr(r, k).table() - if getattr(r, k, None) is not None - else types[k].none) - elif percent: + if not no_header: + header = ['%s%s' % ( + ','.join(labels if labels is not None else by), + ' (%d added, %d removed)' % ( + sum(1 for n in table if n not in diff_table), + sum(1 for n in diff_table if n not in table)) + if diff_results is not None and not percent else '') + if not small_header else ''] + if diff_results is None or percent: for k in fields: - entry.append(getattr(r, k).diff_table() - if getattr(r, k, None) is not None - else types[k].diff_none) + header.append(k) else: for k in fields: - entry.append(getattr(diff_r, k).diff_table() - if getattr(diff_r, k, None) is not None - else types[k].diff_none) + header.append('o'+k) for k in fields: - entry.append(getattr(r, k).diff_table() - if getattr(r, k, None) is not None - else types[k].diff_none) + header.append('n'+k) for k in fields: - entry.append(types[k].diff_diff( - getattr(r, k, None), - getattr(diff_r, k, None))) - if diff_results is None: - entry.append('') + header.append('d'+k) + lines.append(header) + + # delete these to try to catch typos below, we need to rebuild + # these tables at each recursive layer + del table + del diff_table + + # entry helper + def table_entry(name, r, diff_r=None): + # prepend name + entry = [name] + + # normal entry? + if ((compare is None or r == compare_r) + and diff_results is None): + for k in fields: + entry.append( + (getattr(r, k).table(), + getattr(getattr(r, k), 'notes', lambda: [])()) + if getattr(r, k, None) is not None + else types[k].none) + # compare entry? + elif diff_results is None: + for k in fields: + entry.append( + (getattr(r, k).table() + if getattr(r, k, None) is not None + else types[k].none, + (lambda t: ['+∞%'] if t == +mt.inf + else ['-∞%'] if t == -mt.inf + else ['%+.1f%%' % (100*t)])( + types[k].ratio( + getattr(r, k, None), + getattr(compare_r, k, None))))) + # percent entry? elif percent: - entry.append(' (%s)' % ', '.join( - '+∞%' if t == +m.inf - else '-∞%' if t == -m.inf - else '%+.1f%%' % (100*t) - for t in ratios)) + for k in fields: + entry.append( + (getattr(r, k).table() + if getattr(r, k, None) is not None + else types[k].none, + (lambda t: ['+∞%'] if t == +mt.inf + else ['-∞%'] if t == -mt.inf + else ['%+.1f%%' % (100*t)])( + types[k].ratio( + getattr(r, k, None), + getattr(diff_r, k, None))))) + # diff entry? else: - entry.append(' (%s)' % ', '.join( - '+∞%' if t == +m.inf - else '-∞%' if t == -m.inf - else '%+.1f%%' % (100*t) - for t in ratios - if t) - if any(ratios) else '') + for k in fields: + entry.append(getattr(diff_r, k).table() + if getattr(diff_r, k, None) is not None + else types[k].none) + for k in fields: + entry.append(getattr(r, k).table() + if getattr(r, k, None) is not None + else types[k].none) + for k in fields: + entry.append( + (types[k].diff( + getattr(r, k, None), + getattr(diff_r, k, None)), + (lambda t: ['+∞%'] if t == +mt.inf + else ['-∞%'] if t == -mt.inf + else ['%+.1f%%' % (100*t)] if t + else [])( + types[k].ratio( + getattr(r, k, None), + getattr(diff_r, k, None))))) + + # append any notes + if hasattr(Result, '_notes') and r is not None: + notes = sorted(getattr(r, Result._notes)) + if isinstance(entry[-1], tuple): + entry[-1] = (entry[-1][0], entry[-1][1] + notes) + else: + entry[-1] = (entry[-1], notes) + return entry - # entries - if not summary: - for name in names: - r = table.get(name) - if diff_results is None: - diff_r = None - ratios = None + # recursive entry helper + def table_recurse(results_, diff_results_, + depth_, + prefixes=('', '', '', '')): + # build the children table at each layer + table_ = { + ','.join(str(getattr(r, k) + if getattr(r, k) is not None + else '') + for k in by): r + for r in results_} + diff_table_ = { + ','.join(str(getattr(r, k) + if getattr(r, k) is not None + else '') + for k in by): r + for r in diff_results_ or []} + names_ = [n + for n in table_.keys() | diff_table_.keys() + if diff_results is None + or all_ + or any( + types[k].ratio( + getattr(table_.get(n), k, None), + getattr(diff_table_.get(n), k, None)) + for k in fields)] + + # sort again, now with diff info, note that python's sort is stable + names_.sort(key=lambda n: ( + # sort by explicit sort fields + next( + tuple((Rev + if reverse ^ (not k or k in Result._fields) + else lambda x: x)( + tuple((getattr(r_, k_),) + if getattr(r_, k_) is not None + else () + for k_ in ([k] if k else Result._sort))) + for k, reverse in (sort or [])) + for r_ in [table_.get(n), diff_table_.get(n)] + if r_ is not None), + # sort by ratio if diffing + Rev(tuple(types[k].ratio( + getattr(table_.get(n), k, None), + getattr(diff_table_.get(n), k, None)) + for k in fields)) + if diff_results is not None + else (), + # move compare entry to the top, note this can be + # overridden by explicitly sorting by fields + (table_.get(n) != compare_r, + # sort by ratio if comparing + Rev(tuple( + types[k].ratio( + getattr(table_.get(n), k, None), + getattr(compare_r, k, None)) + for k in fields))) + if compare + else (), + # sort by result + (table_[n],) if n in table_ else (), + # and finally by name (diffs may be missing results) + n)) + + for i, name in enumerate(names_): + # find comparable results + r = table_.get(name) + diff_r = diff_table_.get(name) + + # figure out a good label + if labels is not None: + label = next( + ','.join(str(getattr(r_, k) + if getattr(r_, k) is not None + else '') + for k in labels) + for r_ in [r, diff_r] + if r_ is not None) else: - diff_r = diff_table.get(name) - ratios = [ - types[k].ratio( - getattr(r, k, None), - getattr(diff_r, k, None)) - for k in fields] - if not all_ and not any(ratios): - continue - lines.append(table_entry(name, r, diff_r, ratios)) + label = name + + # build line + line = table_entry(label, r, diff_r) + + # add prefixes + line = [x if isinstance(x, tuple) else (x, []) for x in line] + line[0] = (prefixes[0+(i==len(names_)-1)] + line[0][0], line[0][1]) + lines.append(line) + + # recurse? + if name in table_ and depth_ > 1: + table_recurse( + getattr(r, Result._children), + getattr(diff_r, Result._children, None), + depth_-1, + (prefixes[2+(i==len(names_)-1)] + "|-> ", + prefixes[2+(i==len(names_)-1)] + "'-> ", + prefixes[2+(i==len(names_)-1)] + "| ", + prefixes[2+(i==len(names_)-1)] + " ")) + + # build entries + if not summary: + table_recurse(results, diff_results, depth) # total - r = next(iter(fold(Result, results, by=[])), None) - if diff_results is None: - diff_r = None - ratios = None - else: - diff_r = next(iter(fold(Result, diff_results, by=[])), None) - ratios = [ - types[k].ratio( - getattr(r, k, None), - getattr(diff_r, k, None)) - for k in fields] - lines.append(table_entry('TOTAL', r, diff_r, ratios)) - - # find the best widths, note that column 0 contains the names and column -1 - # the ratios, so those are handled a bit differently - widths = [ - ((max(it.chain([w], (len(l[i]) for l in lines)))+1+4-1)//4)*4-1 - for w, i in zip( - it.chain([23], it.repeat(7)), - range(len(lines[0])-1))] + if not no_total: + r = next(iter(fold(Result, results, by=[])), Result()) + if diff_results is None: + diff_r = None + else: + diff_r = next(iter(fold(Result, diff_results, by=[])), Result()) + lines.append(table_entry( + 'TOTAL' if not small_total else '', + r, diff_r)) + + # homogenize + lines = [[x if isinstance(x, tuple) else (x, []) for x in line] + for line in lines] + + # find the best widths, note that column 0 contains the names and is + # handled a bit differently + widths = co.defaultdict(lambda: 7, {0: 7}) + nwidths = co.defaultdict(lambda: 0) + for line in lines: + for i, x in enumerate(line): + widths[i] = max(widths[i], ((len(x[0])+1+4-1)//4)*4-1) + if i != len(line)-1: + nwidths[i] = max(nwidths[i], 1+sum(2+len(n) for n in x[1])) + if not any(line[0][0] for line in lines): + widths[0] = 0 # print our table for line in lines: - print('%-*s %s%s' % ( - widths[0], line[0], - ' '.join('%*s' % (w, x) - for w, x in zip(widths[1:], line[1:-1])), - line[-1])) + print('%-*s %s' % ( + widths[0], line[0][0], + ' '.join('%*s%-*s' % ( + widths[i], x[0], + nwidths[i], ' (%s)' % ', '.join(x[1]) if x[1] else '') + for i, x in enumerate(line[1:], 1)))) +def read_csv(path, Result, *, + depth=1, + prefix=None, + **_): + # prefix? this only applies to field fields + if prefix is None: + if hasattr(Result, '_prefix'): + prefix = '%s_' % Result._prefix + else: + prefix = '' -def main(obj_paths, *, - by=None, - fields=None, - defines=None, - sort=None, - **args): - # find sizes - if not args.get('use', None): - results = collect(obj_paths, **args) - else: - results = [] - with openio(args['use']) as f: + by = Result._by + fields = Result._fields + + with openio(path, 'r') as f: + # csv or json? assume json starts with [ + is_json = (f.buffer.peek(1)[:1] == b'[') + + # read csv? + if not is_json: + results = [] reader = csv.DictReader(f, restval='') for r in reader: + if not any(prefix+k in r and r[prefix+k].strip() + for k in fields): + continue try: - results.append(DataResult( - **{k: r[k] for k in DataResult._by - if k in r and r[k].strip()}, - **{k: r['data_'+k] for k in DataResult._fields - if 'data_'+k in r and r['data_'+k].strip()})) + # note this allows by/fields to overlap + results.append(Result(**( + {k: r[k] for k in by + if k in r + and r[k].strip()} + | {k: r[prefix+k] for k in fields + if prefix+k in r + and r[prefix+k].strip()}))) except TypeError: pass + return results - # fold - results = fold(DataResult, results, by=by, defines=defines) + # read json? + else: + import json + def unjsonify(results, depth_): + results_ = [] + for r in results: + if not any(prefix+k in r and r[prefix+k].strip() + for k in fields): + continue + try: + # note this allows by/fields to overlap + results_.append(Result(**( + {k: r[k] for k in by + if k in r + and r[k] is not None} + | {k: r[prefix+k] for k in fields + if prefix+k in r + and r[prefix+k] is not None} + | ({Result._children: unjsonify( + r[Result._children], + depth_-1)} + if hasattr(Result, '_children') + and Result._children in r + and r[Result._children] is not None + and depth_ > 1 + else {}) + | ({Result._notes: set(r[Result._notes])} + if hasattr(Result, '_notes') + and Result._notes in r + and r[Result._notes] is not None + else {})))) + except TypeError: + pass + return results_ + return unjsonify(json.load(f), depth) - # sort, note that python's sort is stable - results.sort() - if sort: - for k, reverse in reversed(sort): - results.sort( - key=lambda r: tuple( - (getattr(r, k),) if getattr(r, k) is not None else () - for k in ([k] if k else DataResult._sort)), - reverse=reverse ^ (not k or k in DataResult._fields)) +def write_csv(path, Result, results, *, + json=False, + by=None, + fields=None, + depth=1, + prefix=None, + **_): + # prefix? this only applies to field fields + if prefix is None: + if hasattr(Result, '_prefix'): + prefix = '%s_' % Result._prefix + else: + prefix = '' - # write results to CSV - if args.get('output'): - with openio(args['output'], 'w') as f: - writer = csv.DictWriter(f, - (by if by is not None else DataResult._by) - + ['data_'+k for k in ( - fields if fields is not None else DataResult._fields)]) + if by is None: + by = Result._by + if fields is None: + fields = Result._fields + + with openio(path, 'w') as f: + # write csv? + if not json: + writer = csv.DictWriter(f, list( + co.OrderedDict.fromkeys(it.chain( + by, + (prefix+k for k in fields))).keys())) writer.writeheader() for r in results: + # note this allows by/fields to overlap writer.writerow( - {k: getattr(r, k) for k in ( - by if by is not None else DataResult._by)} - | {'data_'+k: getattr(r, k) for k in ( - fields if fields is not None else DataResult._fields)}) + {k: getattr(r, k) + for k in by + if getattr(r, k) is not None} + | {prefix+k: getattr(r, k).__csv__() + for k in fields + if getattr(r, k) is not None}) + + # write json? + else: + import json + # the neat thing about json is we can include recursive results + def jsonify(results, depth_): + results_ = [] + for r in results: + # note this allows by/fields to overlap + results_.append( + {k: getattr(r, k) + for k in by + if getattr(r, k) is not None} + | {prefix+k: getattr(r, k).__csv__() + for k in fields + if getattr(r, k) is not None} + | ({Result._children: jsonify( + getattr(r, Result._children), + depth_-1)} + if hasattr(Result, '_children') + and getattr(r, Result._children) + and depth_ > 1 + else {}) + | ({Result._notes: list( + getattr(r, Result._notes))} + if hasattr(Result, '_notes') + and getattr(r, Result._notes) + else {})) + return results_ + json.dump(jsonify(results, depth), f, + separators=(',', ':')) + + +def main(obj_paths, *, + by=None, + fields=None, + defines=[], + sort=None, + **args): + # figure out what fields we're interested in + if by is None: + if args.get('output') or args.get('output_json'): + by = DataResult._by + else: + by = ['function'] + + if fields is None: + fields = DataResult._fields + + # find sizes + if not args.get('use', None): + # not enough info? + if not obj_paths: + print("error: no *.o files?", + file=sys.stderr) + sys.exit(1) + + # collect info + results = collect_data(obj_paths, + **args) + + else: + results = read_csv(args['use'], DataResult, + **args) + + # fold + results = fold(DataResult, results, + by=by, + defines=defines, + sort=sort) # find previous results? + diff_results = None if args.get('diff'): - diff_results = [] try: - with openio(args['diff']) as f: - reader = csv.DictReader(f, restval='') - for r in reader: - if not any('data_'+k in r and r['data_'+k].strip() - for k in DataResult._fields): - continue - try: - diff_results.append(DataResult( - **{k: r[k] for k in DataResult._by - if k in r and r[k].strip()}, - **{k: r['data_'+k] for k in DataResult._fields - if 'data_'+k in r and r['data_'+k].strip()})) - except TypeError: - pass + diff_results = read_csv( + args.get('diff'), + DataResult, + **args) except FileNotFoundError: - pass + diff_results = [] # fold - diff_results = fold(DataResult, diff_results, by=by, defines=defines) + diff_results = fold(DataResult, diff_results, + by=by, + defines=defines) + # write results to JSON + if args.get('output_json'): + write_csv(args['output_json'], DataResult, results, json=True, + by=by, + fields=fields, + **args) + # write results to CSV + elif args.get('output'): + write_csv(args['output'], DataResult, results, + by=by, + fields=fields, + **args) # print table - if not args.get('quiet'): - table(DataResult, results, - diff_results if args.get('diff') else None, - by=by if by is not None else ['function'], - fields=fields, - sort=sort, - **args) + elif not args.get('quiet'): + table(DataResult, results, diff_results, + by=by, + fields=fields, + sort=sort, + **args) if __name__ == "__main__": import argparse import sys parser = argparse.ArgumentParser( - description="Find data size at the function level.", - allow_abbrev=False) + description="Find data size at the function level.", + allow_abbrev=False) + parser.add_argument( + 'obj_paths', + nargs='*', + help="Input *.o files.") parser.add_argument( - 'obj_paths', - nargs='*', - help="Input *.o files.") + '-v', '--verbose', + action='store_true', + help="Output commands that run behind the scenes.") parser.add_argument( - '-v', '--verbose', - action='store_true', - help="Output commands that run behind the scenes.") + '-q', '--quiet', + action='store_true', + help="Don't show anything, useful when checking for errors.") parser.add_argument( - '-q', '--quiet', - action='store_true', - help="Don't show anything, useful with -o.") + '-o', '--output', + help="Specify CSV file to store results.") parser.add_argument( - '-o', '--output', - help="Specify CSV file to store results.") + '-O', '--output-json', + help="Specify JSON file to store results. This may contain " + "recursive info.") parser.add_argument( - '-u', '--use', - help="Don't parse anything, use this CSV file.") + '-u', '--use', + help="Don't parse anything, use this CSV/JSON file.") parser.add_argument( - '-d', '--diff', - help="Specify CSV file to diff against.") + '-d', '--diff', + help="Specify CSV/JSON file to diff against.") parser.add_argument( - '-a', '--all', - action='store_true', - help="Show all, not just the ones that changed.") + '-p', '--percent', + action='store_true', + help="Only show percentage change, not a full diff.") parser.add_argument( - '-p', '--percent', - action='store_true', - help="Only show percentage change, not a full diff.") + '-c', '--compare', + type=lambda x: tuple(v.strip() for v in x.split(',')), + help="Compare results to the row matching this by pattern.") parser.add_argument( - '-b', '--by', - action='append', - choices=DataResult._by, - help="Group by this field.") + '-a', '--all', + action='store_true', + help="Show all, not just the ones that changed.") parser.add_argument( - '-f', '--field', - dest='fields', - action='append', - choices=DataResult._fields, - help="Show this field.") + '-b', '--by', + action='append', + choices=DataResult._by, + help="Group by this field.") parser.add_argument( - '-D', '--define', - dest='defines', - action='append', - type=lambda x: (lambda k,v: (k, set(v.split(','))))(*x.split('=', 1)), - help="Only include results where this field is this value.") + '-f', '--field', + dest='fields', + action='append', + choices=DataResult._fields, + help="Show this field.") + parser.add_argument( + '-D', '--define', + dest='defines', + action='append', + type=lambda x: ( + lambda k, vs: ( + k.strip(), + {v.strip() for v in vs.split(',')}) + )(*x.split('=', 1)), + help="Only include results where this field is this value. May " + "include comma-separated options and globs.") class AppendSort(argparse.Action): def __call__(self, parser, namespace, value, option): if namespace.sort is None: namespace.sort = [] - namespace.sort.append((value, True if option == '-S' else False)) + namespace.sort.append((value, option in {'-S', '--reverse-sort'})) + parser.add_argument( + '-s', '--sort', + nargs='?', + action=AppendSort, + help="Sort by this field.") + parser.add_argument( + '-S', '--reverse-sort', + nargs='?', + action=AppendSort, + help="Sort by this field, but backwards.") + parser.add_argument( + '--no-header', + action='store_true', + help="Don't show the header.") + parser.add_argument( + '--small-header', + action='store_true', + help="Don't show by field names.") + parser.add_argument( + '--no-total', + action='store_true', + help="Don't show the total.") parser.add_argument( - '-s', '--sort', - nargs='?', - action=AppendSort, - help="Sort by this field.") + '--small-total', + action='store_true', + help="Don't show TOTAL name.") parser.add_argument( - '-S', '--reverse-sort', - nargs='?', - action=AppendSort, - help="Sort by this field, but backwards.") + '-Q', '--small-table', + action='store_true', + help="Equivalent to --small-header + --no-total or --small-total.") parser.add_argument( - '-Y', '--summary', - action='store_true', - help="Only show the total.") + '-Y', '--summary', + action='store_true', + help="Only show the total.") parser.add_argument( - '-F', '--source', - dest='sources', - action='append', - help="Only consider definitions in this file. Defaults to anything " - "in the current directory.") + '--total', + action='store_true', + help="Equivalent to --summary + --no-header + --small-total. " + "Useful for scripting.") parser.add_argument( - '--everything', - action='store_true', - help="Include builtin and libc specific symbols.") + '--prefix', + help="Prefix to use for fields in CSV/JSON output. Defaults " + "to %r." % ("%s_" % DataResult._prefix)) parser.add_argument( - '--nm-types', - default=NM_TYPES, - help="Type of symbols to report, this uses the same single-character " - "type-names emitted by nm. Defaults to %r." % NM_TYPES) + '-!', '--everything', + action='store_true', + help="Include builtin and libc specific symbols.") parser.add_argument( - '--nm-path', - type=lambda x: x.split(), - default=NM_PATH, - help="Path to the nm executable, may include flags. " - "Defaults to %r." % NM_PATH) + '-x', '--no-strip', + action='store_true', + help="Don't strip compiler optimization suffixes from symbols.") parser.add_argument( - '--objdump-path', - type=lambda x: x.split(), - default=OBJDUMP_PATH, - help="Path to the objdump executable, may include flags. " - "Defaults to %r." % OBJDUMP_PATH) + '--objdump-path', + type=lambda x: x.split(), + default=OBJDUMP_PATH, + help="Path to the objdump executable, may include flags. " + "Defaults to %r." % OBJDUMP_PATH) sys.exit(main(**{k: v - for k, v in vars(parser.parse_intermixed_args()).items() - if v is not None})) + for k, v in vars(parser.parse_intermixed_args()).items() + if v is not None})) diff --git a/scripts/dbg.gdb.py b/scripts/dbg.gdb.py new file mode 100644 index 000000000..d053951cd --- /dev/null +++ b/scripts/dbg.gdb.py @@ -0,0 +1,136 @@ +# +# Hooks for gdb: +# (gdb) source ./scripts/dbg.gdb.py +# +# + +import shlex + + +# split spaces but only outside of parens and quotes +def gdbsplit(v): + parens = 0 + quote = None + escape = False + i_ = 0 + for i in range(len(v)): + if v[i].isspace() and not parens and not quote: + v_ = v[i_:i].strip() + if v_: + yield v_ + i_ = i+1 + elif quote: + if escape: + escape = False + elif v[i] == quote: + quote = None + elif v[i] == '\\': + escape = True + elif v[i] in '\'"': + quote = v[i] + elif v[i] in '([{': + parens += 1 + elif v[i] in '}])': + parens -= 1 + v_ = v[i_:].strip() + if v_: + yield v_ + +# common wrapper for dbg scripts +# +# Note some tricks to help interact with bash and gdb: +# +# - Flags are passed as is (-h, -b4096, --trunk) +# - All non-flags are parsed as expressions (file->b.shrub.blocks[0]) +# - String expressions may be useful for paths and stuff ("./disk") +# +class DbgCommand(gdb.Command): + """A littlefs debug script. See -h/--help for more info.""" + name = None + path = None + + def __init__(self): + super().__init__(self.name, + gdb.COMMAND_DATA, + gdb.COMPLETE_EXPRESSION) + + def invoke(self, args, *_): + # parse args + args = list(gdbsplit(args)) + args_ = [] + for a in args: + # pass flags as is + if a.startswith('-'): + args_.append(a) + + # parse and eval + else: + try: + v = gdb.parse_and_eval(a) + t = v.type.strip_typedefs() + if t.code in { + gdb.TYPE_CODE_ENUM, + gdb.TYPE_CODE_FLAGS, + gdb.TYPE_CODE_INT, + gdb.TYPE_CODE_RANGE, + gdb.TYPE_CODE_CHAR, + gdb.TYPE_CODE_BOOL}: + v = str(int(v)) + elif t.code in { + gdb.TYPE_CODE_FLT}: + v = str(float(v)) + else: + try: + v = v.string('utf8') + except gdb.error: + raise gdb.GdbError('Unexpected type: %s' % v.type) + except gdb.error as e: + raise gdb.GdbError(e) + + args_.append(shlex.quote(v)) + args = args_ + + # execute + gdb.execute(' '.join(['!'+self.path, *args])) + + +# at some point this was manual, then I realized I could just glob all +# scripts with this prefix +# +# # dbgerr +# class DbgErr(DbgCommand): +# name = 'dbgerr' +# path = './scripts/dbgerr.py' +# +# # dbgflags +# class DbgFlags(DbgCommand): +# name = 'dbgflags' +# path = './scripts/dbgflags.py' +# +# # dbgtag +# class DbgTag(DbgCommand): +# name = 'dbgtag' +# path = './scripts/dbgtag.py' + +import os +import glob + +for path in glob.glob(os.path.join( + os.path.dirname(__file__), + 'dbg*.py')): + if path == __file__: + continue + + # create dbg class + name = os.path.splitext(os.path.basename(path))[0] + type(name, (DbgCommand,), { + 'name': name, + 'path': path + }) + + +# initialize gdb hooks +for Dbg in DbgCommand.__subclasses__(): + if Dbg.__doc__ is None: + Dbg.__doc__ = DbgCommand.__doc__ + Dbg() diff --git a/scripts/dbgblock.py b/scripts/dbgblock.py new file mode 100755 index 000000000..20048715c --- /dev/null +++ b/scripts/dbgblock.py @@ -0,0 +1,223 @@ +#!/usr/bin/env python3 + +# prevent local imports +if __name__ == "__main__": + __import__('sys').path.pop(0) + +import itertools as it +import os + +try: + import crc32c as crc32c_lib +except ModuleNotFoundError: + crc32c_lib = None + + +# some ways of block geometry representations +# 512 -> 512 +# 512x16 -> (512, 16) +# 0x200x10 -> (512, 16) +def bdgeom(s): + s = s.strip() + b = 10 + if s.startswith('0x') or s.startswith('0X'): + s = s[2:] + b = 16 + elif s.startswith('0o') or s.startswith('0O'): + s = s[2:] + b = 8 + elif s.startswith('0b') or s.startswith('0B'): + s = s[2:] + b = 2 + + if 'x' in s: + s, s_ = s.split('x', 1) + return (int(s, b), int(s_, b)) + else: + return int(s, b) + +# parse some rbyd addr encodings +# 0xa -> (0xa,) +# 0xa.c -> ((0xa, 0xc),) +# 0x{a,b} -> (0xa, 0xb) +# 0x{a,b}.c -> ((0xa, 0xc), (0xb, 0xc)) +def rbydaddr(s): + s = s.strip() + b = 10 + if s.startswith('0x') or s.startswith('0X'): + s = s[2:] + b = 16 + elif s.startswith('0o') or s.startswith('0O'): + s = s[2:] + b = 8 + elif s.startswith('0b') or s.startswith('0B'): + s = s[2:] + b = 2 + + trunk = None + if '.' in s: + s, s_ = s.split('.', 1) + trunk = int(s_, b) + + if s.startswith('{') and '}' in s: + ss = s[1:s.find('}')].split(',') + else: + ss = [s] + + addr = [] + for s in ss: + if trunk is not None: + addr.append((int(s, b), trunk)) + else: + addr.append(int(s, b)) + + return tuple(addr) + +def xxd(data, width=16): + for i in range(0, len(data), width): + yield '%-*s %-*s' % ( + 3*width, + ' '.join('%02x' % b for b in data[i:i+width]), + width, + ''.join( + b if b >= ' ' and b <= '~' else '.' + for b in map(chr, data[i:i+width]))) + +def crc32c(data, crc=0): + if crc32c_lib is not None: + return crc32c_lib.crc32c(data, crc) + else: + crc ^= 0xffffffff + for b in data: + crc ^= b + for j in range(8): + crc = (crc >> 1) ^ ((crc & 1) * 0x82f63b78) + return 0xffffffff ^ crc + +def main(disk, blocks=None, *, + block_size=None, + block_count=None, + off=None, + size=None, + cksum=False): + # is bd geometry specified? + if isinstance(block_size, tuple): + block_size, block_count_ = block_size + if block_count is None: + block_count = block_count_ + + with open(disk, 'rb') as f: + # if block_size is omitted, assume the block device is one big block + if block_size is None: + f.seek(0, os.SEEK_END) + block_size = f.tell() + + # if block_count is omitted, derive the block_count from our file size + if block_count is None: + f.seek(0, os.SEEK_END) + block_count = f.tell() // block_size + + # flatten blocks, default to block 0 + blocks = (list(it.chain.from_iterable( + range(block.start or 0, block.stop or block_count) + if isinstance(block, slice) + else block + for block in blocks)) + if blocks + else [0]) + + # blocks may also encode offsets + blocks, offs, size = ( + [block[0] if isinstance(block, tuple) + else block + for block in blocks], + [off.start if isinstance(off, slice) + else off if off is not None + else size.start if isinstance(size, slice) + else block[1] if isinstance(block, tuple) + else None + for block in blocks], + (size.stop - (size.start or 0) + if size.stop is not None + else None) if isinstance(size, slice) + else size if size is not None + else ((off.stop - (off.start or 0)) + if off.stop is not None + else None) if isinstance(off, slice) + else None) + + # hexdump the blocks + for block, off in zip(blocks, offs): + # bound to block_size + block_ = block if block is not None else 0 + off_ = off if off is not None else 0 + size_ = size if size is not None else block_size - off_ + if off_ >= block_size: + continue + size_ = min(off_ + size_, block_size) - off_ + + # read the block + f.seek((block_ * block_size) + off_) + data = f.read(size_) + + # calculate checksum + cksum = crc32c(data) + + # print the header + print('block %s, size %d, cksum %08x' % ( + '0x%x.%x' % (block_, off_) + if off is not None + else '0x%x' % block_, + size_, + cksum)) + + # render the hex view + for o, line in enumerate(xxd(data)): + print('%08x: %s' % (off_ + 16*o, line)) + + +if __name__ == "__main__": + import argparse + import sys + parser = argparse.ArgumentParser( + description="Debug block devices.", + allow_abbrev=False) + parser.add_argument( + 'disk', + help="File containing the block device.") + parser.add_argument( + 'blocks', + nargs='*', + type=lambda x: ( + slice(*(int(x, 0) if x.strip() else None + for x in x.split(',', 1))) + if ',' in x and '{' not in x + else rbydaddr(x)), + help="Block addresses, may be a range.") + parser.add_argument( + '-b', '--block-size', + type=bdgeom, + help="Block size/geometry in bytes. Accepts x.") + parser.add_argument( + '--block-count', + type=lambda x: int(x, 0), + help="Block count in blocks.") + parser.add_argument( + '--off', + type=lambda x: ( + slice(*(int(x, 0) if x.strip() else None + for x in x.split(',', 1))) + if ',' in x + else int(x, 0)), + help="Show a specific offset, may be a range.") + parser.add_argument( + '-n', '--size', + type=lambda x: ( + slice(*(int(x, 0) if x.strip() else None + for x in x.split(',', 1))) + if ',' in x + else int(x, 0)), + help="Show this many bytes, may be a range.") + sys.exit(main(**{k: v + for k, v in vars(parser.parse_intermixed_args()).items() + if v is not None})) diff --git a/scripts/dbgbmap.py b/scripts/dbgbmap.py new file mode 100755 index 000000000..77414b41a --- /dev/null +++ b/scripts/dbgbmap.py @@ -0,0 +1,5403 @@ +#!/usr/bin/env python3 + +# prevent local imports +if __name__ == "__main__": + __import__('sys').path.pop(0) + +import bisect +import collections as co +import fnmatch +import functools as ft +import io +import itertools as it +import math as mt +import os +import re +import shlex +import shutil +import struct +import time + +try: + import inotify_simple +except ModuleNotFoundError: + inotify_simple = None + +try: + import crc32c as crc32c_lib +except ModuleNotFoundError: + crc32c_lib = None + + +# assign chars/colors to specific filesystem objects +CHARS = { + 'mdir': 'm', + 'btree': 'b', + 'data': 'd', + 'corrupt': '!', + 'conflict': '!', + 'unused': '-', +} +COLORS = { + 'mdir': '33', # yellow + 'btree': '34', # blue + 'data': '32', # green + 'corrupt': '31', # red + 'conflict': '30;41', # background red + 'unused': '1;30', # bold gray +} + +# give more interesting objects a higher priority +Z_ORDER = ['corrupt', 'conflict', 'mdir', 'btree', 'data', 'unused'] + +CHARS_DOTS = " .':" +CHARS_BRAILLE = ( + '⠀⢀⡀⣀⠠⢠⡠⣠⠄⢄⡄⣄⠤⢤⡤⣤' '⠐⢐⡐⣐⠰⢰⡰⣰⠔⢔⡔⣔⠴⢴⡴⣴' + '⠂⢂⡂⣂⠢⢢⡢⣢⠆⢆⡆⣆⠦⢦⡦⣦' '⠒⢒⡒⣒⠲⢲⡲⣲⠖⢖⡖⣖⠶⢶⡶⣶' + '⠈⢈⡈⣈⠨⢨⡨⣨⠌⢌⡌⣌⠬⢬⡬⣬' '⠘⢘⡘⣘⠸⢸⡸⣸⠜⢜⡜⣜⠼⢼⡼⣼' + '⠊⢊⡊⣊⠪⢪⡪⣪⠎⢎⡎⣎⠮⢮⡮⣮' '⠚⢚⡚⣚⠺⢺⡺⣺⠞⢞⡞⣞⠾⢾⡾⣾' + '⠁⢁⡁⣁⠡⢡⡡⣡⠅⢅⡅⣅⠥⢥⡥⣥' '⠑⢑⡑⣑⠱⢱⡱⣱⠕⢕⡕⣕⠵⢵⡵⣵' + '⠃⢃⡃⣃⠣⢣⡣⣣⠇⢇⡇⣇⠧⢧⡧⣧' '⠓⢓⡓⣓⠳⢳⡳⣳⠗⢗⡗⣗⠷⢷⡷⣷' + '⠉⢉⡉⣉⠩⢩⡩⣩⠍⢍⡍⣍⠭⢭⡭⣭' '⠙⢙⡙⣙⠹⢹⡹⣹⠝⢝⡝⣝⠽⢽⡽⣽' + '⠋⢋⡋⣋⠫⢫⡫⣫⠏⢏⡏⣏⠯⢯⡯⣯' '⠛⢛⡛⣛⠻⢻⡻⣻⠟⢟⡟⣟⠿⢿⡿⣿') + +SI_PREFIXES = { + 18: 'E', + 15: 'P', + 12: 'T', + 9: 'G', + 6: 'M', + 3: 'K', + 0: '', + -3: 'm', + -6: 'u', + -9: 'n', + -12: 'p', + -15: 'f', + -18: 'a', +} + +SI2_PREFIXES = { + 60: 'Ei', + 50: 'Pi', + 40: 'Ti', + 30: 'Gi', + 20: 'Mi', + 10: 'Ki', + 0: '', + -10: 'mi', + -20: 'ui', + -30: 'ni', + -40: 'pi', + -50: 'fi', + -60: 'ai', +} + + +RCOMPAT_NONSTANDARD = 0x00000001 # Non-standard filesystem format +RCOMPAT_WRONLY = 0x00000004 # Reading is disallowed +RCOMPAT_MMOSS = 0x00000010 # May use an inlined mdir +RCOMPAT_MSPROUT = 0x00000020 # May use an mdir pointer +RCOMPAT_MSHRUB = 0x00000040 # May use an inlined mtree +RCOMPAT_MTREE = 0x00000080 # May use an mdir btree +RCOMPAT_BMOSS = 0x00000100 # Files may use inlined data +RCOMPAT_BSPROUT = 0x00000200 # Files may use block pointers +RCOMPAT_BSHRUB = 0x00000400 # Files may use inlined btrees +RCOMPAT_BTREE = 0x00000800 # Files may use btrees +RCOMPAT_GRM = 0x00010000 # Global-remove in use + +WCOMPAT_NONSTANDARD = 0x00000001 # Non-standard filesystem format +WCOMPAT_RDONLY = 0x00000002 # Writing is disallowed +WCOMPAT_GCKSUM = 0x00040000 # Global-checksum in use +WCOMPAT_GBMAP = 0x00080000 # Global on-disk block-map in use +WCOMPAT_DIR = 0x01000000 # Directory file types in use + +TAG_NULL = 0x0000 ## v--- ---- +--- ---- +TAG_INTERNAL = 0x0000 ## v--- ---- +ttt tttt +TAG_CONFIG = 0x0100 ## v--- ---1 +ttt tttt +TAG_MAGIC = 0x0131 # v--- ---1 +-11 --rr +TAG_VERSION = 0x0134 # v--- ---1 +-11 -1-- +TAG_RCOMPAT = 0x0135 # v--- ---1 +-11 -1-1 +TAG_WCOMPAT = 0x0136 # v--- ---1 +-11 -11- +TAG_OCOMPAT = 0x0137 # v--- ---1 +-11 -111 +TAG_GEOMETRY = 0x0138 # v--- ---1 +-11 1--- +TAG_NAMELIMIT = 0x0139 # v--- ---1 +-11 1--1 +TAG_FILELIMIT = 0x013a # v--- ---1 +-11 1-1- +TAG_GDELTA = 0x0200 ## v--- --1- +ttt tttt +TAG_GRMDELTA = 0x0230 # v--- --1- +-11 --++ +TAG_GBMAPDELTA = 0x0234 # v--- --1- +-11 -1rr +TAG_NAME = 0x0300 ## v--- --11 +ttt tttt +TAG_BNAME = 0x0300 # v--- --11 +--- ---- +TAG_REG = 0x0301 # v--- --11 +--- ---1 +TAG_DIR = 0x0302 # v--- --11 +--- --1- +TAG_STICKYNOTE = 0x0303 # v--- --11 +--- --11 +TAG_BOOKMARK = 0x0304 # v--- --11 +--- -1-- +TAG_MNAME = 0x0330 # v--- --11 +-11 ---- +TAG_STRUCT = 0x0400 ## v--- -1-- +ttt tttt +TAG_BRANCH = 0x0400 # v--- -1-- +--- --rr +TAG_DATA = 0x0404 # v--- -1-- +--- -1rr +TAG_BLOCK = 0x0408 # v--- -1-- +--- 1err +TAG_DID = 0x0420 # v--- -1-- +-1- ---- +TAG_BSHRUB = 0x0428 # v--- -1-- +-1- 1-rr +TAG_BTREE = 0x042c # v--- -1-- +-1- 11rr +TAG_MROOT = 0x0431 # v--- -1-- +-11 --rr +TAG_MDIR = 0x0435 # v--- -1-- +-11 -1rr +TAG_MTREE = 0x043c # v--- -1-- +-11 11rr +TAG_BMRANGE = 0x0440 # v--- -1-- +1-- ++uu +TAG_BMFREE = 0x0440 # v--- -1-- +1-- ---- +TAG_BMINUSE = 0x0441 # v--- -1-- +1-- ---1 +TAG_BMERASED = 0x0442 # v--- -1-- +1-- --1- +TAG_BMBAD = 0x0443 # v--- -1-- +1-- --11 +TAG_ATTR = 0x0600 ## v--- -11a +aaa aaaa +TAG_UATTR = 0x0600 # v--- -11- +aaa aaaa +TAG_SATTR = 0x0700 # v--- -111 +aaa aaaa +TAG_SHRUB = 0x1000 ## v--1 kkkk +kkk kkkk +TAG_ALT = 0x4000 ## v1cd kkkk +kkk kkkk +TAG_B = 0x0000 +TAG_R = 0x2000 +TAG_LE = 0x0000 +TAG_GT = 0x1000 +TAG_CKSUM = 0x3000 ## v-11 ---- ++++ +pqq +TAG_PHASE = 0x0003 +TAG_PERTURB = 0x0004 +TAG_NOTE = 0x3100 ## v-11 ---1 ++++ ++++ +TAG_ECKSUM = 0x3200 ## v-11 --1- ++++ ++++ +TAG_GCKSUMDELTA = 0x3300 ## v-11 --11 ++++ ++++ + + +# self-parsing tag repr +class Tag: + def __init__(self, name, tag, encoding, help): + self.name = name + self.tag = tag + self.encoding = encoding + self.help = help + # derive mask from encoding + self.mask = sum( + (1 if x in 'v-01' else 0) << len(self.encoding)-1-i + for i, x in enumerate(self.encoding)) + + def __repr__(self): + return 'Tag(%r, %r, %r)' % ( + self.name, + self.tag, + self.encoding) + + def __eq__(self, other): + return self.name == other.name + + def __ne__(self, other): + return self.name != other.name + + def __hash__(self): + return hash(self.name) + + def line(self): + # substitute mask chars when zero + tag = '0x%s' % ''.join( + n if n != '0' else next( + (x for x in self.encoding[i*4:i*4+4] + if x not in 'v-01+'), + '0') + for i, n in enumerate('%04x' % self.tag)) + # group into nibbles + encoding = ' '.join(self.encoding[i*4:i*4+4] + for i in range(len(self.encoding)//4)) + return ('LFS3_%s' % self.name, tag, encoding) + + def specificity(self): + return sum(1 for x in self.encoding if x in 'v-01') + + def matches(self, tag): + return (tag & self.mask) == (self.tag & self.mask) + + def get(self, chars, tag): + return sum( + tag & ((1 if x in chars else 0) << len(self.encoding)-1-i) + for i, x in enumerate(self.encoding)) + + def max(self, chars): + return max(len(self.encoding)-1-i + for i, x in enumerate(self.encoding) if x in chars) + + def min(self, chars): + return min(len(self.encoding)-1-i + for i, x in enumerate(self.encoding) if x in chars) + + def width(self, chars): + return self.max(chars) - self.min(chars) + + def __contains__(self, chars): + return any(x in self.encoding for x in chars) + + @staticmethod + @ft.cache + def tags(): + # parse our script's source to figure out tags + import inspect + import re + tags = [] + tag_pattern = re.compile( + '^(?PTAG_[^ ]*) *= *(?P[^#]*?) *' + '#+ *(?P(?:[^ ] *?){16}) *(?P.*)$') + for line in (inspect.getsource( + inspect.getmodule(inspect.currentframe())) + .replace('\\\n', '') + .splitlines()): + m = tag_pattern.match(line) + if m: + tags.append(Tag( + m.group('name'), + globals()[m.group('name')], + m.group('encoding').replace(' ', ''), + m.group('help'))) + return tags + + # find best matching tag + @staticmethod + def find(tag): + # find tags, note this is cached + tags__ = Tag.tags() + + # find the most specific matching tag, ignoring valid bits + return max((t for t in tags__ if t.matches(tag & 0x7fff)), + key=lambda t: t.specificity(), + default=None) + + # human readable tag repr + @staticmethod + def repr(tag, weight=None, size=None, *, + global_=False, + toff=None): + # find the most specific matching tag, ignoring the shrub bit + t = Tag.find(tag & ~(TAG_SHRUB if tag & 0x7000 == TAG_SHRUB else 0)) + + # build repr + r = [] + # normal tag? + if not tag & TAG_ALT: + if t is not None: + # prefix shrub tags with shrub + if tag & 0x7000 == TAG_SHRUB: + r.append('shrub') + # lowercase name + r.append(t.name.split('_', 1)[1].lower()) + # gstate tag? + if global_: + if r[-1] == 'gdelta': + r[-1] = 'gstate' + elif r[-1].endswith('delta'): + r[-1] = r[-1][:-len('delta')] + # include perturb/phase bits + if 'q' in t: + r.append('q%d' % t.get('q', tag)) + if 'p' in t and tag & TAG_PERTURB: + r.append('p') + + # include unmatched fields, but not just redund, and + # only reserved bits if non-zero + if 'tua' in t or ('+' in t and t.get('+', tag) != 0): + r.append(' 0x%0*x' % ( + (t.width('tuar+')+4-1)//4, + t.get('tuar+', tag))) + # unknown tag? + else: + r.append('0x%04x' % tag) + + # weight? + if weight: + r.append(' w%d' % weight) + # size? don't include if null + if size is not None and (size or tag & 0x7fff): + r.append(' %d' % size) + + # alt pointer? + else: + r.append('alt') + r.append('r' if tag & TAG_R else 'b') + r.append('gt' if tag & TAG_GT else 'le') + r.append(' 0x%0*x' % ( + (t.width('k')+4-1)//4, + t.get('k', tag))) + + # weight? + if weight is not None: + r.append(' w%d' % weight) + # jump? + if size and toff is not None: + r.append(' 0x%x' % (0xffffffff & (toff-size))) + elif size: + r.append(' -%d' % size) + + return ''.join(r) + + +# some ways of block geometry representations +# 512 -> 512 +# 512x16 -> (512, 16) +# 0x200x10 -> (512, 16) +def bdgeom(s): + s = s.strip() + b = 10 + if s.startswith('0x') or s.startswith('0X'): + s = s[2:] + b = 16 + elif s.startswith('0o') or s.startswith('0O'): + s = s[2:] + b = 8 + elif s.startswith('0b') or s.startswith('0B'): + s = s[2:] + b = 2 + + if 'x' in s: + s, s_ = s.split('x', 1) + return (int(s, b), int(s_, b)) + else: + return int(s, b) + +# parse some rbyd addr encodings +# 0xa -> (0xa,) +# 0xa.c -> ((0xa, 0xc),) +# 0x{a,b} -> (0xa, 0xb) +# 0x{a,b}.c -> ((0xa, 0xc), (0xb, 0xc)) +def rbydaddr(s): + s = s.strip() + b = 10 + if s.startswith('0x') or s.startswith('0X'): + s = s[2:] + b = 16 + elif s.startswith('0o') or s.startswith('0O'): + s = s[2:] + b = 8 + elif s.startswith('0b') or s.startswith('0B'): + s = s[2:] + b = 2 + + trunk = None + if '.' in s: + s, s_ = s.split('.', 1) + trunk = int(s_, b) + + if s.startswith('{') and '}' in s: + ss = s[1:s.find('}')].split(',') + else: + ss = [s] + + addr = [] + for s in ss: + if trunk is not None: + addr.append((int(s, b), trunk)) + else: + addr.append(int(s, b)) + + return tuple(addr) + +def crc32c(data, crc=0): + if crc32c_lib is not None: + return crc32c_lib.crc32c(data, crc) + else: + crc ^= 0xffffffff + for b in data: + crc ^= b + for j in range(8): + crc = (crc >> 1) ^ ((crc & 1) * 0x82f63b78) + return 0xffffffff ^ crc + +def pmul(a, b): + r = 0 + while b: + if b & 1: + r ^= a + a <<= 1 + b >>= 1 + return r + +def crc32cmul(a, b): + r = pmul(a, b) + for _ in range(31): + r = (r >> 1) ^ ((r & 1) * 0x82f63b78) + return r + +def crc32ccube(a): + return crc32cmul(crc32cmul(a, a), a) + +def popc(x): + return bin(x).count('1') + +def parity(x): + return popc(x) & 1 + +def fromle32(data, j=0): + return struct.unpack('H', data[j:j+2].ljust(2, b'\0'))[0]; d += 2 + weight, d_ = fromleb128(data, j+d); d += d_ + size, d_ = fromleb128(data, j+d); d += d_ + return tag>>15, tag&0x7fff, weight, size, d + +def frombranch(data, j=0): + d = 0 + block, d_ = fromleb128(data, j+d); d += d_ + trunk, d_ = fromleb128(data, j+d); d += d_ + cksum = fromle32(data, j+d); d += 4 + return block, trunk, cksum, d + +def frombtree(data, j=0): + d = 0 + w, d_ = fromleb128(data, j+d); d += d_ + block, trunk, cksum, d_ = frombranch(data, j+d); d += d_ + return w, block, trunk, cksum, d + +def frommdir(data, j=0): + blocks = [] + d = 0 + while j+d < len(data): + block, d_ = fromleb128(data, j+d) + blocks.append(block) + d += d_ + return tuple(blocks), d + +def fromshrub(data, j=0): + d = 0 + weight, d_ = fromleb128(data, j+d); d += d_ + trunk, d_ = fromleb128(data, j+d); d += d_ + return weight, trunk, d + +def frombptr(data, j=0): + d = 0 + size, d_ = fromleb128(data, j+d); d += d_ + block, d_ = fromleb128(data, j+d); d += d_ + off, d_ = fromleb128(data, j+d); d += d_ + cksize, d_ = fromleb128(data, j+d); d += d_ + cksum = fromle32(data, j+d); d += 4 + return size, block, off, cksize, cksum, d + +def xxd(data, width=16): + for i in range(0, len(data), width): + yield '%-*s %-*s' % ( + 3*width, + ' '.join('%02x' % b for b in data[i:i+width]), + width, + ''.join( + b if b >= ' ' and b <= '~' else '.' + for b in map(chr, data[i:i+width]))) + +# compute the difference between two paths, returning everything +# in a after the paths diverge, as well as the relevant index +def pathdelta(a, b): + if not isinstance(a, list): + a = list(a) + i = 0 + for a_, b_ in zip(a, b): + try: + if type(a_) == type(b_) and a_ == b_: + i += 1 + else: + break + # treat exceptions here as failure to match, most likely + # the compared types are incompatible, it's the caller's + # problem + except Exception: + break + + return [(i+j, a_) for j, a_ in enumerate(a[i:])] + + +# a simple wrapper over an open file with bd geometry +class Bd: + def __init__(self, f, block_size=None, block_count=None): + self.f = f + self.block_size = block_size + self.block_count = block_count + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, self.repr()) + + def repr(self): + return 'bd %sx%s' % (self.block_size, self.block_count) + + def read(self, block, off, size): + self.f.seek(block*self.block_size + off) + return self.f.read(size) + + def readblock(self, block): + self.f.seek(block*self.block_size) + return self.f.read(self.block_size) + +# tagged data in an rbyd +class Rattr: + def __init__(self, tag, weight, blocks, toff, tdata, data): + self.tag = tag + self.weight = weight + if isinstance(blocks, int): + self.blocks = (blocks,) + else: + self.blocks = blocks + self.toff = toff + self.tdata = tdata + self.data = data + + @property + def block(self): + return self.blocks[0] + + @property + def tsize(self): + return len(self.tdata) + + @property + def off(self): + return self.toff + len(self.tdata) + + @property + def size(self): + return len(self.data) + + def __bytes__(self): + return self.data + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, self.repr()) + + def repr(self): + return Tag.repr(self.tag, self.weight, self.size) + + def __iter__(self): + return iter((self.tag, self.weight, self.data)) + + def __eq__(self, other): + return ((self.tag, self.weight, self.data) + == (other.tag, other.weight, other.data)) + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash((self.tag, self.weight, self.data)) + + # convenience for did/name access + def _parse_name(self): + # note we return a null name for non-name tags, this is so + # vestigial names in btree nodes act as a catch-all + if (self.tag & 0xff00) != TAG_NAME: + did = 0 + name = b'' + else: + did, d = fromleb128(self.data) + name = self.data[d:] + + # cache both + self.did = did + self.name = name + + @ft.cached_property + def did(self): + self._parse_name() + return self.did + + @ft.cached_property + def name(self): + self._parse_name() + return self.name + +class Ralt: + def __init__(self, tag, weight, blocks, toff, tdata, jump, + color=None, followed=None): + self.tag = tag + self.weight = weight + if isinstance(blocks, int): + self.blocks = (blocks,) + else: + self.blocks = blocks + self.toff = toff + self.tdata = tdata + self.jump = jump + + if color is not None: + self.color = color + else: + self.color = 'r' if tag & TAG_R else 'b' + self.followed = followed + + @property + def block(self): + return self.blocks[0] + + @property + def tsize(self): + return len(self.tdata) + + @property + def off(self): + return self.toff + len(self.tdata) + + @property + def joff(self): + return self.toff - self.jump + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, self.repr()) + + def repr(self): + return Tag.repr(self.tag, self.weight, self.jump, toff=self.toff) + + def __iter__(self): + return iter((self.tag, self.weight, self.jump)) + + def __eq__(self, other): + return ((self.tag, self.weight, self.jump) + == (other.tag, other.weight, other.jump)) + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash((self.tag, self.weight, self.jump)) + + +# our core rbyd type +class Rbyd: + def __init__(self, blocks, trunk, weight, rev, eoff, cksum, data, *, + shrub=False, + gcksumdelta=None, + redund=0): + if isinstance(blocks, int): + self.blocks = (blocks,) + else: + self.blocks = blocks + self.trunk = trunk + self.weight = weight + self.rev = rev + self.eoff = eoff + self.cksum = cksum + self.data = data + + self.shrub = shrub + self.gcksumdelta = gcksumdelta + self.redund = redund + + @property + def block(self): + return self.blocks[0] + + @property + def corrupt(self): + # use redund=-1 to indicate corrupt rbyds + return self.redund >= 0 + + def addr(self): + if len(self.blocks) == 1: + return '0x%x.%x' % (self.block, self.trunk) + else: + return '0x{%s}.%x' % ( + ','.join('%x' % block for block in self.blocks), + self.trunk) + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, self.repr()) + + def repr(self): + return 'rbyd %s w%s' % (self.addr(), self.weight) + + def __bool__(self): + # use redund=-1 to indicate corrupt rbyds + return self.redund >= 0 + + def __eq__(self, other): + return ((frozenset(self.blocks), self.trunk) + == (frozenset(other.blocks), other.trunk)) + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash((frozenset(self.blocks), self.trunk)) + + @classmethod + def _fetch(cls, data, block, trunk=None): + # fetch the rbyd + rev = fromle32(data, 0) + cksum = 0 + cksum_ = crc32c(data[0:4]) + cksum__ = cksum_ + perturb = False + eoff = 0 + eoff_ = None + j_ = 4 + trunk_ = 0 + trunk__ = 0 + trunk___ = 0 + weight = 0 + weight_ = 0 + weight__ = 0 + gcksumdelta = None + gcksumdelta_ = None + while j_ < len(data) and (not trunk or eoff <= trunk): + # read next tag + v, tag, w, size, d = fromtag(data, j_) + if v != parity(cksum__): + break + cksum__ ^= 0x00000080 if v else 0 + cksum__ = crc32c(data[j_:j_+d], cksum__) + j_ += d + if not tag & TAG_ALT and j_ + size > len(data): + break + + # take care of cksums + if not tag & TAG_ALT: + if (tag & 0xff00) != TAG_CKSUM: + cksum__ = crc32c(data[j_:j_+size], cksum__) + + # found a gcksumdelta? + if (tag & 0xff00) == TAG_GCKSUMDELTA: + gcksumdelta_ = Rattr(tag, w, block, j_-d, + data[j_-d:j_], + data[j_:j_+size]) + + # found a cksum? + else: + # check cksum + cksum___ = fromle32(data, j_) + if cksum__ != cksum___: + break + # commit what we have + eoff = eoff_ if eoff_ else j_ + size + cksum = cksum_ + trunk_ = trunk__ + weight = weight_ + gcksumdelta = gcksumdelta_ + gcksumdelta_ = None + # update perturb bit + perturb = bool(tag & TAG_PERTURB) + # revert to data cksum and perturb + cksum__ = cksum_ ^ (0xfca42daf if perturb else 0) + + # evaluate trunks + if (tag & 0xf000) != TAG_CKSUM: + if not (trunk and j_-d > trunk and not trunk___): + # new trunk? + if not trunk___: + trunk___ = j_-d + weight__ = 0 + + # keep track of weight + weight__ += w + + # end of trunk? + if not tag & TAG_ALT: + # update trunk/weight unless we found a shrub or an + # explicit trunk (which may be a shrub) is requested + if not tag & TAG_SHRUB or trunk___ == trunk: + trunk__ = trunk___ + weight_ = weight__ + # keep track of eoff for best matching trunk + if trunk and j_ + size > trunk: + eoff_ = j_ + size + eoff = eoff_ + cksum = cksum__ ^ ( + 0xfca42daf if perturb else 0) + trunk_ = trunk__ + weight = weight_ + gcksumdelta = gcksumdelta_ + trunk___ = 0 + + # update canonical checksum, xoring out any perturb state + cksum_ = cksum__ ^ (0xfca42daf if perturb else 0) + + if not tag & TAG_ALT: + j_ += size + + return cls(block, trunk_, weight, rev, eoff, cksum, data, + gcksumdelta=gcksumdelta, + redund=0 if trunk_ else -1) + + @classmethod + def fetch(cls, bd, blocks, trunk=None): + # multiple blocks? + if not isinstance(blocks, int): + # fetch all blocks + rbyds = [cls.fetch(bd, block, trunk) for block in blocks] + + # determine most recent revision/trunk + rev, trunk = None, None + for rbyd in rbyds: + # compare with sequence arithmetic + if rbyd and ( + rev is None + or not ((rbyd.rev - rev) & 0x80000000) + or (rbyd.rev == rev and rbyd.trunk > trunk)): + rev, trunk = rbyd.rev, rbyd.trunk + # sort for reproducibility + rbyds.sort(key=lambda rbyd: ( + # prioritize valid redund blocks + 0 if rbyd and rbyd.rev == rev and rbyd.trunk == trunk + else 1, + # default to sorting by block + rbyd.block)) + + # choose an active rbyd + rbyd = rbyds[0] + # keep track of the other blocks + rbyd.blocks = tuple(rbyd.block for rbyd in rbyds) + # keep track of how many redund blocks are valid + rbyd.redund = -1 + sum(1 for rbyd in rbyds + if rbyd and rbyd.rev == rev and rbyd.trunk == trunk) + # and patch the gcksumdelta if we have one + if rbyd.gcksumdelta is not None: + rbyd.gcksumdelta.blocks = rbyd.blocks + return rbyd + + # seek/read the block + block = blocks + data = bd.readblock(block) + + # fetch the rbyd + return cls._fetch(data, block, trunk) + + @classmethod + def fetchck(cls, bd, blocks, trunk, weight, cksum): + # try to fetch the rbyd normally + rbyd = cls.fetch(bd, blocks, trunk) + + # cksum mismatch? trunk/weight mismatch? + if (rbyd.cksum != cksum + or rbyd.trunk != trunk + or rbyd.weight != weight): + # mark as corrupt and keep track of expected trunk/weight + rbyd.redund = -1 + rbyd.trunk = trunk + rbyd.weight = weight + + return rbyd + + @classmethod + def fetchshrub(cls, rbyd, trunk): + # steal the original rbyd's data + # + # this helps avoid race conditions with cksums and stuff + shrub = cls._fetch(rbyd.data, rbyd.block, trunk) + shrub.blocks = rbyd.blocks + shrub.shrub = True + return shrub + + def lookupnext(self, rid, tag=None, *, + path=False): + if not self or rid >= self.weight: + if path: + return None, None, [] + else: + return None, None + + tag = max(tag or 0, 0x1) + lower = 0 + upper = self.weight + path_ = [] + + # descend down tree + j = self.trunk + while True: + _, alt, w, jump, d = fromtag(self.data, j) + + # found an alt? + if alt & TAG_ALT: + # follow? + if ((rid, tag & 0xfff) > (upper-w-1, alt & 0xfff) + if alt & TAG_GT + else ((rid, tag & 0xfff) + <= (lower+w-1, alt & 0xfff))): + lower += upper-lower-w if alt & TAG_GT else 0 + upper -= upper-lower-w if not alt & TAG_GT else 0 + j = j - jump + + if path: + # figure out which color + if alt & TAG_R: + _, nalt, _, _, _ = fromtag(self.data, j+jump+d) + if nalt & TAG_R: + color = 'y' + else: + color = 'r' + else: + color = 'b' + + path_.append(Ralt( + alt, w, self.blocks, j+jump, + self.data[j+jump:j+jump+d], jump, + color=color, + followed=True)) + + # stay on path + else: + lower += w if not alt & TAG_GT else 0 + upper -= w if alt & TAG_GT else 0 + j = j + d + + if path: + # figure out which color + if alt & TAG_R: + _, nalt, _, _, _ = fromtag(self.data, j) + if nalt & TAG_R: + color = 'y' + else: + color = 'r' + else: + color = 'b' + + path_.append(Ralt( + alt, w, self.blocks, j-d, + self.data[j-d:j], jump, + color=color, + followed=False)) + + # found tag + else: + rid_ = upper-1 + tag_ = alt + w_ = upper-lower + + if not tag_ or (rid_, tag_) < (rid, tag): + if path: + return None, None, path_ + else: + return None, None + + rattr_ = Rattr(tag_, w_, self.blocks, j, + self.data[j:j+d], + self.data[j+d:j+d+jump]) + if path: + return rid_, rattr_, path_ + else: + return rid_, rattr_ + + def lookup(self, rid, tag=None, mask=None, *, + path=False): + if tag is None: + tag, mask = 0, 0xffff + if mask is None: + mask = 0 + + r = self.lookupnext(rid, tag & ~mask, + path=path) + if path: + rid_, rattr_, path_ = r + else: + rid_, rattr_ = r + if (rid_ is None + or rid_ != rid + or (rattr_.tag & ~mask & 0xfff) + != (tag & ~mask & 0xfff)): + if path: + return None, path_ + else: + return None + + if path: + return rattr_, path_ + else: + return rattr_ + + def rids(self, *, + path=False): + rid = -1 + while True: + r = self.lookupnext(rid, + path=path) + if path: + rid, name, path_ = r + else: + rid, name = r + # found end of tree? + if rid is None: + break + + if path: + yield rid, name, path_ + else: + yield rid, name + rid += 1 + + def rattrs(self, rid=None, tag=None, mask=None, *, + path=False): + if rid is None: + rid, tag = -1, 0 + while True: + r = self.lookupnext(rid, tag+0x1, + path=path) + if path: + rid, rattr, path_ = r + else: + rid, rattr = r + # found end of tree? + if rid is None: + break + + if path: + yield rid, rattr, path_ + else: + yield rid, rattr + tag = rattr.tag + else: + if tag is None: + tag, mask = 0, 0xffff + if mask is None: + mask = 0 + + tag_ = max((tag & ~mask) - 1, 0) + while True: + r = self.lookupnext(rid, tag_+0x1, + path=path) + if path: + rid_, rattr_, path_ = r + else: + rid_, rattr_ = r + # found end of tree? + if (rid_ is None + or rid_ != rid + or (rattr_.tag & ~mask & 0xfff) + != (tag & ~mask & 0xfff)): + break + + if path: + yield rattr_, path_ + else: + yield rattr_ + tag_ = rattr_.tag + + # lookup by name + def namelookup(self, did, name): + # binary search + best = None, None + lower = 0 + upper = self.weight + while lower < upper: + rid, name_ = self.lookupnext( + lower + (upper-1-lower)//2) + if rid is None: + break + + # bisect search space + if (name_.did, name_.name) > (did, name): + upper = rid-(name_.weight-1) + elif (name_.did, name_.name) < (did, name): + lower = rid + 1 + # keep track of best match + best = rid, name_ + else: + # found a match + return rid, name_ + + return best + + +# our rbyd btree type +class Btree: + def __init__(self, bd, rbyd): + self.bd = bd + self.rbyd = rbyd + + @property + def block(self): + return self.rbyd.block + + @property + def blocks(self): + return self.rbyd.blocks + + @property + def trunk(self): + return self.rbyd.trunk + + @property + def weight(self): + return self.rbyd.weight + + @property + def rev(self): + return self.rbyd.rev + + @property + def cksum(self): + return self.rbyd.cksum + + @property + def shrub(self): + return self.rbyd.shrub + + def addr(self): + return self.rbyd.addr() + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, self.repr()) + + def repr(self): + return 'btree %s w%s' % (self.addr(), self.weight) + + def __eq__(self, other): + return self.rbyd == other.rbyd + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash(self.rbyd) + + @classmethod + def fetch(cls, bd, blocks, trunk=None): + # rbyd fetch does most of the work here + rbyd = Rbyd.fetch(bd, blocks, trunk) + return cls(bd, rbyd) + + @classmethod + def fetchck(cls, bd, blocks, trunk, weight, cksum): + # rbyd fetchck does most of the work here + rbyd = Rbyd.fetchck(bd, blocks, trunk, weight, cksum) + return cls(bd, rbyd) + + @classmethod + def fetchshrub(cls, bd, rbyd, trunk): + shrub = Rbyd.fetchshrub(rbyd, trunk) + return cls(bd, shrub) + + def lookupnext_(self, bid, *, + path=False, + depth=None): + if not self or bid >= self.weight: + if path: + return None, None, None, None, [] + else: + return None, None, None, None + + rbyd = self.rbyd + rid = bid + depth_ = 1 + path_ = [] + + while True: + # corrupt branch? + if not rbyd: + if path: + return bid, rbyd, rid, None, path_ + else: + return bid, rbyd, rid, None + + # first tag indicates the branch's weight + rid_, name_ = rbyd.lookupnext(rid) + if rid_ is None: + if path: + return None, None, None, None, path_ + else: + return None, None, None, None + + # keep track of path + if path: + path_.append((bid + (rid_-rid), rbyd, rid_, name_)) + + # find branch tag if there is one + branch_ = rbyd.lookup(rid_, TAG_BRANCH, 0x3) + + # descend down branch? + if branch_ is not None and ( + not depth or depth_ < depth): + block, trunk, cksum, _ = frombranch(branch_.data) + rbyd = Rbyd.fetchck(self.bd, block, trunk, name_.weight, + cksum) + + rid -= (rid_-(name_.weight-1)) + depth_ += 1 + + else: + if path: + return bid + (rid_-rid), rbyd, rid_, name_, path_ + else: + return bid + (rid_-rid), rbyd, rid_, name_ + + # the non-leaf variants discard the rbyd info, these can be a bit + # more convenient, but at a performance cost + def lookupnext(self, bid, *, + path=False, + depth=None): + # just discard the rbyd info + r = self.lookupnext_(bid, + path=path, + depth=depth) + if path: + bid, rbyd, rid, name, path_ = r + else: + bid, rbyd, rid, name = r + + if path: + return bid, name, path_ + else: + return bid, name + + def lookup(self, bid, tag=None, mask=None, *, + path=False, + depth=None): + # lookup rbyd in btree + # + # note this function expects bid to be known, use lookupnext + # first if you don't care about the exact bid (or better yet, + # lookupnext_ and call lookup on the returned rbyd) + # + # this matches rbyd's lookup behavior, which needs a known rid + # to avoid a double lookup + r = self.lookupnext_(bid, + path=path, + depth=depth) + if path: + bid_, rbyd_, rid_, name_, path_ = r + else: + bid_, rbyd_, rid_, name_ = r + if bid_ is None or bid_ != bid: + if path: + return None, path_ + else: + return None + + # lookup tag in rbyd + rattr_ = rbyd_.lookup(rid_, tag, mask) + if rattr_ is None: + if path: + return None, path_ + else: + return None + + if path: + return rattr_, path_ + else: + return rattr_ + + # note leaves only iterates over leaf rbyds, whereas traverse + # traverses all rbyds + def leaves(self, *, + path=False, + depth=None): + # include our root rbyd even if the weight is zero + if self.weight == 0: + if path: + yield -1, self.rbyd, [] + else: + yield -1, self.rbyd + return + + bid = 0 + while True: + r = self.lookupnext_(bid, + path=path, + depth=depth) + if r: + bid, rbyd, rid, name, path_ = r + else: + bid, rbyd, rid, name = r + if bid is None: + break + + if path: + yield (bid-rid + (rbyd.weight-1), rbyd, + # path tail is usually redundant unless corrupt + path_[:-1] + if path_ and path_[-1][1] == rbyd + else path_) + else: + yield bid-rid + (rbyd.weight-1), rbyd + bid += rbyd.weight - rid + 1 + + def traverse(self, *, + path=False, + depth=None): + ptrunk_ = [] + for bid, rbyd, path_ in self.leaves( + path=True, + depth=depth): + # we only care about the rbyds here + trunk_ = ([(bid_-rid_ + (rbyd_.weight-1), rbyd_) + for bid_, rbyd_, rid_, name_ in path_] + + [(bid, rbyd)]) + for d, (bid_, rbyd_) in pathdelta( + trunk_, ptrunk_): + # but include branch rids in the path if requested + if path: + yield bid_, rbyd_, path_[:d] + else: + yield bid_, rbyd_ + ptrunk_ = trunk_ + + # note bids/rattrs do _not_ include corrupt btree nodes! + def bids(self, *, + leaves=False, + path=False, + depth=None): + for r in self.leaves( + path=path, + depth=depth): + if path: + bid, rbyd, path_ = r + else: + bid, rbyd = r + for rid, name in rbyd.rids(): + bid_ = bid-(rbyd.weight-1) + rid + if leaves: + if path: + yield (bid_, rbyd, rid, name, + path_+[(bid_, rbyd, rid, name)]) + else: + yield bid_, rbyd, rid, name + else: + if path: + yield (bid_, name, + path_+[(bid_, rbyd, rid, name)]) + else: + yield bid_, name + + def rattrs(self, bid=None, tag=None, mask=None, *, + leaves=False, + path=False, + depth=None): + if bid is None: + for r in self.leaves( + path=path, + depth=depth): + if path: + bid, rbyd, path_ = r + else: + bid, rbyd = r + for rid, name in rbyd.rids(): + bid_ = bid-(rbyd.weight-1) + rid + for rattr in rbyd.rattrs(rid): + if leaves: + if path: + yield (bid_, rbyd, rid, rattr, + path_+[(bid_, rbyd, rid, name)]) + else: + yield bid_, rbyd, rid, rattr + else: + if path: + yield (bid_, rattr, + path_+[(bid_, rbyd, rid, name)]) + else: + yield bid_, rattr + else: + r = self.lookupnext_(bid, + path=path, + depth=depth) + if path: + bid, rbyd, rid, name, path_ = r + else: + bid, rbyd, rid, name = r + if bid is None: + return + + for rattr in rbyd.rattrs(rid, tag, mask): + if leaves: + if path: + yield rbyd, rid, rattr, path_ + else: + yield rbyd, rid, rattr + else: + if path: + yield rattr, path_ + else: + yield rattr + + # lookup by name + def namelookup_(self, did, name, *, + path=False, + depth=None): + rbyd = self.rbyd + bid = 0 + depth_ = 1 + path_ = [] + + while True: + # corrupt branch? + if not rbyd: + bid_ = bid+(rbyd.weight-1) + if path: + return bid_, rbyd, rbyd.weight-1, None, path_ + else: + return bid_, rbyd, rbyd.weight-1, None + + rid_, name_ = rbyd.namelookup(did, name) + + # keep track of path + if path: + path_.append((bid + rid_, rbyd, rid_, name_)) + + # find branch tag if there is one + branch_ = rbyd.lookup(rid_, TAG_BRANCH, 0x3) + + # found another branch + if branch_ is not None and ( + not depth or depth_ < depth): + block, trunk, cksum, _ = frombranch(branch_.data) + rbyd = Rbyd.fetchck(self.bd, block, trunk, name_.weight, + cksum) + + # update our bid + bid += rid_ - (name_.weight-1) + depth_ += 1 + + # found best match + else: + if path: + return bid + rid_, rbyd, rid_, name_, path_ + else: + return bid + rid_, rbyd, rid_, name_ + + def namelookup(self, bid, *, + path=False, + depth=None): + # just discard the rbyd info + r = self.namelookup_(did, name, + path=path, + depth=depth) + if path: + bid, rbyd, rid, name, path_ = r + else: + bid, rbyd, rid, name = r + + if path: + return bid, name, path_ + else: + return bid, name + + +# a metadata id, this includes mbits for convenience +class Mid: + def __init__(self, mbid, mrid=None, *, + mbits=None): + # we need one of these to figure out mbits + if mbits is not None: + self.mbits = mbits + elif isinstance(mbid, Mid): + self.mbits = mbid.mbits + else: + assert mbits is not None, "mbits?" + + # accept other mids which can be useful for changing mrids + if isinstance(mbid, Mid): + mbid = mbid.mbid + + # accept either merged mid or separate mbid+mrid + if mrid is None: + mid = mbid + mbid = mid | ((1 << self.mbits) - 1) + mrid = mid & ((1 << self.mbits) - 1) + + # map mrid=-1 + if mrid == ((1 << self.mbits) - 1): + mrid = -1 + + self.mbid = mbid + self.mrid = mrid + + @property + def mid(self): + return ((self.mbid & ~((1 << self.mbits) - 1)) + | (self.mrid & ((1 << self.mbits) - 1))) + + def mbidrepr(self): + return str(self.mbid >> self.mbits) + + def mridrepr(self): + return str(self.mrid) + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, self.repr()) + + def repr(self): + return '%s.%s' % (self.mbidrepr(), self.mridrepr()) + + def __iter__(self): + return iter((self.mbid, self.mrid)) + + # note this is slightly different from mid order when mrid=-1 + def __eq__(self, other): + if isinstance(other, Mid): + return (self.mbid, self.mrid) == (other.mbid, other.mrid) + else: + return self.mid == other + + def __ne__(self, other): + if isinstance(other, Mid): + return (self.mbid, self.mrid) != (other.mbid, other.mrid) + else: + return self.mid != other + + def __hash__(self): + return hash((self.mbid, self.mrid)) + + def __lt__(self, other): + return (self.mbid, self.mrid) < (other.mbid, other.mrid) + + def __le__(self, other): + return (self.mbid, self.mrid) <= (other.mbid, other.mrid) + + def __gt__(self, other): + return (self.mbid, self.mrid) > (other.mbid, other.mrid) + + def __ge__(self, other): + return (self.mbid, self.mrid) >= (other.mbid, other.mrid) + +# mdirs, the gooey atomic center of littlefs +# +# really the only difference between this and our rbyd class is the +# implicit mbid associated with the mdir +class Mdir: + def __init__(self, mid, rbyd, *, + mbits=None): + # we need one of these to figure out mbits + if mbits is not None: + self.mbits = mbits + elif isinstance(mid, Mid): + self.mbits = mid.mbits + elif isinstance(rbyd, Mdir): + self.mbits = rbyd.mbits + else: + assert mbits is not None, "mbits?" + + # strip mrid, bugs will happen if caller relies on mrid here + self.mid = Mid(mid, -1, mbits=self.mbits) + + # accept either another mdir or rbyd + if isinstance(rbyd, Mdir): + self.rbyd = rbyd.rbyd + else: + self.rbyd = rbyd + + @property + def data(self): + return self.rbyd.data + + @property + def block(self): + return self.rbyd.block + + @property + def blocks(self): + return self.rbyd.blocks + + @property + def trunk(self): + return self.rbyd.trunk + + @property + def weight(self): + return self.rbyd.weight + + @property + def rev(self): + return self.rbyd.rev + + @property + def eoff(self): + return self.rbyd.eoff + + @property + def cksum(self): + return self.rbyd.cksum + + @property + def gcksumdelta(self): + return self.rbyd.gcksumdelta + + @property + def corrupt(self): + return self.rbyd.corrupt + + @property + def redund(self): + return self.rbyd.redund + + def addr(self): + if len(self.blocks) == 1: + return '0x%x' % self.block + else: + return '0x{%s}' % ( + ','.join('%x' % block for block in self.blocks)) + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, self.repr()) + + def repr(self): + return 'mdir %s %s w%s' % ( + self.mid.mbidrepr(), + self.addr(), + self.weight) + + def __bool__(self): + return bool(self.rbyd) + + # we _don't_ care about mid for equality, or trunk even + def __eq__(self, other): + return frozenset(self.blocks) == frozenset(other.blocks) + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash(frozenset(self.blocks)) + + @classmethod + def fetch(cls, bd, mid, blocks, trunk=None): + rbyd = Rbyd.fetch(bd, blocks, trunk) + return cls(mid, rbyd, mbits=Mtree.mbits_(bd)) + + def lookupnext(self, mid, tag=None, *, + path=False): + # this is similar to rbyd lookupnext, we just error if + # lookupnext changes mids + if not isinstance(mid, Mid): + mid = Mid(mid, mbits=self.mbits) + r = self.rbyd.lookupnext(mid.mrid, tag, + path=path) + if path: + rid, rattr, path_ = r + else: + rid, rattr = r + + if rid != mid.mrid: + if path: + return None, path_ + else: + return None + + if path: + return rattr, path_ + else: + return rattr + + def lookup(self, mid, tag=None, mask=None, *, + path=False): + if not isinstance(mid, Mid): + mid = Mid(mid, mbits=self.mbits) + return self.rbyd.lookup(mid.mrid, tag, mask, + path=path) + + def mids(self, *, + path=False): + for r in self.rbyd.rids( + path=path): + if path: + rid, name, path_ = r + else: + rid, name = r + + mid = Mid(self.mid, rid) + if path: + yield mid, name, path_ + else: + yield mid, name + + def rattrs(self, mid=None, tag=None, mask=None, *, + path=False): + if mid is None: + for r in self.rbyd.rattrs( + path=path): + if path: + rid, rattr, path_ = r + else: + rid, rattr = r + + mid = Mid(self.mid, rid) + if path: + yield mid, rattr, path_ + else: + yield mid, rattr + else: + if not isinstance(mid, Mid): + mid = Mid(mid, mbits=self.mbits) + yield from self.rbyd.rattrs(mid.mrid, tag, mask, + path=path) + + # lookup by name + def namelookup(self, did, name): + # unlike rbyd namelookup, we need an exact match here + rid, name_ = self.rbyd.namelookup(did, name) + if rid is None or (name_.did, name_.name) != (did, name): + return None, None + + return Mid(self.mid, rid), name_ + +# the mtree, the skeletal structure of littlefs +class Mtree: + def __init__(self, bd, mrootchain, mtree, *, + mrootpath=False, + mtreepath=False, + mbits=None): + if isinstance(mrootchain, Mdir): + mrootchain = [Mdir] + # we at least need the mrootanchor, even if it is corrupt + assert len(mrootchain) >= 1 + + self.bd = bd + if mbits is not None: + self.mbits = mbits + else: + self.mbits = Mtree.mbits_(self.bd) + + self.mrootchain = mrootchain + self.mrootanchor = mrootchain[0] + self.mroot = mrootchain[-1] + self.mtree = mtree + + # mbits is a static value derived from the block_size + @staticmethod + def mbits_(block_size): + if isinstance(block_size, Bd): + block_size = block_size.block_size + return mt.ceil(mt.log2(block_size)) - 3 + + # convenience function for creating mbits-dependent mids + def mid(self, mbid, mrid=None): + return Mid(mbid, mrid, mbits=self.mbits) + + @property + def block(self): + return self.mroot.block + + @property + def blocks(self): + return self.mroot.blocks + + @property + def trunk(self): + return self.mroot.trunk + + @property + def weight(self): + if self.mtree is None: + return 0 + else: + return self.mtree.weight + + @property + def mbweight(self): + return self.weight + + @property + def mrweight(self): + return 1 << self.mbits + + def mbweightrepr(self): + return str(self.mbweight >> self.mbits) + + def mrweightrepr(self): + return str(self.mrweight) + + @property + def rev(self): + return self.mroot.rev + + @property + def cksum(self): + return self.mroot.cksum + + def addr(self): + return self.mroot.addr() + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, self.repr()) + + def repr(self): + return 'mtree %s w%s.%s' % ( + self.addr(), + self.mbweightrepr(), self.mrweightrepr()) + + def __eq__(self, other): + return self.mrootanchor == other.mrootanchor + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash(self.mrootanchor) + + @classmethod + def fetch(cls, bd, blocks=None, trunk=None, *, + depth=None): + # default to blocks 0x{0,1} + if blocks is None: + blocks = [0, 1] + + # figure out mbits + mbits = Mtree.mbits_(bd) + + # fetch the mrootanchor + mrootanchor = Mdir.fetch(bd, -1, blocks, trunk) + + # follow the mroot chain to try to find the active mroot + mroot = mrootanchor + mrootchain = [mrootanchor] + mrootseen = set() + while True: + # corrupted? + if not mroot: + break + # cycle detected? + if mroot in mrootseen: + break + mrootseen.add(mroot) + + # stop here? + if depth and len(mrootchain) >= depth: + break + + # fetch the next mroot + rattr_ = mroot.lookup(-1, TAG_MROOT, 0x3) + if rattr_ is None: + break + blocks_, _ = frommdir(rattr_.data) + mroot = Mdir.fetch(bd, -1, blocks_) + mrootchain.append(mroot) + + # fetch the actual mtree, if there is one + mtree = None + if not depth or len(mrootchain) < depth: + rattr_ = mroot.lookup(-1, TAG_MTREE, 0x3) + if rattr_ is not None: + w_, block_, trunk_, cksum_, _ = frombtree(rattr_.data) + mtree = Btree.fetchck(bd, block_, trunk_, w_, cksum_) + + return cls(bd, mrootchain, mtree, + mbits=mbits) + + def _lookupnext_(self, mid, *, + path=False, + depth=None): + if not isinstance(mid, Mid): + mid = self.mid(mid) + + if path or depth: + # iterate over mrootchain + path_ = [] + for mroot in self.mrootchain: + # stop here? + if depth and len(path_) >= depth: + if path: + return mroot, path_ + else: + return mroot + + name = mroot.lookup(-1, TAG_MAGIC) + path_.append((mroot.mid, mroot, name)) + + # no mtree? must be inlined in mroot + if self.mtree is None: + if mid.mbid != -1: + if path: + return None, path_ + else: + return None + + if path: + return self.mroot, path_ + else: + return self.mroot + + # mtree? lookup in mtree + else: + # need to do two steps here in case lookupnext_ stops early + r = self.mtree.lookupnext_(mid.mid, + path=path or depth, + depth=depth-len(path_) if depth else None) + if path or depth: + bid_, rbyd_, rid_, name_, path__ = r + path_.extend(path__) + else: + bid_, rbyd_, rid_, name_ = r + if bid_ is None: + if path: + return None, path_ + else: + return None + + # corrupt btree node? + if not rbyd_: + if path: + return (bid_, rbyd_, rid_), path_ + else: + return (bid_, rbyd_, rid_) + + # stop here? it's not an mdir, but we only return btree nodes + # if explicitly requested + if depth and len(path_) >= depth: + if path: + return (bid_, rbyd_, rid_), path_ + else: + return (bid_, rbyd_, rid_) + + # fetch the mdir + rattr_ = rbyd_.lookup(rid_, TAG_MDIR, 0x3) + # mdir tag missing? weird + if rattr_ is None: + if path: + return (bid_, rbyd_, rid_), path_ + else: + return (bid_, rbyd_, rid_) + blocks_, _ = frommdir(rattr_.data) + mdir = Mdir.fetch(self.bd, mid, blocks_) + if path: + return mdir, path_ + else: + return mdir + + def lookupnext_(self, mid, *, + mdirs_only=True, + path=False, + depth=None): + # most of the logic is in _lookupnext_, this just helps + # deduplicate the mdirs_only logic + r = self._lookupnext_(mid, + path=path, + depth=depth) + if path: + mdir, path_ = r + else: + mdir = r + if mdir is None or ( + mdirs_only and not isinstance(mdir, Mdir)): + if path: + return None, path_ + else: + return None + + if path: + return mdir, path_ + else: + return mdir + + def lookup(self, mid, *, + path=False, + depth=None): + if not isinstance(mid, Mid): + mid = self.mid(mid) + + # lookup the relevant mdir + r = self.lookupnext_(mid, + path=path, + depth=depth) + if path: + mdir, path_ = r + else: + mdir = r + if mdir is None: + if path: + return None, None, path_ + else: + return None, None + + # not in mdir? + if mid.mrid >= mdir.weight: + if path: + return None, None, path_ + else: + return None, None + + # lookup mid in mdir + rattr = mdir.lookup(mid) + if path: + return mdir, rattr, path_+[(mid, mdir, rattr)] + else: + return mdir, rattr + + # iterate over all mdirs, this includes the mrootchain + def _leaves(self, *, + path=False, + depth=None): + # iterate over mrootchain + if path or depth: + path_ = [] + for mroot in self.mrootchain: + if path: + yield mroot, path_ + else: + yield mroot + + if path or depth: + # stop here? + if depth and len(path_) >= depth: + return + + name = mroot.lookup(-1, TAG_MAGIC) + path_.append((mroot.mid, mroot, name)) + + # do we even have an mtree? + if self.mtree is not None: + # include the mtree root even if the weight is zero + if self.mtree.weight == 0: + if path: + yield -1, self.mtree.rbyd, path_ + else: + yield -1, self.mtree.rbyd + return + + mid = self.mid(0) + while True: + r = self.lookupnext_(mid, + mdirs_only=False, + path=path, + depth=depth) + if path: + mdir, path_ = r + else: + mdir = r + if mdir is None: + break + + # mdir? + if isinstance(mdir, Mdir): + if path: + yield mdir, path_ + else: + yield mdir + mid = self.mid(mid.mbid+1) + # btree node? + else: + bid, rbyd, rid = mdir + if path: + yield ((bid-rid + (rbyd.weight-1), rbyd), + # path tail is usually redundant unless corrupt + path_[:-1] + if path_ + and isinstance(path_[-1][1], Rbyd) + and path_[-1][1] == rbyd + else path_) + else: + yield (bid-rid + (rbyd.weight-1), rbyd) + mid = self.mid(bid-rid + (rbyd.weight-1) + 1) + + def leaves(self, *, + mdirs_only=False, + path=False, + depth=None): + for r in self._leaves( + path=path, + depth=depth): + if path: + mdir, path_ = r + else: + mdir = r + if mdirs_only and not isinstance(mdir, Mdir): + continue + + if path: + yield mdir, path_ + else: + yield mdir + + # traverse over all mdirs and btree nodes + # - mdir => Mdir + # - btree node => (bid, rbyd) + def _traverse(self, *, + path=False, + depth=None): + ptrunk_ = [] + for mdir, path_ in self.leaves( + path=True, + depth=depth): + # we only care about the mdirs/rbyds here + trunk_ = ([(lambda mid_, mdir_, name_: mdir_)(*p) + if isinstance(p[1], Mdir) + else (lambda bid_, rbyd_, rid_, name_: + (bid_-rid_ + (rbyd_.weight-1), rbyd_))(*p) + for p in path_] + + [mdir]) + for d, mdir in pathdelta( + trunk_, ptrunk_): + # but include branch mids/rids in the path if requested + if path: + yield mdir, path_[:d] + else: + yield mdir + ptrunk_ = trunk_ + + def traverse(self, *, + mdirs_only=False, + path=False, + depth=None): + for r in self._traverse( + path=path, + depth=depth): + if path: + mdir, path_ = r + else: + mdir = r + if mdirs_only and not isinstance(mdir, Mdir): + continue + + if path: + yield mdir, path_ + else: + yield mdir + + # these are just aliases + + # the difference between mdirs and leaves is mdirs defaults to only + # mdirs, leaves can include btree nodes if corrupt + def mdirs(self, *, + mdirs_only=True, + path=False, + depth=None): + return self.leaves( + mdirs_only=mdirs_only, + path=path, + depth=depth) + + # note mids/rattrs do _not_ include corrupt btree nodes! + def mids(self, *, + mdirs_only=True, + path=False, + depth=None): + for r in self.mdirs( + mdirs_only=mdirs_only, + path=path, + depth=depth): + if path: + mdir, path_ = r + else: + mdir = r + if isinstance(mdir, Mdir): + for mid, name in mdir.mids(): + if path: + yield (mid, mdir, name, + path_+[(mid, mdir, name)]) + else: + yield mid, mdir, name + else: + bid, rbyd = mdir + for rid, name in rbyd.rids(): + bid_ = bid-(rbyd.weight-1) + rid + mid_ = self.mid(bid_) + mdir_ = (bid_, rbyd, rid) + if path: + yield (mid_, mdir_, name, + path_+[(bid_, rbyd, rid, name)]) + else: + yield mid_, mdir_, name + + def rattrs(self, mid=None, tag=None, mask=None, *, + mdirs_only=True, + path=False, + depth=None): + if mid is None: + for r in self.mdirs( + mdirs_only=mdirs_only, + path=path, + depth=depth): + if path: + mdir, path_ = r + else: + mdir = r + if isinstance(mdir, Mdir): + for mid, rattr in mdir.rattrs(): + if path: + yield (mid, mdir, rattr, + path_+[(mid, mdir, mdir.lookup(mid))]) + else: + yield mid, mdir, rattr + else: + bid, rbyd = mdir + for rid, name in rbyd.rids(): + bid_ = bid-(rbyd.weight-1) + rid + mid_ = self.mid(bid_) + mdir_ = (bid_, rbyd, rid) + for rattr in rbyd.rattrs(rid): + if path: + yield (mid_, mdir_, rattr, + path_+[(bid_, rbyd, rid, name)]) + else: + yield mid_, mdir_, rattr + else: + if not isinstance(mid, Mid): + mid = self.mid(mid) + + r = self.lookupnext_(mid, + path=path, + depth=depth) + if path: + mdir, path_ = r + else: + mdir = r + if mdir is None or ( + mdirs_only and not isinstance(mdir, Mdir)): + return + + if isinstance(mdir, Mdir): + for rattr in mdir.rattrs(mid, tag, mask): + if path: + yield rattr, path_ + else: + yield rattr + else: + bid, rbyd, rid = mdir + for rattr in rbyd.rattrs(rid, tag, mask): + if path: + yield rattr, path_ + else: + yield rattr + + # lookup by name + def _namelookup_(self, did, name, *, + path=False, + depth=None): + if path or depth: + # iterate over mrootchain + path_ = [] + for mroot in self.mrootchain: + # stop here? + if depth and len(path_) >= depth: + if path: + return mroot, path_ + else: + return mroot + + name = mroot.lookup(-1, TAG_MAGIC) + path_.append((mroot.mid, mroot, name)) + + # no mtree? must be inlined in mroot + if self.mtree is None: + if path: + return self.mroot, path_ + else: + return self.mroot + + # mtree? find name in mtree + else: + # need to do two steps here in case namelookup_ stops early + r = self.mtree.namelookup_(did, name, + path=path or depth, + depth=depth-len(path_) if depth else None) + if path or depth: + bid_, rbyd_, rid_, name_, path__ = r + path_.extend(path__) + else: + bid_, rbyd_, rid_, name_ = r + if bid_ is None: + if path: + return None, path_ + else: + return None + + # corrupt btree node? + if not rbyd_: + if path: + return (bid_, rbyd_, rid_), path_ + else: + return (bid_, rbyd_, rid_) + + # stop here? it's not an mdir, but we only return btree nodes + # if explicitly requested + if depth and len(path_) >= depth: + if path: + return (bid_, rbyd_, rid_), path_ + else: + return (bid_, rbyd_, rid_) + + # fetch the mdir + rattr_ = rbyd_.lookup(rid_, TAG_MDIR, 0x3) + # mdir tag missing? weird + if rattr_ is None: + if path: + return (bid_, rbyd_, rid_), path_ + else: + return (bid_, rbyd_, rid_) + blocks_, _ = frommdir(rattr_.data) + mdir = Mdir.fetch(self.bd, self.mid(bid_), blocks_) + if path: + return mdir, path_ + else: + return mdir + + def namelookup_(self, did, name, *, + mdirs_only=True, + path=False, + depth=None): + # most of the logic is in _namelookup_, this just helps + # deduplicate the mdirs_only logic + r = self._namelookup_(did, name, + path=path, + depth=depth) + if path: + mdir, path_ = r + else: + mdir = r + if mdir is None or ( + mdirs_only and not isinstance(mdir, Mdir)): + if path: + return None, path_ + else: + return None + + if path: + return mdir, path_ + else: + return mdir + + def namelookup(self, did, name, *, + path=False, + depth=None): + # lookup the relevant mdir + r = self.namelookup_(did, name, + path=path, + depth=depth) + if path: + mdir, path_ = r + else: + mdir = r + if mdir is None: + if path: + return None, None, None, path_ + else: + return None, None, None + + # find name in mdir + mid_, name_ = mdir.namelookup(did, name) + if mid_ is None: + if path: + return None, None, None, path_ + else: + return None, None, None + + if path: + return mid_, mdir, name_, path_+[(mid_, mdir, name_)] + else: + return mid_, mdir, name_ + + +# in-btree block pointers +class Bptr: + def __init__(self, rattr, block, off, size, cksize, cksum, ckdata, *, + corrupt=False): + self.rattr = rattr + self.block = block + self.off = off + self.size = size + self.cksize = cksize + self.cksum = cksum + self.ckdata = ckdata + + self.corrupt = corrupt + + @property + def tag(self): + return self.rattr.tag + + @property + def weight(self): + return self.rattr.weight + + # this is just for consistency with btrees, rbyds, etc + @property + def blocks(self): + return [self.block] + + # try to avoid unnecessary allocations + @ft.cached_property + def data(self): + return self.ckdata[self.off:self.off+self.size] + + def addr(self): + return '0x%x.%x' % (self.block, self.off) + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, self.repr()) + + def repr(self): + return '%sblock %s w%s %s' % ( + 'shrub' if self.tag & TAG_SHRUB else '', + self.addr(), + self.weight, + self.size) + + # lazily check the cksum + @ft.cached_property + def corrupt(self): + cksum_ = crc32c(self.ckdata) + return (cksum_ != self.cksum) + + @property + def redund(self): + return -1 if self.corrupt else 0 + + def __bool__(self): + return not self.corrupt + + @classmethod + def fetch(cls, bd, rattr, block, off, size, cksize, cksum): + # seek/read cksize bytes from the block, the actual data should + # always be a subset of cksize + ckdata = bd.read(block, 0, cksize) + + return cls(rattr, block, off, size, cksize, cksum, ckdata) + + @classmethod + def fetchck(cls, bd, rattr, blocks, off, size, cksize, cksum): + # fetch the bptr normally + bptr = cls.fetch(bd, rattr, blocks, off, size, cksize, cksum) + + # bit of a hack, but this exposes the lazy cksum checker + del bptr.corrupt + + return bptr + + # yeah, so, this doesn't catch mismatched cksizes, but at least the + # underlying data should be identical assuming no mutation + def __eq__(self, other): + return ((self.block, self.off, self.size) + == (other.block, other.off, other.size)) + + def __ne__(self, other): + return ((self.block, self.off, self.size) + != (other.block, other.off, other.size)) + + def __hash__(self): + return hash((self.block, self.off, self.size)) + + +# lazy config object +class Config: + def __init__(self, mroot): + self.mroot = mroot + + # lookup a specific tag + def lookup(self, tag=None, mask=None): + rattr = self.mroot.rbyd.lookup(-1, tag, mask) + if rattr is None: + return None + + return self._parse(rattr.tag, rattr) + + def __getitem__(self, key): + if not isinstance(key, tuple): + key = (key,) + + return self.lookup(*key) + + def __contains__(self, key): + if not isinstance(key, tuple): + key = (key,) + + return self.lookup(*key) is not None + + def __iter__(self): + for rattr in self.mroot.rbyd.rattrs(-1, TAG_CONFIG, 0xff): + yield self._parse(rattr.tag, rattr) + + # common config operations + class Config: + tag = None + mask = None + + def __init__(self, mroot, tag, rattr): + # replace tag with what we find + self.tag = tag + # and keep track of rattr + self.rattr = rattr + + @property + def block(self): + return self.rattr.block + + @property + def blocks(self): + return self.rattr.blocks + + @property + def toff(self): + return self.rattr.toff + + @property + def tdata(self): + return self.rattr.data + + @property + def off(self): + return self.rattr.off + + @property + def data(self): + return self.rattr.data + + @property + def size(self): + return self.rattr.size + + def __bytes__(self): + return self.data + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, self.repr()) + + def repr(self): + return self.rattr.repr() + + def __iter__(self): + return iter((self.tag, self.data)) + + def __eq__(self, other): + return (self.tag, self.data) == (other.tag, other.data) + + def __ne__(self, other): + return (self.tag, self.data) != (other.tag, other.data) + + def __hash__(self): + return hash((self.tag, self.data)) + + # marker class for unknown config + class Unknown(Config): + pass + + # special handling for known configs + + # the filesystem magic string + class Magic(Config): + tag = TAG_MAGIC + mask = 0x3 + + def repr(self): + return 'magic \"%s\"' % ( + ''.join(b if b >= ' ' and b <= '~' else '.' + for b in map(chr, self.data))) + + # version tuple + class Version(Config): + tag = TAG_VERSION + + def __init__(self, mroot, tag, rattr): + super().__init__(mroot, tag, rattr) + d = 0 + self.major, d_ = fromleb128(self.data, d); d += d_ + self.minor, d_ = fromleb128(self.data, d); d += d_ + + @property + def tuple(self): + return (self.major, self.minor) + + def repr(self): + return 'version v%s.%s' % (self.major, self.minor) + + # compat flags + class Rcompat(Config): + tag = TAG_RCOMPAT + + def __init__(self, mroot, tag, rattr): + super().__init__(mroot, tag, rattr) + self.flags = fromle32(self.data) + + def __int__(self): + return self.flags + + def repr(self): + return 'rcompat 0x%s' % ( + ''.join('%02x' % f for f in reversed(self.data))) + + class Wcompat(Config): + tag = TAG_WCOMPAT + + def __init__(self, mroot, tag, rattr): + super().__init__(mroot, tag, rattr) + self.flags = fromle32(self.data) + + def __int__(self): + return self.flags + + def repr(self): + return 'wcompat 0x%s' % ( + ''.join('%02x' % f for f in reversed(self.data))) + + class Ocompat(Config): + tag = TAG_OCOMPAT + + def __init__(self, mroot, tag, rattr): + super().__init__(mroot, tag, rattr) + self.flags = fromle32(self.data) + + def __int__(self): + return self.flags + + def repr(self): + return 'ocompat 0x%s' % ( + ''.join('%02x' % f for f in reversed(self.data))) + + # block device geometry + class Geometry(Config): + tag = TAG_GEOMETRY + + def __init__(self, mroot, tag, rattr): + super().__init__(mroot, tag, rattr) + d = 0 + block_size, d_ = fromleb128(self.data, d); d += d_ + block_count, d_ = fromleb128(self.data, d); d += d_ + # these are offset by 1 to avoid overflow issues + self.block_size = block_size + 1 + self.block_count = block_count + 1 + + def repr(self): + return 'geometry %sx%s' % (self.block_size, self.block_count) + + # file name limit + class NameLimit(Config): + tag = TAG_NAMELIMIT + + def __init__(self, mroot, tag, rattr): + super().__init__(mroot, tag, rattr) + self.limit, _ = fromleb128(self.data) + + def __int__(self): + return self.limit + + def repr(self): + return 'namelimit %s' % self.limit + + # file size limit + class FileLimit(Config): + tag = TAG_FILELIMIT + + def __init__(self, mroot, tag, rattr): + super().__init__(mroot, tag, rattr) + self.limit, _ = fromleb128(self.data) + + def __int__(self): + return self.limit + + def repr(self): + return 'filelimit %s' % self.limit + + # keep track of known configs + _known = [c for c in Config.__subclasses__() if c.tag is not None] + + # parse if known + def _parse(self, tag, rattr): + # known config? + for c in self._known: + if (c.tag & ~(c.mask or 0)) == (tag & ~(c.mask or 0)): + return c(self.mroot, tag, rattr) + # otherwise return a marker class + else: + return self.Unknown(self.mroot, tag, rattr) + + # create cached accessors for known config + def _parser(c): + def _parser(self): + return self.lookup(c.tag, c.mask) + return _parser + + for c in _known: + locals()[c.__name__.lower()] = ft.cached_property(_parser(c)) + +# lazy gstate object +class Gstate: + def __init__(self, mtree, config): + self.mtree = mtree + self.config = config + + # lookup a specific tag + def lookup(self, tag=None, mask=None): + # collect relevant gdeltas in the mtree + gdeltas = [] + for mdir in self.mtree.mdirs(): + # gcksumdelta is a bit special since it's outside the + # rbyd tree + if tag == TAG_GCKSUMDELTA: + gdelta = mdir.gcksumdelta + else: + gdelta = mdir.rbyd.lookup(-1, tag, mask) + if gdelta is not None: + gdeltas.append((mdir.mid, gdelta)) + + # xor to find gstate + return self._parse(tag, gdeltas) + + def __getitem__(self, key): + if not isinstance(key, tuple): + key = (key,) + + return self.lookup(*key) + + def __contains__(self, key): + # note gstate doesn't really "not exist" like normal attrs, + # missing gstate is equivalent to zero gstate, but we can + # still test if there are any gdeltas that match the given + # tag here + if not isinstance(key, tuple): + key = (key,) + + return any( + (mdir.gcksumdelta if tag == TAG_GCKSUMDELTA + else mdir.rbyd.lookup(-1, *key)) + is not None + for mdir in self.mtree.mdirs()) + + def __iter__(self): + # first figure out what gstate tags actually exist in the + # filesystem + gtags = set() + for mdir in self.mtree.mdirs(): + if mdir.gcksumdelta is not None: + gtags.add(TAG_GCKSUMDELTA) + + for rattr in mdir.rbyd.rattrs(-1): + if (rattr.tag & 0xff00) == TAG_GDELTA: + gtags.add(rattr.tag) + + # sort to keep things stable, moving gcksum to the front + gtags = sorted(gtags, key=lambda t: (-(t & 0xf000), t)) + + # compute all gstate in one pass (well, two technically) + gdeltas = {tag: [] for tag in gtags} + for mdir in self.mtree.mdirs(): + for tag in gtags: + # gcksumdelta is a bit special since it's outside the + # rbyd tree + if tag == TAG_GCKSUMDELTA: + gdelta = mdir.gcksumdelta + else: + gdelta = mdir.rbyd.lookup(-1, tag) + if gdelta is not None: + gdeltas[tag].append((mdir.mid, gdelta)) + + for tag in gtags: + # xor to find gstate + yield self._parse(tag, gdeltas[tag]) + + # common gstate operations + class Gstate: + tag = None + mask = None + rcompat = None + wcompat = None + ocompat = None + + def __init__(self, mtree, config, tag, gdeltas): + # replace tag with what we find + self.tag = tag + # keep track of gdeltas for debugging + self.gdeltas = gdeltas + + # xor together to build our gstate + data = bytes() + for mid, gdelta in gdeltas: + data = bytes( + a^b for a, b in it.zip_longest( + data, gdelta.data, + fillvalue=0)) + self.data = data + + # check compat flags while we can access config + if self.rcompat is not None: + self.rcompat = self.rcompat & ( + int(config.rcompat) if config.rcompat is not None + else 0) + if self.wcompat is not None: + self.wcompat = self.wcompat & ( + int(config.wcompat) if config.wcompat is not None + else 0) + if self.ocompat is not None: + self.ocompat = self.ocompat & ( + int(config.ocompat) if config.ocompat is not None + else 0) + + @property + def blocks(self): + return tuple(it.chain.from_iterable( + gdelta.blocks for _, gdelta in self.gdeltas)) + + # true unless compat flags are missing + def __bool__(self): + return (self.rcompat != 0 + and self.wcompat != 0 + and self.ocompat != 0) + + @property + def size(self): + return len(self.data) + + def __bytes__(self): + return self.data + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, self.repr()) + + def repr(self): + return Tag.repr(self.tag, 0, self.size, global_=True) + + def __iter__(self): + return iter((self.tag, self.data)) + + def __eq__(self, other): + return (self.tag, self.data) == (other.tag, other.data) + + def __ne__(self, other): + return (self.tag, self.data) != (other.tag, other.data) + + def __hash__(self): + return hash((self.tag, self.data)) + + # marker class for unknown gstate + class Unknown(Gstate): + pass + + # special handling for known gstate + + # the global-checksum, cubed + class Gcksum(Gstate): + tag = TAG_GCKSUMDELTA + wcompat = WCOMPAT_GCKSUM + + def __init__(self, mtree, config, tag, gdeltas): + super().__init__(mtree, config, tag, gdeltas) + self.gcksum = fromle32(self.data) + + def __int__(self): + return self.gcksum + + def repr(self): + return 'gcksum %08x' % self.gcksum + + # any global-removes + class Grm(Gstate): + tag = TAG_GRMDELTA + rcompat = RCOMPAT_GRM + + def __init__(self, mtree, config, tag, gdeltas): + super().__init__(mtree, config, tag, gdeltas) + queue = [] + d = 0 + for _ in range(2): + mid, d_ = fromleb128(self.data, d); d += d_ + # a null mid (mid=0.0) terminates the grm queue + if not mid: + break + mid = mtree.mid(mid) + # map mbids -> -1 if mroot-inlined + if mtree.mtree is None: + mid = mtree.mid(-1, mid.mrid) + queue.append(mid) + self.queue = queue + + def repr(self): + if self: + return 'grm [%s]' % ', '.join( + mid.repr() for mid in self.queue) + else: + return 'grm (unused)' + + # the global block map + class Gbmap(Gstate): + tag = TAG_GBMAPDELTA + wcompat = WCOMPAT_GBMAP + + def __init__(self, mtree, config, tag, gdeltas): + super().__init__(mtree, config, tag, gdeltas) + d = 0 + self.window, d_ = fromleb128(self.data, d); d += d_ + self.known, d_ = fromleb128(self.data, d); d += d_ + block, trunk, cksum, d_ = frombranch(self.data, d); d += d_ + self.btree = Btree.fetchck( + mtree.bd, block, trunk, + config.geometry.block_count + if config.geometry is not None else 0, + cksum) + + def repr(self): + if self: + return 'gbmap %s 0x%x %d' % ( + self.btree.addr(), + self.window, self.known) + else: + return 'gbmap (unused)' + + # keep track of known gstate + _known = [g for g in Gstate.__subclasses__() if g.tag is not None] + + # parse if known + def _parse(self, tag, gdeltas): + # known config? + for g in self._known: + if (g.tag & ~(g.mask or 0)) == (tag & ~(g.mask or 0)): + return g(self.mtree, self.config, tag, gdeltas) + # otherwise return a marker class + else: + return self.Unknown(self.mtree, self.config, tag, gdeltas) + + # create cached accessors for known gstate + def _parser(g): + def _parser(self): + return self.lookup(g.tag, g.mask) + return _parser + + for g in _known: + locals()[g.__name__.lower()] = ft.cached_property(_parser(g)) + + +# high-level littlefs representation +class Lfs3: + def __init__(self, bd, mtree, config=None, gstate=None, cksum=None, *, + corrupt=False): + self.bd = bd + self.mtree = mtree + + # create lazy config/gstate objects + self.config = config or Config(self.mroot) + self.gstate = gstate or Gstate(self.mtree, self.config) + + # go ahead and fetch some expected fields + self.version = self.config.version + self.rcompat = self.config.rcompat + self.wcompat = self.config.wcompat + self.ocompat = self.config.ocompat + if self.config.geometry is not None: + self.block_count = self.config.geometry.block_count + self.block_size = self.config.geometry.block_size + else: + self.block_count = self.bd.block_count + self.block_size = self.bd.block_size + + # calculate on-disk gcksum + if cksum is None: + cksum = 0 + for mdir in self.mtree.mdirs(): + cksum ^= mdir.cksum + self.cksum = cksum + + # is the filesystem corrupt? + self.corrupt = corrupt + + # create the root directory, this is a bit of a special case + self.root = self.Root(self) + + # mbits is a static value derived from the block_size + @staticmethod + def mbits_(block_size): + return Mtree.mbits_(block_size) + + @property + def mbits(self): + return self.mtree.mbits + + # convenience function for creating mbits-dependent mids + def mid(self, mbid, mrid=None): + return self.mtree.mid(mbid, mrid) + + # most of our fields map to the mtree + @property + def block(self): + return self.mroot.block + + @property + def blocks(self): + return self.mroot.blocks + + @property + def trunk(self): + return self.mroot.trunk + + @property + def rev(self): + return self.mroot.rev + + @property + def weight(self): + return self.mtree.weight + + @property + def mbweight(self): + return self.mtree.mbweight + + @property + def mrweight(self): + return self.mtree.mrweight + + def mbweightrepr(self): + return self.mtree.mbweightrepr() + + def mrweightrepr(self): + return self.mtree.mrweightrepr() + + @property + def mrootchain(self): + return self.mtree.mrootchain + + @property + def mrootanchor(self): + return self.mtree.mrootanchor + + @property + def mroot(self): + return self.mtree.mroot + + def addr(self): + return self.mroot.addr() + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, self.repr()) + + def repr(self): + return 'littlefs v%s.%s %sx%s %s w%s.%s' % ( + self.version.major if self.version is not None else '?', + self.version.minor if self.version is not None else '?', + self.block_size if self.block_size is not None else '?', + self.block_count if self.block_count is not None else '?', + self.addr(), + self.mbweightrepr(), self.mrweightrepr()) + + def __bool__(self): + return not self.corrupt + + def __eq__(self, other): + return self.mrootanchor == other.mrootanchor + + def __ne__(self, other): + return self.mrootanchor != other.mrootanchor + + def __hash__(self): + return hash(self.mrootanchor) + + @classmethod + def fetch(cls, bd, blocks=None, trunk=None, *, + depth=None, + no_ck=False, + no_ckmroot=False, + no_ckmagic=False, + no_ckgcksum=False): + # Mtree does most of the work here + mtree = Mtree.fetch(bd, blocks, trunk, + depth=depth) + + # create lfs object + lfs = cls(bd, mtree) + + # don't check anything? + if no_ck: + return lfs + + # check mroot + if (not no_ckmroot + and not lfs.corrupt + and not lfs.ckmroot()): + lfs.corrupt = True + + # check magic + if (not no_ckmagic + and not lfs.corrupt + and not lfs.ckmagic()): + lfs.corrupt = True + + # check gcksum + if (not no_ckgcksum + and not lfs.corrupt + and not lfs.ckgcksum()): + lfs.corrupt = True + + return lfs + + # check that the mroot is valid + def ckmroot(self): + return bool(self.mroot) + + # check that the magic string is littlefs + def ckmagic(self): + if self.config.magic is None: + return False + return self.config.magic.data == b'littlefs' + + # check that the gcksum checks out + def ckgcksum(self): + return crc32ccube(self.cksum) == int(self.gstate.gcksum) + + # read custom attrs + def uattrs(self): + return self.mroot.rattrs(-1, TAG_UATTR, 0xff) + + def sattrs(self): + return self.mroot.rattrs(-1, TAG_SATTR, 0xff) + + def attrs(self): + yield from self.uattrs() + yield from self.sattrs() + + # is file in grm queue? + def grmed(self, mid): + if not isinstance(mid, Mid): + mid = self.mid(mid) + + return mid in self.gstate.grm.queue + + # lookup operations + def lookup(self, mid, mdir=None, *, + all=False): + import builtins + all_, all = all, builtins.all + + # is this mid grmed? + if not all_ and self.grmed(mid): + return None + + if mdir is None: + mdir, name = self.mtree.lookup(mid) + if mdir is None: + return None + else: + name = mdir.lookup(mid) + + # stickynote? + if not all_ and name.tag == TAG_STICKYNOTE: + return None + + return self._open(mid, mdir, name.tag, name) + + def namelookup(self, did, name, *, + all=False): + import builtins + all_, all = all, builtins.all + + mid_, mdir_, name_ = self.mtree.namelookup(did, name) + if mid_ is None: + return None + + # is this mid grmed? + if not all_ and self.grmed(mid_): + return None + + # stickynote? + if not all_ and name_.tag == TAG_STICKYNOTE: + return None + + return self._open(mid_, mdir_, name_.tag, name_) + + class PathError(Exception): + pass + + # split a path into its components + # + # note this follows littlefs's internal logic, so dots and dotdot + # entries get resolved _before_ walking the path + @staticmethod + def pathsplit(path): + path_ = path + if isinstance(path_, str): + path_ = path_.encode('utf8') + + # empty path? + if path_ == b'': + raise Lfs3.PathError("invalid path: %r" % path) + + path__ = [] + for p in path_.split(b'/'): + # skip multiple slashes and dots + if p == b'' or p == b'.': + continue + path__.append(p) + path_ = path__ + + # resolve dotdots + path__ = [] + dotdots = 0 + for p in reversed(path_): + if p == b'..': + dotdots += 1 + elif dotdots: + dotdots -= 1 + else: + path__.append(p) + if dotdots: + raise Lfs3.PathError("invalid path: %r" % path) + path__.reverse() + path_ = path__ + + return path_ + + def pathlookup(self, did, path_=None, *, + all=False, + path=False, + depth=None): + import builtins + all_, all = all, builtins.all + + # default to the root directory + if path_ is None: + did, path_ = 0, did + # parse/split the path + if isinstance(path_, (bytes, str)): + path_ = self.pathsplit(path_) + + # start at the root dir + dir = self.root + did = did + if path or depth: + path__ = [] + + for p in path_: + # lookup the next file + file = self.namelookup(did, p, + all=all_) + if file is None: + if path: + return None, path__ + else: + return None + + # file? done? + if not file.recursable: + if path: + return file, path__ + else: + return file + + # recurse down the file tree + dir = file + did = dir.did + if path or depth: + path__.append(dir) + # stop here? + if depth and len(path__) >= depth: + if path: + return None, path__ + else: + return None + + if path: + return dir, path__ + else: + return dir + + def files(self, did=None, *, + all=False, + path=False, + depth=None): + import builtins + all_, all = all, builtins.all + + # default to the root directory + did = did or self.root.did + + # start with the bookmark entry + mid, mdir, name = self.mtree.namelookup(did, b'') + # no bookmark? weird + if mid is None: + return + + # iterate over files until we find a different did + while name.did == did: + # yield file, hiding grms, stickynotes, etc, by default + if all_ or (not self.grmed(mid) + and not name.tag == TAG_BOOKMARK + and not name.tag == TAG_STICKYNOTE): + file = self._open(mid, mdir, name.tag, name) + if path: + yield file, [] + else: + yield file + + # recurse? + if (file.recursable + and depth is not None + and (depth == 0 or depth > 1)): + for r in self.files(file.did, + all=all_, + path=path, + depth=depth-1 if depth else 0): + if path: + file_, path_ = r + yield file_, [file]+path_ + else: + file_ = r + yield file_ + + # increment mid and find the next mdir if needed + mbid, mrid = mid.mbid, mid.mrid + 1 + if mrid == mdir.weight: + mbid, mrid = mbid + (1 << self.mbits), 0 + mdir = self.mtree.lookupnext_(mbid) + if mdir is None: + break + # lookup name and adjust rid if necessary, you don't + # normally need to do this, but we don't want the iteration + # to terminate early on a corrupt filesystem + mrid, name = mdir.rbyd.lookupnext(mrid) + if mrid is None: + break + mid = self.mid(mbid, mrid) + + def orphans(self, + all=False): + import builtins + all_, all = all, builtins.all + + # first find all reachable dids + dids = {self.root.did} + for file in self.files(depth=mt.inf): + if file.recursable: + dids.add(file.did) + + # then iterate over all dids and yield any that aren't reachable + for mid, mdir, name in self.mtree.mids(): + # is this mid grmed? + if not all_ and self.grmed(mid): + continue + + # stickynote? + if not all_ and name.tag == TAG_STICKYNOTE: + continue + + # unreachable? note this lazily parses the did + if name.did not in dids: + file = self._open(mid, mdir, name.tag, name) + # mark as orphaned + file.orphaned = True + yield file + + # traverse the filesystem + def traverse(self, *, + mtree_only=False, + gstate=True, + shrubs=False, + fragments=False, + path=False): + # traverse the mtree + for r in self.mtree.traverse( + path=path): + if path: + mdir, path_ = r + else: + mdir = r + + # mdir? + if isinstance(mdir, Mdir): + if path: + yield mdir, path_ + else: + yield mdir + + # btree node? we only care about the rbyd for simplicity + else: + bid, rbyd = mdir + if path: + yield rbyd, path_ + else: + yield rbyd + + # traverse file bshrubs/btrees + if not mtree_only and isinstance(mdir, Mdir): + for mid, name in mdir.mids(): + file = self._open(mid, mdir, name.tag, name) + for r in file.traverse( + path=path): + if path: + pos, data, path__ = r + path__ = [(mid, mdir, name)]+path__ + else: + pos, data = r + + # inlined data? we usually ignore these + if isinstance(data, Rattr): + if fragments: + if path: + yield data, path_+path__ + else: + yield data + # block pointer? + elif isinstance(data, Bptr): + if path: + yield data, path_+path__ + else: + yield data + # bshrub/btree node? we only care about the rbyd + # for simplicity, we also usually ignore shrubs + # since these live the the parent mdir + else: + if shrubs or not data.shrub: + if path: + yield data, path_+path__ + else: + yield data + + # traverse any gstate + if not mtree_only and gstate: + for gstate_ in self.gstate: + if not gstate_ or getattr(gstate_, 'btree', None) is None: + continue + + for r in gstate_.btree.traverse( + path=path): + if path: + bid, rbyd, path_ = r + else: + bid, rbyd = r + + if path: + yield rbyd, [(self.mid(-1), gstate_)]+path_ + else: + yield rbyd + + # common file operations, note Reg extends this for regular files + class File: + tag = None + mask = None + internal = False + recursable = False + grmed = False + orphaned = False + + def __init__(self, lfs, mid, mdir, tag, name): + self.lfs = lfs + self.mid = mid + self.mdir = mdir + # replace tag with what we find + self.tag = tag + self.name = name + + # fetch the file structure if there is one + self.struct = mdir.lookup(mid, TAG_STRUCT, 0xff) + + # bshrub/btree? + self.bshrub = None + if (self.struct is not None + and (self.struct.tag & ~0x3) == TAG_BSHRUB): + weight, trunk, _ = fromshrub(self.struct.data) + self.bshrub = Btree.fetchshrub(lfs.bd, mdir.rbyd, trunk) + elif (self.struct is not None + and (self.struct.tag & ~0x3) == TAG_BTREE): + weight, block, trunk, cksum, _ = frombtree(self.struct.data) + self.bshrub = Btree.fetchck( + lfs.bd, block, trunk, weight, cksum) + + # did? + self.did = None + if (self.struct is not None + and self.struct.tag == TAG_DID): + self.did, _ = fromleb128(self.struct.data) + + # some other info that is useful for scripts + + # mark as grmed if grmed + if lfs.grmed(mid): + self.grmed = True + + @property + def size(self): + if self.bshrub is not None: + return self.bshrub.weight + else: + return 0 + + def structrepr(self): + if self.struct is not None: + # inlined bshrub? + if (self.struct.tag & ~0x3) == TAG_BSHRUB: + return 'bshrub %s' % self.bshrub.addr() + # btree? + elif (self.struct.tag & ~0x3) == TAG_BTREE: + return 'btree %s' % self.bshrub.addr() + # btree? + else: + return self.struct.repr() + else: + return '' + + def __repr__(self): + return '<%s %s.%s %s>' % ( + self.__class__.__name__, + self.mid.mbidrepr(), self.mid.mridrepr(), + self.repr()) + + def repr(self): + return 'type 0x%02x%s' % ( + self.tag & 0xff, + ', %s' % self.structrepr() + if self.struct is not None else '') + + def __eq__(self, other): + return self.mid == other.mid + + def __ne__(self, other): + return self.mid != other.mid + + def __hash__(self): + return hash(self.mid) + + # read attrs, note this includes _all_ attrs + def rattrs(self): + return self.mdir.rattrs(self.mid) + + # read custom attrs + def uattrs(self): + return self.mdir.rattrs(self.mid, TAG_UATTR, 0xff) + + def sattrs(self): + return self.mdir.rattrs(self.mid, TAG_SATTR, 0xff) + + def attrs(self): + yield from self.uattrs() + yield from self.sattrs() + + # lookup data in the underlying bshrub + def _lookupnext_(self, pos, *, + path=False, + depth=None): + # no bshrub? + if self.bshrub is None: + if path: + return None, None, [] + else: + return None, None + + # lookup data in our bshrub + r = self.bshrub.lookupnext_(pos, + path=path or depth, + depth=depth) + if path or depth: + bid, rbyd, rid, rattr, path_ = r + else: + bid, rbyd, rid, rattr = r + if bid is None: + if path: + return None, None, path_ + else: + return None, None + + # corrupt btree node? + if not rbyd: + if path: + return bid-(rbyd.weight-1), rbyd, path_ + else: + return bid-(rbyd.weight-1), rbyd + + # stop here? + if depth and len(path_) >= depth: + if path: + return bid-(rattr.weight-1), rbyd, path_ + else: + return bid-(rattr.weight-1), rbyd + + # inlined data? + if (rattr.tag & ~0x1003) == TAG_DATA: + if path: + return bid-(rattr.weight-1), rattr, path_ + else: + return bid-(rattr.weight-1), rattr + # block pointer? + elif (rattr.tag & ~0x1003) == TAG_BLOCK: + size, block, off, cksize, cksum, _ = frombptr(rattr.data) + bptr = Bptr.fetchck(self.lfs.bd, rattr, + block, off, size, cksize, cksum) + if path: + return bid-(rattr.weight-1), bptr, path_ + else: + return bid-(rattr.weight-1), bptr + # uh oh, something is broken + else: + if path: + return bid-(rattr.weight-1), rattr, path_ + else: + return bid-(rattr.weight-1), rattr + + def lookupnext_(self, pos, *, + data_only=True, + path=False, + depth=None): + r = self._lookupnext_(pos, + path=path, + depth=depth) + if path: + pos, data, path_ = r + else: + pos, data = r + if pos is None or ( + data_only and not isinstance(data, (Rattr, Bptr))): + if path: + return None, None, path_ + else: + return None, None + + if path: + return pos, data, path_ + else: + return pos, data + + def _leaves(self, *, + path=False, + depth=None): + pos = 0 + while True: + r = self.lookupnext_(pos, + data_only=False, + path=path, + depth=depth) + if path: + pos, data, path_ = r + else: + pos, data = r + if pos is None: + break + + # data? + if isinstance(data, (Rattr, Bptr)): + if path: + yield pos, data, path_ + else: + yield pos, data + pos += data.weight + # btree node? + else: + rbyd = data + if path: + yield (pos, rbyd, + # path tail is usually redundant unless corrupt + path_[:-1] + if path_ and path_[-1][1] == rbyd + else path_) + else: + yield pos, rbyd + pos += rbyd.weight + + def leaves(self, *, + data_only=False, + path=False, + depth=None): + for r in self._leaves( + path=path, + depth=depth): + if path: + pos, data, path_ = r + else: + pos, data = r + if data_only and not isinstance(data, (Rattr, Bptr)): + continue + + if path: + yield pos, data, path_ + else: + yield pos, data + + def _traverse(self, *, + path=False, + depth=None): + ptrunk_ = [] + for pos, data, path_ in self.leaves( + path=True, + depth=depth): + # we only care about the data/rbyds here + trunk_ = ([(bid_-rid_, rbyd_) + for bid_, rbyd_, rid_, name_ in path_] + + [(pos, data)]) + for d, (pos, data) in pathdelta( + trunk_, ptrunk_): + # but include branch rids in path if requested + if path: + yield pos, data, path_[:d] + else: + yield pos, data + ptrunk_ = trunk_ + + def traverse(self, *, + data_only=False, + path=False, + depth=None): + for r in self._traverse( + path=path, + depth=depth): + if path: + pos, data, path_ = r + else: + pos, data = r + if data_only and not isinstance(data, (Rattr, Bptr)): + continue + + if path: + yield pos, data, path_ + else: + yield pos, data + + def datas(self, *, + data_only=True, + path=False, + depth=None): + return self.leaves( + data_only=data_only, + path=path, + depth=depth) + + # some convience operations for reading data + def bytes(self, *, + depth=None): + for pos, data in self.datas(depth=depth): + if data.size > 0: + yield data.data + if data.weight > data.size: + yield b'\0' * (data.weight-data.size) + + def read(self, *, + depth=None): + return b''.join(self.bytes()) + + # bleh, with that out of the way, here are our known file types + + # regular files + class Reg(File): + tag = TAG_REG + + def repr(self): + return 'reg %s%s' % ( + self.size, + ', %s' % self.structrepr() + if self.struct is not None else '') + + # directories + class Dir(File): + tag = TAG_DIR + + def __init__(self, lfs, mid, mdir, tag, name): + super().__init__(lfs, mid, mdir, tag, name) + + # we're recursable if we're a non-grmed directory with a did + if (isinstance(self, Lfs3.Dir) + and not self.grmed + and self.did is not None): + self.recursable = True + + def repr(self): + return 'dir %s%s' % ( + '0x%x' % self.did + if self.did is not None else '?', + ', %s' % self.structrepr() + if self.struct is not None + and self.struct.tag != TAG_DID else '') + + # provide some convenient filesystem access relative to our did + def namelookup(self, name, **args): + if self.did is None: + return None + return self.lfs.namelookup(self.did, name, **args) + + def pathlookup(self, path_, **args): + if self.did is None: + if args.get('path'): + return None, [] + else: + return None + return self.lfs.pathlookup(self.did, path_, **args) + + def files(self, **args): + if self.did is None: + return iter(()) + return self.lfs.files(self.did, **args) + + # root is a bit special + class Root(Dir): + tag = None + + def __init__(self, lfs): + # root always has mid=-1 and did=0 + super().__init__(lfs, lfs.mid(-1), lfs.mroot, TAG_DIR, None) + self.did = 0 + self.recursable = True + + def repr(self): + return 'root' + + # bookmarks keep track of where directories start + class Bookmark(File): + tag = TAG_BOOKMARK + internal = True + + def repr(self): + return 'bookmark %s%s' % ( + '0x%x' % self.name.did + if self.name.did is not None else '?', + ', %s' % self.structrepr() + if self.struct is not None else '') + + # stickynotes, i.e. uncommitted files, behave the same as files + # for the most part + class Stickynote(File): + tag = TAG_STICKYNOTE + internal = True + + def repr(self): + return 'stickynote%s' % ( + ' %s, %s' % (self.size, self.structrepr()) + if self.struct is not None else '') + + # marker class for unknown file types + class Unknown(File): + pass + + # keep track of known file types + _known = [f for f in File.__subclasses__() if f.tag is not None] + + # fetch/parse state if known + def _open(self, mid, mdir, tag, name): + # known file type? + tag = name.tag + for f in self._known: + if (f.tag & ~(f.mask or 0)) == (tag & ~(f.mask or 0)): + return f(self, mid, mdir, tag, name) + # otherwise return a marker class + else: + return self.Unknown(self, mid, mdir, tag, name) + + + +# keep-open stuff +if inotify_simple is None: + Inotify = None +else: + class Inotify(inotify_simple.INotify): + def __init__(self, paths): + super().__init__() + + # wait for interesting events + flags = (inotify_simple.flags.ATTRIB + | inotify_simple.flags.CREATE + | inotify_simple.flags.DELETE + | inotify_simple.flags.DELETE_SELF + | inotify_simple.flags.MODIFY + | inotify_simple.flags.MOVED_FROM + | inotify_simple.flags.MOVED_TO + | inotify_simple.flags.MOVE_SELF) + + # recurse into directories + for path in paths: + if os.path.isdir(path): + for dir, _, files in os.walk(path): + self.add_watch(dir, flags) + for f in files: + self.add_watch(os.path.join(dir, f), flags) + else: + self.add_watch(path, flags) + +# a pseudo-stdout ring buffer +class RingIO: + def __init__(self, maxlen=None, head=False): + self.maxlen = maxlen + self.head = head + self.lines = co.deque( + maxlen=max(maxlen, 0) if maxlen is not None else None) + self.tail = io.StringIO() + + # trigger automatic sizing + self.resize(self.maxlen) + + @property + def width(self): + # just fetch this on demand, we don't actually use width + return shutil.get_terminal_size((80, 5))[0] + + @property + def height(self): + # calculate based on terminal height? + if self.maxlen is None or self.maxlen <= 0: + return max( + shutil.get_terminal_size((80, 5))[1] + + (self.maxlen or 0), + 0) + # limit to maxlen + else: + return self.maxlen + + def resize(self, maxlen): + self.maxlen = maxlen + if maxlen is not None and maxlen <= 0: + maxlen = self.height + if maxlen != self.lines.maxlen: + self.lines = co.deque(self.lines, maxlen=maxlen) + + def __len__(self): + return len(self.lines) + + def write(self, s): + # note using split here ensures the trailing string has no newline + lines = s.split('\n') + + if len(lines) > 1 and self.tail.getvalue(): + self.tail.write(lines[0]) + lines[0] = self.tail.getvalue() + self.tail = io.StringIO() + + self.lines.extend(lines[:-1]) + + if lines[-1]: + self.tail.write(lines[-1]) + + # keep track of maximum drawn canvas + canvas_lines = 1 + + def draw(self): + # did terminal size change? + self.resize(self.maxlen) + + # copy lines + lines = self.lines.copy() + # pad to fill any existing canvas, but truncate to terminal size + h = shutil.get_terminal_size((80, 5))[1] + lines.extend('' for _ in range( + len(lines), + min(RingIO.canvas_lines, h))) + while len(lines) > h: + if self.head: + lines.pop() + else: + lines.popleft() + + # build up the redraw in memory first and render in a single + # write call, this minimizes flickering caused by the cursor + # jumping around + canvas = [] + + # hide the cursor + canvas.append('\x1b[?25l') + + # give ourself a canvas + while RingIO.canvas_lines < len(lines): + canvas.append('\n') + RingIO.canvas_lines += 1 + + # write lines from top to bottom so later lines overwrite earlier + # lines, note xA/xB stop at terminal boundaries + for i, line in enumerate(lines): + # move to col 0 + canvas.append('\r') + # move up to line + if len(lines)-1-i > 0: + canvas.append('\x1b[%dA' % (len(lines)-1-i)) + # clear line + canvas.append('\x1b[K') + # disable line wrap + canvas.append('\x1b[?7l') + # print the line + canvas.append(line) + # enable line wrap + canvas.append('\x1b[?7h') # enable line wrap + # move back down + if len(lines)-1-i > 0: + canvas.append('\x1b[%dB' % (len(lines)-1-i)) + + # show the cursor again + canvas.append('\x1b[?25h') + + # write to stdout and flush + sys.stdout.write(''.join(canvas)) + sys.stdout.flush() + +# a representation of optionally key-mapped attrs +class CsvAttr: + def __init__(self, attrs, defaults=None): + if attrs is None: + attrs = [] + if isinstance(attrs, dict): + attrs = attrs.items() + + # normalize + self.attrs = [] + self.keyed = co.OrderedDict() + for attr in attrs: + if not isinstance(attr, tuple): + attr = ((), attr) + if attr[0] in {None, (), (None,), ('*',)}: + attr = ((), attr[1]) + if not isinstance(attr[0], tuple): + attr = ((attr[0],), attr[1]) + + self.attrs.append(attr) + if attr[0] not in self.keyed: + self.keyed[attr[0]] = [] + self.keyed[attr[0]].append(attr[1]) + + # create attrs object for defaults + if isinstance(defaults, CsvAttr): + self.defaults = defaults + elif defaults is not None: + self.defaults = CsvAttr(defaults) + else: + self.defaults = None + + def __repr__(self): + if self.defaults is None: + return 'CsvAttr(%r)' % ( + [(','.join(attr[0]), attr[1]) + for attr in self.attrs]) + else: + return 'CsvAttr(%r, %r)' % ( + [(','.join(attr[0]), attr[1]) + for attr in self.attrs], + [(','.join(attr[0]), attr[1]) + for attr in self.defaults.attrs]) + + def __iter__(self): + if () in self.keyed: + return it.cycle(self.keyed[()]) + elif self.defaults is not None: + return iter(self.defaults) + else: + return iter(()) + + def __bool__(self): + return bool(self.attrs) + + def __getitem__(self, key): + if isinstance(key, tuple): + if len(key) > 0 and not isinstance(key[0], str): + i, key = key + if not isinstance(key, tuple): + key = (key,) + else: + i, key = 0, key + elif isinstance(key, str): + i, key = 0, (key,) + else: + i, key = key, () + + # try to lookup by key + best = None + for ks, vs in self.keyed.items(): + prefix = [] + for j, k in enumerate(ks): + if j < len(key) and fnmatch.fnmatchcase(key[j], k): + prefix.append(k) + else: + prefix = None + break + + if prefix is not None and ( + best is None or len(prefix) >= len(best[0])): + best = (prefix, vs) + + if best is not None: + # cycle based on index + return best[1][i % len(best[1])] + + # fallback to defaults? + if self.defaults is not None: + return self.defaults[i, key] + + raise KeyError(i, key) + + def get(self, key, default=None): + try: + return self.__getitem__(key) + except KeyError: + return default + + def __contains__(self, key): + try: + self.__getitem__(key) + return True + except KeyError: + return False + + # get all results for a given key + def getall(self, key, default=None): + if not isinstance(key, tuple): + key = (key,) + + # try to lookup by key + best = None + for ks, vs in self.keyed.items(): + prefix = [] + for j, k in enumerate(ks): + if j < len(key) and fnmatch.fnmatchcase(key[j], k): + prefix.append(k) + else: + prefix = None + break + + if prefix is not None and ( + best is None or len(prefix) >= len(best[0])): + best = (prefix, vs) + + if best is not None: + return best[1] + + # fallback to defaults? + if self.defaults is not None: + return self.defaults.getall(key, default) + + raise default + + # a key function for sorting by key order + def key(self, key): + if not isinstance(key, tuple): + key = (key,) + + best = None + for i, ks in enumerate(self.keyed.keys()): + prefix = [] + for j, k in enumerate(ks): + if j < len(key) and (not k or key[j] == k): + prefix.append(k) + else: + prefix = None + break + + if prefix is not None and ( + best is None or len(prefix) >= len(best[0])): + best = (prefix, i) + + if best is not None: + return best[1] + + # fallback to defaults? + if self.defaults is not None: + return len(self.keyed) + self.defaults.key(key) + + return len(self.keyed) + +# SI-prefix formatter +def si(x): + if x == 0: + return '0' + # figure out prefix and scale + p = 3*mt.floor(mt.log(abs(x), 10**3)) + p = min(18, max(-18, p)) + # format with 3 digits of precision + s = '%.3f' % (abs(x) / (10.0**p)) + s = s[:3+1] + # truncate but only digits that follow the dot + if '.' in s: + s = s.rstrip('0') + s = s.rstrip('.') + return '%s%s%s' % ('-' if x < 0 else '', s, SI_PREFIXES[p]) + +# SI-prefix formatter for powers-of-two +def si2(x): + if x == 0: + return '0' + # figure out prefix and scale + p = 10*mt.floor(mt.log(abs(x), 2**10)) + p = min(30, max(-30, p)) + # format with 3 digits of precision + s = '%.3f' % (abs(x) / (2.0**p)) + s = s[:3+1] + # truncate but only digits that follow the dot + if '.' in s: + s = s.rstrip('0') + s = s.rstrip('.') + return '%s%s%s' % ('-' if x < 0 else '', s, SI2_PREFIXES[p]) + +# parse %-escaped strings +# +# attrs can override __getitem__ for lazy attr generation +def punescape(s, attrs=None): + pattern = re.compile( + '%[%n]' + '|' '%x..' + '|' '%u....' + '|' '%U........' + '|' '%\((?P[^)]*)\)' + '(?P[+\- #0-9\.]*[siIdboxXfFeEgG])') + def unescape(m): + if m.group()[1] == '%': return '%' + elif m.group()[1] == 'n': return '\n' + elif m.group()[1] == 'x': return chr(int(m.group()[2:], 16)) + elif m.group()[1] == 'u': return chr(int(m.group()[2:], 16)) + elif m.group()[1] == 'U': return chr(int(m.group()[2:], 16)) + elif m.group()[1] == '(': + if attrs is not None: + try: + v = attrs[m.group('field')] + except KeyError: + return m.group() + else: + return m.group() + f = m.group('format') + if f[-1] in 'dboxX': + if isinstance(v, str): + v = dat(v, 0) + v = int(v) + elif f[-1] in 'iIfFeEgG': + if isinstance(v, str): + v = dat(v, 0) + v = float(v) + if f[-1] in 'iI': + v = (si if 'i' in f[-1] else si2)(v) + f = f.replace('i', 's').replace('I', 's') + if '+' in f and not v.startswith('-'): + v = '+'+v + f = f.replace('+', '').replace('-', '') + else: + f = ('<' if '-' in f else '>') + f.replace('-', '') + v = str(v) + # note we need Python's new format syntax for binary + return ('{:%s}' % f).format(v) + else: assert False + + return re.sub(pattern, unescape, s) + +# split %-escaped strings into chars +def psplit(s): + pattern = re.compile( + '%[%n]' + '|' '%x..' + '|' '%u....' + '|' '%U........' + '|' '%\((?P[^)]*)\)' + '(?P[+\- #0-9\.]*[siIdboxXfFeEgG])') + return [m.group() for m in re.finditer(pattern.pattern + '|.', s)] + + +# a little ascii renderer +class Canvas: + def __init__(self, width, height, *, + color=False, + dots=False, + braille=False): + # scale if we're printing with dots or braille + if braille: + xscale, yscale = 2, 4 + elif dots: + xscale, yscale = 1, 2 + else: + xscale, yscale = 1, 1 + + self.width_ = width + self.height_ = height + self.width = xscale*width + self.height = yscale*height + self.xscale = xscale + self.yscale = yscale + self.color_ = color + self.dots = dots + self.braille = braille + + # create initial canvas + self.chars = [0] * (width*height) + self.colors = [''] * (width*height) + + def char(self, x, y, char=None): + # ignore out of bounds + if x < 0 or y < 0 or x >= self.width or y >= self.height: + return False + + x_ = x // self.xscale + y_ = y // self.yscale + if char is not None: + c = self.chars[x_ + y_*self.width_] + # mask in sub-char pixel? + if isinstance(char, bool): + if not isinstance(c, int): + c = 0 + self.chars[x_ + y_*self.width_] = (c + | (1 + << ((y%self.yscale)*self.xscale + + (self.xscale-1)-(x%self.xscale)))) + else: + self.chars[x_ + y_*self.width_] = char + else: + c = self.chars[x_ + y_*self.width_] + if isinstance(c, int): + return ((c + >> ((y%self.yscale)*self.xscale + + (self.xscale-1)-(x%self.xscale))) + & 1) == 1 + else: + return c + + def color(self, x, y, color=None): + # ignore out of bounds + if x < 0 or y < 0 or x >= self.width or y >= self.height: + return '' + + x_ = x // self.xscale + y_ = y // self.yscale + if color is not None: + self.colors[x_ + y_*self.width_] = color + else: + return self.colors[x_ + y_*self.width_] + + def __getitem__(self, xy): + x, y = xy + return self.char(x, y) + + def __setitem__(self, xy, char): + x, y = xy + self.char(x, y, char) + + def point(self, x, y, *, + char=True, + color=''): + self.char(x, y, char) + self.color(x, y, color) + + def line(self, x1, y1, x2, y2, *, + char=True, + color=''): + # incremental error line algorithm + ex = abs(x2 - x1) + ey = -abs(y2 - y1) + dx = +1 if x1 < x2 else -1 + dy = +1 if y1 < y2 else -1 + e = ex + ey + + while True: + self.point(x1, y1, char=char, color=color) + e2 = 2*e + + if x1 == x2 and y1 == y2: + break + + if e2 > ey: + e += ey + x1 += dx + + if x1 == x2 and y1 == y2: + break + + if e2 < ex: + e += ex + y1 += dy + + self.point(x2, y2, char=char, color=color) + + def rect(self, x, y, w, h, *, + char=True, + color=''): + for j in range(h): + for i in range(w): + self.point(x+i, y+j, char=char, color=color) + + def label(self, x, y, label, width=None, height=None, *, + color=''): + x_ = x + y_ = y + for char in label: + if char == '\n': + x_ = x + y_ -= self.yscale + else: + if ((width is None or x_ < x+width) + and (height is None or y_ > y-height)): + self.point(x_, y_, char=char, color=color) + x_ += self.xscale + + def draw(self, row): + y_ = self.height_-1 - row + row_ = [] + for x_ in range(self.width_): + # char? + c = self.chars[x_ + y_*self.width_] + if isinstance(c, int): + if self.braille: + assert c < 256 + c = CHARS_BRAILLE[c] + elif self.dots: + assert c < 4 + c = CHARS_DOTS[c] + else: + assert c < 2 + c = '.' if c else ' ' + + # color? + if self.color_: + color = self.colors[x_ + y_*self.width_] + if color: + c = '\x1b[%sm%s\x1b[m' % (color, c) + + row_.append(c) + + return ''.join(row_) + + +# naive space filling curve (the default) +def naive_curve(width, height): + for y in range(height): + for x in range(width): + yield x, y + +# space filling Hilbert-curve +def hilbert_curve(width, height): + # based on generalized Hilbert curves: + # https://github.com/jakubcerveny/gilbert + # + def hilbert_(x, y, a_x, a_y, b_x, b_y): + w = abs(a_x+a_y) + h = abs(b_x+b_y) + a_dx = -1 if a_x < 0 else +1 if a_x > 0 else 0 + a_dy = -1 if a_y < 0 else +1 if a_y > 0 else 0 + b_dx = -1 if b_x < 0 else +1 if b_x > 0 else 0 + b_dy = -1 if b_y < 0 else +1 if b_y > 0 else 0 + + # trivial row + if h == 1: + for _ in range(w): + yield x, y + x, y = x+a_dx, y+a_dy + return + + # trivial column + if w == 1: + for _ in range(h): + yield x, y + x, y = x+b_dx, y+b_dy + return + + a_x_, a_y_ = a_x//2, a_y//2 + b_x_, b_y_ = b_x//2, b_y//2 + w_ = abs(a_x_+a_y_) + h_ = abs(b_x_+b_y_) + + if 2*w > 3*h: + # prefer even steps + if w_ % 2 != 0 and w > 2: + a_x_, a_y_ = a_x_+a_dx, a_y_+a_dy + + # split in two + yield from hilbert_( + x, y, + a_x_, a_y_, b_x, b_y) + yield from hilbert_( + x+a_x_, y+a_y_, + a_x-a_x_, a_y-a_y_, b_x, b_y) + else: + # prefer even steps + if h_ % 2 != 0 and h > 2: + b_x_, b_y_ = b_x_+b_dx, b_y_+b_dy + + # split in three + yield from hilbert_( + x, y, + b_x_, b_y_, a_x_, a_y_) + yield from hilbert_( + x+b_x_, y+b_y_, + a_x, a_y, b_x-b_x_, b_y-b_y_) + yield from hilbert_( + x+(a_x-a_dx)+(b_x_-b_dx), y+(a_y-a_dy)+(b_y_-b_dy), + -b_x_, -b_y_, -(a_x-a_x_), -(a_y-a_y_)) + + if width >= height: + yield from hilbert_(0, 0, +width, 0, 0, +height) + else: + yield from hilbert_(0, 0, 0, +height, +width, 0) + +# space filling Z-curve/Lebesgue-curve +def lebesgue_curve(width, height): + # we create a truncated Z-curve by simply filtering out the + # points that are outside our region + for i in range(2**(2*mt.ceil(mt.log2(max(width, height))))): + # we just operate on binary strings here because it's easier + b = '{:0{}b}'.format(i, 2*mt.ceil(mt.log2(i+1)/2)) + x = int(b[1::2], 2) if b[1::2] else 0 + y = int(b[0::2], 2) if b[0::2] else 0 + if x < width and y < height: + yield x, y + + + +# an abstract block representation +class BmapBlock: + def __init__(self, block, type='unused', value=None, usage=range(0), *, + siblings=None, children=None, + x=None, y=None, width=None, height=None): + self.block = block + self.type = type + self.value = value + self.usage = usage + self.siblings = siblings if siblings is not None else set() + self.children = children if children is not None else set() + self.x = x + self.y = y + self.width = width + self.height = height + + def __repr__(self): + return 'BmapBlock(0x%x, %r, x=%s, y=%s, width=%s, height=%s)' % ( + self.block, + self.type, + self.x, self.y, self.width, self.height) + + def __eq__(self, other): + return self.block == other.block + + def __ne__(self, other): + return self.block != other.block + + def __hash__(self): + return hash(self.block) + + def __lt__(self, other): + return self.block < other.block + + def __le__(self, other): + return self.block <= other.block + + def __gt__(self, other): + return self.block > other.block + + def __ge__(self, other): + return self.block >= other.block + + # align to pixel boundaries + def align(self): + # this extra +0.1 and using points instead of width/height is + # to help minimize rounding errors + x0 = int(self.x+0.1) + y0 = int(self.y+0.1) + x1 = int(self.x+self.width+0.1) + y1 = int(self.y+self.height+0.1) + self.x = x0 + self.y = y0 + self.width = x1 - x0 + self.height = y1 - y0 + + # generate a label + @ft.cached_property + def label(self): + if self.type == 'mdir': + return '%s %s %s w%s\ncksum %08x' % ( + self.type, + self.value.mid.mbidrepr(), + self.value.addr(), + self.value.weight, + self.value.cksum) + elif self.type == 'btree': + return '%s %s w%s\ncksum %08x' % ( + self.type, + self.value.addr(), + self.value.weight, + self.value.cksum) + elif self.type == 'data': + return '%s %s %s\ncksize %s\ncksum %08x' % ( + self.type, + '0x%x.%x' % (self.block, self.value.off), + self.value.size, + self.value.cksize, + self.value.cksum) + elif self.type != 'unused': + return '%s\n%s' % ( + self.type, + '0x%x' % self.block) + else: + return '' + + # generate attrs for punescaping + @ft.cached_property + def attrs(self): + if self.type == 'mdir': + return { + 'block': self.block, + 'type': self.type, + 'addr': self.value.addr(), + 'trunk': self.value.trunk, + 'weight': self.value.weight, + 'cksum': self.value.cksum, + 'usage': len(self.usage), + } + elif self.type == 'btree': + return { + 'block': self.block, + 'type': self.type, + 'addr': self.value.addr(), + 'trunk': self.value.trunk, + 'weight': self.value.weight, + 'cksum': self.value.cksum, + 'usage': len(self.usage), + } + elif self.type == 'data': + return { + 'block': self.block, + 'type': self.type, + 'addr': self.value.addr(), + 'off': self.value.off, + 'size': self.value.size, + 'cksize': self.value.cksize, + 'cksum': self.value.cksum, + 'usage': len(self.usage), + } + else: + return { + 'block': self.block, + 'type': self.type, + 'usage': len(self.usage), + } + + +def main_(ring, disk, mroots=None, *, + trunk=None, + block_size=None, + block_count=None, + blocks=None, + no_ckmeta=False, + no_ckdata=False, + mtree_only=False, + chars=[], + colors=[], + color='auto', + dots=False, + braille=False, + width=None, + height=None, + block_cols=None, + block_rows=None, + block_ratio=None, + no_header=False, + hilbert=False, + lebesgue=False, + contiguous=False, + to_scale=None, + to_ratio=1/1, + tiny=False, + title=None, + title_littlefs=False, + title_usage=False, + **args): + # give ring a writeln function + def writeln(self, s=''): + self.write(s) + self.write('\n') + ring.writeln = writeln.__get__(ring) + + # figure out what color should be + if color == 'auto': + color = sys.stdout.isatty() + elif color == 'always': + color = True + else: + color = False + + # tiny mode? + if tiny: + if block_ratio is None: + block_ratio = 1 + if to_scale is None: + to_scale = 1 + no_header = True + + if block_ratio is None: + # try to align block_ratio to chars, even in braille/dots + # mode (we can't color sub-chars) + if braille or dots: + block_ratio = 1/2 + else: + block_ratio = 1 + + # what chars/colors/labels to use? + chars_ = [] + for char in chars: + if isinstance(char, tuple): + chars_.extend((char[0], c) for c in psplit(char[1])) + else: + chars_.extend(psplit(char)) + chars_ = CsvAttr(chars_, defaults=[True] if braille or dots else CHARS) + + colors_ = CsvAttr(colors, defaults=COLORS) + + # figure out width/height + if width is None: + width_ = min(80, shutil.get_terminal_size((80, 5))[0]) + elif width > 0: + width_ = width + else: + width_ = max(0, shutil.get_terminal_size((80, 5))[0] + width) + + if height is None: + height_ = 2 if not no_header else 1 + elif height > 0: + height_ = height + else: + height_ = max(0, shutil.get_terminal_size((80, 5))[1] + height) + + # is bd geometry specified? + if isinstance(block_size, tuple): + block_size, block_count_ = block_size + if block_count is None: + block_count = block_count_ + + # flatten mroots, default to 0x{0,1} + mroots = list(it.chain.from_iterable(mroots)) if mroots else [0, 1] + + # mroots may also encode trunks + mroots, trunk = ( + [block[0] if isinstance(block, tuple) + else block + for block in mroots], + trunk if trunk is not None + else ft.reduce( + lambda x, y: y, + (block[1] for block in mroots + if isinstance(block, tuple)), + None)) + + # we seek around a bunch, so just keep the disk open + with open(disk, 'rb') as f: + # if block_size is omitted, assume the block device is one big block + if block_size is None: + f.seek(0, os.SEEK_END) + block_size = f.tell() + + # fetch the filesystem + bd = Bd(f, block_size, block_count) + lfs = Lfs3.fetch(bd, mroots, trunk, + # don't bother to check things if we're not reporting errors + no_ck=not args.get('error_on_corrupt')) + corrupted = not bool(lfs) + + # if we can't figure out the block_count, guess + block_size_ = block_size + block_count_ = block_count + if block_count is None: + if lfs.config.geometry is not None: + block_count_ = lfs.config.geometry.block_count + else: + f.seek(0, os.SEEK_END) + block_count_ = mt.ceil(f.tell() / block_size) + + # flatten blocks, default to all blocks + blocks_ = list( + range(blocks.start or 0, blocks.stop or block_count_) + if isinstance(blocks, slice) + else range(blocks, blocks+1) + if blocks + else range(block_count_)) + + # traverse the filesystem and create a block map + bmap = {b: BmapBlock(b, 'unused') for b in blocks_} + mdir_count = 0 + btree_count = 0 + data_count = 0 + total_count = 0 + for child in lfs.traverse( + mtree_only=mtree_only): + # track each block in our window + for b in child.blocks: + if b not in bmap: + continue + + # mdir? + if isinstance(child, Mdir): + type = 'mdir' + if b in child.blocks[:1+child.redund]: + usage = range(child.eoff) + else: + usage = range(0) + mdir_count += 1 + total_count += 1 + + # btree node? + elif isinstance(child, Rbyd): + type = 'btree' + if b in child.blocks[:1+child.redund]: + usage = range(child.eoff) + else: + usage = range(0) + btree_count += 1 + total_count += 1 + + # bptr? + elif isinstance(child, Bptr): + type = 'data' + usage = range(child.off, child.off+child.size) + data_count += 1 + total_count += 1 + + else: + assert False, "%r?" % b + + # check for some common issues + + # block conflict? + # + # note we can't compare more than types due to different + # trunks, slicing, etc + if (b in bmap + and bmap[b].type != 'unused' + and bmap[b].type != type): + if bmap[b].type == 'conflict': + bmap[b].value.append(child) + else: + bmap[b] = BmapBlock(b, 'conflict', + [bmap[b].value, child], + range(block_size_)) + corrupted = True + + # corrupt metadata? + elif (not no_ckmeta + and isinstance(child, (Mdir, Rbyd)) + and not child): + bmap[b] = BmapBlock(b, 'corrupt', child, range(block_size_)) + corrupted = True + + # corrupt data? + elif (not no_ckdata + and isinstance(child, Bptr) + and not child): + bmap[b] = BmapBlock(b, 'corrupt', child, range(block_size_)) + corrupted = True + + # normal block + else: + bmap[b] = BmapBlock(b, type, child, usage) + + # one last thing, build a title + if title: + title_ = punescape(title, { + 'magic': 'littlefs%s' % ( + '' if lfs.ckmagic() else '?'), + 'version': 'v%s.%s' % ( + lfs.version.major if lfs.version is not None else '?', + lfs.version.minor if lfs.version is not None else '?'), + 'version_major': + lfs.version.major if lfs.version is not None else '?', + 'version_minor': + lfs.version.minor if lfs.version is not None else '?', + 'geometry': '%sx%s' % ( + lfs.block_size if lfs.block_size is not None else '?', + lfs.block_count if lfs.block_count is not None else '?'), + 'block_size': + lfs.block_size if lfs.block_size is not None else '?', + 'block_count': + lfs.block_count if lfs.block_count is not None else '?', + 'addr': lfs.addr(), + 'weight': 'w%s.%s' % (lfs.mbweightrepr(), lfs.mrweightrepr()), + 'mbweight': lfs.mbweightrepr(), + 'mrweight': lfs.mrweightrepr(), + 'rev': '%08x' % lfs.rev, + 'cksum': '%08x%s' % ( + lfs.cksum, + '' if lfs.ckgcksum() else '?'), + 'total': total_count, + 'total_percent': 100*total_count / max(len(bmap), 1), + 'mdir': mdir_count, + 'mdir_percent': 100*mdir_count / max(len(bmap), 1), + 'btree': btree_count, + 'btree_percent': 100*btree_count / max(len(bmap), 1), + 'data': data_count, + 'data_percent': 100*data_count / max(len(bmap), 1), + }) + elif title_littlefs: + title_ = ('littlefs%s v%s.%s %sx%s %s w%s.%s, ' + 'rev %08x, ' + 'cksum %08x%s' % ( + '' if lfs.ckmagic() else '?', + lfs.version.major if lfs.version is not None else '?', + lfs.version.minor if lfs.version is not None else '?', + lfs.block_size if lfs.block_size is not None else '?', + lfs.block_count if lfs.block_count is not None else '?', + lfs.addr(), + lfs.mbweightrepr(), lfs.mrweightrepr(), + lfs.rev, + lfs.cksum, + '' if lfs.ckgcksum() else '?')) + else: + title_ = ('bd %sx%s, %s mdir, %s btree, %s data' % ( + lfs.block_size if lfs.block_size is not None else '?', + lfs.block_count if lfs.block_count is not None else '?', + '%.1f%%' % (100*mdir_count / max(len(bmap), 1)), + '%.1f%%' % (100*btree_count / max(len(bmap), 1)), + '%.1f%%' % (100*data_count / max(len(bmap), 1)))) + + # scale width/height if requested + if (to_scale is not None + and (width is None or height is None)): + # don't include header in scale + width__ = width_ + height__ = height_ - (1 if not no_header else 0) + + # scale width only + if height is not None: + width__ = mt.ceil((len(bmap) * to_scale) / max(height__, 1)) + # scale height only + elif width is not None: + height__ = mt.ceil((len(bmap) * to_scale) / max(width__, 1)) + # scale based on aspect-ratio + else: + width__ = mt.ceil(mt.sqrt(len(bmap) * to_scale * to_ratio)) + height__ = mt.ceil((len(bmap) * to_scale) / max(width__, 1)) + + width_ = width__ + height_ = height__ + (1 if not no_header else 0) + + # create a canvas + canvas = Canvas( + width_, + height_ - (1 if not no_header else 0), + color=color, + dots=dots, + braille=braille) + + # these curves are expensive to calculate, so memoize these + if hilbert: + curve = ft.cache(lambda w, h: list(hilbert_curve(w, h))) + elif lebesgue: + curve = ft.cache(lambda w, h: list(lebesgue_curve(w, h))) + else: + curve = ft.cache(lambda w, h: list(naive_curve(w, h))) + + # if contiguous, compute the global curve + if contiguous: + global_block = min(bmap.keys(), default=0) + global_curve = list(curve(canvas.width, canvas.height)) + + # if blocky, figure out block sizes/locations + else: + # figure out block_cols_/block_rows_ + if block_cols is not None and block_rows is not None: + block_cols_ = block_cols + block_rows_ = block_rows + elif block_rows is not None: + block_cols_ = mt.ceil(len(bmap) / block_rows) + block_rows_ = block_rows + elif block_cols is not None: + block_cols_ = block_cols + block_rows_ = mt.ceil(len(bmap) / block_cols) + else: + # divide by 2 until we hit our target ratio, this works + # well for things that are often powers-of-two + # + # also prioritize rows at low resolution + block_cols_ = 1 + block_rows_ = len(bmap) + while (block_rows_ > canvas.height + or abs(((canvas.width/(block_cols_*2)) + / max(canvas.height/mt.ceil(block_rows_/2), 1)) + - block_ratio) + < abs(((canvas.width/block_cols_) + / max(canvas.height/block_rows_, 1))) + - block_ratio): + block_cols_ *= 2 + block_rows_ = mt.ceil(block_rows_ / 2) + + block_width_ = canvas.width / block_cols_ + block_height_ = canvas.height / block_rows_ + + # assign block locations based on block_rows_/block_cols_ and + # the requested space filling curve + for (x, y), b in zip( + curve(block_cols_, block_rows_), + sorted(bmap.values())): + b.x = x * block_width_ + b.y = y * block_height_ + b.width = block_width_ + b.height = block_height_ + + # align to pixel boundaries + b.align() + + # bump up to at least one pixel for every block, dont't + # worry about out-of-bounds, Canvas handles this for us + b.width = max(b.width, 1) + b.height = max(b.height, 1) + + # assign chars based on block type + for b in bmap.values(): + b.chars = {} + for type in [b.type] + (['unused'] if args.get('usage') else []): + char__ = chars_.get((b.block, (type, '0x%x' % b.block))) + if char__ is not None: + if isinstance(char__, str): + # don't punescape unless we have to + if '%' in char__: + char__ = punescape(char__, b.attrs) + char__ = char__[0] # limit to 1 char + b.chars[type] = char__ + + # assign colors based on block type + for b in bmap.values(): + b.colors = {} + for type in [b.type] + (['unused'] if args.get('usage') else []): + color__ = colors_.get((b.block, (type, '0x%x' % b.block))) + if color__ is not None: + # don't punescape unless we have to + if '%' in color__: + color__ = punescape(color__, b.attrs) + b.colors[type] = color__ + + # render to canvas in a specific z-order that prioritizes + # interesting blocks + for type in reversed(Z_ORDER): + # don't render unused blocks in braille/dots mode + if (braille or dots) and type == 'unused': + continue + + for b in bmap.values(): + # a bit of a hack, but render all blocks as unused + # in the first pass in usage mode + if args.get('usage') and type == 'unused': + type__ = 'unused' + usage__ = range(block_size_) + else: + type__ = b.type + usage__ = b.usage + + if type__ != type: + continue + + # contiguous? + if contiguous: + # where are we in the curve? + if args.get('usage'): + # skip blocks with no usage + if not usage__: + continue + block__ = b.block - global_block + usage__ = range( + mt.floor(((block__*block_size_ + usage__.start) + / (block_size_ * len(bmap))) + * len(global_curve)), + mt.ceil(((block__*block_size_ + usage__.stop) + / (block_size_ * len(bmap))) + * len(global_curve))) + else: + block__ = b.block - global_block + usage__ = range( + mt.floor((block__/len(bmap)) * len(global_curve)), + mt.ceil((block__/len(bmap)) * len(global_curve))) + + # map to global curve + for i in usage__: + if i >= len(global_curve): + continue + x__, y__ = global_curve[i] + + # flip y + y__ = canvas.height - (y__+1) + + canvas.point(x__, y__, + char=b.chars[type], + color=b.colors[type]) + + # blocky? + else: + x__ = b.x + y__ = b.y + width__ = b.width + height__ = b.height + + # flip y + y__ = canvas.height - (y__+height__) + + # render byte-level usage? + if args.get('usage'): + # skip blocks with no usage + if not usage__: + continue + # scale from bytes -> pixels + usage__ = range( + mt.floor((usage__.start/block_size_) + * (width__*height__)), + mt.ceil((usage__.stop/block_size_) + * (width__*height__))) + # map to in-block curve + for i, (dx, dy) in enumerate(curve(width__, height__)): + if i in usage__: + # flip y + canvas.point(x__+dx, y__+(height__-(dy+1)), + char=b.chars[type], + color=b.colors[type]) + + # render simple blocks + else: + canvas.rect(x__, y__, width__, height__, + char=b.chars[type], + color=b.colors[type]) + + # print some summary info + if not no_header: + ring.writeln(title_) + + # draw canvas + for row in range(canvas.height//canvas.yscale): + line = canvas.draw(row) + ring.writeln(line) + + if args.get('error_on_corrupt') and corrupted: + sys.exit(2) + + +def main(disk, mroots=None, *, + width=None, + height=None, + no_header=None, + keep_open=False, + lines=None, + head=False, + cat=False, + wait=False, + **args): + # keep-open? + if keep_open: + try: + # keep track of history if lines specified + if lines is not None: + ring = RingIO(lines+1 + if not no_header and lines > 0 + else lines) + while True: + # register inotify before running the command, this avoids + # modification race conditions + if Inotify: + inotify = Inotify([disk]) + + # cat? write directly to stdout + if cat: + main_(sys.stdout, disk, mroots, + width=width, + # make space for shell prompt + height=-1 if height is ... else height, + no_header=no_header, + **args) + # not cat? write to a bounded ring + else: + ring_ = RingIO(head=head) + main_(ring_, disk, mroots, + width=width, + height=0 if height is ... else height, + no_header=no_header, + **args) + # no history? draw immediately + if lines is None: + ring_.draw() + # history? merge with previous lines + else: + # write header separately? + if not no_header: + if not ring.lines: + ring.lines.append('') + ring.lines.extend(it.islice(ring_.lines, 1, None)) + ring.lines[0] = ring_.lines[0] + else: + ring.lines.extend(ring_.lines) + ring.draw() + + # try to inotifywait + if Inotify: + inotify.read() + inotify.close() + # sleep a minimum amount of time to avoid flickering + time.sleep(wait if wait is not None + else 2 if not Inotify + else 0.01) + except KeyboardInterrupt: + pass + + if not cat: + sys.stdout.write('\n') + + # single-pass? + else: + main_(sys.stdout, disk, mroots, + width=width, + # make space for shell prompt + height=-1 if height is ... else height, + no_header=no_header, + **args) + + +if __name__ == "__main__": + import argparse + import sys + parser = argparse.ArgumentParser( + description="Render currently used blocks in a littlefs image.", + allow_abbrev=False) + parser.add_argument( + 'disk', + help="File containing the block device.") + parser.add_argument( + 'mroots', + nargs='*', + type=rbydaddr, + help="Block address of the mroots. Defaults to 0x{0,1}.") + parser.add_argument( + '--trunk', + type=lambda x: int(x, 0), + help="Use this offset as the trunk of the mroots.") + parser.add_argument( + '-b', '--block-size', + type=bdgeom, + help="Block size/geometry in bytes. Accepts x.") + parser.add_argument( + '--block-count', + type=lambda x: int(x, 0), + help="Block count in blocks.") + parser.add_argument( + '-@', '--blocks', + type=lambda x: ( + slice(*(int(x, 0) if x.strip() else None + for x in x.split(',', 1))) + if ',' in x + else int(x, 0)), + help="Show a specific block, may be a range.") + parser.add_argument( + '--no-ckmeta', + action='store_true', + help="Don't check metadata blocks for errors.") + parser.add_argument( + '--no-ckdata', + action='store_true', + help="Don't check metadata + data blocks for errors.") + parser.add_argument( + '--mtree-only', + action='store_true', + help="Only traverse the mtree.") + # need a special Action here because this % causes problems + class StoreTrueUsage(argparse._StoreTrueAction): + def format_usage(self): + return '-%%' + parser.add_argument( + '-%', '--usage', + action=StoreTrueUsage, + help="Show how much of each block is in use.") + parser.add_argument( + '-.', '--add-char', '--chars', + dest='chars', + action='append', + type=lambda x: ( + lambda ks, v: ( + tuple(k.strip() for k in ks.split(',')), + v.strip()) + )(*x.split('=', 1)) + if '=' in x else x.strip(), + help="Add characters to use. Can be assigned to a specific " + "block type/block. Accepts %% modifiers.") + parser.add_argument( + '-C', '--add-color', + dest='colors', + action='append', + type=lambda x: ( + lambda ks, v: ( + tuple(k.strip() for k in ks.split(',')), + v.strip()) + )(*x.split('=', 1)) + if '=' in x else x.strip(), + help="Add a color to use. Can be assigned to a specific " + "block type/block. Accepts %% modifiers.") + parser.add_argument( + '--color', + choices=['never', 'always', 'auto'], + default='auto', + help="When to use terminal colors. Defaults to 'auto'.") + parser.add_argument( + '-:', '--dots', + action='store_true', + help="Use 1x2 ascii dot characters.") + parser.add_argument( + '-⣿', '--braille', + action='store_true', + help="Use 2x4 unicode braille characters. Note that braille " + "characters sometimes suffer from inconsistent widths.") + parser.add_argument( + '-W', '--width', + nargs='?', + type=lambda x: int(x, 0), + const=0, + help="Width in columns. <=0 uses the terminal width. Defaults " + "to min(terminal, 80).") + parser.add_argument( + '-H', '--height', + nargs='?', + type=lambda x: int(x, 0), + const=..., # handles shell prompt spacing, which is a bit subtle + help="Height in rows. <=0 uses the terminal height. Defaults " + "to 1.") + parser.add_argument( + '-X', '--block-cols', + type=lambda x: int(x, 0), + help="Number of blocks on the x-axis. Guesses from --block-count " + "and --block-ratio by default.") + parser.add_argument( + '-Y', '--block-rows', + type=lambda x: int(x, 0), + help="Number of blocks on the y-axis. Guesses from --block-count " + "and --block-ratio by default.") + parser.add_argument( + '--block-ratio', + dest='block_ratio', + type=lambda x: ( + (lambda a, b: a / b)(*(float(v) for v in x.split(':', 1))) + if ':' in x else float(x)), + help="Target ratio for block sizes. Defaults to 1:1 or 1:2 " + "for -:/--dots and -⣿/--braille.") + parser.add_argument( + '--no-header', + action='store_true', + help="Don't show the header.") + parser.add_argument( + '-U', '--hilbert', + action='store_true', + help="Render as a space-filling Hilbert curve.") + parser.add_argument( + '-Z', '--lebesgue', + action='store_true', + help="Render as a space-filling Z-curve.") + parser.add_argument( + '-u', '--contiguous', + action='store_true', + help="Render as one contiguous curve instead of organizing by " + "blocks first.") + parser.add_argument( + '--to-scale', + nargs='?', + type=lambda x: ( + (lambda a, b: a / b)(*(float(v) for v in x.split(':', 1))) + if ':' in x else float(x)), + const=1, + help="Scale the resulting map such that 1 char ~= 1/scale " + "blocks. Defaults to scale=1. ") + parser.add_argument( + '--to-ratio', + type=lambda x: ( + (lambda a, b: a / b)(*(float(v) for v in x.split(':', 1))) + if ':' in x else float(x)), + help="Aspect ratio to use with --to-scale. Defaults to 1:1.") + parser.add_argument( + '--tiny', + action='store_true', + help="Tiny mode, alias for --block-ratio=1, --to-scale=1, " + "and --no-header.") + parser.add_argument( + '--title', + help="Add a title. Accepts %% modifiers.") + parser.add_argument( + '--title-littlefs', + action='store_true', + help="Use the littlefs mount string as the title.") + parser.add_argument( + '--title-usage', + action='store_true', + help="Use the mdir/btree/data usage as the title. This is the " + "default.") + parser.add_argument( + '-e', '--error-on-corrupt', + action='store_true', + help="Error if the filesystem is corrupt.") + parser.add_argument( + '-k', '--keep-open', + action='store_true', + help="Continue to open and redraw the CSV files in a loop.") + parser.add_argument( + '-n', '--lines', + nargs='?', + type=lambda x: int(x, 0), + const=0, + help="Show this many lines of history. <=0 uses the terminal " + "height. Defaults to 1.") + parser.add_argument( + '-^', '--head', + action='store_true', + help="Show the first n lines.") + parser.add_argument( + '-c', '--cat', + action='store_true', + help="Pipe directly to stdout.") + parser.add_argument( + '-w', '--wait', + type=float, + help="Time in seconds to sleep between redraws when running " + "with -k. Defaults to 2 seconds.") + sys.exit(main(**{k: v + for k, v in vars(parser.parse_intermixed_args()).items() + if v is not None})) diff --git a/scripts/dbgbmapsvg.py b/scripts/dbgbmapsvg.py new file mode 100755 index 000000000..647bef85d --- /dev/null +++ b/scripts/dbgbmapsvg.py @@ -0,0 +1,5681 @@ +#!/usr/bin/env python3 +# +# Inspired by d3 and brendangregg's flamegraph svg: +# - https://d3js.org +# - https://github.com/brendangregg/FlameGraph +# + +# prevent local imports +if __name__ == "__main__": + __import__('sys').path.pop(0) + +import bisect +import collections as co +import fnmatch +import functools as ft +import itertools as it +import json +import math as mt +import os +import re +import shlex +import struct + +try: + import crc32c as crc32c_lib +except ModuleNotFoundError: + crc32c_lib = None + + + +# assign colors to specific filesystem objects +# +# some nicer colors borrowed from Seaborn +# note these include a non-opaque alpha +# +# COLORS = [ +# '#7995c4', # was '#4c72b0bf', # blue +# '#e6a37d', # was '#dd8452bf', # orange +# '#80be8e', # was '#55a868bf', # green +# '#d37a7d', # was '#c44e52bf', # red +# '#a195c6', # was '#8172b3bf', # purple +# '#ae9a88', # was '#937860bf', # brown +# '#e3a8d2', # was '#da8bc3bf', # pink +# '#a9a9a9', # was '#8c8c8cbf', # gray +# '#d9cb97', # was '#ccb974bf', # yellow +# '#8bc8da', # was '#64b5cdbf', # cyan +# ] +# COLORS_DARK = [ +# '#7997b7', # was '#a1c9f4bf', # blue +# '#bf8761', # was '#ffb482bf', # orange +# '#6aac79', # was '#8de5a1bf', # green +# '#bf7774', # was '#ff9f9bbf', # red +# '#9c8cbf', # was '#d0bbffbf', # purple +# '#a68c74', # was '#debb9bbf', # brown +# '#bb84ab', # was '#fab0e4bf', # pink +# '#9b9b9b', # was '#cfcfcfbf', # gray +# '#bfbe7a', # was '#fffea3bf', # yellow +# '#8bb5b4', # was '#b9f2f0bf', # cyan +# ] +# +COLORS = { + 'mdir': '#d9cb97', # was '#ccb974bf', # yellow + 'btree': '#7995c4', # was '#4c72b0bf', # blue + 'data': '#80be8e', # was '#55a868bf', # green + 'corrupt': '#d37a7d', # was '#c44e52bf', # red + 'conflict': '#d37a7d', # was '#c44e52bf', # red + 'unused': '#e5e5e5', # light gray +} +COLORS_DARK = { + 'mdir': '#bfbe7a', # was '#fffea3bf', # yellow + 'btree': '#7997b7', # was '#a1c9f4bf', # blue + 'data': '#6aac79', # was '#8de5a1bf', # green + 'corrupt': '#bf7774', # was '#ff9f9bbf', # red + 'conflict': '#bf7774', # was '#ff9f9bbf', # red + 'unused': '#333333', # dark gray +} + +WIDTH = 750 +HEIGHT = 350 +FONT = ['sans-serif'] +FONT_SIZE = 10 + +SI_PREFIXES = { + 18: 'E', + 15: 'P', + 12: 'T', + 9: 'G', + 6: 'M', + 3: 'K', + 0: '', + -3: 'm', + -6: 'u', + -9: 'n', + -12: 'p', + -15: 'f', + -18: 'a', +} + +SI2_PREFIXES = { + 60: 'Ei', + 50: 'Pi', + 40: 'Ti', + 30: 'Gi', + 20: 'Mi', + 10: 'Ki', + 0: '', + -10: 'mi', + -20: 'ui', + -30: 'ni', + -40: 'pi', + -50: 'fi', + -60: 'ai', +} + + + +RCOMPAT_NONSTANDARD = 0x00000001 # Non-standard filesystem format +RCOMPAT_WRONLY = 0x00000004 # Reading is disallowed +RCOMPAT_MMOSS = 0x00000010 # May use an inlined mdir +RCOMPAT_MSPROUT = 0x00000020 # May use an mdir pointer +RCOMPAT_MSHRUB = 0x00000040 # May use an inlined mtree +RCOMPAT_MTREE = 0x00000080 # May use an mdir btree +RCOMPAT_BMOSS = 0x00000100 # Files may use inlined data +RCOMPAT_BSPROUT = 0x00000200 # Files may use block pointers +RCOMPAT_BSHRUB = 0x00000400 # Files may use inlined btrees +RCOMPAT_BTREE = 0x00000800 # Files may use btrees +RCOMPAT_GRM = 0x00010000 # Global-remove in use + +WCOMPAT_NONSTANDARD = 0x00000001 # Non-standard filesystem format +WCOMPAT_RDONLY = 0x00000002 # Writing is disallowed +WCOMPAT_GCKSUM = 0x00040000 # Global-checksum in use +WCOMPAT_GBMAP = 0x00080000 # Global on-disk block-map in use +WCOMPAT_DIR = 0x01000000 # Directory file types in use + +TAG_NULL = 0x0000 ## v--- ---- +--- ---- +TAG_INTERNAL = 0x0000 ## v--- ---- +ttt tttt +TAG_CONFIG = 0x0100 ## v--- ---1 +ttt tttt +TAG_MAGIC = 0x0131 # v--- ---1 +-11 --rr +TAG_VERSION = 0x0134 # v--- ---1 +-11 -1-- +TAG_RCOMPAT = 0x0135 # v--- ---1 +-11 -1-1 +TAG_WCOMPAT = 0x0136 # v--- ---1 +-11 -11- +TAG_OCOMPAT = 0x0137 # v--- ---1 +-11 -111 +TAG_GEOMETRY = 0x0138 # v--- ---1 +-11 1--- +TAG_NAMELIMIT = 0x0139 # v--- ---1 +-11 1--1 +TAG_FILELIMIT = 0x013a # v--- ---1 +-11 1-1- +TAG_GDELTA = 0x0200 ## v--- --1- +ttt tttt +TAG_GRMDELTA = 0x0230 # v--- --1- +-11 --++ +TAG_GBMAPDELTA = 0x0234 # v--- --1- +-11 -1rr +TAG_NAME = 0x0300 ## v--- --11 +ttt tttt +TAG_BNAME = 0x0300 # v--- --11 +--- ---- +TAG_REG = 0x0301 # v--- --11 +--- ---1 +TAG_DIR = 0x0302 # v--- --11 +--- --1- +TAG_STICKYNOTE = 0x0303 # v--- --11 +--- --11 +TAG_BOOKMARK = 0x0304 # v--- --11 +--- -1-- +TAG_MNAME = 0x0330 # v--- --11 +-11 ---- +TAG_STRUCT = 0x0400 ## v--- -1-- +ttt tttt +TAG_BRANCH = 0x0400 # v--- -1-- +--- --rr +TAG_DATA = 0x0404 # v--- -1-- +--- -1rr +TAG_BLOCK = 0x0408 # v--- -1-- +--- 1err +TAG_DID = 0x0420 # v--- -1-- +-1- ---- +TAG_BSHRUB = 0x0428 # v--- -1-- +-1- 1-rr +TAG_BTREE = 0x042c # v--- -1-- +-1- 11rr +TAG_MROOT = 0x0431 # v--- -1-- +-11 --rr +TAG_MDIR = 0x0435 # v--- -1-- +-11 -1rr +TAG_MTREE = 0x043c # v--- -1-- +-11 11rr +TAG_BMRANGE = 0x0440 # v--- -1-- +1-- ++uu +TAG_BMFREE = 0x0440 # v--- -1-- +1-- ---- +TAG_BMINUSE = 0x0441 # v--- -1-- +1-- ---1 +TAG_BMERASED = 0x0442 # v--- -1-- +1-- --1- +TAG_BMBAD = 0x0443 # v--- -1-- +1-- --11 +TAG_ATTR = 0x0600 ## v--- -11a +aaa aaaa +TAG_UATTR = 0x0600 # v--- -11- +aaa aaaa +TAG_SATTR = 0x0700 # v--- -111 +aaa aaaa +TAG_SHRUB = 0x1000 ## v--1 kkkk +kkk kkkk +TAG_ALT = 0x4000 ## v1cd kkkk +kkk kkkk +TAG_B = 0x0000 +TAG_R = 0x2000 +TAG_LE = 0x0000 +TAG_GT = 0x1000 +TAG_CKSUM = 0x3000 ## v-11 ---- ++++ +pqq +TAG_PHASE = 0x0003 +TAG_PERTURB = 0x0004 +TAG_NOTE = 0x3100 ## v-11 ---1 ++++ ++++ +TAG_ECKSUM = 0x3200 ## v-11 --1- ++++ ++++ +TAG_GCKSUMDELTA = 0x3300 ## v-11 --11 ++++ ++++ + + +# self-parsing tag repr +class Tag: + def __init__(self, name, tag, encoding, help): + self.name = name + self.tag = tag + self.encoding = encoding + self.help = help + # derive mask from encoding + self.mask = sum( + (1 if x in 'v-01' else 0) << len(self.encoding)-1-i + for i, x in enumerate(self.encoding)) + + def __repr__(self): + return 'Tag(%r, %r, %r)' % ( + self.name, + self.tag, + self.encoding) + + def __eq__(self, other): + return self.name == other.name + + def __ne__(self, other): + return self.name != other.name + + def __hash__(self): + return hash(self.name) + + def line(self): + # substitute mask chars when zero + tag = '0x%s' % ''.join( + n if n != '0' else next( + (x for x in self.encoding[i*4:i*4+4] + if x not in 'v-01+'), + '0') + for i, n in enumerate('%04x' % self.tag)) + # group into nibbles + encoding = ' '.join(self.encoding[i*4:i*4+4] + for i in range(len(self.encoding)//4)) + return ('LFS3_%s' % self.name, tag, encoding) + + def specificity(self): + return sum(1 for x in self.encoding if x in 'v-01') + + def matches(self, tag): + return (tag & self.mask) == (self.tag & self.mask) + + def get(self, chars, tag): + return sum( + tag & ((1 if x in chars else 0) << len(self.encoding)-1-i) + for i, x in enumerate(self.encoding)) + + def max(self, chars): + return max(len(self.encoding)-1-i + for i, x in enumerate(self.encoding) if x in chars) + + def min(self, chars): + return min(len(self.encoding)-1-i + for i, x in enumerate(self.encoding) if x in chars) + + def width(self, chars): + return self.max(chars) - self.min(chars) + + def __contains__(self, chars): + return any(x in self.encoding for x in chars) + + @staticmethod + @ft.cache + def tags(): + # parse our script's source to figure out tags + import inspect + import re + tags = [] + tag_pattern = re.compile( + '^(?PTAG_[^ ]*) *= *(?P[^#]*?) *' + '#+ *(?P(?:[^ ] *?){16}) *(?P.*)$') + for line in (inspect.getsource( + inspect.getmodule(inspect.currentframe())) + .replace('\\\n', '') + .splitlines()): + m = tag_pattern.match(line) + if m: + tags.append(Tag( + m.group('name'), + globals()[m.group('name')], + m.group('encoding').replace(' ', ''), + m.group('help'))) + return tags + + # find best matching tag + @staticmethod + def find(tag): + # find tags, note this is cached + tags__ = Tag.tags() + + # find the most specific matching tag, ignoring valid bits + return max((t for t in tags__ if t.matches(tag & 0x7fff)), + key=lambda t: t.specificity(), + default=None) + + # human readable tag repr + @staticmethod + def repr(tag, weight=None, size=None, *, + global_=False, + toff=None): + # find the most specific matching tag, ignoring the shrub bit + t = Tag.find(tag & ~(TAG_SHRUB if tag & 0x7000 == TAG_SHRUB else 0)) + + # build repr + r = [] + # normal tag? + if not tag & TAG_ALT: + if t is not None: + # prefix shrub tags with shrub + if tag & 0x7000 == TAG_SHRUB: + r.append('shrub') + # lowercase name + r.append(t.name.split('_', 1)[1].lower()) + # gstate tag? + if global_: + if r[-1] == 'gdelta': + r[-1] = 'gstate' + elif r[-1].endswith('delta'): + r[-1] = r[-1][:-len('delta')] + # include perturb/phase bits + if 'q' in t: + r.append('q%d' % t.get('q', tag)) + if 'p' in t and tag & TAG_PERTURB: + r.append('p') + + # include unmatched fields, but not just redund, and + # only reserved bits if non-zero + if 'tua' in t or ('+' in t and t.get('+', tag) != 0): + r.append(' 0x%0*x' % ( + (t.width('tuar+')+4-1)//4, + t.get('tuar+', tag))) + # unknown tag? + else: + r.append('0x%04x' % tag) + + # weight? + if weight: + r.append(' w%d' % weight) + # size? don't include if null + if size is not None and (size or tag & 0x7fff): + r.append(' %d' % size) + + # alt pointer? + else: + r.append('alt') + r.append('r' if tag & TAG_R else 'b') + r.append('gt' if tag & TAG_GT else 'le') + r.append(' 0x%0*x' % ( + (t.width('k')+4-1)//4, + t.get('k', tag))) + + # weight? + if weight is not None: + r.append(' w%d' % weight) + # jump? + if size and toff is not None: + r.append(' 0x%x' % (0xffffffff & (toff-size))) + elif size: + r.append(' -%d' % size) + + return ''.join(r) + + +# open with '-' for stdin/stdout +def openio(path, mode='r', buffering=-1): + import os + if path == '-': + if 'r' in mode: + return os.fdopen(os.dup(sys.stdin.fileno()), mode, buffering) + else: + return os.fdopen(os.dup(sys.stdout.fileno()), mode, buffering) + else: + return open(path, mode, buffering) + +# some ways of block geometry representations +# 512 -> 512 +# 512x16 -> (512, 16) +# 0x200x10 -> (512, 16) +def bdgeom(s): + s = s.strip() + b = 10 + if s.startswith('0x') or s.startswith('0X'): + s = s[2:] + b = 16 + elif s.startswith('0o') or s.startswith('0O'): + s = s[2:] + b = 8 + elif s.startswith('0b') or s.startswith('0B'): + s = s[2:] + b = 2 + + if 'x' in s: + s, s_ = s.split('x', 1) + return (int(s, b), int(s_, b)) + else: + return int(s, b) + +# parse some rbyd addr encodings +# 0xa -> (0xa,) +# 0xa.c -> ((0xa, 0xc),) +# 0x{a,b} -> (0xa, 0xb) +# 0x{a,b}.c -> ((0xa, 0xc), (0xb, 0xc)) +def rbydaddr(s): + s = s.strip() + b = 10 + if s.startswith('0x') or s.startswith('0X'): + s = s[2:] + b = 16 + elif s.startswith('0o') or s.startswith('0O'): + s = s[2:] + b = 8 + elif s.startswith('0b') or s.startswith('0B'): + s = s[2:] + b = 2 + + trunk = None + if '.' in s: + s, s_ = s.split('.', 1) + trunk = int(s_, b) + + if s.startswith('{') and '}' in s: + ss = s[1:s.find('}')].split(',') + else: + ss = [s] + + addr = [] + for s in ss: + if trunk is not None: + addr.append((int(s, b), trunk)) + else: + addr.append(int(s, b)) + + return tuple(addr) + +def crc32c(data, crc=0): + if crc32c_lib is not None: + return crc32c_lib.crc32c(data, crc) + else: + crc ^= 0xffffffff + for b in data: + crc ^= b + for j in range(8): + crc = (crc >> 1) ^ ((crc & 1) * 0x82f63b78) + return 0xffffffff ^ crc + +def pmul(a, b): + r = 0 + while b: + if b & 1: + r ^= a + a <<= 1 + b >>= 1 + return r + +def crc32cmul(a, b): + r = pmul(a, b) + for _ in range(31): + r = (r >> 1) ^ ((r & 1) * 0x82f63b78) + return r + +def crc32ccube(a): + return crc32cmul(crc32cmul(a, a), a) + +def popc(x): + return bin(x).count('1') + +def parity(x): + return popc(x) & 1 + +def fromle32(data, j=0): + return struct.unpack('H', data[j:j+2].ljust(2, b'\0'))[0]; d += 2 + weight, d_ = fromleb128(data, j+d); d += d_ + size, d_ = fromleb128(data, j+d); d += d_ + return tag>>15, tag&0x7fff, weight, size, d + +def frombranch(data, j=0): + d = 0 + block, d_ = fromleb128(data, j+d); d += d_ + trunk, d_ = fromleb128(data, j+d); d += d_ + cksum = fromle32(data, j+d); d += 4 + return block, trunk, cksum, d + +def frombtree(data, j=0): + d = 0 + w, d_ = fromleb128(data, j+d); d += d_ + block, trunk, cksum, d_ = frombranch(data, j+d); d += d_ + return w, block, trunk, cksum, d + +def frommdir(data, j=0): + blocks = [] + d = 0 + while j+d < len(data): + block, d_ = fromleb128(data, j+d) + blocks.append(block) + d += d_ + return tuple(blocks), d + +def fromshrub(data, j=0): + d = 0 + weight, d_ = fromleb128(data, j+d); d += d_ + trunk, d_ = fromleb128(data, j+d); d += d_ + return weight, trunk, d + +def frombptr(data, j=0): + d = 0 + size, d_ = fromleb128(data, j+d); d += d_ + block, d_ = fromleb128(data, j+d); d += d_ + off, d_ = fromleb128(data, j+d); d += d_ + cksize, d_ = fromleb128(data, j+d); d += d_ + cksum = fromle32(data, j+d); d += 4 + return size, block, off, cksize, cksum, d + +def xxd(data, width=16): + for i in range(0, len(data), width): + yield '%-*s %-*s' % ( + 3*width, + ' '.join('%02x' % b for b in data[i:i+width]), + width, + ''.join( + b if b >= ' ' and b <= '~' else '.' + for b in map(chr, data[i:i+width]))) + +# compute the difference between two paths, returning everything +# in a after the paths diverge, as well as the relevant index +def pathdelta(a, b): + if not isinstance(a, list): + a = list(a) + i = 0 + for a_, b_ in zip(a, b): + try: + if type(a_) == type(b_) and a_ == b_: + i += 1 + else: + break + # treat exceptions here as failure to match, most likely + # the compared types are incompatible, it's the caller's + # problem + except Exception: + break + + return [(i+j, a_) for j, a_ in enumerate(a[i:])] + + +# a simple wrapper over an open file with bd geometry +class Bd: + def __init__(self, f, block_size=None, block_count=None): + self.f = f + self.block_size = block_size + self.block_count = block_count + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, self.repr()) + + def repr(self): + return 'bd %sx%s' % (self.block_size, self.block_count) + + def read(self, block, off, size): + self.f.seek(block*self.block_size + off) + return self.f.read(size) + + def readblock(self, block): + self.f.seek(block*self.block_size) + return self.f.read(self.block_size) + +# tagged data in an rbyd +class Rattr: + def __init__(self, tag, weight, blocks, toff, tdata, data): + self.tag = tag + self.weight = weight + if isinstance(blocks, int): + self.blocks = (blocks,) + else: + self.blocks = blocks + self.toff = toff + self.tdata = tdata + self.data = data + + @property + def block(self): + return self.blocks[0] + + @property + def tsize(self): + return len(self.tdata) + + @property + def off(self): + return self.toff + len(self.tdata) + + @property + def size(self): + return len(self.data) + + def __bytes__(self): + return self.data + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, self.repr()) + + def repr(self): + return Tag.repr(self.tag, self.weight, self.size) + + def __iter__(self): + return iter((self.tag, self.weight, self.data)) + + def __eq__(self, other): + return ((self.tag, self.weight, self.data) + == (other.tag, other.weight, other.data)) + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash((self.tag, self.weight, self.data)) + + # convenience for did/name access + def _parse_name(self): + # note we return a null name for non-name tags, this is so + # vestigial names in btree nodes act as a catch-all + if (self.tag & 0xff00) != TAG_NAME: + did = 0 + name = b'' + else: + did, d = fromleb128(self.data) + name = self.data[d:] + + # cache both + self.did = did + self.name = name + + @ft.cached_property + def did(self): + self._parse_name() + return self.did + + @ft.cached_property + def name(self): + self._parse_name() + return self.name + +class Ralt: + def __init__(self, tag, weight, blocks, toff, tdata, jump, + color=None, followed=None): + self.tag = tag + self.weight = weight + if isinstance(blocks, int): + self.blocks = (blocks,) + else: + self.blocks = blocks + self.toff = toff + self.tdata = tdata + self.jump = jump + + if color is not None: + self.color = color + else: + self.color = 'r' if tag & TAG_R else 'b' + self.followed = followed + + @property + def block(self): + return self.blocks[0] + + @property + def tsize(self): + return len(self.tdata) + + @property + def off(self): + return self.toff + len(self.tdata) + + @property + def joff(self): + return self.toff - self.jump + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, self.repr()) + + def repr(self): + return Tag.repr(self.tag, self.weight, self.jump, toff=self.toff) + + def __iter__(self): + return iter((self.tag, self.weight, self.jump)) + + def __eq__(self, other): + return ((self.tag, self.weight, self.jump) + == (other.tag, other.weight, other.jump)) + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash((self.tag, self.weight, self.jump)) + + +# our core rbyd type +class Rbyd: + def __init__(self, blocks, trunk, weight, rev, eoff, cksum, data, *, + shrub=False, + gcksumdelta=None, + redund=0): + if isinstance(blocks, int): + self.blocks = (blocks,) + else: + self.blocks = blocks + self.trunk = trunk + self.weight = weight + self.rev = rev + self.eoff = eoff + self.cksum = cksum + self.data = data + + self.shrub = shrub + self.gcksumdelta = gcksumdelta + self.redund = redund + + @property + def block(self): + return self.blocks[0] + + @property + def corrupt(self): + # use redund=-1 to indicate corrupt rbyds + return self.redund >= 0 + + def addr(self): + if len(self.blocks) == 1: + return '0x%x.%x' % (self.block, self.trunk) + else: + return '0x{%s}.%x' % ( + ','.join('%x' % block for block in self.blocks), + self.trunk) + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, self.repr()) + + def repr(self): + return 'rbyd %s w%s' % (self.addr(), self.weight) + + def __bool__(self): + # use redund=-1 to indicate corrupt rbyds + return self.redund >= 0 + + def __eq__(self, other): + return ((frozenset(self.blocks), self.trunk) + == (frozenset(other.blocks), other.trunk)) + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash((frozenset(self.blocks), self.trunk)) + + @classmethod + def _fetch(cls, data, block, trunk=None): + # fetch the rbyd + rev = fromle32(data, 0) + cksum = 0 + cksum_ = crc32c(data[0:4]) + cksum__ = cksum_ + perturb = False + eoff = 0 + eoff_ = None + j_ = 4 + trunk_ = 0 + trunk__ = 0 + trunk___ = 0 + weight = 0 + weight_ = 0 + weight__ = 0 + gcksumdelta = None + gcksumdelta_ = None + while j_ < len(data) and (not trunk or eoff <= trunk): + # read next tag + v, tag, w, size, d = fromtag(data, j_) + if v != parity(cksum__): + break + cksum__ ^= 0x00000080 if v else 0 + cksum__ = crc32c(data[j_:j_+d], cksum__) + j_ += d + if not tag & TAG_ALT and j_ + size > len(data): + break + + # take care of cksums + if not tag & TAG_ALT: + if (tag & 0xff00) != TAG_CKSUM: + cksum__ = crc32c(data[j_:j_+size], cksum__) + + # found a gcksumdelta? + if (tag & 0xff00) == TAG_GCKSUMDELTA: + gcksumdelta_ = Rattr(tag, w, block, j_-d, + data[j_-d:j_], + data[j_:j_+size]) + + # found a cksum? + else: + # check cksum + cksum___ = fromle32(data, j_) + if cksum__ != cksum___: + break + # commit what we have + eoff = eoff_ if eoff_ else j_ + size + cksum = cksum_ + trunk_ = trunk__ + weight = weight_ + gcksumdelta = gcksumdelta_ + gcksumdelta_ = None + # update perturb bit + perturb = bool(tag & TAG_PERTURB) + # revert to data cksum and perturb + cksum__ = cksum_ ^ (0xfca42daf if perturb else 0) + + # evaluate trunks + if (tag & 0xf000) != TAG_CKSUM: + if not (trunk and j_-d > trunk and not trunk___): + # new trunk? + if not trunk___: + trunk___ = j_-d + weight__ = 0 + + # keep track of weight + weight__ += w + + # end of trunk? + if not tag & TAG_ALT: + # update trunk/weight unless we found a shrub or an + # explicit trunk (which may be a shrub) is requested + if not tag & TAG_SHRUB or trunk___ == trunk: + trunk__ = trunk___ + weight_ = weight__ + # keep track of eoff for best matching trunk + if trunk and j_ + size > trunk: + eoff_ = j_ + size + eoff = eoff_ + cksum = cksum__ ^ ( + 0xfca42daf if perturb else 0) + trunk_ = trunk__ + weight = weight_ + gcksumdelta = gcksumdelta_ + trunk___ = 0 + + # update canonical checksum, xoring out any perturb state + cksum_ = cksum__ ^ (0xfca42daf if perturb else 0) + + if not tag & TAG_ALT: + j_ += size + + return cls(block, trunk_, weight, rev, eoff, cksum, data, + gcksumdelta=gcksumdelta, + redund=0 if trunk_ else -1) + + @classmethod + def fetch(cls, bd, blocks, trunk=None): + # multiple blocks? + if not isinstance(blocks, int): + # fetch all blocks + rbyds = [cls.fetch(bd, block, trunk) for block in blocks] + + # determine most recent revision/trunk + rev, trunk = None, None + for rbyd in rbyds: + # compare with sequence arithmetic + if rbyd and ( + rev is None + or not ((rbyd.rev - rev) & 0x80000000) + or (rbyd.rev == rev and rbyd.trunk > trunk)): + rev, trunk = rbyd.rev, rbyd.trunk + # sort for reproducibility + rbyds.sort(key=lambda rbyd: ( + # prioritize valid redund blocks + 0 if rbyd and rbyd.rev == rev and rbyd.trunk == trunk + else 1, + # default to sorting by block + rbyd.block)) + + # choose an active rbyd + rbyd = rbyds[0] + # keep track of the other blocks + rbyd.blocks = tuple(rbyd.block for rbyd in rbyds) + # keep track of how many redund blocks are valid + rbyd.redund = -1 + sum(1 for rbyd in rbyds + if rbyd and rbyd.rev == rev and rbyd.trunk == trunk) + # and patch the gcksumdelta if we have one + if rbyd.gcksumdelta is not None: + rbyd.gcksumdelta.blocks = rbyd.blocks + return rbyd + + # seek/read the block + block = blocks + data = bd.readblock(block) + + # fetch the rbyd + return cls._fetch(data, block, trunk) + + @classmethod + def fetchck(cls, bd, blocks, trunk, weight, cksum): + # try to fetch the rbyd normally + rbyd = cls.fetch(bd, blocks, trunk) + + # cksum mismatch? trunk/weight mismatch? + if (rbyd.cksum != cksum + or rbyd.trunk != trunk + or rbyd.weight != weight): + # mark as corrupt and keep track of expected trunk/weight + rbyd.redund = -1 + rbyd.trunk = trunk + rbyd.weight = weight + + return rbyd + + @classmethod + def fetchshrub(cls, rbyd, trunk): + # steal the original rbyd's data + # + # this helps avoid race conditions with cksums and stuff + shrub = cls._fetch(rbyd.data, rbyd.block, trunk) + shrub.blocks = rbyd.blocks + shrub.shrub = True + return shrub + + def lookupnext(self, rid, tag=None, *, + path=False): + if not self or rid >= self.weight: + if path: + return None, None, [] + else: + return None, None + + tag = max(tag or 0, 0x1) + lower = 0 + upper = self.weight + path_ = [] + + # descend down tree + j = self.trunk + while True: + _, alt, w, jump, d = fromtag(self.data, j) + + # found an alt? + if alt & TAG_ALT: + # follow? + if ((rid, tag & 0xfff) > (upper-w-1, alt & 0xfff) + if alt & TAG_GT + else ((rid, tag & 0xfff) + <= (lower+w-1, alt & 0xfff))): + lower += upper-lower-w if alt & TAG_GT else 0 + upper -= upper-lower-w if not alt & TAG_GT else 0 + j = j - jump + + if path: + # figure out which color + if alt & TAG_R: + _, nalt, _, _, _ = fromtag(self.data, j+jump+d) + if nalt & TAG_R: + color = 'y' + else: + color = 'r' + else: + color = 'b' + + path_.append(Ralt( + alt, w, self.blocks, j+jump, + self.data[j+jump:j+jump+d], jump, + color=color, + followed=True)) + + # stay on path + else: + lower += w if not alt & TAG_GT else 0 + upper -= w if alt & TAG_GT else 0 + j = j + d + + if path: + # figure out which color + if alt & TAG_R: + _, nalt, _, _, _ = fromtag(self.data, j) + if nalt & TAG_R: + color = 'y' + else: + color = 'r' + else: + color = 'b' + + path_.append(Ralt( + alt, w, self.blocks, j-d, + self.data[j-d:j], jump, + color=color, + followed=False)) + + # found tag + else: + rid_ = upper-1 + tag_ = alt + w_ = upper-lower + + if not tag_ or (rid_, tag_) < (rid, tag): + if path: + return None, None, path_ + else: + return None, None + + rattr_ = Rattr(tag_, w_, self.blocks, j, + self.data[j:j+d], + self.data[j+d:j+d+jump]) + if path: + return rid_, rattr_, path_ + else: + return rid_, rattr_ + + def lookup(self, rid, tag=None, mask=None, *, + path=False): + if tag is None: + tag, mask = 0, 0xffff + if mask is None: + mask = 0 + + r = self.lookupnext(rid, tag & ~mask, + path=path) + if path: + rid_, rattr_, path_ = r + else: + rid_, rattr_ = r + if (rid_ is None + or rid_ != rid + or (rattr_.tag & ~mask & 0xfff) + != (tag & ~mask & 0xfff)): + if path: + return None, path_ + else: + return None + + if path: + return rattr_, path_ + else: + return rattr_ + + def rids(self, *, + path=False): + rid = -1 + while True: + r = self.lookupnext(rid, + path=path) + if path: + rid, name, path_ = r + else: + rid, name = r + # found end of tree? + if rid is None: + break + + if path: + yield rid, name, path_ + else: + yield rid, name + rid += 1 + + def rattrs(self, rid=None, tag=None, mask=None, *, + path=False): + if rid is None: + rid, tag = -1, 0 + while True: + r = self.lookupnext(rid, tag+0x1, + path=path) + if path: + rid, rattr, path_ = r + else: + rid, rattr = r + # found end of tree? + if rid is None: + break + + if path: + yield rid, rattr, path_ + else: + yield rid, rattr + tag = rattr.tag + else: + if tag is None: + tag, mask = 0, 0xffff + if mask is None: + mask = 0 + + tag_ = max((tag & ~mask) - 1, 0) + while True: + r = self.lookupnext(rid, tag_+0x1, + path=path) + if path: + rid_, rattr_, path_ = r + else: + rid_, rattr_ = r + # found end of tree? + if (rid_ is None + or rid_ != rid + or (rattr_.tag & ~mask & 0xfff) + != (tag & ~mask & 0xfff)): + break + + if path: + yield rattr_, path_ + else: + yield rattr_ + tag_ = rattr_.tag + + # lookup by name + def namelookup(self, did, name): + # binary search + best = None, None + lower = 0 + upper = self.weight + while lower < upper: + rid, name_ = self.lookupnext( + lower + (upper-1-lower)//2) + if rid is None: + break + + # bisect search space + if (name_.did, name_.name) > (did, name): + upper = rid-(name_.weight-1) + elif (name_.did, name_.name) < (did, name): + lower = rid + 1 + # keep track of best match + best = rid, name_ + else: + # found a match + return rid, name_ + + return best + + +# our rbyd btree type +class Btree: + def __init__(self, bd, rbyd): + self.bd = bd + self.rbyd = rbyd + + @property + def block(self): + return self.rbyd.block + + @property + def blocks(self): + return self.rbyd.blocks + + @property + def trunk(self): + return self.rbyd.trunk + + @property + def weight(self): + return self.rbyd.weight + + @property + def rev(self): + return self.rbyd.rev + + @property + def cksum(self): + return self.rbyd.cksum + + @property + def shrub(self): + return self.rbyd.shrub + + def addr(self): + return self.rbyd.addr() + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, self.repr()) + + def repr(self): + return 'btree %s w%s' % (self.addr(), self.weight) + + def __eq__(self, other): + return self.rbyd == other.rbyd + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash(self.rbyd) + + @classmethod + def fetch(cls, bd, blocks, trunk=None): + # rbyd fetch does most of the work here + rbyd = Rbyd.fetch(bd, blocks, trunk) + return cls(bd, rbyd) + + @classmethod + def fetchck(cls, bd, blocks, trunk, weight, cksum): + # rbyd fetchck does most of the work here + rbyd = Rbyd.fetchck(bd, blocks, trunk, weight, cksum) + return cls(bd, rbyd) + + @classmethod + def fetchshrub(cls, bd, rbyd, trunk): + shrub = Rbyd.fetchshrub(rbyd, trunk) + return cls(bd, shrub) + + def lookupnext_(self, bid, *, + path=False, + depth=None): + if not self or bid >= self.weight: + if path: + return None, None, None, None, [] + else: + return None, None, None, None + + rbyd = self.rbyd + rid = bid + depth_ = 1 + path_ = [] + + while True: + # corrupt branch? + if not rbyd: + if path: + return bid, rbyd, rid, None, path_ + else: + return bid, rbyd, rid, None + + # first tag indicates the branch's weight + rid_, name_ = rbyd.lookupnext(rid) + if rid_ is None: + if path: + return None, None, None, None, path_ + else: + return None, None, None, None + + # keep track of path + if path: + path_.append((bid + (rid_-rid), rbyd, rid_, name_)) + + # find branch tag if there is one + branch_ = rbyd.lookup(rid_, TAG_BRANCH, 0x3) + + # descend down branch? + if branch_ is not None and ( + not depth or depth_ < depth): + block, trunk, cksum, _ = frombranch(branch_.data) + rbyd = Rbyd.fetchck(self.bd, block, trunk, name_.weight, + cksum) + + rid -= (rid_-(name_.weight-1)) + depth_ += 1 + + else: + if path: + return bid + (rid_-rid), rbyd, rid_, name_, path_ + else: + return bid + (rid_-rid), rbyd, rid_, name_ + + # the non-leaf variants discard the rbyd info, these can be a bit + # more convenient, but at a performance cost + def lookupnext(self, bid, *, + path=False, + depth=None): + # just discard the rbyd info + r = self.lookupnext_(bid, + path=path, + depth=depth) + if path: + bid, rbyd, rid, name, path_ = r + else: + bid, rbyd, rid, name = r + + if path: + return bid, name, path_ + else: + return bid, name + + def lookup(self, bid, tag=None, mask=None, *, + path=False, + depth=None): + # lookup rbyd in btree + # + # note this function expects bid to be known, use lookupnext + # first if you don't care about the exact bid (or better yet, + # lookupnext_ and call lookup on the returned rbyd) + # + # this matches rbyd's lookup behavior, which needs a known rid + # to avoid a double lookup + r = self.lookupnext_(bid, + path=path, + depth=depth) + if path: + bid_, rbyd_, rid_, name_, path_ = r + else: + bid_, rbyd_, rid_, name_ = r + if bid_ is None or bid_ != bid: + if path: + return None, path_ + else: + return None + + # lookup tag in rbyd + rattr_ = rbyd_.lookup(rid_, tag, mask) + if rattr_ is None: + if path: + return None, path_ + else: + return None + + if path: + return rattr_, path_ + else: + return rattr_ + + # note leaves only iterates over leaf rbyds, whereas traverse + # traverses all rbyds + def leaves(self, *, + path=False, + depth=None): + # include our root rbyd even if the weight is zero + if self.weight == 0: + if path: + yield -1, self.rbyd, [] + else: + yield -1, self.rbyd + return + + bid = 0 + while True: + r = self.lookupnext_(bid, + path=path, + depth=depth) + if r: + bid, rbyd, rid, name, path_ = r + else: + bid, rbyd, rid, name = r + if bid is None: + break + + if path: + yield (bid-rid + (rbyd.weight-1), rbyd, + # path tail is usually redundant unless corrupt + path_[:-1] + if path_ and path_[-1][1] == rbyd + else path_) + else: + yield bid-rid + (rbyd.weight-1), rbyd + bid += rbyd.weight - rid + 1 + + def traverse(self, *, + path=False, + depth=None): + ptrunk_ = [] + for bid, rbyd, path_ in self.leaves( + path=True, + depth=depth): + # we only care about the rbyds here + trunk_ = ([(bid_-rid_ + (rbyd_.weight-1), rbyd_) + for bid_, rbyd_, rid_, name_ in path_] + + [(bid, rbyd)]) + for d, (bid_, rbyd_) in pathdelta( + trunk_, ptrunk_): + # but include branch rids in the path if requested + if path: + yield bid_, rbyd_, path_[:d] + else: + yield bid_, rbyd_ + ptrunk_ = trunk_ + + # note bids/rattrs do _not_ include corrupt btree nodes! + def bids(self, *, + leaves=False, + path=False, + depth=None): + for r in self.leaves( + path=path, + depth=depth): + if path: + bid, rbyd, path_ = r + else: + bid, rbyd = r + for rid, name in rbyd.rids(): + bid_ = bid-(rbyd.weight-1) + rid + if leaves: + if path: + yield (bid_, rbyd, rid, name, + path_+[(bid_, rbyd, rid, name)]) + else: + yield bid_, rbyd, rid, name + else: + if path: + yield (bid_, name, + path_+[(bid_, rbyd, rid, name)]) + else: + yield bid_, name + + def rattrs(self, bid=None, tag=None, mask=None, *, + leaves=False, + path=False, + depth=None): + if bid is None: + for r in self.leaves( + path=path, + depth=depth): + if path: + bid, rbyd, path_ = r + else: + bid, rbyd = r + for rid, name in rbyd.rids(): + bid_ = bid-(rbyd.weight-1) + rid + for rattr in rbyd.rattrs(rid): + if leaves: + if path: + yield (bid_, rbyd, rid, rattr, + path_+[(bid_, rbyd, rid, name)]) + else: + yield bid_, rbyd, rid, rattr + else: + if path: + yield (bid_, rattr, + path_+[(bid_, rbyd, rid, name)]) + else: + yield bid_, rattr + else: + r = self.lookupnext_(bid, + path=path, + depth=depth) + if path: + bid, rbyd, rid, name, path_ = r + else: + bid, rbyd, rid, name = r + if bid is None: + return + + for rattr in rbyd.rattrs(rid, tag, mask): + if leaves: + if path: + yield rbyd, rid, rattr, path_ + else: + yield rbyd, rid, rattr + else: + if path: + yield rattr, path_ + else: + yield rattr + + # lookup by name + def namelookup_(self, did, name, *, + path=False, + depth=None): + rbyd = self.rbyd + bid = 0 + depth_ = 1 + path_ = [] + + while True: + # corrupt branch? + if not rbyd: + bid_ = bid+(rbyd.weight-1) + if path: + return bid_, rbyd, rbyd.weight-1, None, path_ + else: + return bid_, rbyd, rbyd.weight-1, None + + rid_, name_ = rbyd.namelookup(did, name) + + # keep track of path + if path: + path_.append((bid + rid_, rbyd, rid_, name_)) + + # find branch tag if there is one + branch_ = rbyd.lookup(rid_, TAG_BRANCH, 0x3) + + # found another branch + if branch_ is not None and ( + not depth or depth_ < depth): + block, trunk, cksum, _ = frombranch(branch_.data) + rbyd = Rbyd.fetchck(self.bd, block, trunk, name_.weight, + cksum) + + # update our bid + bid += rid_ - (name_.weight-1) + depth_ += 1 + + # found best match + else: + if path: + return bid + rid_, rbyd, rid_, name_, path_ + else: + return bid + rid_, rbyd, rid_, name_ + + def namelookup(self, bid, *, + path=False, + depth=None): + # just discard the rbyd info + r = self.namelookup_(did, name, + path=path, + depth=depth) + if path: + bid, rbyd, rid, name, path_ = r + else: + bid, rbyd, rid, name = r + + if path: + return bid, name, path_ + else: + return bid, name + + +# a metadata id, this includes mbits for convenience +class Mid: + def __init__(self, mbid, mrid=None, *, + mbits=None): + # we need one of these to figure out mbits + if mbits is not None: + self.mbits = mbits + elif isinstance(mbid, Mid): + self.mbits = mbid.mbits + else: + assert mbits is not None, "mbits?" + + # accept other mids which can be useful for changing mrids + if isinstance(mbid, Mid): + mbid = mbid.mbid + + # accept either merged mid or separate mbid+mrid + if mrid is None: + mid = mbid + mbid = mid | ((1 << self.mbits) - 1) + mrid = mid & ((1 << self.mbits) - 1) + + # map mrid=-1 + if mrid == ((1 << self.mbits) - 1): + mrid = -1 + + self.mbid = mbid + self.mrid = mrid + + @property + def mid(self): + return ((self.mbid & ~((1 << self.mbits) - 1)) + | (self.mrid & ((1 << self.mbits) - 1))) + + def mbidrepr(self): + return str(self.mbid >> self.mbits) + + def mridrepr(self): + return str(self.mrid) + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, self.repr()) + + def repr(self): + return '%s.%s' % (self.mbidrepr(), self.mridrepr()) + + def __iter__(self): + return iter((self.mbid, self.mrid)) + + # note this is slightly different from mid order when mrid=-1 + def __eq__(self, other): + if isinstance(other, Mid): + return (self.mbid, self.mrid) == (other.mbid, other.mrid) + else: + return self.mid == other + + def __ne__(self, other): + if isinstance(other, Mid): + return (self.mbid, self.mrid) != (other.mbid, other.mrid) + else: + return self.mid != other + + def __hash__(self): + return hash((self.mbid, self.mrid)) + + def __lt__(self, other): + return (self.mbid, self.mrid) < (other.mbid, other.mrid) + + def __le__(self, other): + return (self.mbid, self.mrid) <= (other.mbid, other.mrid) + + def __gt__(self, other): + return (self.mbid, self.mrid) > (other.mbid, other.mrid) + + def __ge__(self, other): + return (self.mbid, self.mrid) >= (other.mbid, other.mrid) + +# mdirs, the gooey atomic center of littlefs +# +# really the only difference between this and our rbyd class is the +# implicit mbid associated with the mdir +class Mdir: + def __init__(self, mid, rbyd, *, + mbits=None): + # we need one of these to figure out mbits + if mbits is not None: + self.mbits = mbits + elif isinstance(mid, Mid): + self.mbits = mid.mbits + elif isinstance(rbyd, Mdir): + self.mbits = rbyd.mbits + else: + assert mbits is not None, "mbits?" + + # strip mrid, bugs will happen if caller relies on mrid here + self.mid = Mid(mid, -1, mbits=self.mbits) + + # accept either another mdir or rbyd + if isinstance(rbyd, Mdir): + self.rbyd = rbyd.rbyd + else: + self.rbyd = rbyd + + @property + def data(self): + return self.rbyd.data + + @property + def block(self): + return self.rbyd.block + + @property + def blocks(self): + return self.rbyd.blocks + + @property + def trunk(self): + return self.rbyd.trunk + + @property + def weight(self): + return self.rbyd.weight + + @property + def rev(self): + return self.rbyd.rev + + @property + def eoff(self): + return self.rbyd.eoff + + @property + def cksum(self): + return self.rbyd.cksum + + @property + def gcksumdelta(self): + return self.rbyd.gcksumdelta + + @property + def corrupt(self): + return self.rbyd.corrupt + + @property + def redund(self): + return self.rbyd.redund + + def addr(self): + if len(self.blocks) == 1: + return '0x%x' % self.block + else: + return '0x{%s}' % ( + ','.join('%x' % block for block in self.blocks)) + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, self.repr()) + + def repr(self): + return 'mdir %s %s w%s' % ( + self.mid.mbidrepr(), + self.addr(), + self.weight) + + def __bool__(self): + return bool(self.rbyd) + + # we _don't_ care about mid for equality, or trunk even + def __eq__(self, other): + return frozenset(self.blocks) == frozenset(other.blocks) + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash(frozenset(self.blocks)) + + @classmethod + def fetch(cls, bd, mid, blocks, trunk=None): + rbyd = Rbyd.fetch(bd, blocks, trunk) + return cls(mid, rbyd, mbits=Mtree.mbits_(bd)) + + def lookupnext(self, mid, tag=None, *, + path=False): + # this is similar to rbyd lookupnext, we just error if + # lookupnext changes mids + if not isinstance(mid, Mid): + mid = Mid(mid, mbits=self.mbits) + r = self.rbyd.lookupnext(mid.mrid, tag, + path=path) + if path: + rid, rattr, path_ = r + else: + rid, rattr = r + + if rid != mid.mrid: + if path: + return None, path_ + else: + return None + + if path: + return rattr, path_ + else: + return rattr + + def lookup(self, mid, tag=None, mask=None, *, + path=False): + if not isinstance(mid, Mid): + mid = Mid(mid, mbits=self.mbits) + return self.rbyd.lookup(mid.mrid, tag, mask, + path=path) + + def mids(self, *, + path=False): + for r in self.rbyd.rids( + path=path): + if path: + rid, name, path_ = r + else: + rid, name = r + + mid = Mid(self.mid, rid) + if path: + yield mid, name, path_ + else: + yield mid, name + + def rattrs(self, mid=None, tag=None, mask=None, *, + path=False): + if mid is None: + for r in self.rbyd.rattrs( + path=path): + if path: + rid, rattr, path_ = r + else: + rid, rattr = r + + mid = Mid(self.mid, rid) + if path: + yield mid, rattr, path_ + else: + yield mid, rattr + else: + if not isinstance(mid, Mid): + mid = Mid(mid, mbits=self.mbits) + yield from self.rbyd.rattrs(mid.mrid, tag, mask, + path=path) + + # lookup by name + def namelookup(self, did, name): + # unlike rbyd namelookup, we need an exact match here + rid, name_ = self.rbyd.namelookup(did, name) + if rid is None or (name_.did, name_.name) != (did, name): + return None, None + + return Mid(self.mid, rid), name_ + +# the mtree, the skeletal structure of littlefs +class Mtree: + def __init__(self, bd, mrootchain, mtree, *, + mrootpath=False, + mtreepath=False, + mbits=None): + if isinstance(mrootchain, Mdir): + mrootchain = [Mdir] + # we at least need the mrootanchor, even if it is corrupt + assert len(mrootchain) >= 1 + + self.bd = bd + if mbits is not None: + self.mbits = mbits + else: + self.mbits = Mtree.mbits_(self.bd) + + self.mrootchain = mrootchain + self.mrootanchor = mrootchain[0] + self.mroot = mrootchain[-1] + self.mtree = mtree + + # mbits is a static value derived from the block_size + @staticmethod + def mbits_(block_size): + if isinstance(block_size, Bd): + block_size = block_size.block_size + return mt.ceil(mt.log2(block_size)) - 3 + + # convenience function for creating mbits-dependent mids + def mid(self, mbid, mrid=None): + return Mid(mbid, mrid, mbits=self.mbits) + + @property + def block(self): + return self.mroot.block + + @property + def blocks(self): + return self.mroot.blocks + + @property + def trunk(self): + return self.mroot.trunk + + @property + def weight(self): + if self.mtree is None: + return 0 + else: + return self.mtree.weight + + @property + def mbweight(self): + return self.weight + + @property + def mrweight(self): + return 1 << self.mbits + + def mbweightrepr(self): + return str(self.mbweight >> self.mbits) + + def mrweightrepr(self): + return str(self.mrweight) + + @property + def rev(self): + return self.mroot.rev + + @property + def cksum(self): + return self.mroot.cksum + + def addr(self): + return self.mroot.addr() + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, self.repr()) + + def repr(self): + return 'mtree %s w%s.%s' % ( + self.addr(), + self.mbweightrepr(), self.mrweightrepr()) + + def __eq__(self, other): + return self.mrootanchor == other.mrootanchor + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash(self.mrootanchor) + + @classmethod + def fetch(cls, bd, blocks=None, trunk=None, *, + depth=None): + # default to blocks 0x{0,1} + if blocks is None: + blocks = [0, 1] + + # figure out mbits + mbits = Mtree.mbits_(bd) + + # fetch the mrootanchor + mrootanchor = Mdir.fetch(bd, -1, blocks, trunk) + + # follow the mroot chain to try to find the active mroot + mroot = mrootanchor + mrootchain = [mrootanchor] + mrootseen = set() + while True: + # corrupted? + if not mroot: + break + # cycle detected? + if mroot in mrootseen: + break + mrootseen.add(mroot) + + # stop here? + if depth and len(mrootchain) >= depth: + break + + # fetch the next mroot + rattr_ = mroot.lookup(-1, TAG_MROOT, 0x3) + if rattr_ is None: + break + blocks_, _ = frommdir(rattr_.data) + mroot = Mdir.fetch(bd, -1, blocks_) + mrootchain.append(mroot) + + # fetch the actual mtree, if there is one + mtree = None + if not depth or len(mrootchain) < depth: + rattr_ = mroot.lookup(-1, TAG_MTREE, 0x3) + if rattr_ is not None: + w_, block_, trunk_, cksum_, _ = frombtree(rattr_.data) + mtree = Btree.fetchck(bd, block_, trunk_, w_, cksum_) + + return cls(bd, mrootchain, mtree, + mbits=mbits) + + def _lookupnext_(self, mid, *, + path=False, + depth=None): + if not isinstance(mid, Mid): + mid = self.mid(mid) + + if path or depth: + # iterate over mrootchain + path_ = [] + for mroot in self.mrootchain: + # stop here? + if depth and len(path_) >= depth: + if path: + return mroot, path_ + else: + return mroot + + name = mroot.lookup(-1, TAG_MAGIC) + path_.append((mroot.mid, mroot, name)) + + # no mtree? must be inlined in mroot + if self.mtree is None: + if mid.mbid != -1: + if path: + return None, path_ + else: + return None + + if path: + return self.mroot, path_ + else: + return self.mroot + + # mtree? lookup in mtree + else: + # need to do two steps here in case lookupnext_ stops early + r = self.mtree.lookupnext_(mid.mid, + path=path or depth, + depth=depth-len(path_) if depth else None) + if path or depth: + bid_, rbyd_, rid_, name_, path__ = r + path_.extend(path__) + else: + bid_, rbyd_, rid_, name_ = r + if bid_ is None: + if path: + return None, path_ + else: + return None + + # corrupt btree node? + if not rbyd_: + if path: + return (bid_, rbyd_, rid_), path_ + else: + return (bid_, rbyd_, rid_) + + # stop here? it's not an mdir, but we only return btree nodes + # if explicitly requested + if depth and len(path_) >= depth: + if path: + return (bid_, rbyd_, rid_), path_ + else: + return (bid_, rbyd_, rid_) + + # fetch the mdir + rattr_ = rbyd_.lookup(rid_, TAG_MDIR, 0x3) + # mdir tag missing? weird + if rattr_ is None: + if path: + return (bid_, rbyd_, rid_), path_ + else: + return (bid_, rbyd_, rid_) + blocks_, _ = frommdir(rattr_.data) + mdir = Mdir.fetch(self.bd, mid, blocks_) + if path: + return mdir, path_ + else: + return mdir + + def lookupnext_(self, mid, *, + mdirs_only=True, + path=False, + depth=None): + # most of the logic is in _lookupnext_, this just helps + # deduplicate the mdirs_only logic + r = self._lookupnext_(mid, + path=path, + depth=depth) + if path: + mdir, path_ = r + else: + mdir = r + if mdir is None or ( + mdirs_only and not isinstance(mdir, Mdir)): + if path: + return None, path_ + else: + return None + + if path: + return mdir, path_ + else: + return mdir + + def lookup(self, mid, *, + path=False, + depth=None): + if not isinstance(mid, Mid): + mid = self.mid(mid) + + # lookup the relevant mdir + r = self.lookupnext_(mid, + path=path, + depth=depth) + if path: + mdir, path_ = r + else: + mdir = r + if mdir is None: + if path: + return None, None, path_ + else: + return None, None + + # not in mdir? + if mid.mrid >= mdir.weight: + if path: + return None, None, path_ + else: + return None, None + + # lookup mid in mdir + rattr = mdir.lookup(mid) + if path: + return mdir, rattr, path_+[(mid, mdir, rattr)] + else: + return mdir, rattr + + # iterate over all mdirs, this includes the mrootchain + def _leaves(self, *, + path=False, + depth=None): + # iterate over mrootchain + if path or depth: + path_ = [] + for mroot in self.mrootchain: + if path: + yield mroot, path_ + else: + yield mroot + + if path or depth: + # stop here? + if depth and len(path_) >= depth: + return + + name = mroot.lookup(-1, TAG_MAGIC) + path_.append((mroot.mid, mroot, name)) + + # do we even have an mtree? + if self.mtree is not None: + # include the mtree root even if the weight is zero + if self.mtree.weight == 0: + if path: + yield -1, self.mtree.rbyd, path_ + else: + yield -1, self.mtree.rbyd + return + + mid = self.mid(0) + while True: + r = self.lookupnext_(mid, + mdirs_only=False, + path=path, + depth=depth) + if path: + mdir, path_ = r + else: + mdir = r + if mdir is None: + break + + # mdir? + if isinstance(mdir, Mdir): + if path: + yield mdir, path_ + else: + yield mdir + mid = self.mid(mid.mbid+1) + # btree node? + else: + bid, rbyd, rid = mdir + if path: + yield ((bid-rid + (rbyd.weight-1), rbyd), + # path tail is usually redundant unless corrupt + path_[:-1] + if path_ + and isinstance(path_[-1][1], Rbyd) + and path_[-1][1] == rbyd + else path_) + else: + yield (bid-rid + (rbyd.weight-1), rbyd) + mid = self.mid(bid-rid + (rbyd.weight-1) + 1) + + def leaves(self, *, + mdirs_only=False, + path=False, + depth=None): + for r in self._leaves( + path=path, + depth=depth): + if path: + mdir, path_ = r + else: + mdir = r + if mdirs_only and not isinstance(mdir, Mdir): + continue + + if path: + yield mdir, path_ + else: + yield mdir + + # traverse over all mdirs and btree nodes + # - mdir => Mdir + # - btree node => (bid, rbyd) + def _traverse(self, *, + path=False, + depth=None): + ptrunk_ = [] + for mdir, path_ in self.leaves( + path=True, + depth=depth): + # we only care about the mdirs/rbyds here + trunk_ = ([(lambda mid_, mdir_, name_: mdir_)(*p) + if isinstance(p[1], Mdir) + else (lambda bid_, rbyd_, rid_, name_: + (bid_-rid_ + (rbyd_.weight-1), rbyd_))(*p) + for p in path_] + + [mdir]) + for d, mdir in pathdelta( + trunk_, ptrunk_): + # but include branch mids/rids in the path if requested + if path: + yield mdir, path_[:d] + else: + yield mdir + ptrunk_ = trunk_ + + def traverse(self, *, + mdirs_only=False, + path=False, + depth=None): + for r in self._traverse( + path=path, + depth=depth): + if path: + mdir, path_ = r + else: + mdir = r + if mdirs_only and not isinstance(mdir, Mdir): + continue + + if path: + yield mdir, path_ + else: + yield mdir + + # these are just aliases + + # the difference between mdirs and leaves is mdirs defaults to only + # mdirs, leaves can include btree nodes if corrupt + def mdirs(self, *, + mdirs_only=True, + path=False, + depth=None): + return self.leaves( + mdirs_only=mdirs_only, + path=path, + depth=depth) + + # note mids/rattrs do _not_ include corrupt btree nodes! + def mids(self, *, + mdirs_only=True, + path=False, + depth=None): + for r in self.mdirs( + mdirs_only=mdirs_only, + path=path, + depth=depth): + if path: + mdir, path_ = r + else: + mdir = r + if isinstance(mdir, Mdir): + for mid, name in mdir.mids(): + if path: + yield (mid, mdir, name, + path_+[(mid, mdir, name)]) + else: + yield mid, mdir, name + else: + bid, rbyd = mdir + for rid, name in rbyd.rids(): + bid_ = bid-(rbyd.weight-1) + rid + mid_ = self.mid(bid_) + mdir_ = (bid_, rbyd, rid) + if path: + yield (mid_, mdir_, name, + path_+[(bid_, rbyd, rid, name)]) + else: + yield mid_, mdir_, name + + def rattrs(self, mid=None, tag=None, mask=None, *, + mdirs_only=True, + path=False, + depth=None): + if mid is None: + for r in self.mdirs( + mdirs_only=mdirs_only, + path=path, + depth=depth): + if path: + mdir, path_ = r + else: + mdir = r + if isinstance(mdir, Mdir): + for mid, rattr in mdir.rattrs(): + if path: + yield (mid, mdir, rattr, + path_+[(mid, mdir, mdir.lookup(mid))]) + else: + yield mid, mdir, rattr + else: + bid, rbyd = mdir + for rid, name in rbyd.rids(): + bid_ = bid-(rbyd.weight-1) + rid + mid_ = self.mid(bid_) + mdir_ = (bid_, rbyd, rid) + for rattr in rbyd.rattrs(rid): + if path: + yield (mid_, mdir_, rattr, + path_+[(bid_, rbyd, rid, name)]) + else: + yield mid_, mdir_, rattr + else: + if not isinstance(mid, Mid): + mid = self.mid(mid) + + r = self.lookupnext_(mid, + path=path, + depth=depth) + if path: + mdir, path_ = r + else: + mdir = r + if mdir is None or ( + mdirs_only and not isinstance(mdir, Mdir)): + return + + if isinstance(mdir, Mdir): + for rattr in mdir.rattrs(mid, tag, mask): + if path: + yield rattr, path_ + else: + yield rattr + else: + bid, rbyd, rid = mdir + for rattr in rbyd.rattrs(rid, tag, mask): + if path: + yield rattr, path_ + else: + yield rattr + + # lookup by name + def _namelookup_(self, did, name, *, + path=False, + depth=None): + if path or depth: + # iterate over mrootchain + path_ = [] + for mroot in self.mrootchain: + # stop here? + if depth and len(path_) >= depth: + if path: + return mroot, path_ + else: + return mroot + + name = mroot.lookup(-1, TAG_MAGIC) + path_.append((mroot.mid, mroot, name)) + + # no mtree? must be inlined in mroot + if self.mtree is None: + if path: + return self.mroot, path_ + else: + return self.mroot + + # mtree? find name in mtree + else: + # need to do two steps here in case namelookup_ stops early + r = self.mtree.namelookup_(did, name, + path=path or depth, + depth=depth-len(path_) if depth else None) + if path or depth: + bid_, rbyd_, rid_, name_, path__ = r + path_.extend(path__) + else: + bid_, rbyd_, rid_, name_ = r + if bid_ is None: + if path: + return None, path_ + else: + return None + + # corrupt btree node? + if not rbyd_: + if path: + return (bid_, rbyd_, rid_), path_ + else: + return (bid_, rbyd_, rid_) + + # stop here? it's not an mdir, but we only return btree nodes + # if explicitly requested + if depth and len(path_) >= depth: + if path: + return (bid_, rbyd_, rid_), path_ + else: + return (bid_, rbyd_, rid_) + + # fetch the mdir + rattr_ = rbyd_.lookup(rid_, TAG_MDIR, 0x3) + # mdir tag missing? weird + if rattr_ is None: + if path: + return (bid_, rbyd_, rid_), path_ + else: + return (bid_, rbyd_, rid_) + blocks_, _ = frommdir(rattr_.data) + mdir = Mdir.fetch(self.bd, self.mid(bid_), blocks_) + if path: + return mdir, path_ + else: + return mdir + + def namelookup_(self, did, name, *, + mdirs_only=True, + path=False, + depth=None): + # most of the logic is in _namelookup_, this just helps + # deduplicate the mdirs_only logic + r = self._namelookup_(did, name, + path=path, + depth=depth) + if path: + mdir, path_ = r + else: + mdir = r + if mdir is None or ( + mdirs_only and not isinstance(mdir, Mdir)): + if path: + return None, path_ + else: + return None + + if path: + return mdir, path_ + else: + return mdir + + def namelookup(self, did, name, *, + path=False, + depth=None): + # lookup the relevant mdir + r = self.namelookup_(did, name, + path=path, + depth=depth) + if path: + mdir, path_ = r + else: + mdir = r + if mdir is None: + if path: + return None, None, None, path_ + else: + return None, None, None + + # find name in mdir + mid_, name_ = mdir.namelookup(did, name) + if mid_ is None: + if path: + return None, None, None, path_ + else: + return None, None, None + + if path: + return mid_, mdir, name_, path_+[(mid_, mdir, name_)] + else: + return mid_, mdir, name_ + + +# in-btree block pointers +class Bptr: + def __init__(self, rattr, block, off, size, cksize, cksum, ckdata, *, + corrupt=False): + self.rattr = rattr + self.block = block + self.off = off + self.size = size + self.cksize = cksize + self.cksum = cksum + self.ckdata = ckdata + + self.corrupt = corrupt + + @property + def tag(self): + return self.rattr.tag + + @property + def weight(self): + return self.rattr.weight + + # this is just for consistency with btrees, rbyds, etc + @property + def blocks(self): + return [self.block] + + # try to avoid unnecessary allocations + @ft.cached_property + def data(self): + return self.ckdata[self.off:self.off+self.size] + + def addr(self): + return '0x%x.%x' % (self.block, self.off) + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, self.repr()) + + def repr(self): + return '%sblock %s w%s %s' % ( + 'shrub' if self.tag & TAG_SHRUB else '', + self.addr(), + self.weight, + self.size) + + # lazily check the cksum + @ft.cached_property + def corrupt(self): + cksum_ = crc32c(self.ckdata) + return (cksum_ != self.cksum) + + @property + def redund(self): + return -1 if self.corrupt else 0 + + def __bool__(self): + return not self.corrupt + + @classmethod + def fetch(cls, bd, rattr, block, off, size, cksize, cksum): + # seek/read cksize bytes from the block, the actual data should + # always be a subset of cksize + ckdata = bd.read(block, 0, cksize) + + return cls(rattr, block, off, size, cksize, cksum, ckdata) + + @classmethod + def fetchck(cls, bd, rattr, blocks, off, size, cksize, cksum): + # fetch the bptr normally + bptr = cls.fetch(bd, rattr, blocks, off, size, cksize, cksum) + + # bit of a hack, but this exposes the lazy cksum checker + del bptr.corrupt + + return bptr + + # yeah, so, this doesn't catch mismatched cksizes, but at least the + # underlying data should be identical assuming no mutation + def __eq__(self, other): + return ((self.block, self.off, self.size) + == (other.block, other.off, other.size)) + + def __ne__(self, other): + return ((self.block, self.off, self.size) + != (other.block, other.off, other.size)) + + def __hash__(self): + return hash((self.block, self.off, self.size)) + + +# lazy config object +class Config: + def __init__(self, mroot): + self.mroot = mroot + + # lookup a specific tag + def lookup(self, tag=None, mask=None): + rattr = self.mroot.rbyd.lookup(-1, tag, mask) + if rattr is None: + return None + + return self._parse(rattr.tag, rattr) + + def __getitem__(self, key): + if not isinstance(key, tuple): + key = (key,) + + return self.lookup(*key) + + def __contains__(self, key): + if not isinstance(key, tuple): + key = (key,) + + return self.lookup(*key) is not None + + def __iter__(self): + for rattr in self.mroot.rbyd.rattrs(-1, TAG_CONFIG, 0xff): + yield self._parse(rattr.tag, rattr) + + # common config operations + class Config: + tag = None + mask = None + + def __init__(self, mroot, tag, rattr): + # replace tag with what we find + self.tag = tag + # and keep track of rattr + self.rattr = rattr + + @property + def block(self): + return self.rattr.block + + @property + def blocks(self): + return self.rattr.blocks + + @property + def toff(self): + return self.rattr.toff + + @property + def tdata(self): + return self.rattr.data + + @property + def off(self): + return self.rattr.off + + @property + def data(self): + return self.rattr.data + + @property + def size(self): + return self.rattr.size + + def __bytes__(self): + return self.data + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, self.repr()) + + def repr(self): + return self.rattr.repr() + + def __iter__(self): + return iter((self.tag, self.data)) + + def __eq__(self, other): + return (self.tag, self.data) == (other.tag, other.data) + + def __ne__(self, other): + return (self.tag, self.data) != (other.tag, other.data) + + def __hash__(self): + return hash((self.tag, self.data)) + + # marker class for unknown config + class Unknown(Config): + pass + + # special handling for known configs + + # the filesystem magic string + class Magic(Config): + tag = TAG_MAGIC + mask = 0x3 + + def repr(self): + return 'magic \"%s\"' % ( + ''.join(b if b >= ' ' and b <= '~' else '.' + for b in map(chr, self.data))) + + # version tuple + class Version(Config): + tag = TAG_VERSION + + def __init__(self, mroot, tag, rattr): + super().__init__(mroot, tag, rattr) + d = 0 + self.major, d_ = fromleb128(self.data, d); d += d_ + self.minor, d_ = fromleb128(self.data, d); d += d_ + + @property + def tuple(self): + return (self.major, self.minor) + + def repr(self): + return 'version v%s.%s' % (self.major, self.minor) + + # compat flags + class Rcompat(Config): + tag = TAG_RCOMPAT + + def __init__(self, mroot, tag, rattr): + super().__init__(mroot, tag, rattr) + self.flags = fromle32(self.data) + + def __int__(self): + return self.flags + + def repr(self): + return 'rcompat 0x%s' % ( + ''.join('%02x' % f for f in reversed(self.data))) + + class Wcompat(Config): + tag = TAG_WCOMPAT + + def __init__(self, mroot, tag, rattr): + super().__init__(mroot, tag, rattr) + self.flags = fromle32(self.data) + + def __int__(self): + return self.flags + + def repr(self): + return 'wcompat 0x%s' % ( + ''.join('%02x' % f for f in reversed(self.data))) + + class Ocompat(Config): + tag = TAG_OCOMPAT + + def __init__(self, mroot, tag, rattr): + super().__init__(mroot, tag, rattr) + self.flags = fromle32(self.data) + + def __int__(self): + return self.flags + + def repr(self): + return 'ocompat 0x%s' % ( + ''.join('%02x' % f for f in reversed(self.data))) + + # block device geometry + class Geometry(Config): + tag = TAG_GEOMETRY + + def __init__(self, mroot, tag, rattr): + super().__init__(mroot, tag, rattr) + d = 0 + block_size, d_ = fromleb128(self.data, d); d += d_ + block_count, d_ = fromleb128(self.data, d); d += d_ + # these are offset by 1 to avoid overflow issues + self.block_size = block_size + 1 + self.block_count = block_count + 1 + + def repr(self): + return 'geometry %sx%s' % (self.block_size, self.block_count) + + # file name limit + class NameLimit(Config): + tag = TAG_NAMELIMIT + + def __init__(self, mroot, tag, rattr): + super().__init__(mroot, tag, rattr) + self.limit, _ = fromleb128(self.data) + + def __int__(self): + return self.limit + + def repr(self): + return 'namelimit %s' % self.limit + + # file size limit + class FileLimit(Config): + tag = TAG_FILELIMIT + + def __init__(self, mroot, tag, rattr): + super().__init__(mroot, tag, rattr) + self.limit, _ = fromleb128(self.data) + + def __int__(self): + return self.limit + + def repr(self): + return 'filelimit %s' % self.limit + + # keep track of known configs + _known = [c for c in Config.__subclasses__() if c.tag is not None] + + # parse if known + def _parse(self, tag, rattr): + # known config? + for c in self._known: + if (c.tag & ~(c.mask or 0)) == (tag & ~(c.mask or 0)): + return c(self.mroot, tag, rattr) + # otherwise return a marker class + else: + return self.Unknown(self.mroot, tag, rattr) + + # create cached accessors for known config + def _parser(c): + def _parser(self): + return self.lookup(c.tag, c.mask) + return _parser + + for c in _known: + locals()[c.__name__.lower()] = ft.cached_property(_parser(c)) + +# lazy gstate object +class Gstate: + def __init__(self, mtree, config): + self.mtree = mtree + self.config = config + + # lookup a specific tag + def lookup(self, tag=None, mask=None): + # collect relevant gdeltas in the mtree + gdeltas = [] + for mdir in self.mtree.mdirs(): + # gcksumdelta is a bit special since it's outside the + # rbyd tree + if tag == TAG_GCKSUMDELTA: + gdelta = mdir.gcksumdelta + else: + gdelta = mdir.rbyd.lookup(-1, tag, mask) + if gdelta is not None: + gdeltas.append((mdir.mid, gdelta)) + + # xor to find gstate + return self._parse(tag, gdeltas) + + def __getitem__(self, key): + if not isinstance(key, tuple): + key = (key,) + + return self.lookup(*key) + + def __contains__(self, key): + # note gstate doesn't really "not exist" like normal attrs, + # missing gstate is equivalent to zero gstate, but we can + # still test if there are any gdeltas that match the given + # tag here + if not isinstance(key, tuple): + key = (key,) + + return any( + (mdir.gcksumdelta if tag == TAG_GCKSUMDELTA + else mdir.rbyd.lookup(-1, *key)) + is not None + for mdir in self.mtree.mdirs()) + + def __iter__(self): + # first figure out what gstate tags actually exist in the + # filesystem + gtags = set() + for mdir in self.mtree.mdirs(): + if mdir.gcksumdelta is not None: + gtags.add(TAG_GCKSUMDELTA) + + for rattr in mdir.rbyd.rattrs(-1): + if (rattr.tag & 0xff00) == TAG_GDELTA: + gtags.add(rattr.tag) + + # sort to keep things stable, moving gcksum to the front + gtags = sorted(gtags, key=lambda t: (-(t & 0xf000), t)) + + # compute all gstate in one pass (well, two technically) + gdeltas = {tag: [] for tag in gtags} + for mdir in self.mtree.mdirs(): + for tag in gtags: + # gcksumdelta is a bit special since it's outside the + # rbyd tree + if tag == TAG_GCKSUMDELTA: + gdelta = mdir.gcksumdelta + else: + gdelta = mdir.rbyd.lookup(-1, tag) + if gdelta is not None: + gdeltas[tag].append((mdir.mid, gdelta)) + + for tag in gtags: + # xor to find gstate + yield self._parse(tag, gdeltas[tag]) + + # common gstate operations + class Gstate: + tag = None + mask = None + rcompat = None + wcompat = None + ocompat = None + + def __init__(self, mtree, config, tag, gdeltas): + # replace tag with what we find + self.tag = tag + # keep track of gdeltas for debugging + self.gdeltas = gdeltas + + # xor together to build our gstate + data = bytes() + for mid, gdelta in gdeltas: + data = bytes( + a^b for a, b in it.zip_longest( + data, gdelta.data, + fillvalue=0)) + self.data = data + + # check compat flags while we can access config + if self.rcompat is not None: + self.rcompat = self.rcompat & ( + int(config.rcompat) if config.rcompat is not None + else 0) + if self.wcompat is not None: + self.wcompat = self.wcompat & ( + int(config.wcompat) if config.wcompat is not None + else 0) + if self.ocompat is not None: + self.ocompat = self.ocompat & ( + int(config.ocompat) if config.ocompat is not None + else 0) + + @property + def blocks(self): + return tuple(it.chain.from_iterable( + gdelta.blocks for _, gdelta in self.gdeltas)) + + # true unless compat flags are missing + def __bool__(self): + return (self.rcompat != 0 + and self.wcompat != 0 + and self.ocompat != 0) + + @property + def size(self): + return len(self.data) + + def __bytes__(self): + return self.data + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, self.repr()) + + def repr(self): + return Tag.repr(self.tag, 0, self.size, global_=True) + + def __iter__(self): + return iter((self.tag, self.data)) + + def __eq__(self, other): + return (self.tag, self.data) == (other.tag, other.data) + + def __ne__(self, other): + return (self.tag, self.data) != (other.tag, other.data) + + def __hash__(self): + return hash((self.tag, self.data)) + + # marker class for unknown gstate + class Unknown(Gstate): + pass + + # special handling for known gstate + + # the global-checksum, cubed + class Gcksum(Gstate): + tag = TAG_GCKSUMDELTA + wcompat = WCOMPAT_GCKSUM + + def __init__(self, mtree, config, tag, gdeltas): + super().__init__(mtree, config, tag, gdeltas) + self.gcksum = fromle32(self.data) + + def __int__(self): + return self.gcksum + + def repr(self): + return 'gcksum %08x' % self.gcksum + + # any global-removes + class Grm(Gstate): + tag = TAG_GRMDELTA + rcompat = RCOMPAT_GRM + + def __init__(self, mtree, config, tag, gdeltas): + super().__init__(mtree, config, tag, gdeltas) + queue = [] + d = 0 + for _ in range(2): + mid, d_ = fromleb128(self.data, d); d += d_ + # a null mid (mid=0.0) terminates the grm queue + if not mid: + break + mid = mtree.mid(mid) + # map mbids -> -1 if mroot-inlined + if mtree.mtree is None: + mid = mtree.mid(-1, mid.mrid) + queue.append(mid) + self.queue = queue + + def repr(self): + if self: + return 'grm [%s]' % ', '.join( + mid.repr() for mid in self.queue) + else: + return 'grm (unused)' + + # the global block map + class Gbmap(Gstate): + tag = TAG_GBMAPDELTA + wcompat = WCOMPAT_GBMAP + + def __init__(self, mtree, config, tag, gdeltas): + super().__init__(mtree, config, tag, gdeltas) + d = 0 + self.window, d_ = fromleb128(self.data, d); d += d_ + self.known, d_ = fromleb128(self.data, d); d += d_ + block, trunk, cksum, d_ = frombranch(self.data, d); d += d_ + self.btree = Btree.fetchck( + mtree.bd, block, trunk, + config.geometry.block_count + if config.geometry is not None else 0, + cksum) + + def repr(self): + if self: + return 'gbmap %s 0x%x %d' % ( + self.btree.addr(), + self.window, self.known) + else: + return 'gbmap (unused)' + + # keep track of known gstate + _known = [g for g in Gstate.__subclasses__() if g.tag is not None] + + # parse if known + def _parse(self, tag, gdeltas): + # known config? + for g in self._known: + if (g.tag & ~(g.mask or 0)) == (tag & ~(g.mask or 0)): + return g(self.mtree, self.config, tag, gdeltas) + # otherwise return a marker class + else: + return self.Unknown(self.mtree, self.config, tag, gdeltas) + + # create cached accessors for known gstate + def _parser(g): + def _parser(self): + return self.lookup(g.tag, g.mask) + return _parser + + for g in _known: + locals()[g.__name__.lower()] = ft.cached_property(_parser(g)) + + +# high-level littlefs representation +class Lfs3: + def __init__(self, bd, mtree, config=None, gstate=None, cksum=None, *, + corrupt=False): + self.bd = bd + self.mtree = mtree + + # create lazy config/gstate objects + self.config = config or Config(self.mroot) + self.gstate = gstate or Gstate(self.mtree, self.config) + + # go ahead and fetch some expected fields + self.version = self.config.version + self.rcompat = self.config.rcompat + self.wcompat = self.config.wcompat + self.ocompat = self.config.ocompat + if self.config.geometry is not None: + self.block_count = self.config.geometry.block_count + self.block_size = self.config.geometry.block_size + else: + self.block_count = self.bd.block_count + self.block_size = self.bd.block_size + + # calculate on-disk gcksum + if cksum is None: + cksum = 0 + for mdir in self.mtree.mdirs(): + cksum ^= mdir.cksum + self.cksum = cksum + + # is the filesystem corrupt? + self.corrupt = corrupt + + # create the root directory, this is a bit of a special case + self.root = self.Root(self) + + # mbits is a static value derived from the block_size + @staticmethod + def mbits_(block_size): + return Mtree.mbits_(block_size) + + @property + def mbits(self): + return self.mtree.mbits + + # convenience function for creating mbits-dependent mids + def mid(self, mbid, mrid=None): + return self.mtree.mid(mbid, mrid) + + # most of our fields map to the mtree + @property + def block(self): + return self.mroot.block + + @property + def blocks(self): + return self.mroot.blocks + + @property + def trunk(self): + return self.mroot.trunk + + @property + def rev(self): + return self.mroot.rev + + @property + def weight(self): + return self.mtree.weight + + @property + def mbweight(self): + return self.mtree.mbweight + + @property + def mrweight(self): + return self.mtree.mrweight + + def mbweightrepr(self): + return self.mtree.mbweightrepr() + + def mrweightrepr(self): + return self.mtree.mrweightrepr() + + @property + def mrootchain(self): + return self.mtree.mrootchain + + @property + def mrootanchor(self): + return self.mtree.mrootanchor + + @property + def mroot(self): + return self.mtree.mroot + + def addr(self): + return self.mroot.addr() + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, self.repr()) + + def repr(self): + return 'littlefs v%s.%s %sx%s %s w%s.%s' % ( + self.version.major if self.version is not None else '?', + self.version.minor if self.version is not None else '?', + self.block_size if self.block_size is not None else '?', + self.block_count if self.block_count is not None else '?', + self.addr(), + self.mbweightrepr(), self.mrweightrepr()) + + def __bool__(self): + return not self.corrupt + + def __eq__(self, other): + return self.mrootanchor == other.mrootanchor + + def __ne__(self, other): + return self.mrootanchor != other.mrootanchor + + def __hash__(self): + return hash(self.mrootanchor) + + @classmethod + def fetch(cls, bd, blocks=None, trunk=None, *, + depth=None, + no_ck=False, + no_ckmroot=False, + no_ckmagic=False, + no_ckgcksum=False): + # Mtree does most of the work here + mtree = Mtree.fetch(bd, blocks, trunk, + depth=depth) + + # create lfs object + lfs = cls(bd, mtree) + + # don't check anything? + if no_ck: + return lfs + + # check mroot + if (not no_ckmroot + and not lfs.corrupt + and not lfs.ckmroot()): + lfs.corrupt = True + + # check magic + if (not no_ckmagic + and not lfs.corrupt + and not lfs.ckmagic()): + lfs.corrupt = True + + # check gcksum + if (not no_ckgcksum + and not lfs.corrupt + and not lfs.ckgcksum()): + lfs.corrupt = True + + return lfs + + # check that the mroot is valid + def ckmroot(self): + return bool(self.mroot) + + # check that the magic string is littlefs + def ckmagic(self): + if self.config.magic is None: + return False + return self.config.magic.data == b'littlefs' + + # check that the gcksum checks out + def ckgcksum(self): + return crc32ccube(self.cksum) == int(self.gstate.gcksum) + + # read custom attrs + def uattrs(self): + return self.mroot.rattrs(-1, TAG_UATTR, 0xff) + + def sattrs(self): + return self.mroot.rattrs(-1, TAG_SATTR, 0xff) + + def attrs(self): + yield from self.uattrs() + yield from self.sattrs() + + # is file in grm queue? + def grmed(self, mid): + if not isinstance(mid, Mid): + mid = self.mid(mid) + + return mid in self.gstate.grm.queue + + # lookup operations + def lookup(self, mid, mdir=None, *, + all=False): + import builtins + all_, all = all, builtins.all + + # is this mid grmed? + if not all_ and self.grmed(mid): + return None + + if mdir is None: + mdir, name = self.mtree.lookup(mid) + if mdir is None: + return None + else: + name = mdir.lookup(mid) + + # stickynote? + if not all_ and name.tag == TAG_STICKYNOTE: + return None + + return self._open(mid, mdir, name.tag, name) + + def namelookup(self, did, name, *, + all=False): + import builtins + all_, all = all, builtins.all + + mid_, mdir_, name_ = self.mtree.namelookup(did, name) + if mid_ is None: + return None + + # is this mid grmed? + if not all_ and self.grmed(mid_): + return None + + # stickynote? + if not all_ and name_.tag == TAG_STICKYNOTE: + return None + + return self._open(mid_, mdir_, name_.tag, name_) + + class PathError(Exception): + pass + + # split a path into its components + # + # note this follows littlefs's internal logic, so dots and dotdot + # entries get resolved _before_ walking the path + @staticmethod + def pathsplit(path): + path_ = path + if isinstance(path_, str): + path_ = path_.encode('utf8') + + # empty path? + if path_ == b'': + raise Lfs3.PathError("invalid path: %r" % path) + + path__ = [] + for p in path_.split(b'/'): + # skip multiple slashes and dots + if p == b'' or p == b'.': + continue + path__.append(p) + path_ = path__ + + # resolve dotdots + path__ = [] + dotdots = 0 + for p in reversed(path_): + if p == b'..': + dotdots += 1 + elif dotdots: + dotdots -= 1 + else: + path__.append(p) + if dotdots: + raise Lfs3.PathError("invalid path: %r" % path) + path__.reverse() + path_ = path__ + + return path_ + + def pathlookup(self, did, path_=None, *, + all=False, + path=False, + depth=None): + import builtins + all_, all = all, builtins.all + + # default to the root directory + if path_ is None: + did, path_ = 0, did + # parse/split the path + if isinstance(path_, (bytes, str)): + path_ = self.pathsplit(path_) + + # start at the root dir + dir = self.root + did = did + if path or depth: + path__ = [] + + for p in path_: + # lookup the next file + file = self.namelookup(did, p, + all=all_) + if file is None: + if path: + return None, path__ + else: + return None + + # file? done? + if not file.recursable: + if path: + return file, path__ + else: + return file + + # recurse down the file tree + dir = file + did = dir.did + if path or depth: + path__.append(dir) + # stop here? + if depth and len(path__) >= depth: + if path: + return None, path__ + else: + return None + + if path: + return dir, path__ + else: + return dir + + def files(self, did=None, *, + all=False, + path=False, + depth=None): + import builtins + all_, all = all, builtins.all + + # default to the root directory + did = did or self.root.did + + # start with the bookmark entry + mid, mdir, name = self.mtree.namelookup(did, b'') + # no bookmark? weird + if mid is None: + return + + # iterate over files until we find a different did + while name.did == did: + # yield file, hiding grms, stickynotes, etc, by default + if all_ or (not self.grmed(mid) + and not name.tag == TAG_BOOKMARK + and not name.tag == TAG_STICKYNOTE): + file = self._open(mid, mdir, name.tag, name) + if path: + yield file, [] + else: + yield file + + # recurse? + if (file.recursable + and depth is not None + and (depth == 0 or depth > 1)): + for r in self.files(file.did, + all=all_, + path=path, + depth=depth-1 if depth else 0): + if path: + file_, path_ = r + yield file_, [file]+path_ + else: + file_ = r + yield file_ + + # increment mid and find the next mdir if needed + mbid, mrid = mid.mbid, mid.mrid + 1 + if mrid == mdir.weight: + mbid, mrid = mbid + (1 << self.mbits), 0 + mdir = self.mtree.lookupnext_(mbid) + if mdir is None: + break + # lookup name and adjust rid if necessary, you don't + # normally need to do this, but we don't want the iteration + # to terminate early on a corrupt filesystem + mrid, name = mdir.rbyd.lookupnext(mrid) + if mrid is None: + break + mid = self.mid(mbid, mrid) + + def orphans(self, + all=False): + import builtins + all_, all = all, builtins.all + + # first find all reachable dids + dids = {self.root.did} + for file in self.files(depth=mt.inf): + if file.recursable: + dids.add(file.did) + + # then iterate over all dids and yield any that aren't reachable + for mid, mdir, name in self.mtree.mids(): + # is this mid grmed? + if not all_ and self.grmed(mid): + continue + + # stickynote? + if not all_ and name.tag == TAG_STICKYNOTE: + continue + + # unreachable? note this lazily parses the did + if name.did not in dids: + file = self._open(mid, mdir, name.tag, name) + # mark as orphaned + file.orphaned = True + yield file + + # traverse the filesystem + def traverse(self, *, + mtree_only=False, + gstate=True, + shrubs=False, + fragments=False, + path=False): + # traverse the mtree + for r in self.mtree.traverse( + path=path): + if path: + mdir, path_ = r + else: + mdir = r + + # mdir? + if isinstance(mdir, Mdir): + if path: + yield mdir, path_ + else: + yield mdir + + # btree node? we only care about the rbyd for simplicity + else: + bid, rbyd = mdir + if path: + yield rbyd, path_ + else: + yield rbyd + + # traverse file bshrubs/btrees + if not mtree_only and isinstance(mdir, Mdir): + for mid, name in mdir.mids(): + file = self._open(mid, mdir, name.tag, name) + for r in file.traverse( + path=path): + if path: + pos, data, path__ = r + path__ = [(mid, mdir, name)]+path__ + else: + pos, data = r + + # inlined data? we usually ignore these + if isinstance(data, Rattr): + if fragments: + if path: + yield data, path_+path__ + else: + yield data + # block pointer? + elif isinstance(data, Bptr): + if path: + yield data, path_+path__ + else: + yield data + # bshrub/btree node? we only care about the rbyd + # for simplicity, we also usually ignore shrubs + # since these live the the parent mdir + else: + if shrubs or not data.shrub: + if path: + yield data, path_+path__ + else: + yield data + + # traverse any gstate + if not mtree_only and gstate: + for gstate_ in self.gstate: + if not gstate_ or getattr(gstate_, 'btree', None) is None: + continue + + for r in gstate_.btree.traverse( + path=path): + if path: + bid, rbyd, path_ = r + else: + bid, rbyd = r + + if path: + yield rbyd, [(self.mid(-1), gstate_)]+path_ + else: + yield rbyd + + # common file operations, note Reg extends this for regular files + class File: + tag = None + mask = None + internal = False + recursable = False + grmed = False + orphaned = False + + def __init__(self, lfs, mid, mdir, tag, name): + self.lfs = lfs + self.mid = mid + self.mdir = mdir + # replace tag with what we find + self.tag = tag + self.name = name + + # fetch the file structure if there is one + self.struct = mdir.lookup(mid, TAG_STRUCT, 0xff) + + # bshrub/btree? + self.bshrub = None + if (self.struct is not None + and (self.struct.tag & ~0x3) == TAG_BSHRUB): + weight, trunk, _ = fromshrub(self.struct.data) + self.bshrub = Btree.fetchshrub(lfs.bd, mdir.rbyd, trunk) + elif (self.struct is not None + and (self.struct.tag & ~0x3) == TAG_BTREE): + weight, block, trunk, cksum, _ = frombtree(self.struct.data) + self.bshrub = Btree.fetchck( + lfs.bd, block, trunk, weight, cksum) + + # did? + self.did = None + if (self.struct is not None + and self.struct.tag == TAG_DID): + self.did, _ = fromleb128(self.struct.data) + + # some other info that is useful for scripts + + # mark as grmed if grmed + if lfs.grmed(mid): + self.grmed = True + + @property + def size(self): + if self.bshrub is not None: + return self.bshrub.weight + else: + return 0 + + def structrepr(self): + if self.struct is not None: + # inlined bshrub? + if (self.struct.tag & ~0x3) == TAG_BSHRUB: + return 'bshrub %s' % self.bshrub.addr() + # btree? + elif (self.struct.tag & ~0x3) == TAG_BTREE: + return 'btree %s' % self.bshrub.addr() + # btree? + else: + return self.struct.repr() + else: + return '' + + def __repr__(self): + return '<%s %s.%s %s>' % ( + self.__class__.__name__, + self.mid.mbidrepr(), self.mid.mridrepr(), + self.repr()) + + def repr(self): + return 'type 0x%02x%s' % ( + self.tag & 0xff, + ', %s' % self.structrepr() + if self.struct is not None else '') + + def __eq__(self, other): + return self.mid == other.mid + + def __ne__(self, other): + return self.mid != other.mid + + def __hash__(self): + return hash(self.mid) + + # read attrs, note this includes _all_ attrs + def rattrs(self): + return self.mdir.rattrs(self.mid) + + # read custom attrs + def uattrs(self): + return self.mdir.rattrs(self.mid, TAG_UATTR, 0xff) + + def sattrs(self): + return self.mdir.rattrs(self.mid, TAG_SATTR, 0xff) + + def attrs(self): + yield from self.uattrs() + yield from self.sattrs() + + # lookup data in the underlying bshrub + def _lookupnext_(self, pos, *, + path=False, + depth=None): + # no bshrub? + if self.bshrub is None: + if path: + return None, None, [] + else: + return None, None + + # lookup data in our bshrub + r = self.bshrub.lookupnext_(pos, + path=path or depth, + depth=depth) + if path or depth: + bid, rbyd, rid, rattr, path_ = r + else: + bid, rbyd, rid, rattr = r + if bid is None: + if path: + return None, None, path_ + else: + return None, None + + # corrupt btree node? + if not rbyd: + if path: + return bid-(rbyd.weight-1), rbyd, path_ + else: + return bid-(rbyd.weight-1), rbyd + + # stop here? + if depth and len(path_) >= depth: + if path: + return bid-(rattr.weight-1), rbyd, path_ + else: + return bid-(rattr.weight-1), rbyd + + # inlined data? + if (rattr.tag & ~0x1003) == TAG_DATA: + if path: + return bid-(rattr.weight-1), rattr, path_ + else: + return bid-(rattr.weight-1), rattr + # block pointer? + elif (rattr.tag & ~0x1003) == TAG_BLOCK: + size, block, off, cksize, cksum, _ = frombptr(rattr.data) + bptr = Bptr.fetchck(self.lfs.bd, rattr, + block, off, size, cksize, cksum) + if path: + return bid-(rattr.weight-1), bptr, path_ + else: + return bid-(rattr.weight-1), bptr + # uh oh, something is broken + else: + if path: + return bid-(rattr.weight-1), rattr, path_ + else: + return bid-(rattr.weight-1), rattr + + def lookupnext_(self, pos, *, + data_only=True, + path=False, + depth=None): + r = self._lookupnext_(pos, + path=path, + depth=depth) + if path: + pos, data, path_ = r + else: + pos, data = r + if pos is None or ( + data_only and not isinstance(data, (Rattr, Bptr))): + if path: + return None, None, path_ + else: + return None, None + + if path: + return pos, data, path_ + else: + return pos, data + + def _leaves(self, *, + path=False, + depth=None): + pos = 0 + while True: + r = self.lookupnext_(pos, + data_only=False, + path=path, + depth=depth) + if path: + pos, data, path_ = r + else: + pos, data = r + if pos is None: + break + + # data? + if isinstance(data, (Rattr, Bptr)): + if path: + yield pos, data, path_ + else: + yield pos, data + pos += data.weight + # btree node? + else: + rbyd = data + if path: + yield (pos, rbyd, + # path tail is usually redundant unless corrupt + path_[:-1] + if path_ and path_[-1][1] == rbyd + else path_) + else: + yield pos, rbyd + pos += rbyd.weight + + def leaves(self, *, + data_only=False, + path=False, + depth=None): + for r in self._leaves( + path=path, + depth=depth): + if path: + pos, data, path_ = r + else: + pos, data = r + if data_only and not isinstance(data, (Rattr, Bptr)): + continue + + if path: + yield pos, data, path_ + else: + yield pos, data + + def _traverse(self, *, + path=False, + depth=None): + ptrunk_ = [] + for pos, data, path_ in self.leaves( + path=True, + depth=depth): + # we only care about the data/rbyds here + trunk_ = ([(bid_-rid_, rbyd_) + for bid_, rbyd_, rid_, name_ in path_] + + [(pos, data)]) + for d, (pos, data) in pathdelta( + trunk_, ptrunk_): + # but include branch rids in path if requested + if path: + yield pos, data, path_[:d] + else: + yield pos, data + ptrunk_ = trunk_ + + def traverse(self, *, + data_only=False, + path=False, + depth=None): + for r in self._traverse( + path=path, + depth=depth): + if path: + pos, data, path_ = r + else: + pos, data = r + if data_only and not isinstance(data, (Rattr, Bptr)): + continue + + if path: + yield pos, data, path_ + else: + yield pos, data + + def datas(self, *, + data_only=True, + path=False, + depth=None): + return self.leaves( + data_only=data_only, + path=path, + depth=depth) + + # some convience operations for reading data + def bytes(self, *, + depth=None): + for pos, data in self.datas(depth=depth): + if data.size > 0: + yield data.data + if data.weight > data.size: + yield b'\0' * (data.weight-data.size) + + def read(self, *, + depth=None): + return b''.join(self.bytes()) + + # bleh, with that out of the way, here are our known file types + + # regular files + class Reg(File): + tag = TAG_REG + + def repr(self): + return 'reg %s%s' % ( + self.size, + ', %s' % self.structrepr() + if self.struct is not None else '') + + # directories + class Dir(File): + tag = TAG_DIR + + def __init__(self, lfs, mid, mdir, tag, name): + super().__init__(lfs, mid, mdir, tag, name) + + # we're recursable if we're a non-grmed directory with a did + if (isinstance(self, Lfs3.Dir) + and not self.grmed + and self.did is not None): + self.recursable = True + + def repr(self): + return 'dir %s%s' % ( + '0x%x' % self.did + if self.did is not None else '?', + ', %s' % self.structrepr() + if self.struct is not None + and self.struct.tag != TAG_DID else '') + + # provide some convenient filesystem access relative to our did + def namelookup(self, name, **args): + if self.did is None: + return None + return self.lfs.namelookup(self.did, name, **args) + + def pathlookup(self, path_, **args): + if self.did is None: + if args.get('path'): + return None, [] + else: + return None + return self.lfs.pathlookup(self.did, path_, **args) + + def files(self, **args): + if self.did is None: + return iter(()) + return self.lfs.files(self.did, **args) + + # root is a bit special + class Root(Dir): + tag = None + + def __init__(self, lfs): + # root always has mid=-1 and did=0 + super().__init__(lfs, lfs.mid(-1), lfs.mroot, TAG_DIR, None) + self.did = 0 + self.recursable = True + + def repr(self): + return 'root' + + # bookmarks keep track of where directories start + class Bookmark(File): + tag = TAG_BOOKMARK + internal = True + + def repr(self): + return 'bookmark %s%s' % ( + '0x%x' % self.name.did + if self.name.did is not None else '?', + ', %s' % self.structrepr() + if self.struct is not None else '') + + # stickynotes, i.e. uncommitted files, behave the same as files + # for the most part + class Stickynote(File): + tag = TAG_STICKYNOTE + internal = True + + def repr(self): + return 'stickynote%s' % ( + ' %s, %s' % (self.size, self.structrepr()) + if self.struct is not None else '') + + # marker class for unknown file types + class Unknown(File): + pass + + # keep track of known file types + _known = [f for f in File.__subclasses__() if f.tag is not None] + + # fetch/parse state if known + def _open(self, mid, mdir, tag, name): + # known file type? + tag = name.tag + for f in self._known: + if (f.tag & ~(f.mask or 0)) == (tag & ~(f.mask or 0)): + return f(self, mid, mdir, tag, name) + # otherwise return a marker class + else: + return self.Unknown(self, mid, mdir, tag, name) + + +# a representation of optionally key-mapped attrs +class CsvAttr: + def __init__(self, attrs, defaults=None): + if attrs is None: + attrs = [] + if isinstance(attrs, dict): + attrs = attrs.items() + + # normalize + self.attrs = [] + self.keyed = co.OrderedDict() + for attr in attrs: + if not isinstance(attr, tuple): + attr = ((), attr) + if attr[0] in {None, (), (None,), ('*',)}: + attr = ((), attr[1]) + if not isinstance(attr[0], tuple): + attr = ((attr[0],), attr[1]) + + self.attrs.append(attr) + if attr[0] not in self.keyed: + self.keyed[attr[0]] = [] + self.keyed[attr[0]].append(attr[1]) + + # create attrs object for defaults + if isinstance(defaults, CsvAttr): + self.defaults = defaults + elif defaults is not None: + self.defaults = CsvAttr(defaults) + else: + self.defaults = None + + def __repr__(self): + if self.defaults is None: + return 'CsvAttr(%r)' % ( + [(','.join(attr[0]), attr[1]) + for attr in self.attrs]) + else: + return 'CsvAttr(%r, %r)' % ( + [(','.join(attr[0]), attr[1]) + for attr in self.attrs], + [(','.join(attr[0]), attr[1]) + for attr in self.defaults.attrs]) + + def __iter__(self): + if () in self.keyed: + return it.cycle(self.keyed[()]) + elif self.defaults is not None: + return iter(self.defaults) + else: + return iter(()) + + def __bool__(self): + return bool(self.attrs) + + def __getitem__(self, key): + if isinstance(key, tuple): + if len(key) > 0 and not isinstance(key[0], str): + i, key = key + if not isinstance(key, tuple): + key = (key,) + else: + i, key = 0, key + elif isinstance(key, str): + i, key = 0, (key,) + else: + i, key = key, () + + # try to lookup by key + best = None + for ks, vs in self.keyed.items(): + prefix = [] + for j, k in enumerate(ks): + if j < len(key) and fnmatch.fnmatchcase(key[j], k): + prefix.append(k) + else: + prefix = None + break + + if prefix is not None and ( + best is None or len(prefix) >= len(best[0])): + best = (prefix, vs) + + if best is not None: + # cycle based on index + return best[1][i % len(best[1])] + + # fallback to defaults? + if self.defaults is not None: + return self.defaults[i, key] + + raise KeyError(i, key) + + def get(self, key, default=None): + try: + return self.__getitem__(key) + except KeyError: + return default + + def __contains__(self, key): + try: + self.__getitem__(key) + return True + except KeyError: + return False + + # get all results for a given key + def getall(self, key, default=None): + if not isinstance(key, tuple): + key = (key,) + + # try to lookup by key + best = None + for ks, vs in self.keyed.items(): + prefix = [] + for j, k in enumerate(ks): + if j < len(key) and fnmatch.fnmatchcase(key[j], k): + prefix.append(k) + else: + prefix = None + break + + if prefix is not None and ( + best is None or len(prefix) >= len(best[0])): + best = (prefix, vs) + + if best is not None: + return best[1] + + # fallback to defaults? + if self.defaults is not None: + return self.defaults.getall(key, default) + + raise default + + # a key function for sorting by key order + def key(self, key): + if not isinstance(key, tuple): + key = (key,) + + best = None + for i, ks in enumerate(self.keyed.keys()): + prefix = [] + for j, k in enumerate(ks): + if j < len(key) and (not k or key[j] == k): + prefix.append(k) + else: + prefix = None + break + + if prefix is not None and ( + best is None or len(prefix) >= len(best[0])): + best = (prefix, i) + + if best is not None: + return best[1] + + # fallback to defaults? + if self.defaults is not None: + return len(self.keyed) + self.defaults.key(key) + + return len(self.keyed) + +# SI-prefix formatter +def si(x): + if x == 0: + return '0' + # figure out prefix and scale + p = 3*mt.floor(mt.log(abs(x), 10**3)) + p = min(18, max(-18, p)) + # format with 3 digits of precision + s = '%.3f' % (abs(x) / (10.0**p)) + s = s[:3+1] + # truncate but only digits that follow the dot + if '.' in s: + s = s.rstrip('0') + s = s.rstrip('.') + return '%s%s%s' % ('-' if x < 0 else '', s, SI_PREFIXES[p]) + +# SI-prefix formatter for powers-of-two +def si2(x): + if x == 0: + return '0' + # figure out prefix and scale + p = 10*mt.floor(mt.log(abs(x), 2**10)) + p = min(30, max(-30, p)) + # format with 3 digits of precision + s = '%.3f' % (abs(x) / (2.0**p)) + s = s[:3+1] + # truncate but only digits that follow the dot + if '.' in s: + s = s.rstrip('0') + s = s.rstrip('.') + return '%s%s%s' % ('-' if x < 0 else '', s, SI2_PREFIXES[p]) + +# parse %-escaped strings +# +# attrs can override __getitem__ for lazy attr generation +def punescape(s, attrs=None): + pattern = re.compile( + '%[%n]' + '|' '%x..' + '|' '%u....' + '|' '%U........' + '|' '%\((?P[^)]*)\)' + '(?P[+\- #0-9\.]*[siIdboxXfFeEgG])') + def unescape(m): + if m.group()[1] == '%': return '%' + elif m.group()[1] == 'n': return '\n' + elif m.group()[1] == 'x': return chr(int(m.group()[2:], 16)) + elif m.group()[1] == 'u': return chr(int(m.group()[2:], 16)) + elif m.group()[1] == 'U': return chr(int(m.group()[2:], 16)) + elif m.group()[1] == '(': + if attrs is not None: + try: + v = attrs[m.group('field')] + except KeyError: + return m.group() + else: + return m.group() + f = m.group('format') + if f[-1] in 'dboxX': + if isinstance(v, str): + v = dat(v, 0) + v = int(v) + elif f[-1] in 'iIfFeEgG': + if isinstance(v, str): + v = dat(v, 0) + v = float(v) + if f[-1] in 'iI': + v = (si if 'i' in f[-1] else si2)(v) + f = f.replace('i', 's').replace('I', 's') + if '+' in f and not v.startswith('-'): + v = '+'+v + f = f.replace('+', '').replace('-', '') + else: + f = ('<' if '-' in f else '>') + f.replace('-', '') + v = str(v) + # note we need Python's new format syntax for binary + return ('{:%s}' % f).format(v) + else: assert False + + return re.sub(pattern, unescape, s) + + +# naive space filling curve (the default) +def naive_curve(width, height): + for y in range(height): + for x in range(width): + yield x, y + +# space filling Hilbert-curve +def hilbert_curve(width, height): + # based on generalized Hilbert curves: + # https://github.com/jakubcerveny/gilbert + # + def hilbert_(x, y, a_x, a_y, b_x, b_y): + w = abs(a_x+a_y) + h = abs(b_x+b_y) + a_dx = -1 if a_x < 0 else +1 if a_x > 0 else 0 + a_dy = -1 if a_y < 0 else +1 if a_y > 0 else 0 + b_dx = -1 if b_x < 0 else +1 if b_x > 0 else 0 + b_dy = -1 if b_y < 0 else +1 if b_y > 0 else 0 + + # trivial row + if h == 1: + for _ in range(w): + yield x, y + x, y = x+a_dx, y+a_dy + return + + # trivial column + if w == 1: + for _ in range(h): + yield x, y + x, y = x+b_dx, y+b_dy + return + + a_x_, a_y_ = a_x//2, a_y//2 + b_x_, b_y_ = b_x//2, b_y//2 + w_ = abs(a_x_+a_y_) + h_ = abs(b_x_+b_y_) + + if 2*w > 3*h: + # prefer even steps + if w_ % 2 != 0 and w > 2: + a_x_, a_y_ = a_x_+a_dx, a_y_+a_dy + + # split in two + yield from hilbert_( + x, y, + a_x_, a_y_, b_x, b_y) + yield from hilbert_( + x+a_x_, y+a_y_, + a_x-a_x_, a_y-a_y_, b_x, b_y) + else: + # prefer even steps + if h_ % 2 != 0 and h > 2: + b_x_, b_y_ = b_x_+b_dx, b_y_+b_dy + + # split in three + yield from hilbert_( + x, y, + b_x_, b_y_, a_x_, a_y_) + yield from hilbert_( + x+b_x_, y+b_y_, + a_x, a_y, b_x-b_x_, b_y-b_y_) + yield from hilbert_( + x+(a_x-a_dx)+(b_x_-b_dx), y+(a_y-a_dy)+(b_y_-b_dy), + -b_x_, -b_y_, -(a_x-a_x_), -(a_y-a_y_)) + + if width >= height: + yield from hilbert_(0, 0, +width, 0, 0, +height) + else: + yield from hilbert_(0, 0, 0, +height, +width, 0) + +# space filling Z-curve/Lebesgue-curve +def lebesgue_curve(width, height): + # we create a truncated Z-curve by simply filtering out the + # points that are outside our region + for i in range(2**(2*mt.ceil(mt.log2(max(width, height))))): + # we just operate on binary strings here because it's easier + b = '{:0{}b}'.format(i, 2*mt.ceil(mt.log2(i+1)/2)) + x = int(b[1::2], 2) if b[1::2] else 0 + y = int(b[0::2], 2) if b[0::2] else 0 + if x < width and y < height: + yield x, y + + +# an abstract block representation +class BmapBlock: + def __init__(self, block, type='unused', value=None, usage=range(0), *, + siblings=None, children=None, + x=None, y=None, width=None, height=None): + self.block = block + self.type = type + self.value = value + self.usage = usage + self.siblings = siblings if siblings is not None else set() + self.children = children if children is not None else set() + self.x = x + self.y = y + self.width = width + self.height = height + + def __repr__(self): + return 'BmapBlock(0x%x, %r, x=%s, y=%s, width=%s, height=%s)' % ( + self.block, + self.type, + self.x, self.y, self.width, self.height) + + def __eq__(self, other): + return self.block == other.block + + def __ne__(self, other): + return self.block != other.block + + def __hash__(self): + return hash(self.block) + + def __lt__(self, other): + return self.block < other.block + + def __le__(self, other): + return self.block <= other.block + + def __gt__(self, other): + return self.block > other.block + + def __ge__(self, other): + return self.block >= other.block + + # align to pixel boundaries + def align(self): + # this extra +0.1 and using points instead of width/height is + # to help minimize rounding errors + x0 = int(self.x+0.1) + y0 = int(self.y+0.1) + x1 = int(self.x+self.width+0.1) + y1 = int(self.y+self.height+0.1) + self.x = x0 + self.y = y0 + self.width = x1 - x0 + self.height = y1 - y0 + + # generate a label + @ft.cached_property + def label(self): + if self.type == 'mdir': + return '%s %s %s w%s\ncksum %08x' % ( + self.type, + self.value.mid.mbidrepr(), + self.value.addr(), + self.value.weight, + self.value.cksum) + elif self.type == 'btree': + return '%s %s w%s\ncksum %08x' % ( + self.type, + self.value.addr(), + self.value.weight, + self.value.cksum) + elif self.type == 'data': + return '%s %s %s\ncksize %s\ncksum %08x' % ( + self.type, + '0x%x.%x' % (self.block, self.value.off), + self.value.size, + self.value.cksize, + self.value.cksum) + elif self.type != 'unused': + return '%s\n%s' % ( + self.type, + '0x%x' % self.block) + else: + return '' + + # generate attrs for punescaping + @ft.cached_property + def attrs(self): + if self.type == 'mdir': + return { + 'block': self.block, + 'type': self.type, + 'addr': self.value.addr(), + 'trunk': self.value.trunk, + 'weight': self.value.weight, + 'cksum': self.value.cksum, + 'usage': len(self.usage), + } + elif self.type == 'btree': + return { + 'block': self.block, + 'type': self.type, + 'addr': self.value.addr(), + 'trunk': self.value.trunk, + 'weight': self.value.weight, + 'cksum': self.value.cksum, + 'usage': len(self.usage), + } + elif self.type == 'data': + return { + 'block': self.block, + 'type': self.type, + 'addr': self.value.addr(), + 'off': self.value.off, + 'size': self.value.size, + 'cksize': self.value.cksize, + 'cksum': self.value.cksum, + 'usage': len(self.usage), + } + else: + return { + 'block': self.block, + 'type': self.type, + 'usage': len(self.usage), + } + + + +def main(disk, output, mroots=None, *, + trunk=None, + block_size=None, + block_count=None, + blocks=None, + no_ckmeta=False, + no_ckdata=False, + mtree_only=False, + quiet=False, + labels=[], + colors=[], + width=None, + height=None, + block_cols=None, + block_rows=None, + block_ratio=None, + no_header=False, + no_mode=False, + hilbert=False, + lebesgue=False, + no_javascript=False, + mode_tree=False, + mode_branches=False, + mode_references=False, + mode_redund=False, + to_scale=None, + to_ratio=1/1, + tiny=False, + title=None, + title_littlefs=False, + title_usage=False, + padding=None, + no_label=False, + dark=False, + font=FONT, + font_size=FONT_SIZE, + background=None, + **args): + # tiny mode? + if tiny: + if block_ratio is None: + block_ratio = 1 + if to_scale is None: + to_scale = 1 + if padding is None: + padding = 0 + no_header = True + no_label = True + no_javascript = True + + if block_ratio is None: + # golden ratio + block_ratio = 1 / ((1 + mt.sqrt(5))/2) + + if padding is None: + padding = 1 + + # default to all modes + if (not mode_tree + and not mode_branches + and not mode_references + and not mode_redund): + mode_tree = True + mode_branches = True + mode_references = True + mode_redund = True + + # what colors/labels to use? + colors_ = CsvAttr(colors, defaults=COLORS_DARK if dark else COLORS) + + labels_ = CsvAttr(labels) + + if background is not None: + background_ = background + elif dark: + background_ = '#000000' + else: + background_ = '#ffffff' + + # figure out width/height + if width is not None: + width_ = width + else: + width_ = WIDTH + + if height is not None: + height_ = height + else: + height_ = HEIGHT + + # is bd geometry specified? + if isinstance(block_size, tuple): + block_size, block_count_ = block_size + if block_count is None: + block_count = block_count_ + + # flatten mroots, default to 0x{0,1} + mroots = list(it.chain.from_iterable(mroots)) if mroots else [0, 1] + + # mroots may also encode trunks + mroots, trunk = ( + [block[0] if isinstance(block, tuple) + else block + for block in mroots], + trunk if trunk is not None + else ft.reduce( + lambda x, y: y, + (block[1] for block in mroots + if isinstance(block, tuple)), + None)) + + # we seek around a bunch, so just keep the disk open + with open(disk, 'rb') as f: + # if block_size is omitted, assume the block device is one big block + if block_size is None: + f.seek(0, os.SEEK_END) + block_size = f.tell() + + # fetch the filesystem + bd = Bd(f, block_size, block_count) + lfs = Lfs3.fetch(bd, mroots, trunk) + corrupted = not bool(lfs) + + # if we can't figure out the block_count, guess + block_size_ = block_size + block_count_ = block_count + if block_count is None: + if lfs.config.geometry is not None: + block_count_ = lfs.config.geometry.block_count + else: + f.seek(0, os.SEEK_END) + block_count_ = mt.ceil(f.tell() / block_size) + + # flatten blocks, default to all blocks + blocks_ = list( + range(blocks.start or 0, blocks.stop or block_count_) + if isinstance(blocks, slice) + else range(blocks, blocks+1) + if blocks + else range(block_count_)) + + # traverse the filesystem and create a block map + bmap = {b: BmapBlock(b, 'unused') for b in blocks_} + mdir_count = 0 + btree_count = 0 + data_count = 0 + total_count = 0 + for child, path in lfs.traverse( + mtree_only=mtree_only, + path=True): + # track each block in our window + for b in child.blocks: + if b not in bmap: + continue + + # mdir? + if isinstance(child, Mdir): + type = 'mdir' + if b in child.blocks[:1+child.redund]: + usage = range(child.eoff) + else: + usage = range(0) + mdir_count += 1 + total_count += 1 + + # btree node? + elif isinstance(child, Rbyd): + type = 'btree' + if b in child.blocks[:1+child.redund]: + usage = range(child.eoff) + else: + usage = range(0) + btree_count += 1 + total_count += 1 + + # bptr? + elif isinstance(child, Bptr): + type = 'data' + usage = range(child.off, child.off+child.size) + data_count += 1 + total_count += 1 + + else: + assert False, "%r?" % b + + # check for some common issues + + # block conflict? + # + # note we can't compare more than types due to different + # trunks, slicing, etc + if (b in bmap + and bmap[b].type != 'unused' + and bmap[b].type != type): + if bmap[b].type == 'conflict': + bmap[b].value.append(child) + else: + bmap[b] = BmapBlock(b, 'conflict', + [bmap[b].value, child], + range(block_size_)) + corrupted = True + + # corrupt metadata? + elif (not no_ckmeta + and isinstance(child, (Mdir, Rbyd)) + and not child): + bmap[b] = BmapBlock(b, 'corrupt', child, range(block_size_)) + corrupted = True + + # corrupt data? + elif (not no_ckdata + and isinstance(child, Bptr) + and not child): + bmap[b] = BmapBlock(b, 'corrupt', child, range(block_size_)) + corrupted = True + + # normal block + else: + bmap[b] = BmapBlock(b, type, child, usage) + + # keep track of siblings + bmap[b].siblings.update( + b_ for b_ in child.blocks + if b_ != b and b_ in bmap) + + # update parents with children + if path: + parent = path[-1][1] + for b in parent.blocks: + if b in bmap: + bmap[b].children.update( + b_ for b_ in child.blocks + if b_ in bmap) + + # one last thing, build a title + if title: + title_ = punescape(title, { + 'magic': 'littlefs%s' % ( + '' if lfs.ckmagic() else '?'), + 'version': 'v%s.%s' % ( + lfs.version.major if lfs.version is not None else '?', + lfs.version.minor if lfs.version is not None else '?'), + 'version_major': + lfs.version.major if lfs.version is not None else '?', + 'version_minor': + lfs.version.minor if lfs.version is not None else '?', + 'geometry': '%sx%s' % ( + lfs.block_size if lfs.block_size is not None else '?', + lfs.block_count if lfs.block_count is not None else '?'), + 'block_size': + lfs.block_size if lfs.block_size is not None else '?', + 'block_count': + lfs.block_count if lfs.block_count is not None else '?', + 'addr': lfs.addr(), + 'weight': 'w%s.%s' % (lfs.mbweightrepr(), lfs.mrweightrepr()), + 'mbweight': lfs.mbweightrepr(), + 'mrweight': lfs.mrweightrepr(), + 'rev': '%08x' % lfs.rev, + 'cksum': '%08x%s' % ( + lfs.cksum, + '' if lfs.ckgcksum() else '?'), + 'total': total_count, + 'total_percent': 100*total_count / max(len(bmap), 1), + 'mdir': mdir_count, + 'mdir_percent': 100*mdir_count / max(len(bmap), 1), + 'btree': btree_count, + 'btree_percent': 100*btree_count / max(len(bmap), 1), + 'data': data_count, + 'data_percent': 100*data_count / max(len(bmap), 1), + }) + elif not title_usage: + title_ = ('littlefs%s v%s.%s %sx%s %s w%s.%s, ' + 'rev %08x, ' + 'cksum %08x%s' % ( + '' if lfs.ckmagic() else '?', + lfs.version.major if lfs.version is not None else '?', + lfs.version.minor if lfs.version is not None else '?', + lfs.block_size if lfs.block_size is not None else '?', + lfs.block_count if lfs.block_count is not None else '?', + lfs.addr(), + lfs.mbweightrepr(), lfs.mrweightrepr(), + lfs.rev, + lfs.cksum, + '' if lfs.ckgcksum() else '?')) + else: + title_ = ('bd %sx%s, %s mdir, %s btree, %s data' % ( + lfs.block_size if lfs.block_size is not None else '?', + lfs.block_count if lfs.block_count is not None else '?', + '%.1f%%' % (100*mdir_count / max(len(bmap), 1)), + '%.1f%%' % (100*btree_count / max(len(bmap), 1)), + '%.1f%%' % (100*data_count / max(len(bmap), 1)))) + + # scale width/height if requested + if (to_scale is not None + and (width is None or height is None)): + # don't include header in scale + width__ = width_ + height__ = height_ + if not no_header: + height__ -= mt.ceil(FONT_SIZE * 1.3) + + # scale width only + if height is not None: + width__ = mt.ceil((len(bmap) * to_scale) / max(height__, 1)) + # scale height only + elif width is not None: + height__ = mt.ceil((len(bmap) * to_scale) / max(width__, 1)) + # scale based on aspect-ratio + else: + width__ = mt.ceil(mt.sqrt(len(bmap) * to_scale * to_ratio)) + height__ = mt.ceil((len(bmap) * to_scale) / max(width__, 1)) + + if not no_header: + height__ += mt.ceil(FONT_SIZE * 1.3) + width_ = width__ + height_ = height__ + + # create space for header + x__ = 0 + y__ = 0 + width__ = width_ + height__ = height_ + if not no_header: + y__ += mt.ceil(FONT_SIZE * 1.3) + height__ -= min(mt.ceil(FONT_SIZE * 1.3), height__) + + # figure out block_cols_/block_rows_ + if block_cols is not None and block_rows is not None: + block_cols_ = block_cols + block_rows_ = block_rows + elif block_rows is not None: + block_cols_ = mt.ceil(len(bmap) / block_rows) + block_rows_ = block_rows + elif block_cols is not None: + block_cols_ = block_cols + block_rows_ = mt.ceil(len(bmap) / block_cols) + else: + # divide by 2 until we hit our target ratio, this works + # well for things that are often powers-of-two + # + # also prioritize rows at low resolution + block_cols_ = 1 + block_rows_ = len(bmap) + while (block_rows_ > height__ + or abs(((width__/(block_cols_*2)) + / max(height__/mt.ceil(block_rows_/2), 1)) + - block_ratio) + < abs(((width__/block_cols_) + / max(height__/block_rows_, 1))) + - block_ratio): + block_cols_ *= 2 + block_rows_ = mt.ceil(block_rows_ / 2) + + block_width_ = width__ / block_cols_ + block_height_ = height__ / block_rows_ + + # assign block locations based on block_rows_/block_cols_ and + # the requested space filling curve + for (x, y), b in zip( + (hilbert_curve if hilbert + else lebesgue_curve if lebesgue + else naive_curve)(block_cols_, block_rows_), + sorted(bmap.values())): + b.x = x__ + (x * block_width_) + b.y = y__ + (y * block_height_) + b.width = block_width_ + b.height = block_height_ + + # apply top padding + if x == 0: + b.x += padding + b.width -= min(padding, b.width) + if y == 0: + b.y += padding + b.height -= min(padding, b.height) + # apply bottom padding + b.width -= min(padding, b.width) + b.height -= min(padding, b.height) + + # align to pixel boundaries + b.align() + + # bump up to at least one pixel for every block + b.width = max(b.width, 1) + b.height = max(b.height, 1) + + # assign colors based on block type + for b in bmap.values(): + color__ = colors_.get((b.block, (b.type, '0x%x' % b.block))) + if color__ is not None: + if '%' in color__: + color__ = punescape(color__, b.attrs) + b.color = color__ + + # assign labels + for b in bmap.values(): + label__ = labels_.get((b.block, (b.type, '0x%x' % b.block))) + if label__ is not None: + if '%' in label__: + label__ = punescape(label__, b.attrs) + b.label = label__ + + + # create svg file + with openio(output, 'w') as f: + def writeln(self, s=''): + self.write(s) + self.write('\n') + f.writeln = writeln.__get__(f) + + # yes this is svg + f.write('' % dict( + width=width_, + height=height_, + font=','.join(font), + font_size=font_size, + background=background_, + user_select='none' if not no_javascript else 'auto')) + + # create header + if not no_header: + f.write('' % dict( + js= 'cursor="pointer" ' + 'onclick="click_header(this,event)">' + if not no_javascript else '')) + # add an invisible rect to make things more clickable + f.write('' % dict( + x=0, + y=0, + width=width_, + height=y__)) + f.write('') + f.write('' % dict( + color='#ffffff' if dark else '#000000')) + f.write('') + f.write(title_) + f.write('') + if not no_mode and not no_javascript: + f.write('' % dict( + x=width_-3)) + f.write('mode: %s' % ( + 'tree' if mode_tree + else 'branches' if mode_branches + else 'references' if mode_references + else 'redund')) + f.write('') + f.write('') + f.write('') + + # create block tiles + for b in bmap.values(): + # skip anything with zero weight/height after aligning things + if b.width == 0 or b.height == 0: + continue + + f.write('' % dict( + block=b.block, + type=b.type, + x=b.x, + y=b.y, + js= 'data-block="%(block)d" ' + # precompute x/y for javascript, svg makes this + # weirdly difficult to figure out post-transform + 'data-x="%(x)d" ' + 'data-y="%(y)d" ' + 'data-width="%(width)d" ' + 'data-height="%(height)d" ' + 'onmouseenter="enter_block(this,event)" ' + 'onmouseleave="leave_block(this,event)" ' + 'onclick="click_block(this,event)">' % dict( + block=b.block, + x=b.x, + y=b.y, + width=b.width, + height=b.height) + if not no_javascript else '')) + # add an invisible rect to make things more clickable + f.write('' % dict( + width=b.width + padding, + height=b.height + padding)) + f.write('') + f.write('') + f.write(b.label) + f.write('') + f.write('' % dict( + block=b.block, + color=b.color, + width=b.width, + height=b.height)) + f.write('') + if not no_label: + f.write('' % b.block) + f.write('' % b.block) + f.write('') + f.write('') + f.write('' % b.block) + for j, l in enumerate(b.label.split('\n')): + if j == 0: + f.write('') + f.write(l) + f.write('') + else: + f.write('') + f.write(l) + f.write('') + f.write('') + f.write('') + + if not no_javascript: + # arrowhead for arrows + f.write('') + f.write('' % dict( + color='#000000' if dark else '#555555')) + f.write('') + f.write('') + f.write('') + + # javascript for arrows + # + # why tf does svg support javascript? + f.write('') + + f.write('') + + + # print some summary info + if not quiet: + print('updated %s, ' + 'littlefs%s v%s.%s %sx%s %s w%s.%s, ' + 'cksum %08x%s' % ( + output, + '' if lfs.ckmagic() else '?', + lfs.version.major if lfs.version is not None else '?', + lfs.version.minor if lfs.version is not None else '?', + lfs.block_size if lfs.block_size is not None else '?', + lfs.block_count if lfs.block_count is not None else '?', + lfs.addr(), + lfs.mbweightrepr(), lfs.mrweightrepr(), + lfs.cksum, + '' if lfs.ckgcksum() else '?')) + + if args.get('error_on_corrupt') and corrupted: + sys.exit(2) + + +if __name__ == "__main__": + import argparse + import sys + parser = argparse.ArgumentParser( + description="Render currently used blocks in a littlefs image " + "as an interactive SVG block map.", + allow_abbrev=False) + parser.add_argument( + 'disk', + help="File containing the block device.") + parser.add_argument( + 'mroots', + nargs='*', + type=rbydaddr, + help="Block address of the mroots. Defaults to 0x{0,1}.") + parser.add_argument( + '-o', '--output', + required=True, + help="Output *.svg file.") + parser.add_argument( + '--trunk', + type=lambda x: int(x, 0), + help="Use this offset as the trunk of the mroots.") + parser.add_argument( + '-b', '--block-size', + type=bdgeom, + help="Block size/geometry in bytes. Accepts x.") + parser.add_argument( + '--block-count', + type=lambda x: int(x, 0), + help="Block count in blocks.") + parser.add_argument( + '-@', '--blocks', + type=lambda x: ( + slice(*(int(x, 0) if x.strip() else None + for x in x.split(',', 1))) + if ',' in x + else int(x, 0)), + help="Show a specific block, may be a range.") + parser.add_argument( + '--no-ckmeta', + action='store_true', + help="Don't check metadata blocks for errors.") + parser.add_argument( + '--no-ckdata', + action='store_true', + help="Don't check metadata + data blocks for errors.") + parser.add_argument( + '--mtree-only', + action='store_true', + help="Only traverse the mtree.") + parser.add_argument( + '-q', '--quiet', + action='store_true', + help="Don't print info.") + + parser.add_argument( + '-L', '--add-label', + dest='labels', + action='append', + type=lambda x: ( + lambda ks, v: ( + tuple(k.strip() for k in ks.split(',')), + v.strip()) + )(*x.split('=', 1)) + if '=' in x else x.strip(), + help="Add a label to use. Can be assigned to a specific " + "block type/block. Accepts %% modifiers.") + parser.add_argument( + '-C', '--add-color', + dest='colors', + action='append', + type=lambda x: ( + lambda ks, v: ( + tuple(k.strip() for k in ks.split(',')), + v.strip()) + )(*x.split('=', 1)) + if '=' in x else x.strip(), + help="Add a color to use. Can be assigned to a specific " + "block type/block. Accepts %% modifiers.") + parser.add_argument( + '-W', '--width', + type=lambda x: int(x, 0), + help="Width in pixels. Defaults to %r." % WIDTH) + parser.add_argument( + '-H', '--height', + type=lambda x: int(x, 0), + help="Height in pixels. Defaults to %r." % HEIGHT) + parser.add_argument( + '-X', '--block-cols', + type=lambda x: int(x, 0), + help="Number of blocks on the x-axis. Guesses from --block-count " + "and --block-ratio by default.") + parser.add_argument( + '-Y', '--block-rows', + type=lambda x: int(x, 0), + help="Number of blocks on the y-axis. Guesses from --block-count " + "and --block-ratio by default.") + parser.add_argument( + '--block-ratio', + dest='block_ratio', + type=lambda x: ( + (lambda a, b: a / b)(*(float(v) for v in x.split(':', 1))) + if ':' in x else float(x)), + help="Target ratio for block sizes. Defaults to the golden ratio.") + parser.add_argument( + '--no-header', + action='store_true', + help="Don't show the header.") + parser.add_argument( + '--no-mode', + action='store_true', + help="Don't show the mode state.") + parser.add_argument( + '-U', '--hilbert', + action='store_true', + help="Render as a space-filling Hilbert curve.") + parser.add_argument( + '-Z', '--lebesgue', + action='store_true', + help="Render as a space-filling Z-curve.") + parser.add_argument( + '-J', '--no-javascript', + action='store_true', + help="Don't add javascript for interactability.") + parser.add_argument( + '--mode-tree', + action='store_true', + help="Include the tree rendering mode.") + parser.add_argument( + '--mode-branches', + action='store_true', + help="Include the branches rendering mode.") + parser.add_argument( + '--mode-references', + action='store_true', + help="Include the references rendering mode.") + parser.add_argument( + '--mode-redund', + action='store_true', + help="Include the redund rendering mode.") + parser.add_argument( + '--to-scale', + nargs='?', + type=lambda x: ( + (lambda a, b: a / b)(*(float(v) for v in x.split(':', 1))) + if ':' in x else float(x)), + const=1, + help="Scale the resulting treemap such that 1 pixel ~= 1/scale " + "blocks. Defaults to scale=1. ") + parser.add_argument( + '--to-ratio', + type=lambda x: ( + (lambda a, b: a / b)(*(float(v) for v in x.split(':', 1))) + if ':' in x else float(x)), + help="Aspect ratio to use with --to-scale. Defaults to 1:1.") + parser.add_argument( + '--tiny', + action='store_true', + help="Tiny mode, alias for --block-ratio=1, --to-scale=1, " + "--padding=0, --no-header, --no-label, and --no-javascript.") + parser.add_argument( + '--title', + help="Add a title. Accepts %% modifiers.") + parser.add_argument( + '--title-littlefs', + action='store_true', + help="Use the littlefs mount string as the title. This is the " + "default.") + parser.add_argument( + '--title-usage', + action='store_true', + help="Use the mdir/btree/data usage as the title.") + parser.add_argument( + '--padding', + type=float, + help="Padding to add to each block. Defaults to 1.") + parser.add_argument( + '--no-label', + action='store_true', + help="Don't render any labels.") + parser.add_argument( + '--dark', + action='store_true', + help="Use the dark style.") + parser.add_argument( + '--font', + type=lambda x: [x.strip() for x in x.split(',')], + help="Font family to use.") + parser.add_argument( + '--font-size', + help="Font size to use. Defaults to %r." % FONT_SIZE) + parser.add_argument( + '--background', + help="Background color to use. Note #00000000 can make the " + "background transparent.") + parser.add_argument( + '-e', '--error-on-corrupt', + action='store_true', + help="Error if the filesystem is corrupt.") + sys.exit(main(**{k: v + for k, v in vars(parser.parse_intermixed_args()).items() + if v is not None})) diff --git a/scripts/dbgbtree.py b/scripts/dbgbtree.py new file mode 100755 index 000000000..3f8829a6f --- /dev/null +++ b/scripts/dbgbtree.py @@ -0,0 +1,1951 @@ +#!/usr/bin/env python3 + +# prevent local imports +if __name__ == "__main__": + __import__('sys').path.pop(0) + +import bisect +import collections as co +import functools as ft +import itertools as it +import math as mt +import os +import struct + +try: + import crc32c as crc32c_lib +except ModuleNotFoundError: + crc32c_lib = None + + +TAG_NULL = 0x0000 ## v--- ---- +--- ---- +TAG_INTERNAL = 0x0000 ## v--- ---- +ttt tttt +TAG_CONFIG = 0x0100 ## v--- ---1 +ttt tttt +TAG_MAGIC = 0x0131 # v--- ---1 +-11 --rr +TAG_VERSION = 0x0134 # v--- ---1 +-11 -1-- +TAG_RCOMPAT = 0x0135 # v--- ---1 +-11 -1-1 +TAG_WCOMPAT = 0x0136 # v--- ---1 +-11 -11- +TAG_OCOMPAT = 0x0137 # v--- ---1 +-11 -111 +TAG_GEOMETRY = 0x0138 # v--- ---1 +-11 1--- +TAG_NAMELIMIT = 0x0139 # v--- ---1 +-11 1--1 +TAG_FILELIMIT = 0x013a # v--- ---1 +-11 1-1- +TAG_GDELTA = 0x0200 ## v--- --1- +ttt tttt +TAG_GRMDELTA = 0x0230 # v--- --1- +-11 --++ +TAG_GBMAPDELTA = 0x0234 # v--- --1- +-11 -1rr +TAG_NAME = 0x0300 ## v--- --11 +ttt tttt +TAG_BNAME = 0x0300 # v--- --11 +--- ---- +TAG_REG = 0x0301 # v--- --11 +--- ---1 +TAG_DIR = 0x0302 # v--- --11 +--- --1- +TAG_STICKYNOTE = 0x0303 # v--- --11 +--- --11 +TAG_BOOKMARK = 0x0304 # v--- --11 +--- -1-- +TAG_MNAME = 0x0330 # v--- --11 +-11 ---- +TAG_STRUCT = 0x0400 ## v--- -1-- +ttt tttt +TAG_BRANCH = 0x0400 # v--- -1-- +--- --rr +TAG_DATA = 0x0404 # v--- -1-- +--- -1rr +TAG_BLOCK = 0x0408 # v--- -1-- +--- 1err +TAG_DID = 0x0420 # v--- -1-- +-1- ---- +TAG_BSHRUB = 0x0428 # v--- -1-- +-1- 1-rr +TAG_BTREE = 0x042c # v--- -1-- +-1- 11rr +TAG_MROOT = 0x0431 # v--- -1-- +-11 --rr +TAG_MDIR = 0x0435 # v--- -1-- +-11 -1rr +TAG_MTREE = 0x043c # v--- -1-- +-11 11rr +TAG_BMRANGE = 0x0440 # v--- -1-- +1-- ++uu +TAG_BMFREE = 0x0440 # v--- -1-- +1-- ---- +TAG_BMINUSE = 0x0441 # v--- -1-- +1-- ---1 +TAG_BMERASED = 0x0442 # v--- -1-- +1-- --1- +TAG_BMBAD = 0x0443 # v--- -1-- +1-- --11 +TAG_ATTR = 0x0600 ## v--- -11a +aaa aaaa +TAG_UATTR = 0x0600 # v--- -11- +aaa aaaa +TAG_SATTR = 0x0700 # v--- -111 +aaa aaaa +TAG_SHRUB = 0x1000 ## v--1 kkkk +kkk kkkk +TAG_ALT = 0x4000 ## v1cd kkkk +kkk kkkk +TAG_B = 0x0000 +TAG_R = 0x2000 +TAG_LE = 0x0000 +TAG_GT = 0x1000 +TAG_CKSUM = 0x3000 ## v-11 ---- ++++ +pqq +TAG_PHASE = 0x0003 +TAG_PERTURB = 0x0004 +TAG_NOTE = 0x3100 ## v-11 ---1 ++++ ++++ +TAG_ECKSUM = 0x3200 ## v-11 --1- ++++ ++++ +TAG_GCKSUMDELTA = 0x3300 ## v-11 --11 ++++ ++++ + + +# self-parsing tag repr +class Tag: + def __init__(self, name, tag, encoding, help): + self.name = name + self.tag = tag + self.encoding = encoding + self.help = help + # derive mask from encoding + self.mask = sum( + (1 if x in 'v-01' else 0) << len(self.encoding)-1-i + for i, x in enumerate(self.encoding)) + + def __repr__(self): + return 'Tag(%r, %r, %r)' % ( + self.name, + self.tag, + self.encoding) + + def __eq__(self, other): + return self.name == other.name + + def __ne__(self, other): + return self.name != other.name + + def __hash__(self): + return hash(self.name) + + def line(self): + # substitute mask chars when zero + tag = '0x%s' % ''.join( + n if n != '0' else next( + (x for x in self.encoding[i*4:i*4+4] + if x not in 'v-01+'), + '0') + for i, n in enumerate('%04x' % self.tag)) + # group into nibbles + encoding = ' '.join(self.encoding[i*4:i*4+4] + for i in range(len(self.encoding)//4)) + return ('LFS3_%s' % self.name, tag, encoding) + + def specificity(self): + return sum(1 for x in self.encoding if x in 'v-01') + + def matches(self, tag): + return (tag & self.mask) == (self.tag & self.mask) + + def get(self, chars, tag): + return sum( + tag & ((1 if x in chars else 0) << len(self.encoding)-1-i) + for i, x in enumerate(self.encoding)) + + def max(self, chars): + return max(len(self.encoding)-1-i + for i, x in enumerate(self.encoding) if x in chars) + + def min(self, chars): + return min(len(self.encoding)-1-i + for i, x in enumerate(self.encoding) if x in chars) + + def width(self, chars): + return self.max(chars) - self.min(chars) + + def __contains__(self, chars): + return any(x in self.encoding for x in chars) + + @staticmethod + @ft.cache + def tags(): + # parse our script's source to figure out tags + import inspect + import re + tags = [] + tag_pattern = re.compile( + '^(?PTAG_[^ ]*) *= *(?P[^#]*?) *' + '#+ *(?P(?:[^ ] *?){16}) *(?P.*)$') + for line in (inspect.getsource( + inspect.getmodule(inspect.currentframe())) + .replace('\\\n', '') + .splitlines()): + m = tag_pattern.match(line) + if m: + tags.append(Tag( + m.group('name'), + globals()[m.group('name')], + m.group('encoding').replace(' ', ''), + m.group('help'))) + return tags + + # find best matching tag + @staticmethod + def find(tag): + # find tags, note this is cached + tags__ = Tag.tags() + + # find the most specific matching tag, ignoring valid bits + return max((t for t in tags__ if t.matches(tag & 0x7fff)), + key=lambda t: t.specificity(), + default=None) + + # human readable tag repr + @staticmethod + def repr(tag, weight=None, size=None, *, + global_=False, + toff=None): + # find the most specific matching tag, ignoring the shrub bit + t = Tag.find(tag & ~(TAG_SHRUB if tag & 0x7000 == TAG_SHRUB else 0)) + + # build repr + r = [] + # normal tag? + if not tag & TAG_ALT: + if t is not None: + # prefix shrub tags with shrub + if tag & 0x7000 == TAG_SHRUB: + r.append('shrub') + # lowercase name + r.append(t.name.split('_', 1)[1].lower()) + # gstate tag? + if global_: + if r[-1] == 'gdelta': + r[-1] = 'gstate' + elif r[-1].endswith('delta'): + r[-1] = r[-1][:-len('delta')] + # include perturb/phase bits + if 'q' in t: + r.append('q%d' % t.get('q', tag)) + if 'p' in t and tag & TAG_PERTURB: + r.append('p') + + # include unmatched fields, but not just redund, and + # only reserved bits if non-zero + if 'tua' in t or ('+' in t and t.get('+', tag) != 0): + r.append(' 0x%0*x' % ( + (t.width('tuar+')+4-1)//4, + t.get('tuar+', tag))) + # unknown tag? + else: + r.append('0x%04x' % tag) + + # weight? + if weight: + r.append(' w%d' % weight) + # size? don't include if null + if size is not None and (size or tag & 0x7fff): + r.append(' %d' % size) + + # alt pointer? + else: + r.append('alt') + r.append('r' if tag & TAG_R else 'b') + r.append('gt' if tag & TAG_GT else 'le') + r.append(' 0x%0*x' % ( + (t.width('k')+4-1)//4, + t.get('k', tag))) + + # weight? + if weight is not None: + r.append(' w%d' % weight) + # jump? + if size and toff is not None: + r.append(' 0x%x' % (0xffffffff & (toff-size))) + elif size: + r.append(' -%d' % size) + + return ''.join(r) + + +# some ways of block geometry representations +# 512 -> 512 +# 512x16 -> (512, 16) +# 0x200x10 -> (512, 16) +def bdgeom(s): + s = s.strip() + b = 10 + if s.startswith('0x') or s.startswith('0X'): + s = s[2:] + b = 16 + elif s.startswith('0o') or s.startswith('0O'): + s = s[2:] + b = 8 + elif s.startswith('0b') or s.startswith('0B'): + s = s[2:] + b = 2 + + if 'x' in s: + s, s_ = s.split('x', 1) + return (int(s, b), int(s_, b)) + else: + return int(s, b) + +# parse some rbyd addr encodings +# 0xa -> (0xa,) +# 0xa.c -> ((0xa, 0xc),) +# 0x{a,b} -> (0xa, 0xb) +# 0x{a,b}.c -> ((0xa, 0xc), (0xb, 0xc)) +def rbydaddr(s): + s = s.strip() + b = 10 + if s.startswith('0x') or s.startswith('0X'): + s = s[2:] + b = 16 + elif s.startswith('0o') or s.startswith('0O'): + s = s[2:] + b = 8 + elif s.startswith('0b') or s.startswith('0B'): + s = s[2:] + b = 2 + + trunk = None + if '.' in s: + s, s_ = s.split('.', 1) + trunk = int(s_, b) + + if s.startswith('{') and '}' in s: + ss = s[1:s.find('}')].split(',') + else: + ss = [s] + + addr = [] + for s in ss: + if trunk is not None: + addr.append((int(s, b), trunk)) + else: + addr.append(int(s, b)) + + return tuple(addr) + +def crc32c(data, crc=0): + if crc32c_lib is not None: + return crc32c_lib.crc32c(data, crc) + else: + crc ^= 0xffffffff + for b in data: + crc ^= b + for j in range(8): + crc = (crc >> 1) ^ ((crc & 1) * 0x82f63b78) + return 0xffffffff ^ crc + +def popc(x): + return bin(x).count('1') + +def parity(x): + return popc(x) & 1 + +def fromle32(data, j=0): + return struct.unpack('H', data[j:j+2].ljust(2, b'\0'))[0]; d += 2 + weight, d_ = fromleb128(data, j+d); d += d_ + size, d_ = fromleb128(data, j+d); d += d_ + return tag>>15, tag&0x7fff, weight, size, d + +def frombranch(data, j=0): + d = 0 + block, d_ = fromleb128(data, j+d); d += d_ + trunk, d_ = fromleb128(data, j+d); d += d_ + cksum = fromle32(data, j+d); d += 4 + return block, trunk, cksum, d + +def xxd(data, width=16): + for i in range(0, len(data), width): + yield '%-*s %-*s' % ( + 3*width, + ' '.join('%02x' % b for b in data[i:i+width]), + width, + ''.join( + b if b >= ' ' and b <= '~' else '.' + for b in map(chr, data[i:i+width]))) + +# compute the difference between two paths, returning everything +# in a after the paths diverge, as well as the relevant index +def pathdelta(a, b): + if not isinstance(a, list): + a = list(a) + i = 0 + for a_, b_ in zip(a, b): + try: + if type(a_) == type(b_) and a_ == b_: + i += 1 + else: + break + # treat exceptions here as failure to match, most likely + # the compared types are incompatible, it's the caller's + # problem + except Exception: + break + + return [(i+j, a_) for j, a_ in enumerate(a[i:])] + + +# a simple wrapper over an open file with bd geometry +class Bd: + def __init__(self, f, block_size=None, block_count=None): + self.f = f + self.block_size = block_size + self.block_count = block_count + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, self.repr()) + + def repr(self): + return 'bd %sx%s' % (self.block_size, self.block_count) + + def read(self, block, off, size): + self.f.seek(block*self.block_size + off) + return self.f.read(size) + + def readblock(self, block): + self.f.seek(block*self.block_size) + return self.f.read(self.block_size) + +# tagged data in an rbyd +class Rattr: + def __init__(self, tag, weight, blocks, toff, tdata, data): + self.tag = tag + self.weight = weight + if isinstance(blocks, int): + self.blocks = (blocks,) + else: + self.blocks = blocks + self.toff = toff + self.tdata = tdata + self.data = data + + @property + def block(self): + return self.blocks[0] + + @property + def tsize(self): + return len(self.tdata) + + @property + def off(self): + return self.toff + len(self.tdata) + + @property + def size(self): + return len(self.data) + + def __bytes__(self): + return self.data + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, self.repr()) + + def repr(self): + return Tag.repr(self.tag, self.weight, self.size) + + def __iter__(self): + return iter((self.tag, self.weight, self.data)) + + def __eq__(self, other): + return ((self.tag, self.weight, self.data) + == (other.tag, other.weight, other.data)) + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash((self.tag, self.weight, self.data)) + + # convenience for did/name access + def _parse_name(self): + # note we return a null name for non-name tags, this is so + # vestigial names in btree nodes act as a catch-all + if (self.tag & 0xff00) != TAG_NAME: + did = 0 + name = b'' + else: + did, d = fromleb128(self.data) + name = self.data[d:] + + # cache both + self.did = did + self.name = name + + @ft.cached_property + def did(self): + self._parse_name() + return self.did + + @ft.cached_property + def name(self): + self._parse_name() + return self.name + +class Ralt: + def __init__(self, tag, weight, blocks, toff, tdata, jump, + color=None, followed=None): + self.tag = tag + self.weight = weight + if isinstance(blocks, int): + self.blocks = (blocks,) + else: + self.blocks = blocks + self.toff = toff + self.tdata = tdata + self.jump = jump + + if color is not None: + self.color = color + else: + self.color = 'r' if tag & TAG_R else 'b' + self.followed = followed + + @property + def block(self): + return self.blocks[0] + + @property + def tsize(self): + return len(self.tdata) + + @property + def off(self): + return self.toff + len(self.tdata) + + @property + def joff(self): + return self.toff - self.jump + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, self.repr()) + + def repr(self): + return Tag.repr(self.tag, self.weight, self.jump, toff=self.toff) + + def __iter__(self): + return iter((self.tag, self.weight, self.jump)) + + def __eq__(self, other): + return ((self.tag, self.weight, self.jump) + == (other.tag, other.weight, other.jump)) + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash((self.tag, self.weight, self.jump)) + + +# our core rbyd type +class Rbyd: + def __init__(self, blocks, trunk, weight, rev, eoff, cksum, data, *, + shrub=False, + gcksumdelta=None, + redund=0): + if isinstance(blocks, int): + self.blocks = (blocks,) + else: + self.blocks = blocks + self.trunk = trunk + self.weight = weight + self.rev = rev + self.eoff = eoff + self.cksum = cksum + self.data = data + + self.shrub = shrub + self.gcksumdelta = gcksumdelta + self.redund = redund + + @property + def block(self): + return self.blocks[0] + + @property + def corrupt(self): + # use redund=-1 to indicate corrupt rbyds + return self.redund >= 0 + + def addr(self): + if len(self.blocks) == 1: + return '0x%x.%x' % (self.block, self.trunk) + else: + return '0x{%s}.%x' % ( + ','.join('%x' % block for block in self.blocks), + self.trunk) + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, self.repr()) + + def repr(self): + return 'rbyd %s w%s' % (self.addr(), self.weight) + + def __bool__(self): + # use redund=-1 to indicate corrupt rbyds + return self.redund >= 0 + + def __eq__(self, other): + return ((frozenset(self.blocks), self.trunk) + == (frozenset(other.blocks), other.trunk)) + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash((frozenset(self.blocks), self.trunk)) + + @classmethod + def _fetch(cls, data, block, trunk=None): + # fetch the rbyd + rev = fromle32(data, 0) + cksum = 0 + cksum_ = crc32c(data[0:4]) + cksum__ = cksum_ + perturb = False + eoff = 0 + eoff_ = None + j_ = 4 + trunk_ = 0 + trunk__ = 0 + trunk___ = 0 + weight = 0 + weight_ = 0 + weight__ = 0 + gcksumdelta = None + gcksumdelta_ = None + while j_ < len(data) and (not trunk or eoff <= trunk): + # read next tag + v, tag, w, size, d = fromtag(data, j_) + if v != parity(cksum__): + break + cksum__ ^= 0x00000080 if v else 0 + cksum__ = crc32c(data[j_:j_+d], cksum__) + j_ += d + if not tag & TAG_ALT and j_ + size > len(data): + break + + # take care of cksums + if not tag & TAG_ALT: + if (tag & 0xff00) != TAG_CKSUM: + cksum__ = crc32c(data[j_:j_+size], cksum__) + + # found a gcksumdelta? + if (tag & 0xff00) == TAG_GCKSUMDELTA: + gcksumdelta_ = Rattr(tag, w, block, j_-d, + data[j_-d:j_], + data[j_:j_+size]) + + # found a cksum? + else: + # check cksum + cksum___ = fromle32(data, j_) + if cksum__ != cksum___: + break + # commit what we have + eoff = eoff_ if eoff_ else j_ + size + cksum = cksum_ + trunk_ = trunk__ + weight = weight_ + gcksumdelta = gcksumdelta_ + gcksumdelta_ = None + # update perturb bit + perturb = bool(tag & TAG_PERTURB) + # revert to data cksum and perturb + cksum__ = cksum_ ^ (0xfca42daf if perturb else 0) + + # evaluate trunks + if (tag & 0xf000) != TAG_CKSUM: + if not (trunk and j_-d > trunk and not trunk___): + # new trunk? + if not trunk___: + trunk___ = j_-d + weight__ = 0 + + # keep track of weight + weight__ += w + + # end of trunk? + if not tag & TAG_ALT: + # update trunk/weight unless we found a shrub or an + # explicit trunk (which may be a shrub) is requested + if not tag & TAG_SHRUB or trunk___ == trunk: + trunk__ = trunk___ + weight_ = weight__ + # keep track of eoff for best matching trunk + if trunk and j_ + size > trunk: + eoff_ = j_ + size + eoff = eoff_ + cksum = cksum__ ^ ( + 0xfca42daf if perturb else 0) + trunk_ = trunk__ + weight = weight_ + gcksumdelta = gcksumdelta_ + trunk___ = 0 + + # update canonical checksum, xoring out any perturb state + cksum_ = cksum__ ^ (0xfca42daf if perturb else 0) + + if not tag & TAG_ALT: + j_ += size + + return cls(block, trunk_, weight, rev, eoff, cksum, data, + gcksumdelta=gcksumdelta, + redund=0 if trunk_ else -1) + + @classmethod + def fetch(cls, bd, blocks, trunk=None): + # multiple blocks? + if not isinstance(blocks, int): + # fetch all blocks + rbyds = [cls.fetch(bd, block, trunk) for block in blocks] + + # determine most recent revision/trunk + rev, trunk = None, None + for rbyd in rbyds: + # compare with sequence arithmetic + if rbyd and ( + rev is None + or not ((rbyd.rev - rev) & 0x80000000) + or (rbyd.rev == rev and rbyd.trunk > trunk)): + rev, trunk = rbyd.rev, rbyd.trunk + # sort for reproducibility + rbyds.sort(key=lambda rbyd: ( + # prioritize valid redund blocks + 0 if rbyd and rbyd.rev == rev and rbyd.trunk == trunk + else 1, + # default to sorting by block + rbyd.block)) + + # choose an active rbyd + rbyd = rbyds[0] + # keep track of the other blocks + rbyd.blocks = tuple(rbyd.block for rbyd in rbyds) + # keep track of how many redund blocks are valid + rbyd.redund = -1 + sum(1 for rbyd in rbyds + if rbyd and rbyd.rev == rev and rbyd.trunk == trunk) + # and patch the gcksumdelta if we have one + if rbyd.gcksumdelta is not None: + rbyd.gcksumdelta.blocks = rbyd.blocks + return rbyd + + # seek/read the block + block = blocks + data = bd.readblock(block) + + # fetch the rbyd + return cls._fetch(data, block, trunk) + + @classmethod + def fetchck(cls, bd, blocks, trunk, weight, cksum): + # try to fetch the rbyd normally + rbyd = cls.fetch(bd, blocks, trunk) + + # cksum mismatch? trunk/weight mismatch? + if (rbyd.cksum != cksum + or rbyd.trunk != trunk + or rbyd.weight != weight): + # mark as corrupt and keep track of expected trunk/weight + rbyd.redund = -1 + rbyd.trunk = trunk + rbyd.weight = weight + + return rbyd + + @classmethod + def fetchshrub(cls, rbyd, trunk): + # steal the original rbyd's data + # + # this helps avoid race conditions with cksums and stuff + shrub = cls._fetch(rbyd.data, rbyd.block, trunk) + shrub.blocks = rbyd.blocks + shrub.shrub = True + return shrub + + def lookupnext(self, rid, tag=None, *, + path=False): + if not self or rid >= self.weight: + if path: + return None, None, [] + else: + return None, None + + tag = max(tag or 0, 0x1) + lower = 0 + upper = self.weight + path_ = [] + + # descend down tree + j = self.trunk + while True: + _, alt, w, jump, d = fromtag(self.data, j) + + # found an alt? + if alt & TAG_ALT: + # follow? + if ((rid, tag & 0xfff) > (upper-w-1, alt & 0xfff) + if alt & TAG_GT + else ((rid, tag & 0xfff) + <= (lower+w-1, alt & 0xfff))): + lower += upper-lower-w if alt & TAG_GT else 0 + upper -= upper-lower-w if not alt & TAG_GT else 0 + j = j - jump + + if path: + # figure out which color + if alt & TAG_R: + _, nalt, _, _, _ = fromtag(self.data, j+jump+d) + if nalt & TAG_R: + color = 'y' + else: + color = 'r' + else: + color = 'b' + + path_.append(Ralt( + alt, w, self.blocks, j+jump, + self.data[j+jump:j+jump+d], jump, + color=color, + followed=True)) + + # stay on path + else: + lower += w if not alt & TAG_GT else 0 + upper -= w if alt & TAG_GT else 0 + j = j + d + + if path: + # figure out which color + if alt & TAG_R: + _, nalt, _, _, _ = fromtag(self.data, j) + if nalt & TAG_R: + color = 'y' + else: + color = 'r' + else: + color = 'b' + + path_.append(Ralt( + alt, w, self.blocks, j-d, + self.data[j-d:j], jump, + color=color, + followed=False)) + + # found tag + else: + rid_ = upper-1 + tag_ = alt + w_ = upper-lower + + if not tag_ or (rid_, tag_) < (rid, tag): + if path: + return None, None, path_ + else: + return None, None + + rattr_ = Rattr(tag_, w_, self.blocks, j, + self.data[j:j+d], + self.data[j+d:j+d+jump]) + if path: + return rid_, rattr_, path_ + else: + return rid_, rattr_ + + def lookup(self, rid, tag=None, mask=None, *, + path=False): + if tag is None: + tag, mask = 0, 0xffff + if mask is None: + mask = 0 + + r = self.lookupnext(rid, tag & ~mask, + path=path) + if path: + rid_, rattr_, path_ = r + else: + rid_, rattr_ = r + if (rid_ is None + or rid_ != rid + or (rattr_.tag & ~mask & 0xfff) + != (tag & ~mask & 0xfff)): + if path: + return None, path_ + else: + return None + + if path: + return rattr_, path_ + else: + return rattr_ + + def rids(self, *, + path=False): + rid = -1 + while True: + r = self.lookupnext(rid, + path=path) + if path: + rid, name, path_ = r + else: + rid, name = r + # found end of tree? + if rid is None: + break + + if path: + yield rid, name, path_ + else: + yield rid, name + rid += 1 + + def rattrs(self, rid=None, tag=None, mask=None, *, + path=False): + if rid is None: + rid, tag = -1, 0 + while True: + r = self.lookupnext(rid, tag+0x1, + path=path) + if path: + rid, rattr, path_ = r + else: + rid, rattr = r + # found end of tree? + if rid is None: + break + + if path: + yield rid, rattr, path_ + else: + yield rid, rattr + tag = rattr.tag + else: + if tag is None: + tag, mask = 0, 0xffff + if mask is None: + mask = 0 + + tag_ = max((tag & ~mask) - 1, 0) + while True: + r = self.lookupnext(rid, tag_+0x1, + path=path) + if path: + rid_, rattr_, path_ = r + else: + rid_, rattr_ = r + # found end of tree? + if (rid_ is None + or rid_ != rid + or (rattr_.tag & ~mask & 0xfff) + != (tag & ~mask & 0xfff)): + break + + if path: + yield rattr_, path_ + else: + yield rattr_ + tag_ = rattr_.tag + + # lookup by name + def namelookup(self, did, name): + # binary search + best = None, None + lower = 0 + upper = self.weight + while lower < upper: + rid, name_ = self.lookupnext( + lower + (upper-1-lower)//2) + if rid is None: + break + + # bisect search space + if (name_.did, name_.name) > (did, name): + upper = rid-(name_.weight-1) + elif (name_.did, name_.name) < (did, name): + lower = rid + 1 + # keep track of best match + best = rid, name_ + else: + # found a match + return rid, name_ + + return best + + +# our rbyd btree type +class Btree: + def __init__(self, bd, rbyd): + self.bd = bd + self.rbyd = rbyd + + @property + def block(self): + return self.rbyd.block + + @property + def blocks(self): + return self.rbyd.blocks + + @property + def trunk(self): + return self.rbyd.trunk + + @property + def weight(self): + return self.rbyd.weight + + @property + def rev(self): + return self.rbyd.rev + + @property + def cksum(self): + return self.rbyd.cksum + + @property + def shrub(self): + return self.rbyd.shrub + + def addr(self): + return self.rbyd.addr() + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, self.repr()) + + def repr(self): + return 'btree %s w%s' % (self.addr(), self.weight) + + def __eq__(self, other): + return self.rbyd == other.rbyd + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash(self.rbyd) + + @classmethod + def fetch(cls, bd, blocks, trunk=None): + # rbyd fetch does most of the work here + rbyd = Rbyd.fetch(bd, blocks, trunk) + return cls(bd, rbyd) + + @classmethod + def fetchck(cls, bd, blocks, trunk, weight, cksum): + # rbyd fetchck does most of the work here + rbyd = Rbyd.fetchck(bd, blocks, trunk, weight, cksum) + return cls(bd, rbyd) + + @classmethod + def fetchshrub(cls, bd, rbyd, trunk): + shrub = Rbyd.fetchshrub(rbyd, trunk) + return cls(bd, shrub) + + def lookupnext_(self, bid, *, + path=False, + depth=None): + if not self or bid >= self.weight: + if path: + return None, None, None, None, [] + else: + return None, None, None, None + + rbyd = self.rbyd + rid = bid + depth_ = 1 + path_ = [] + + while True: + # corrupt branch? + if not rbyd: + if path: + return bid, rbyd, rid, None, path_ + else: + return bid, rbyd, rid, None + + # first tag indicates the branch's weight + rid_, name_ = rbyd.lookupnext(rid) + if rid_ is None: + if path: + return None, None, None, None, path_ + else: + return None, None, None, None + + # keep track of path + if path: + path_.append((bid + (rid_-rid), rbyd, rid_, name_)) + + # find branch tag if there is one + branch_ = rbyd.lookup(rid_, TAG_BRANCH, 0x3) + + # descend down branch? + if branch_ is not None and ( + not depth or depth_ < depth): + block, trunk, cksum, _ = frombranch(branch_.data) + rbyd = Rbyd.fetchck(self.bd, block, trunk, name_.weight, + cksum) + + rid -= (rid_-(name_.weight-1)) + depth_ += 1 + + else: + if path: + return bid + (rid_-rid), rbyd, rid_, name_, path_ + else: + return bid + (rid_-rid), rbyd, rid_, name_ + + # the non-leaf variants discard the rbyd info, these can be a bit + # more convenient, but at a performance cost + def lookupnext(self, bid, *, + path=False, + depth=None): + # just discard the rbyd info + r = self.lookupnext_(bid, + path=path, + depth=depth) + if path: + bid, rbyd, rid, name, path_ = r + else: + bid, rbyd, rid, name = r + + if path: + return bid, name, path_ + else: + return bid, name + + def lookup(self, bid, tag=None, mask=None, *, + path=False, + depth=None): + # lookup rbyd in btree + # + # note this function expects bid to be known, use lookupnext + # first if you don't care about the exact bid (or better yet, + # lookupnext_ and call lookup on the returned rbyd) + # + # this matches rbyd's lookup behavior, which needs a known rid + # to avoid a double lookup + r = self.lookupnext_(bid, + path=path, + depth=depth) + if path: + bid_, rbyd_, rid_, name_, path_ = r + else: + bid_, rbyd_, rid_, name_ = r + if bid_ is None or bid_ != bid: + if path: + return None, path_ + else: + return None + + # lookup tag in rbyd + rattr_ = rbyd_.lookup(rid_, tag, mask) + if rattr_ is None: + if path: + return None, path_ + else: + return None + + if path: + return rattr_, path_ + else: + return rattr_ + + # note leaves only iterates over leaf rbyds, whereas traverse + # traverses all rbyds + def leaves(self, *, + path=False, + depth=None): + # include our root rbyd even if the weight is zero + if self.weight == 0: + if path: + yield -1, self.rbyd, [] + else: + yield -1, self.rbyd + return + + bid = 0 + while True: + r = self.lookupnext_(bid, + path=path, + depth=depth) + if r: + bid, rbyd, rid, name, path_ = r + else: + bid, rbyd, rid, name = r + if bid is None: + break + + if path: + yield (bid-rid + (rbyd.weight-1), rbyd, + # path tail is usually redundant unless corrupt + path_[:-1] + if path_ and path_[-1][1] == rbyd + else path_) + else: + yield bid-rid + (rbyd.weight-1), rbyd + bid += rbyd.weight - rid + 1 + + def traverse(self, *, + path=False, + depth=None): + ptrunk_ = [] + for bid, rbyd, path_ in self.leaves( + path=True, + depth=depth): + # we only care about the rbyds here + trunk_ = ([(bid_-rid_ + (rbyd_.weight-1), rbyd_) + for bid_, rbyd_, rid_, name_ in path_] + + [(bid, rbyd)]) + for d, (bid_, rbyd_) in pathdelta( + trunk_, ptrunk_): + # but include branch rids in the path if requested + if path: + yield bid_, rbyd_, path_[:d] + else: + yield bid_, rbyd_ + ptrunk_ = trunk_ + + # note bids/rattrs do _not_ include corrupt btree nodes! + def bids(self, *, + leaves=False, + path=False, + depth=None): + for r in self.leaves( + path=path, + depth=depth): + if path: + bid, rbyd, path_ = r + else: + bid, rbyd = r + for rid, name in rbyd.rids(): + bid_ = bid-(rbyd.weight-1) + rid + if leaves: + if path: + yield (bid_, rbyd, rid, name, + path_+[(bid_, rbyd, rid, name)]) + else: + yield bid_, rbyd, rid, name + else: + if path: + yield (bid_, name, + path_+[(bid_, rbyd, rid, name)]) + else: + yield bid_, name + + def rattrs(self, bid=None, tag=None, mask=None, *, + leaves=False, + path=False, + depth=None): + if bid is None: + for r in self.leaves( + path=path, + depth=depth): + if path: + bid, rbyd, path_ = r + else: + bid, rbyd = r + for rid, name in rbyd.rids(): + bid_ = bid-(rbyd.weight-1) + rid + for rattr in rbyd.rattrs(rid): + if leaves: + if path: + yield (bid_, rbyd, rid, rattr, + path_+[(bid_, rbyd, rid, name)]) + else: + yield bid_, rbyd, rid, rattr + else: + if path: + yield (bid_, rattr, + path_+[(bid_, rbyd, rid, name)]) + else: + yield bid_, rattr + else: + r = self.lookupnext_(bid, + path=path, + depth=depth) + if path: + bid, rbyd, rid, name, path_ = r + else: + bid, rbyd, rid, name = r + if bid is None: + return + + for rattr in rbyd.rattrs(rid, tag, mask): + if leaves: + if path: + yield rbyd, rid, rattr, path_ + else: + yield rbyd, rid, rattr + else: + if path: + yield rattr, path_ + else: + yield rattr + + # lookup by name + def namelookup_(self, did, name, *, + path=False, + depth=None): + rbyd = self.rbyd + bid = 0 + depth_ = 1 + path_ = [] + + while True: + # corrupt branch? + if not rbyd: + bid_ = bid+(rbyd.weight-1) + if path: + return bid_, rbyd, rbyd.weight-1, None, path_ + else: + return bid_, rbyd, rbyd.weight-1, None + + rid_, name_ = rbyd.namelookup(did, name) + + # keep track of path + if path: + path_.append((bid + rid_, rbyd, rid_, name_)) + + # find branch tag if there is one + branch_ = rbyd.lookup(rid_, TAG_BRANCH, 0x3) + + # found another branch + if branch_ is not None and ( + not depth or depth_ < depth): + block, trunk, cksum, _ = frombranch(branch_.data) + rbyd = Rbyd.fetchck(self.bd, block, trunk, name_.weight, + cksum) + + # update our bid + bid += rid_ - (name_.weight-1) + depth_ += 1 + + # found best match + else: + if path: + return bid + rid_, rbyd, rid_, name_, path_ + else: + return bid + rid_, rbyd, rid_, name_ + + def namelookup(self, bid, *, + path=False, + depth=None): + # just discard the rbyd info + r = self.namelookup_(did, name, + path=path, + depth=depth) + if path: + bid, rbyd, rid, name, path_ = r + else: + bid, rbyd, rid, name = r + + if path: + return bid, name, path_ + else: + return bid, name + + + +# tree renderer +class TreeArt: + # tree branches are an abstract thing for tree rendering + class Branch(co.namedtuple('Branch', ['a', 'b', 'z', 'color'])): + __slots__ = () + def __new__(cls, a, b, z=0, color='b'): + # a and b are context specific + return super().__new__(cls, a, b, z, color) + + def __repr__(self): + return '%s(%s, %s, %s, %s)' % ( + self.__class__.__name__, + self.a, + self.b, + self.z, + self.color) + + # don't include color in branch comparisons, or else our tree + # renderings can end up with inconsistent colors between runs + def __eq__(self, other): + return (self.a, self.b, self.z) == (other.a, other.b, other.z) + + def __ne__(self, other): + return (self.a, self.b, self.z) != (other.a, other.b, other.z) + + def __hash__(self): + return hash((self.a, self.b, self.z)) + + # also order by z first, which can be useful for reproducibly + # prioritizing branches when simplifying trees + def __lt__(self, other): + return (self.z, self.a, self.b) < (other.z, other.a, other.b) + + def __le__(self, other): + return (self.z, self.a, self.b) <= (other.z, other.a, other.b) + + def __gt__(self, other): + return (self.z, self.a, self.b) > (other.z, other.a, other.b) + + def __ge__(self, other): + return (self.z, self.a, self.b) >= (other.z, other.a, other.b) + + # apply a function to a/b while trying to avoid copies + def map(self, filter_, map_=None): + if map_ is None: + filter_, map_ = None, filter_ + + a = self.a + if filter_ is None or filter_(a): + a = map_(a) + + b = self.b + if filter_ is None or filter_(b): + b = map_(b) + + if a != self.a or b != self.b: + return self.__class__( + a if a != self.a else self.a, + b if b != self.b else self.b, + self.z, + self.color) + else: + return self + + def __init__(self, tree): + self.tree = tree + self.depth = max((t.z+1 for t in tree), default=0) + if self.depth > 0: + self.width = 2*self.depth + 2 + else: + self.width = 0 + + def __iter__(self): + return iter(self.tree) + + def __bool__(self): + return bool(self.tree) + + def __len__(self): + return len(self.tree) + + # render an rbyd rbyd tree for debugging + @classmethod + def _fromrbydrtree(cls, rbyd, **args): + trunks = co.defaultdict(lambda: (-1, 0)) + alts = co.defaultdict(lambda: {}) + + for rid, rattr, path in rbyd.rattrs(path=True): + # keep track of trunks/alts + trunks[rattr.toff] = (rid, rattr.tag) + + for ralt in path: + if ralt.followed: + alts[ralt.toff] |= {'f': ralt.joff, 'c': ralt.color} + else: + alts[ralt.toff] |= {'nf': ralt.off, 'c': ralt.color} + + if args.get('tree_rbyd_all'): + # treat unreachable alts as converging paths + for j_, alt in alts.items(): + if 'f' not in alt: + alt['f'] = alt['nf'] + elif 'nf' not in alt: + alt['nf'] = alt['f'] + + else: + # prune any alts with unreachable edges + pruned = {} + for j, alt in alts.items(): + if 'f' not in alt: + pruned[j] = alt['nf'] + elif 'nf' not in alt: + pruned[j] = alt['f'] + for j in pruned.keys(): + del alts[j] + + for j, alt in alts.items(): + while alt['f'] in pruned: + alt['f'] = pruned[alt['f']] + while alt['nf'] in pruned: + alt['nf'] = pruned[alt['nf']] + + # find the trunk and depth of each alt + def rec_trunk(j): + if j not in alts: + return trunks[j] + else: + if 'nft' not in alts[j]: + alts[j]['nft'] = rec_trunk(alts[j]['nf']) + return alts[j]['nft'] + + for j in alts.keys(): + rec_trunk(j) + for j, alt in alts.items(): + if alt['f'] in alts: + alt['ft'] = alts[alt['f']]['nft'] + else: + alt['ft'] = trunks[alt['f']] + + def rec_height(j): + if j not in alts: + return 0 + else: + if 'h' not in alts[j]: + alts[j]['h'] = max( + rec_height(alts[j]['f']), + rec_height(alts[j]['nf'])) + 1 + return alts[j]['h'] + + for j in alts.keys(): + rec_height(j) + + t_depth = max((alt['h']+1 for alt in alts.values()), default=0) + + # convert to more general tree representation + tree = set() + for j, alt in alts.items(): + # note all non-trunk edges should be colored black + tree.add(cls.Branch( + alt['nft'], + alt['nft'], + t_depth-1 - alt['h'], + alt['c'])) + if alt['ft'] != alt['nft']: + tree.add(cls.Branch( + alt['nft'], + alt['ft'], + t_depth-1 - alt['h'], + 'b')) + + return cls(tree) + + # render an rbyd btree tree for debugging + @classmethod + def _fromrbydbtree(cls, rbyd, **args): + # for rbyds this is just a pointer to every rid + tree = set() + root = None + for rid, name in rbyd.rids(): + b = (rid, name.tag) + if root is None: + root = b + tree.add(cls.Branch(root, b)) + return cls(tree) + + # render an rbyd tree for debugging + @classmethod + def fromrbyd(cls, rbyd, **args): + if args.get('tree_btree'): + return cls._fromrbydbtree(rbyd, **args) + else: + return cls._fromrbydrtree(rbyd, **args) + + # render some nice ascii trees + def repr(self, x, color=False): + if self.depth == 0: + return '' + + def branchrepr(tree, x, d, was): + for t in tree: + if t.z == d and t.b == x: + if any(t.z == d and t.a == x + for t in tree): + return '+-', t.color, t.color + elif any(t.z == d + and x > min(t.a, t.b) + and x < max(t.a, t.b) + for t in tree): + return '|-', t.color, t.color + elif t.a < t.b: + return '\'-', t.color, t.color + else: + return '.-', t.color, t.color + for t in tree: + if t.z == d and t.a == x: + return '+ ', t.color, None + for t in tree: + if (t.z == d + and x > min(t.a, t.b) + and x < max(t.a, t.b)): + return '| ', t.color, was + if was: + return '--', was, was + return ' ', None, None + + trunk = [] + was = None + for d in range(self.depth): + t, c, was = branchrepr(self.tree, x, d, was) + + trunk.append('%s%s%s%s' % ( + '\x1b[33m' if color and c == 'y' + else '\x1b[31m' if color and c == 'r' + else '\x1b[1;30m' if color and c == 'b' + else '', + t, + ('>' if was else ' ') if d == self.depth-1 else '', + '\x1b[m' if color and c else '')) + + return '%s ' % ''.join(trunk) + +# some more renderers + +# render a btree rbyd tree for debugging +@classmethod +def _treeartfrombtreertree(cls, btree, *, + depth=None, + inner=False, + **args): + # precompute rbyd trees so we know the max depth at each layer + # to nicely align trees + rtrees = {} + rdepths = {} + for bid, rbyd, path in btree.traverse(path=True, depth=depth): + if not rbyd: + continue + + rtree = cls.fromrbyd(rbyd, **args) + rtrees[rbyd] = rtree + rdepths[len(path)] = max(rdepths.get(len(path), 0), rtree.depth) + + # map rbyd branches into our btree space + tree = set() + for bid, rbyd, path in btree.traverse(path=True, depth=depth): + if not rbyd: + continue + + # yes we can find new rbyds if disk is being mutated, just + # ignore these + if rbyd not in rtrees: + continue + + rtree = rtrees[rbyd] + rz = max((t.z+1 for t in rtree), default=0) + d = sum(rdepths[d]+1 for d in range(len(path))) + + # map into our btree space + for t in rtree: + # note we adjust our bid to be left-leaning, this allows + # a global order and makes tree rendering quite a bit easier + a_rid, a_tag = t.a + b_rid, b_tag = t.b + _, (_, a_w, _) = rbyd.lookupnext(a_rid) + _, (_, b_w, _) = rbyd.lookupnext(b_rid) + tree.add(cls.Branch( + (bid-(rbyd.weight-1)+a_rid-(a_w-1), len(path), a_tag), + (bid-(rbyd.weight-1)+b_rid-(b_w-1), len(path), b_tag), + d + rdepths[len(path)]-rz + t.z, + t.color)) + + # connect rbyd branches to rbyd roots + if path: + l_bid, l_rbyd, l_rid, l_name = path[-1] + l_branch = l_rbyd.lookup(l_rid, TAG_BRANCH, 0x3) + + if rtree: + r_rid, r_tag = min(rtree, key=lambda t: t.z).a + _, (_, r_w, _) = rbyd.lookupnext(r_rid) + else: + r_rid, (r_tag, r_w, _) = rbyd.lookupnext(-1) + + tree.add(cls.Branch( + (l_bid-(l_name.weight-1), len(path)-1, l_branch.tag), + (bid-(rbyd.weight-1)+r_rid-(r_w-1), len(path), r_tag), + d-1)) + + # remap branches to leaves if we aren't showing inner branches + if not inner: + # step through each btree layer backwards + b_depth = max((t.a[1]+1 for t in tree), default=0) + + for d in reversed(range(b_depth-1)): + # find bid ranges at this level + bids = set() + for t in tree: + if t.b[1] == d: + bids.add(t.b[0]) + bids = sorted(bids) + + # find the best root for each bid range + roots = {} + for i in range(len(bids)): + for t in tree: + if (t.a[1] > d + and t.a[0] >= bids[i] + and (i == len(bids)-1 or t.a[0] < bids[i+1]) + and (bids[i] not in roots + or t < roots[bids[i]])): + roots[bids[i]] = t + + # remap branches to leaf-roots + tree = {t.map( + lambda x: x[1] == d and x[0] in roots, + lambda x: roots[x[0]].a) + for t in tree} + + return cls(tree) + +# render a btree btree tree for debugging +@classmethod +def _treeartfrombtreebtree(cls, btree, *, + depth=None, + inner=False, + **args): + # find all branches + tree = set() + root = None + branches = {} + for bid, name, path in btree.bids( + path=True, + depth=depth): + # create branch for each jump in path + # + # note we adjust our bid to be left-leaning, this allows + # a global order and makes tree rendering quite a bit easier + a = root + for d, (bid_, rbyd_, rid_, name_) in enumerate(path): + # map into our btree space + bid__ = bid_-(name_.weight-1) + b = (bid__, d, name_.tag) + + # remap branches to leaves if we aren't showing inner + # branches + if not inner: + if b not in branches: + bid_, rbyd_, rid_, name_ = path[-1] + bid__ = bid_-(name_.weight-1) + branches[b] = (bid__, len(path)-1, name_.tag) + b = branches[b] + + # render the root path on first rid, this is arbitrary + if root is None: + root, a = b, b + + tree.add(cls.Branch(a, b, d)) + a = b + + return cls(tree) + +# render a btree tree for debugging +@classmethod +def treeartfrombtree(cls, btree, **args): + if args.get('tree_btree'): + return cls._frombtreebtree(btree, **args) + else: + return cls._frombtreertree(btree, **args) + +TreeArt._frombtreertree = _treeartfrombtreertree +TreeArt._frombtreebtree = _treeartfrombtreebtree +TreeArt.frombtree = treeartfrombtree + + + +def main(disk, roots=None, *, + trunk=None, + block_size=None, + block_count=None, + quiet=False, + color='auto', + **args): + # figure out what color should be + if color == 'auto': + color = sys.stdout.isatty() + elif color == 'always': + color = True + else: + color = False + + # is bd geometry specified? + if isinstance(block_size, tuple): + block_size, block_count_ = block_size + if block_count is None: + block_count = block_count_ + + # flatten roots, default to block 0 + roots = list(it.chain.from_iterable(roots)) if roots else [0] + + # roots may also encode trunks + roots, trunk = ( + [block[0] if isinstance(block, tuple) + else block + for block in roots], + trunk if trunk is not None + else ft.reduce( + lambda x, y: y, + (block[1] for block in roots + if isinstance(block, tuple)), + None)) + + # we seek around a bunch, so just keep the disk open + with open(disk, 'rb') as f: + # if block_size is omitted, assume the block device is one big block + if block_size is None: + f.seek(0, os.SEEK_END) + block_size = f.tell() + + # fetch the btree + bd = Bd(f, block_size, block_count) + btree = Btree.fetch(bd, roots, trunk) + + # print some information about the btree + if not quiet: + print('btree %s w%d, rev %08x, cksum %08x' % ( + btree.addr(), + btree.weight, + btree.rev, + btree.cksum)) + + # precompute tree renderings + t_width = 0 + if (args.get('tree_rbyd') + or args.get('tree_rbyd_all') + or args.get('tree_btree')): + treeart = TreeArt.frombtree(btree, **args) + t_width = treeart.width + + # dynamically size the id field + w_width = mt.ceil(mt.log10(max(1, btree.weight)+1)) + + # prbyd keeps track of the last rendered rbyd, we update + # in dbg_branch to always print interleaved addresses + prbyd = None + def dbg_branch(d, bid, rbyd, rid, name): + nonlocal prbyd + + # show human-readable representation + for rattr in rbyd.rattrs(rid): + print('%10s %s%*s %-*s %s' % ( + '%04x.%04x:' % (rbyd.block, rbyd.trunk) + if prbyd is None or rbyd != prbyd + else '', + treeart.repr((bid-(name.weight-1), d, rattr.tag), color) + if args.get('tree_rbyd') + or args.get('tree_rbyd_all') + or args.get('tree_btree') + else '', + 2*w_width+1, '%d-%d' % (bid-(rattr.weight-1), bid) + if rattr.weight > 1 + else bid if rattr.weight > 0 + else '', + 21+w_width, rattr.repr(), + next(xxd(rattr.data, 8), '') + if not args.get('raw') + and not args.get('no_truncate') + else '')) + prbyd = rbyd + + # show on-disk encoding of tags/data + if args.get('raw'): + for o, line in enumerate(xxd(rattr.tdata)): + print('%9s: %*s%*s %s' % ( + '%04x' % (rattr.toff + o*16), + t_width, '', + 2*w_width+1, '', + line)) + if args.get('raw') or args.get('no_truncate'): + for o, line in enumerate(xxd(rattr.data)): + print('%9s: %*s%*s %s' % ( + '%04x' % (rattr.off + o*16), + t_width, '', + 2*w_width+1, '', + line)) + + # traverse and print entries + ppath = [] + corrupted = False + for bid, rbyd, path in btree.leaves( + path=True, + depth=args.get('depth')): + # print inner branches if requested + if args.get('inner') and not quiet: + for d, (bid_, rbyd_, rid_, name_) in pathdelta( + path, ppath): + dbg_branch(d, bid_, rbyd_, rid_, name_) + ppath = path + + # corrupted? try to keep printing the tree + if not rbyd: + if not quiet: + print('%s%04x.%04x: %*s%s%s' % ( + '\x1b[31m' if color else '', + rbyd.block, rbyd.trunk, + t_width, '', + '(corrupted rbyd %s)' % rbyd.addr(), + '\x1b[m' if color else '')) + prbyd = None + corrupted = True + continue + + if not quiet: + for rid, name in rbyd.rids(): + bid_ = bid-(rbyd.weight-1) + rid + # show the leaf entry/branch + dbg_branch(len(path), bid_, rbyd, rid, name) + + if args.get('error_on_corrupt') and corrupted: + sys.exit(2) + + +if __name__ == "__main__": + import argparse + import sys + parser = argparse.ArgumentParser( + description="Debug rbyd B-trees.", + allow_abbrev=False) + parser.add_argument( + 'disk', + help="File containing the block device.") + parser.add_argument( + 'roots', + nargs='*', + type=rbydaddr, + help="Block address of the roots of the tree.") + parser.add_argument( + '--trunk', + type=lambda x: int(x, 0), + help="Use this offset as the trunk of the tree.") + parser.add_argument( + '-b', '--block-size', + type=bdgeom, + help="Block size/geometry in bytes. Accepts x.") + parser.add_argument( + '--block-count', + type=lambda x: int(x, 0), + help="Block count in blocks.") + parser.add_argument( + '-q', '--quiet', + action='store_true', + help="Don't show anything, useful when checking for errors.") + parser.add_argument( + '--color', + choices=['never', 'always', 'auto'], + default='auto', + help="When to use terminal colors. Defaults to 'auto'.") + parser.add_argument( + '-x', '--raw', + action='store_true', + help="Show the raw data including tag encodings.") + parser.add_argument( + '-T', '--no-truncate', + action='store_true', + help="Don't truncate, show the full contents.") + parser.add_argument( + '-R', '--tree', '--rbyd', '--tree-rbyd', + dest='tree_rbyd', + action='store_true', + help="Show the rbyd tree.") + parser.add_argument( + '-Y', '--rbyd-all', '--tree-rbyd-all', + dest='tree_rbyd_all', + action='store_true', + help="Show the full rbyd tree.") + parser.add_argument( + '-B', '--btree', '--tree-btree', + dest='tree_btree', + action='store_true', + help="Show a simplified btree tree.") + parser.add_argument( + '-i', '--inner', + action='store_true', + help="Show inner branches.") + parser.add_argument( + '-z', '--depth', + nargs='?', + type=lambda x: int(x, 0), + const=0, + help="Depth of the btree to show.") + parser.add_argument( + '-e', '--error-on-corrupt', + action='store_true', + help="Error if B-tree is corrupt.") + sys.exit(main(**{k: v + for k, v in vars(parser.parse_intermixed_args()).items() + if v is not None})) diff --git a/scripts/dbgcat.py b/scripts/dbgcat.py new file mode 100755 index 000000000..133a0707c --- /dev/null +++ b/scripts/dbgcat.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python3 + +# prevent local imports +if __name__ == "__main__": + __import__('sys').path.pop(0) + +import itertools as it +import os + +try: + import crc32c as crc32c_lib +except ModuleNotFoundError: + crc32c_lib = None + + +# some ways of block geometry representations +# 512 -> 512 +# 512x16 -> (512, 16) +# 0x200x10 -> (512, 16) +def bdgeom(s): + s = s.strip() + b = 10 + if s.startswith('0x') or s.startswith('0X'): + s = s[2:] + b = 16 + elif s.startswith('0o') or s.startswith('0O'): + s = s[2:] + b = 8 + elif s.startswith('0b') or s.startswith('0B'): + s = s[2:] + b = 2 + + if 'x' in s: + s, s_ = s.split('x', 1) + return (int(s, b), int(s_, b)) + else: + return int(s, b) + +# parse some rbyd addr encodings +# 0xa -> (0xa,) +# 0xa.c -> ((0xa, 0xc),) +# 0x{a,b} -> (0xa, 0xb) +# 0x{a,b}.c -> ((0xa, 0xc), (0xb, 0xc)) +def rbydaddr(s): + s = s.strip() + b = 10 + if s.startswith('0x') or s.startswith('0X'): + s = s[2:] + b = 16 + elif s.startswith('0o') or s.startswith('0O'): + s = s[2:] + b = 8 + elif s.startswith('0b') or s.startswith('0B'): + s = s[2:] + b = 2 + + trunk = None + if '.' in s: + s, s_ = s.split('.', 1) + trunk = int(s_, b) + + if s.startswith('{') and '}' in s: + ss = s[1:s.find('}')].split(',') + else: + ss = [s] + + addr = [] + for s in ss: + if trunk is not None: + addr.append((int(s, b), trunk)) + else: + addr.append(int(s, b)) + + return tuple(addr) + +def xxd(data, width=16): + for i in range(0, len(data), width): + yield '%-*s %-*s' % ( + 3*width, + ' '.join('%02x' % b for b in data[i:i+width]), + width, + ''.join( + b if b >= ' ' and b <= '~' else '.' + for b in map(chr, data[i:i+width]))) + +def crc32c(data, crc=0): + if crc32c_lib is not None: + return crc32c_lib.crc32c(data, crc) + else: + crc ^= 0xffffffff + for b in data: + crc ^= b + for j in range(8): + crc = (crc >> 1) ^ ((crc & 1) * 0x82f63b78) + return 0xffffffff ^ crc + +def main(disk, blocks=None, *, + block_size=None, + block_count=None, + off=None, + size=None): + # is bd geometry specified? + if isinstance(block_size, tuple): + block_size, block_count_ = block_size + if block_count is None: + block_count = block_count_ + + with open(disk, 'rb') as f: + # if block_size is omitted, assume the block device is one big block + if block_size is None: + f.seek(0, os.SEEK_END) + block_size = f.tell() + block_count = 1 + + # if block_count is omitted, derive the block_count from our file size + if block_count is None: + f.seek(0, os.SEEK_END) + block_count = f.tell() // block_size + + # flatten blocks, default to block 0 + blocks = (list(it.chain.from_iterable( + range(block.start or 0, block.stop or block_count) + if isinstance(block, slice) + else block + for block in blocks)) + if blocks + else [0]) + + # blocks may also encode offsets + blocks, offs, size = ( + [block[0] if isinstance(block, tuple) + else block + for block in blocks], + [off.start if isinstance(off, slice) + else off if off is not None + else size.start if isinstance(size, slice) + else block[1] if isinstance(block, tuple) + else None + for block in blocks], + (size.stop - (size.start or 0) + if size.stop is not None + else None) if isinstance(size, slice) + else size if size is not None + else ((off.stop - (off.start or 0)) + if off.stop is not None + else None) if isinstance(off, slice) + else None) + + # cat the blocks + for block, off in zip(blocks, offs): + # bound to block_size + block_ = block if block is not None else 0 + off_ = off if off is not None else 0 + size_ = size if size is not None else block_size - off_ + if off_ >= block_size: + continue + size_ = min(off_ + size_, block_size) - off_ + + # cat the block + f.seek((block_ * block_size) + off_) + data = f.read(size_) + sys.stdout.buffer.write(data) + + sys.stdout.flush() + + +if __name__ == "__main__": + import argparse + import sys + parser = argparse.ArgumentParser( + description="Cat data from a block device.", + allow_abbrev=False) + parser.add_argument( + 'disk', + help="File containing the block device.") + parser.add_argument( + 'blocks', + nargs='*', + type=lambda x: ( + slice(*(int(x, 0) if x.strip() else None + for x in x.split(',', 1))) + if ',' in x and '{' not in x + else rbydaddr(x)), + help="Block addresses, may be a range.") + parser.add_argument( + '-b', '--block-size', + type=bdgeom, + help="Block size/geometry in bytes. Accepts x.") + parser.add_argument( + '--block-count', + type=lambda x: int(x, 0), + help="Block count in blocks.") + parser.add_argument( + '--off', + type=lambda x: ( + slice(*(int(x, 0) if x.strip() else None + for x in x.split(',', 1))) + if ',' in x + else int(x, 0)), + help="Show a specific offset, may be a range.") + parser.add_argument( + '-n', '--size', + type=lambda x: ( + slice(*(int(x, 0) if x.strip() else None + for x in x.split(',', 1))) + if ',' in x + else int(x, 0)), + help="Show this many bytes, may be a range.") + sys.exit(main(**{k: v + for k, v in vars(parser.parse_intermixed_args()).items() + if v is not None})) diff --git a/scripts/dbgerr.py b/scripts/dbgerr.py new file mode 100755 index 000000000..8db406311 --- /dev/null +++ b/scripts/dbgerr.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 + +# prevent local imports +if __name__ == "__main__": + __import__('sys').path.pop(0) + +import functools as ft + + +# Error codes +ERR_OK = 0 # No error +ERR_UNKNOWN = -1 # Unknown error +ERR_INVAL = -22 # Invalid parameter +ERR_NOTSUP = -95 # Operation not supported +ERR_BUSY = -16 # Device or resource busy +ERR_IO = -5 # Error during device operation +ERR_CORRUPT = -84 # Corrupted +ERR_NOENT = -2 # No directory entry +ERR_EXIST = -17 # Entry already exists +ERR_NOTDIR = -20 # Entry is not a dir +ERR_ISDIR = -21 # Entry is a dir +ERR_NOTEMPTY = -39 # Dir is not empty +ERR_FBIG = -27 # File too large +ERR_NOSPC = -28 # No space left on device +ERR_NOMEM = -12 # No more memory available +ERR_NOATTR = -61 # No data/attr available +ERR_NAMETOOLONG = -36 # File name too long +ERR_RANGE = -34 # Result out of range + + +# self-parsing error codes +class Err: + def __init__(self, name, code, help): + self.name = name + self.code = code + self.help = help + + def __repr__(self): + return 'Err(%r, %r, %r)' % ( + self.name, + self.code, + self.help) + + def __eq__(self, other): + return self.name == other.name + + def __ne__(self, other): + return self.name != other.name + + def __hash__(self): + return hash(self.name) + + def line(self): + return ('LFS3_%s' % self.name, '%d' % self.code, self.help) + + @staticmethod + @ft.cache + def errs(): + # parse our script's source to figure out errs + import inspect + import re + errs = [] + err_pattern = re.compile( + '^(?PERR_[^ ]*) *= *(?P[^#]*?) *' + '#+ *(?P.*)$') + for line in (inspect.getsource( + inspect.getmodule(inspect.currentframe())) + .replace('\\\n', '') + .splitlines()): + m = err_pattern.match(line) + if m: + errs.append(Err( + m.group('name'), + globals()[m.group('name')], + m.group('help'))) + return errs + + +def main(errs, *, + list=False): + import builtins + list_, list = list, builtins.list + + # find errs + errs__ = Err.errs() + + lines = [] + # list all known error codes + if list_: + for e in errs__: + lines.append(e.line()) + + # find errs by name or value + else: + for e_ in errs: + found = False + # find by LFS3_ERR_+name + for e in errs__: + if 'LFS3_%s' % e.name.upper() == e_.upper(): + lines.append(e.line()) + found = True + if found: + continue + # find by ERR_+name + for e in errs__: + if e.name.upper() == e_.upper(): + lines.append(e.line()) + found = True + if found: + continue + # find by name + for e in errs__: + if e.name.split('_', 1)[1] == e_.upper(): + lines.append(e.line()) + found = True + if found: + continue + # find by E+name + for e in errs__: + if 'E%s' % e.name.split('_', 1)[1].upper() == e_.upper(): + lines.append(e.line()) + found = True + if found: + continue + try: + # find by err code + for e in errs__: + if e.code == int(e_, 0): + lines.append(e.line()) + found = True + if found: + continue + # find by negated err code + for e in errs__: + if e.code == -int(e_, 0): + lines.append(e.line()) + found = True + if found: + continue + except ValueError: + lines.append(('?', e_, 'Unknown err code')) + + # first find widths + w = [0, 0] + for l in lines: + w[0] = max(w[0], len(l[0])) + w[1] = max(w[1], len(l[1])) + + # then print results + for l in lines: + print('%-*s %-*s %s' % ( + w[0], l[0], + w[1], l[1], + l[2])) + + +if __name__ == "__main__": + import argparse + import sys + parser = argparse.ArgumentParser( + description="Decode littlefs error codes.", + allow_abbrev=False) + parser.add_argument( + 'errs', + nargs='*', + help="Error codes or error names to decode.") + parser.add_argument( + '-l', '--list', + action='store_true', + help="List all known error codes.") + sys.exit(main(**{k: v + for k, v in vars(parser.parse_intermixed_args()).items() + if v is not None})) diff --git a/scripts/dbgflags.py b/scripts/dbgflags.py new file mode 100755 index 000000000..3470558df --- /dev/null +++ b/scripts/dbgflags.py @@ -0,0 +1,450 @@ +#!/usr/bin/env python3 + +# prevent local imports +if __name__ == "__main__": + __import__('sys').path.pop(0) + +import collections as co +import functools as ft + + +# Flag prefixes +PREFIX_O = ['+o', '+open'] # Filter by LFS3_O_* flags +PREFIX_SEEK = ['+seek'] # Filter by LFS3_SEEK_* flags +PREFIX_A = ['+a', '+attr'] # Filter by LFS3_A_* flags +PREFIX_F = ['+f', '+format'] # Filter by LFS3_F_* flags +PREFIX_M = ['+m', '+mount'] # Filter by LFS3_M_* flags +PREFIX_CK = ['+ck'] # Filter by LFS3_CK_* flags +PREFIX_GC = ['+gc'] # Filter by LFS3_GC_* flags +PREFIX_I = ['+i', '+info'] # Filter by LFS3_I_* flags +PREFIX_T = ['+t', '+trv'] # Filter by LFS3_T_* flags +PREFIX_ALLOC = ['+alloc'] # Filter by LFS3_ALLOC_* flags +PREFIX_RCOMPAT = ['+rc', '+rcompat'] # Filter by LFS3_RCOMPAT_* flags +PREFIX_WCOMPAT = ['+wc', '+wcompat'] # Filter by LFS3_WCOMPAT_* flags +PREFIX_OCOMPAT = ['+oc', '+ocompat'] # Filter by LFS3_OCOMPAT_* flags + + +# File open flags +O_MODE = 3 # -m The file's access mode +O_RDONLY = 0 # -^ Open a file as read only +O_WRONLY = 1 # -^ Open a file as write only +O_RDWR = 2 # -^ Open a file as read and write +O_CREAT = 0x00000004 # -- Create a file if it does not exist +O_EXCL = 0x00000008 # -- Fail if a file already exists +O_TRUNC = 0x00000010 # -- Truncate the existing file to zero size +O_APPEND = 0x00000020 # -- Move to end of file on every write +O_FLUSH = 0x00000040 # y- Flush data on every write +O_SYNC = 0x00000080 # y- Sync metadata on every write +O_DESYNC = 0x00100000 # -- Do not sync or recieve file updates + +O_CKMETA = 0x00010000 # -- Check metadata checksums +O_CKDATA = 0x00020000 # -- Check metadata + data checksums + +o_WRSET = 3 # i- Open a file as an atomic write +o_TYPE = 0xf0000000 # im The file's type +o_REG = 0x10000000 # i^ Type = regular-file +o_DIR = 0x20000000 # i^ Type = directory +o_STICKYNOTE = 0x30000000 # i^ Type = stickynote +o_BOOKMARK = 0x40000000 # i^ Type = bookmark +o_ORPHAN = 0x50000000 # i^ Type = orphan +o_TRAVERSAL = 0x60000000 # i^ Type = traversal +o_UNKNOWN = 0x70000000 # i^ Type = unknown +o_ZOMBIE = 0x08000000 # i- File has been removed +o_UNCREAT = 0x04000000 # i- File does not exist yet +o_UNSYNC = 0x02000000 # i- File's metadata does not match disk +o_UNCRYST = 0x01000000 # i- File's leaf not fully crystallized +o_UNGRAFT = 0x00800000 # i- File's leaf does not match disk +o_UNFLUSH = 0x00400000 # i- File's cache does not match disk + +# File seek flags +SEEK_MODE = 0xffffffff # -m Seek mode +SEEK_SET = 0 # -^ Seek relative to an absolute position +SEEK_CUR = 1 # -^ Seek relative to the current file position +SEEK_END = 2 # -^ Seek relative to the end of the file + +# Custom attribute flags +A_MODE = 3 # -m The attr's access mode +A_RDONLY = 0 # -^ Open an attr as read only +A_WRONLY = 1 # -^ Open an attr as write only +A_RDWR = 2 # -^ Open an attr as read and write +A_LAZY = 0x04 # -- Only write attr if file changed + +# Filesystem format flags +F_MODE = 1 # -m Format's access mode +F_RDWR = 0 # -^ Format the filesystem as read and write +F_GBMAP = 0x02000000 # y- Use the global on-disk block-map + +F_REVDBG = 0x00000010 # y- Add debug info to revision counts +F_REVNOISE = 0x00000020 # y- Add noise to revision counts +F_CKPROGS = 0x00100000 # y- Check progs by reading back progged data +F_CKFETCHES = 0x00200000 # y- Check block checksums before first use +F_CKMETAPARITY = 0x00400000 # y- Check metadata tag parity bits +F_CKDATACKSUMS = 0x01000000 # y- Check data checksums on reads + +F_MKCONSISTENT = 0x00000800 # y- Make the filesystem consistent +F_LOOKAHEAD = 0x00001000 # y- Repopulate lookahead buffer +F_LOOKGBMAP = 0x00002000 # y- Repopulate the gbmap +F_COMPACTMETA = 0x00008000 # y- Compact metadata logs +F_CKMETA = 0x00010000 # y- Check metadata checksums +F_CKDATA = 0x00020000 # y- Check metadata + data checksums + +# Filesystem mount flags +M_MODE = 1 # -m Mount's access mode +M_RDWR = 0 # -^ Mount the filesystem as read and write +M_RDONLY = 1 # -^ Mount the filesystem as read only +M_FLUSH = 0x00000040 # y- Open all files with LFS3_O_FLUSH +M_SYNC = 0x00000080 # y- Open all files with LFS3_O_SYNC +M_REVDBG = 0x00000010 # y- Add debug info to revision counts +M_REVNOISE = 0x00000020 # y- Add noise to revision counts +M_CKPROGS = 0x00100000 # y- Check progs by reading back progged data +M_CKFETCHES = 0x00200000 # y- Check block checksums before first use +M_CKMETAPARITY = 0x00400000 # y- Check metadata tag parity bits +M_CKDATACKSUMS = 0x01000000 # y- Check data checksums on reads + +M_MKCONSISTENT = 0x00000800 # y- Make the filesystem consistent +M_LOOKAHEAD = 0x00001000 # y- Repopulate lookahead buffer +M_LOOKGBMAP = 0x00002000 # y- Repopulate the gbmap +M_COMPACTMETA = 0x00008000 # y- Compact metadata logs +M_CKMETA = 0x00010000 # y- Check metadata checksums +M_CKDATA = 0x00020000 # y- Check metadata + data checksums + +# File/filesystem check flags +CK_MKCONSISTENT = 0x00000800 # -- Make the filesystem consistent +CK_LOOKAHEAD = 0x00001000 # -- Repopulate lookahead buffer +CK_LOOKGBMAP = 0x00002000 # -- Repopulate the gbmap +CK_COMPACTMETA = 0x00008000 # -- Compact metadata logs +CK_CKMETA = 0x00010000 # -- Check metadata checksums +CK_CKDATA = 0x00020000 # -- Check metadata + data checksums + +# GC flags +GC_MKCONSISTENT = 0x00000800 # -- Make the filesystem consistent +GC_LOOKAHEAD = 0x00001000 # -- Repopulate lookahead buffer +GC_LOOKGBMAP = 0x00002000 # -- Repopulate the gbmap +GC_COMPACTMETA = 0x00008000 # -- Compact metadata logs +GC_CKMETA = 0x00010000 # -- Check metadata checksums +GC_CKDATA = 0x00020000 # -- Check metadata + data checksums + +# Filesystem info flags +I_RDONLY = 0x00000001 # -- Mounted read only +I_GBMAP = 0x02000000 # -- Global on-disk block-map in use + +I_FLUSH = 0x00000040 # -- Mounted with LFS3_M_FLUSH +I_SYNC = 0x00000080 # -- Mounted with LFS3_M_SYNC +I_REVDBG = 0x00000010 # -- Mounted with LFS3_M_REVDBG +I_REVNOISE = 0x00000020 # -- Mounted with LFS3_M_REVNOISE +I_CKPROGS = 0x00100000 # -- Mounted with LFS3_M_CKPROGS +I_CKFETCHES = 0x00200000 # -- Mounted with LFS3_M_CKFETCHES +I_CKMETAPARITY = 0x00400000 # -- Mounted with LFS3_M_CKMETAPARITY +I_CKDATACKSUMS = 0x01000000 # -- Mounted with LFS3_M_CKDATACKSUMS + +I_MKCONSISTENT = 0x00000800 # -- Filesystem needs mkconsistent to write +I_LOOKAHEAD = 0x00001000 # -- Lookahead buffer is not full +I_LOOKGBMAP = 0x00002000 # -- The gbmap is not full +I_COMPACTMETA = 0x00008000 # -- Filesystem may have uncompacted metadata +I_CKMETA = 0x00010000 # -- Metadata checksums not checked recently +I_CKDATA = 0x00020000 # -- Data checksums not checked recently + +# Traversal flags +T_MODE = 1 # -m The traversal's access mode +T_RDWR = 0 # -^ Open traversal as read and write +T_RDONLY = 1 # -^ Open traversal as read only +T_MTREEONLY = 0x00000002 # -- Only traverse the mtree +T_EXCL = 0x00000008 # -- Error if filesystem modified +T_MKCONSISTENT = 0x00000800 # -- Make the filesystem consistent +T_LOOKAHEAD = 0x00001000 # -- Repopulate lookahead buffer +T_LOOKGBMAP = 0x00002000 # -- Repopulate the gbmap +T_COMPACTMETA = 0x00008000 # -- Compact metadata logs +T_CKMETA = 0x00010000 # -- Check metadata checksums +T_CKDATA = 0x00020000 # -- Check metadata + data checksums + +t_TYPE = 0xf0000000 # im The traversal's type +t_REG = 0x10000000 # i^ Type = regular-file +t_DIR = 0x20000000 # i^ Type = directory +t_STICKYNOTE = 0x30000000 # i^ Type = stickynote +t_BOOKMARK = 0x40000000 # i^ Type = bookmark +t_ORPHAN = 0x50000000 # i^ Type = orphan +t_TRAVERSAL = 0x60000000 # i^ Type = traversal +t_UNKNOWN = 0x70000000 # i^ Type = unknown +t_BTYPE = 0x00f00000 # im The current block type +t_MDIR = 0x00100000 # i^ Btype = mdir +t_BTREE = 0x00200000 # i^ Btype = btree +t_DATA = 0x00300000 # i^ Btype = data +t_ZOMBIE = 0x08000000 # i- File has been removed +t_CKPOINTED = 0x04000000 # i- Filesystem ckpointed during traversal +t_DIRTY = 0x02000000 # i- Filesystem ckpointed outside traversal +t_STALE = 0x01000000 # i- Block queue probably out-of-date + +# Block allocator flags +alloc_ERASE = 0x00000001 # i- Please erase the block + +# Read-compat flags +RCOMPAT_NONSTANDARD = 0x00000001 # -- Non-standard filesystem format +RCOMPAT_WRONLY = 0x00000004 # -- Reading is disallowed +RCOMPAT_MMOSS = 0x00000010 # -- May use an inlined mdir +RCOMPAT_MSPROUT = 0x00000020 # -- May use an mdir pointer +RCOMPAT_MSHRUB = 0x00000040 # -- May use an inlined mtree +RCOMPAT_MTREE = 0x00000080 # -- May use an mdir btree +RCOMPAT_BMOSS = 0x00000100 # -- Files may use inlined data +RCOMPAT_BSPROUT = 0x00000200 # -- Files may use block pointers +RCOMPAT_BSHRUB = 0x00000400 # -- Files may use inlined btrees +RCOMPAT_BTREE = 0x00000800 # -- Files may use btrees +RCOMPAT_GRM = 0x00010000 # -- Global-remove in use +rcompat_OVERFLOW = 0x80000000 # i- Can't represent all flags + +# Write-compat flags +WCOMPAT_NONSTANDARD = 0x00000001 # -- Non-standard filesystem format +WCOMPAT_RDONLY = 0x00000002 # -- Writing is disallowed +WCOMPAT_GCKSUM = 0x00040000 # -- Global-checksum in use +WCOMPAT_GBMAP = 0x00080000 # -- Global on-disk block-map in use +WCOMPAT_DIR = 0x01000000 # -- Directory file types in use +wcompat_OVERFLOW = 0x80000000 # i- Can't represent all flags + +# Optional-compat flags +OCOMPAT_NONSTANDARD = 0x00000001 # -- Non-standard filesystem format +ocompat_OVERFLOW = 0x80000000 # i- Can't represent all flags + + +# self-parsing prefixes +class Prefix: + def __init__(self, name, aliases, help): + self.name = name + self.aliases = aliases + self.help = help + + def __repr__(self): + return 'Prefix(%r, %r, %r)' % ( + self.name, + self.aliases, + self.help) + + def __eq__(self, other): + return self.name == other.name + + def __ne__(self, other): + return self.name != other.name + + def __hash__(self): + return hash(self.name) + + @staticmethod + @ft.cache + def prefixes(): + # parse our script's source to figure out prefixes + import inspect + import re + prefixes = [] + prefix_pattern = re.compile( + '^(?PPREFIX_[^ ]*) *= *(?P[^#]*?) *' + '#+ *(?P.*)$') + for line in (inspect.getsource( + inspect.getmodule(inspect.currentframe())) + .replace('\\\n', '') + .splitlines()): + m = prefix_pattern.match(line) + if m: + prefixes.append(Prefix( + m.group('name'), + globals()[m.group('name')], + m.group('help'))) + return prefixes + +# self-parsing flags +class Flag: + def __init__(self, name, flag, help, *, + prefix=None, + yes=False, + internal=False, + mask=False, + type=False): + self.name = name + self.flag = flag + self.help = help + self.prefix = prefix + self.yes = yes + self.internal = internal + self.mask = mask + self.type = type + + def __repr__(self): + return 'Flag(%r, %r, %r)' % ( + self.name, + self.flag, + self.help) + + def __eq__(self, other): + return self.name == other.name + + def __ne__(self, other): + return self.name != other.name + + def __hash__(self): + return hash(self.name) + + def line(self): + return ('LFS3_%s' % self.name, '0x%08x' % self.flag, self.help) + + @staticmethod + @ft.cache + def flags(): + # parse our script's source to figure out flags + import inspect + import re + + # limit to known prefixes + prefixes_ = {p.name.split('_', 1)[1].upper(): p + for p in Prefix.prefixes()} + # keep track of last mask + mask_ = None + + flags = [] + flag_pattern = re.compile( + '^(?P(?i:%s)_[^ ]*) ' + '*= *(?P[^#]*?) *' + '#+ (?P[^ ]+) *(?P.*)$' + % '|'.join(prefixes_.keys())) + for line in (inspect.getsource( + inspect.getmodule(inspect.currentframe())) + .replace('\\\n', '') + .splitlines()): + m = flag_pattern.match(line) + if m: + flags.append(Flag( + m.group('name'), + globals()[m.group('name')], + m.group('help'), + # associate flags -> prefix + prefix=prefixes_[ + m.group('name').split('_', 1)[0].upper()], + yes='y' in m.group('mode'), + internal='i' in m.group('mode'), + mask='m' in m.group('mode'), + # associate types -> mask + type=mask_ if '^' in m.group('mode') else False)) + + # keep track of last mask + if flags[-1].mask: + mask_ = flags[-1] + + return flags + + +def main(flags, *, + list=False, + all=False, + prefixes=[]): + import builtins + list_, list = list, builtins.list + all_, all = all, builtins.all + + # find flags + flags__ = Flag.flags() + + # filter by prefixes if there are any prefixes + if prefixes: + prefixes = set(prefixes) + flags__ = [f for f in flags__ if f.prefix in prefixes] + + lines = [] + # list all known flags + if list_: + for f in flags__: + if not all_ and (f.internal or f.type): + continue + lines.append(f.line()) + + # find flags by name or value + else: + for f_ in flags: + found = False + # find by LFS3_+prefix+_+name + for f in flags__: + if 'LFS3_%s' % f.name.upper() == f_.upper(): + lines.append(f.line()) + found = True + if found: + continue + # find by prefix+_+name + for f in flags__: + if '%s' % f.name.upper() == f_.upper(): + lines.append(f.line()) + found = True + if found: + continue + # find by name + for f in flags__: + if f.name.split('_', 1)[1].upper() == f_.upper(): + lines.append(f.line()) + found = True + if found: + continue + # find by value + try: + f__ = int(f_, 0) + f___ = f__ + for f in flags__: + # ignore type masks here + if f.mask: + continue + # matches flag? + if not f.type and (f__ & f.flag) == f.flag: + lines.append(f.line()) + f___ &= ~f.flag + # matches type? + elif f.type and (f__ & f.type.flag) == f.flag: + lines.append(f.line()) + f___ &= ~f.type.flag + if f___: + lines.append(('?', '0x%08x' % f___, 'Unknown flags')) + except ValueError: + lines.append(('?', f_, 'Unknown flag')) + + # first find widths + w = [0, 0] + for l in lines: + w[0] = max(w[0], len(l[0])) + w[1] = max(w[1], len(l[1])) + + # then print results + for l in lines: + print('%-*s %-*s %s' % ( + w[0], l[0], + w[1], l[1], + l[2])) + + +if __name__ == "__main__": + import argparse + import sys + parser = argparse.ArgumentParser( + description="Decode littlefs flags.", + allow_abbrev=False, + # allow + for prefix filters + prefix_chars='-+') + parser.add_argument( + 'flags', + nargs='*', + help="Flags or names of flags to decode.") + parser.add_argument( + '-l', '--list', + action='store_true', + help="List all known flags.") + parser.add_argument( + '-a', '--all', + action='store_true', + help="Also show internal flags and types.") + class AppendPrefix(argparse.Action): + def __init__(self, nargs=None, **kwargs): + super().__init__(nargs=0, **kwargs) + def __call__(self, parser, namespace, value, option): + if getattr(namespace, 'prefixes', None) is None: + namespace.prefixes = [] + namespace.prefixes.append(self.const) + for p in Prefix.prefixes(): + parser.add_argument( + *p.aliases, + action=AppendPrefix, + const=p, + help=p.help+'.') + sys.exit(main(**{k: v + for k, v in vars(parser.parse_intermixed_args()).items() + if v is not None})) diff --git a/scripts/dbgle32.py b/scripts/dbgle32.py new file mode 100755 index 000000000..aff8d00f2 --- /dev/null +++ b/scripts/dbgle32.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 + +# prevent local imports +if __name__ == "__main__": + __import__('sys').path.pop(0) + +import io +import math as mt +import os +import struct +import sys + + +# open with '-' for stdin/stdout +def openio(path, mode='r', buffering=-1): + import os + if path == '-': + if 'r' in mode: + return os.fdopen(os.dup(sys.stdin.fileno()), mode, buffering) + else: + return os.fdopen(os.dup(sys.stdout.fileno()), mode, buffering) + else: + return open(path, mode, buffering) + +def dbg_le32s(data, *, + word_bits=32): + # figure out le32 size in bytes + if word_bits != 0: + n = mt.ceil(word_bits / 8) + + # parse le32s, or le + lines = [] + j = 0 + while j < len(data): + word = 0 + d = 0 + while (j+d < len(data) + and (d < n if word_bits != 0 else True)): + word |= data[j+d] << d + d += 1 + + lines.append(( + ' '.join('%02x' % b for b in data[j:j+d]), + word)) + j += d + + # figure out widths + w = [0] + for l in lines: + w[0] = max(w[0], len(l[0])) + + # then print results + for l in lines: + print('%-*s %s' % ( + w[0], l[0], + l[1])) + +def main(le32s, *, + hex=False, + input=None, + word_bits=32): + import builtins + hex_, hex = hex, builtins.hex + + # interpret as a sequence of hex bytes + if hex_: + bytes_ = [b for le32 in le32s for b in le32.split()] + dbg_le32s(bytes(int(b, 16) for b in bytes_), + word_bits=word_bits) + + # parse le32s in a file + elif input: + with openio(input, 'rb') as f: + dbg_le32s(f.read(), + word_bits=word_bits) + + # we don't currently have a default interpretation + else: + print("error: no -x/--hex or -i/--input?", + file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + import argparse + import sys + parser = argparse.ArgumentParser( + description="Decode le32s.", + allow_abbrev=False) + parser.add_argument( + 'le32s', + nargs='*', + help="Le32s to decode.") + parser.add_argument( + '-x', '--hex', + action='store_true', + help="Interpret as a sequence of hex bytes.") + parser.add_argument( + '-i', '--input', + help="Read le32s from this file. Can use - for stdin.") + parser.add_argument( + '-w', '--word', '--word-bits', + dest='word_bits', + nargs='?', + type=lambda x: int(x, 0), + const=0, + help="Word size in bits. 0 is unbounded. Defaults to 32.") + sys.exit(main(**{k: v + for k, v in vars(parser.parse_intermixed_args()).items() + if v is not None})) diff --git a/scripts/dbgleb128.py b/scripts/dbgleb128.py new file mode 100755 index 000000000..af98a181e --- /dev/null +++ b/scripts/dbgleb128.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 + +# prevent local imports +if __name__ == "__main__": + __import__('sys').path.pop(0) + +import io +import math as mt +import os +import struct +import sys + + +# open with '-' for stdin/stdout +def openio(path, mode='r', buffering=-1): + import os + if path == '-': + if 'r' in mode: + return os.fdopen(os.dup(sys.stdin.fileno()), mode, buffering) + else: + return os.fdopen(os.dup(sys.stdout.fileno()), mode, buffering) + else: + return open(path, mode, buffering) + +def fromleb128(data, j=0): + word = 0 + d = 0 + while j+d < len(data): + b = data[j+d] + word |= (b & 0x7f) << 7*d + word &= 0xffffffff + if not b & 0x80: + return word, d+1 + d += 1 + return word, d + +def dbg_leb128s(data, *, + word_bits=32): + # figure out leb128 size in bytes + if word_bits != 0: + n = mt.ceil(word_bits / 7) + + # parse leb128s + lines = [] + j = 0 + while j < len(data): + # bounded leb128s? + if word_bits != 0: + word, d = fromleb128(data[j:j+n]) + # unbounded? + else: + word, d = fromleb128(data, j) + + lines.append(( + ' '.join('%02x' % b for b in data[j:j+d]), + word)) + j += d + + # figure out widths + w = [0] + for l in lines: + w[0] = max(w[0], len(l[0])) + + # then print results + for l in lines: + print('%-*s %s' % ( + w[0], l[0], + l[1])) + +def main(leb128s, *, + hex=False, + input=None, + word_bits=32): + import builtins + hex_, hex = hex, builtins.hex + + # interpret as a sequence of hex bytes + if hex_: + bytes_ = [b for leb128 in leb128s for b in leb128.split()] + dbg_leb128s(bytes(int(b, 16) for b in bytes_), + word_bits=word_bits) + + # parse leb128s in a file + elif input: + with openio(input, 'rb') as f: + dbg_leb128s(f.read(), + word_bits=word_bits) + + # we don't currently have a default interpretation + else: + print("error: no -x/--hex or -i/--input?", + file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + import argparse + import sys + parser = argparse.ArgumentParser( + description="Decode leb128s.", + allow_abbrev=False) + parser.add_argument( + 'leb128s', + nargs='*', + help="Leb128s to decode.") + parser.add_argument( + '-x', '--hex', + action='store_true', + help="Interpret as a sequence of hex bytes.") + parser.add_argument( + '-i', '--input', + help="Read leb128s from this file. Can use - for stdin.") + parser.add_argument( + '-w', '--word', '--word-bits', + dest='word_bits', + nargs='?', + type=lambda x: int(x, 0), + const=0, + help="Word size in bits. 0 is unbounded. Defaults to 32.") + sys.exit(main(**{k: v + for k, v in vars(parser.parse_intermixed_args()).items() + if v is not None})) diff --git a/scripts/dbglfs3.py b/scripts/dbglfs3.py new file mode 100755 index 000000000..f8a0a6beb --- /dev/null +++ b/scripts/dbglfs3.py @@ -0,0 +1,5091 @@ +#!/usr/bin/env python3 + +# prevent local imports +if __name__ == "__main__": + __import__('sys').path.pop(0) + +import bisect +import collections as co +import functools as ft +import itertools as it +import math as mt +import os +import struct +import sys + +try: + import crc32c as crc32c_lib +except ModuleNotFoundError: + crc32c_lib = None + + +RCOMPAT_NONSTANDARD = 0x00000001 # Non-standard filesystem format +RCOMPAT_WRONLY = 0x00000004 # Reading is disallowed +RCOMPAT_MMOSS = 0x00000010 # May use an inlined mdir +RCOMPAT_MSPROUT = 0x00000020 # May use an mdir pointer +RCOMPAT_MSHRUB = 0x00000040 # May use an inlined mtree +RCOMPAT_MTREE = 0x00000080 # May use an mdir btree +RCOMPAT_BMOSS = 0x00000100 # Files may use inlined data +RCOMPAT_BSPROUT = 0x00000200 # Files may use block pointers +RCOMPAT_BSHRUB = 0x00000400 # Files may use inlined btrees +RCOMPAT_BTREE = 0x00000800 # Files may use btrees +RCOMPAT_GRM = 0x00010000 # Global-remove in use + +WCOMPAT_NONSTANDARD = 0x00000001 # Non-standard filesystem format +WCOMPAT_RDONLY = 0x00000002 # Writing is disallowed +WCOMPAT_GCKSUM = 0x00040000 # Global-checksum in use +WCOMPAT_GBMAP = 0x00080000 # Global on-disk block-map in use +WCOMPAT_DIR = 0x01000000 # Directory file types in use + +TAG_NULL = 0x0000 ## v--- ---- +--- ---- +TAG_INTERNAL = 0x0000 ## v--- ---- +ttt tttt +TAG_CONFIG = 0x0100 ## v--- ---1 +ttt tttt +TAG_MAGIC = 0x0131 # v--- ---1 +-11 --rr +TAG_VERSION = 0x0134 # v--- ---1 +-11 -1-- +TAG_RCOMPAT = 0x0135 # v--- ---1 +-11 -1-1 +TAG_WCOMPAT = 0x0136 # v--- ---1 +-11 -11- +TAG_OCOMPAT = 0x0137 # v--- ---1 +-11 -111 +TAG_GEOMETRY = 0x0138 # v--- ---1 +-11 1--- +TAG_NAMELIMIT = 0x0139 # v--- ---1 +-11 1--1 +TAG_FILELIMIT = 0x013a # v--- ---1 +-11 1-1- +TAG_GDELTA = 0x0200 ## v--- --1- +ttt tttt +TAG_GRMDELTA = 0x0230 # v--- --1- +-11 --++ +TAG_GBMAPDELTA = 0x0234 # v--- --1- +-11 -1rr +TAG_NAME = 0x0300 ## v--- --11 +ttt tttt +TAG_BNAME = 0x0300 # v--- --11 +--- ---- +TAG_REG = 0x0301 # v--- --11 +--- ---1 +TAG_DIR = 0x0302 # v--- --11 +--- --1- +TAG_STICKYNOTE = 0x0303 # v--- --11 +--- --11 +TAG_BOOKMARK = 0x0304 # v--- --11 +--- -1-- +TAG_MNAME = 0x0330 # v--- --11 +-11 ---- +TAG_STRUCT = 0x0400 ## v--- -1-- +ttt tttt +TAG_BRANCH = 0x0400 # v--- -1-- +--- --rr +TAG_DATA = 0x0404 # v--- -1-- +--- -1rr +TAG_BLOCK = 0x0408 # v--- -1-- +--- 1err +TAG_DID = 0x0420 # v--- -1-- +-1- ---- +TAG_BSHRUB = 0x0428 # v--- -1-- +-1- 1-rr +TAG_BTREE = 0x042c # v--- -1-- +-1- 11rr +TAG_MROOT = 0x0431 # v--- -1-- +-11 --rr +TAG_MDIR = 0x0435 # v--- -1-- +-11 -1rr +TAG_MTREE = 0x043c # v--- -1-- +-11 11rr +TAG_BMRANGE = 0x0440 # v--- -1-- +1-- ++uu +TAG_BMFREE = 0x0440 # v--- -1-- +1-- ---- +TAG_BMINUSE = 0x0441 # v--- -1-- +1-- ---1 +TAG_BMERASED = 0x0442 # v--- -1-- +1-- --1- +TAG_BMBAD = 0x0443 # v--- -1-- +1-- --11 +TAG_ATTR = 0x0600 ## v--- -11a +aaa aaaa +TAG_UATTR = 0x0600 # v--- -11- +aaa aaaa +TAG_SATTR = 0x0700 # v--- -111 +aaa aaaa +TAG_SHRUB = 0x1000 ## v--1 kkkk +kkk kkkk +TAG_ALT = 0x4000 ## v1cd kkkk +kkk kkkk +TAG_B = 0x0000 +TAG_R = 0x2000 +TAG_LE = 0x0000 +TAG_GT = 0x1000 +TAG_CKSUM = 0x3000 ## v-11 ---- ++++ +pqq +TAG_PHASE = 0x0003 +TAG_PERTURB = 0x0004 +TAG_NOTE = 0x3100 ## v-11 ---1 ++++ ++++ +TAG_ECKSUM = 0x3200 ## v-11 --1- ++++ ++++ +TAG_GCKSUMDELTA = 0x3300 ## v-11 --11 ++++ ++++ + + +# self-parsing tag repr +class Tag: + def __init__(self, name, tag, encoding, help): + self.name = name + self.tag = tag + self.encoding = encoding + self.help = help + # derive mask from encoding + self.mask = sum( + (1 if x in 'v-01' else 0) << len(self.encoding)-1-i + for i, x in enumerate(self.encoding)) + + def __repr__(self): + return 'Tag(%r, %r, %r)' % ( + self.name, + self.tag, + self.encoding) + + def __eq__(self, other): + return self.name == other.name + + def __ne__(self, other): + return self.name != other.name + + def __hash__(self): + return hash(self.name) + + def line(self): + # substitute mask chars when zero + tag = '0x%s' % ''.join( + n if n != '0' else next( + (x for x in self.encoding[i*4:i*4+4] + if x not in 'v-01+'), + '0') + for i, n in enumerate('%04x' % self.tag)) + # group into nibbles + encoding = ' '.join(self.encoding[i*4:i*4+4] + for i in range(len(self.encoding)//4)) + return ('LFS3_%s' % self.name, tag, encoding) + + def specificity(self): + return sum(1 for x in self.encoding if x in 'v-01') + + def matches(self, tag): + return (tag & self.mask) == (self.tag & self.mask) + + def get(self, chars, tag): + return sum( + tag & ((1 if x in chars else 0) << len(self.encoding)-1-i) + for i, x in enumerate(self.encoding)) + + def max(self, chars): + return max(len(self.encoding)-1-i + for i, x in enumerate(self.encoding) if x in chars) + + def min(self, chars): + return min(len(self.encoding)-1-i + for i, x in enumerate(self.encoding) if x in chars) + + def width(self, chars): + return self.max(chars) - self.min(chars) + + def __contains__(self, chars): + return any(x in self.encoding for x in chars) + + @staticmethod + @ft.cache + def tags(): + # parse our script's source to figure out tags + import inspect + import re + tags = [] + tag_pattern = re.compile( + '^(?PTAG_[^ ]*) *= *(?P[^#]*?) *' + '#+ *(?P(?:[^ ] *?){16}) *(?P.*)$') + for line in (inspect.getsource( + inspect.getmodule(inspect.currentframe())) + .replace('\\\n', '') + .splitlines()): + m = tag_pattern.match(line) + if m: + tags.append(Tag( + m.group('name'), + globals()[m.group('name')], + m.group('encoding').replace(' ', ''), + m.group('help'))) + return tags + + # find best matching tag + @staticmethod + def find(tag): + # find tags, note this is cached + tags__ = Tag.tags() + + # find the most specific matching tag, ignoring valid bits + return max((t for t in tags__ if t.matches(tag & 0x7fff)), + key=lambda t: t.specificity(), + default=None) + + # human readable tag repr + @staticmethod + def repr(tag, weight=None, size=None, *, + global_=False, + toff=None): + # find the most specific matching tag, ignoring the shrub bit + t = Tag.find(tag & ~(TAG_SHRUB if tag & 0x7000 == TAG_SHRUB else 0)) + + # build repr + r = [] + # normal tag? + if not tag & TAG_ALT: + if t is not None: + # prefix shrub tags with shrub + if tag & 0x7000 == TAG_SHRUB: + r.append('shrub') + # lowercase name + r.append(t.name.split('_', 1)[1].lower()) + # gstate tag? + if global_: + if r[-1] == 'gdelta': + r[-1] = 'gstate' + elif r[-1].endswith('delta'): + r[-1] = r[-1][:-len('delta')] + # include perturb/phase bits + if 'q' in t: + r.append('q%d' % t.get('q', tag)) + if 'p' in t and tag & TAG_PERTURB: + r.append('p') + + # include unmatched fields, but not just redund, and + # only reserved bits if non-zero + if 'tua' in t or ('+' in t and t.get('+', tag) != 0): + r.append(' 0x%0*x' % ( + (t.width('tuar+')+4-1)//4, + t.get('tuar+', tag))) + # unknown tag? + else: + r.append('0x%04x' % tag) + + # weight? + if weight: + r.append(' w%d' % weight) + # size? don't include if null + if size is not None and (size or tag & 0x7fff): + r.append(' %d' % size) + + # alt pointer? + else: + r.append('alt') + r.append('r' if tag & TAG_R else 'b') + r.append('gt' if tag & TAG_GT else 'le') + r.append(' 0x%0*x' % ( + (t.width('k')+4-1)//4, + t.get('k', tag))) + + # weight? + if weight is not None: + r.append(' w%d' % weight) + # jump? + if size and toff is not None: + r.append(' 0x%x' % (0xffffffff & (toff-size))) + elif size: + r.append(' -%d' % size) + + return ''.join(r) + + +# some ways of block geometry representations +# 512 -> 512 +# 512x16 -> (512, 16) +# 0x200x10 -> (512, 16) +def bdgeom(s): + s = s.strip() + b = 10 + if s.startswith('0x') or s.startswith('0X'): + s = s[2:] + b = 16 + elif s.startswith('0o') or s.startswith('0O'): + s = s[2:] + b = 8 + elif s.startswith('0b') or s.startswith('0B'): + s = s[2:] + b = 2 + + if 'x' in s: + s, s_ = s.split('x', 1) + return (int(s, b), int(s_, b)) + else: + return int(s, b) + +# parse some rbyd addr encodings +# 0xa -> (0xa,) +# 0xa.c -> ((0xa, 0xc),) +# 0x{a,b} -> (0xa, 0xb) +# 0x{a,b}.c -> ((0xa, 0xc), (0xb, 0xc)) +def rbydaddr(s): + s = s.strip() + b = 10 + if s.startswith('0x') or s.startswith('0X'): + s = s[2:] + b = 16 + elif s.startswith('0o') or s.startswith('0O'): + s = s[2:] + b = 8 + elif s.startswith('0b') or s.startswith('0B'): + s = s[2:] + b = 2 + + trunk = None + if '.' in s: + s, s_ = s.split('.', 1) + trunk = int(s_, b) + + if s.startswith('{') and '}' in s: + ss = s[1:s.find('}')].split(',') + else: + ss = [s] + + addr = [] + for s in ss: + if trunk is not None: + addr.append((int(s, b), trunk)) + else: + addr.append(int(s, b)) + + return tuple(addr) + +def crc32c(data, crc=0): + if crc32c_lib is not None: + return crc32c_lib.crc32c(data, crc) + else: + crc ^= 0xffffffff + for b in data: + crc ^= b + for j in range(8): + crc = (crc >> 1) ^ ((crc & 1) * 0x82f63b78) + return 0xffffffff ^ crc + +def pmul(a, b): + r = 0 + while b: + if b & 1: + r ^= a + a <<= 1 + b >>= 1 + return r + +def crc32cmul(a, b): + r = pmul(a, b) + for _ in range(31): + r = (r >> 1) ^ ((r & 1) * 0x82f63b78) + return r + +def crc32ccube(a): + return crc32cmul(crc32cmul(a, a), a) + +def popc(x): + return bin(x).count('1') + +def parity(x): + return popc(x) & 1 + +def fromle32(data, j=0): + return struct.unpack('H', data[j:j+2].ljust(2, b'\0'))[0]; d += 2 + weight, d_ = fromleb128(data, j+d); d += d_ + size, d_ = fromleb128(data, j+d); d += d_ + return tag>>15, tag&0x7fff, weight, size, d + +def frombranch(data, j=0): + d = 0 + block, d_ = fromleb128(data, j+d); d += d_ + trunk, d_ = fromleb128(data, j+d); d += d_ + cksum = fromle32(data, j+d); d += 4 + return block, trunk, cksum, d + +def frombtree(data, j=0): + d = 0 + w, d_ = fromleb128(data, j+d); d += d_ + block, trunk, cksum, d_ = frombranch(data, j+d); d += d_ + return w, block, trunk, cksum, d + +def frommdir(data, j=0): + blocks = [] + d = 0 + while j+d < len(data): + block, d_ = fromleb128(data, j+d) + blocks.append(block) + d += d_ + return tuple(blocks), d + +def fromshrub(data, j=0): + d = 0 + weight, d_ = fromleb128(data, j+d); d += d_ + trunk, d_ = fromleb128(data, j+d); d += d_ + return weight, trunk, d + +def frombptr(data, j=0): + d = 0 + size, d_ = fromleb128(data, j+d); d += d_ + block, d_ = fromleb128(data, j+d); d += d_ + off, d_ = fromleb128(data, j+d); d += d_ + cksize, d_ = fromleb128(data, j+d); d += d_ + cksum = fromle32(data, j+d); d += 4 + return size, block, off, cksize, cksum, d + +def xxd(data, width=16): + for i in range(0, len(data), width): + yield '%-*s %-*s' % ( + 3*width, + ' '.join('%02x' % b for b in data[i:i+width]), + width, + ''.join( + b if b >= ' ' and b <= '~' else '.' + for b in map(chr, data[i:i+width]))) + +# compute the difference between two paths, returning everything +# in a after the paths diverge, as well as the relevant index +def pathdelta(a, b): + if not isinstance(a, list): + a = list(a) + i = 0 + for a_, b_ in zip(a, b): + try: + if type(a_) == type(b_) and a_ == b_: + i += 1 + else: + break + # treat exceptions here as failure to match, most likely + # the compared types are incompatible, it's the caller's + # problem + except Exception: + break + + return [(i+j, a_) for j, a_ in enumerate(a[i:])] + + +# a simple wrapper over an open file with bd geometry +class Bd: + def __init__(self, f, block_size=None, block_count=None): + self.f = f + self.block_size = block_size + self.block_count = block_count + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, self.repr()) + + def repr(self): + return 'bd %sx%s' % (self.block_size, self.block_count) + + def read(self, block, off, size): + self.f.seek(block*self.block_size + off) + return self.f.read(size) + + def readblock(self, block): + self.f.seek(block*self.block_size) + return self.f.read(self.block_size) + +# tagged data in an rbyd +class Rattr: + def __init__(self, tag, weight, blocks, toff, tdata, data): + self.tag = tag + self.weight = weight + if isinstance(blocks, int): + self.blocks = (blocks,) + else: + self.blocks = blocks + self.toff = toff + self.tdata = tdata + self.data = data + + @property + def block(self): + return self.blocks[0] + + @property + def tsize(self): + return len(self.tdata) + + @property + def off(self): + return self.toff + len(self.tdata) + + @property + def size(self): + return len(self.data) + + def __bytes__(self): + return self.data + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, self.repr()) + + def repr(self): + return Tag.repr(self.tag, self.weight, self.size) + + def __iter__(self): + return iter((self.tag, self.weight, self.data)) + + def __eq__(self, other): + return ((self.tag, self.weight, self.data) + == (other.tag, other.weight, other.data)) + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash((self.tag, self.weight, self.data)) + + # convenience for did/name access + def _parse_name(self): + # note we return a null name for non-name tags, this is so + # vestigial names in btree nodes act as a catch-all + if (self.tag & 0xff00) != TAG_NAME: + did = 0 + name = b'' + else: + did, d = fromleb128(self.data) + name = self.data[d:] + + # cache both + self.did = did + self.name = name + + @ft.cached_property + def did(self): + self._parse_name() + return self.did + + @ft.cached_property + def name(self): + self._parse_name() + return self.name + +class Ralt: + def __init__(self, tag, weight, blocks, toff, tdata, jump, + color=None, followed=None): + self.tag = tag + self.weight = weight + if isinstance(blocks, int): + self.blocks = (blocks,) + else: + self.blocks = blocks + self.toff = toff + self.tdata = tdata + self.jump = jump + + if color is not None: + self.color = color + else: + self.color = 'r' if tag & TAG_R else 'b' + self.followed = followed + + @property + def block(self): + return self.blocks[0] + + @property + def tsize(self): + return len(self.tdata) + + @property + def off(self): + return self.toff + len(self.tdata) + + @property + def joff(self): + return self.toff - self.jump + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, self.repr()) + + def repr(self): + return Tag.repr(self.tag, self.weight, self.jump, toff=self.toff) + + def __iter__(self): + return iter((self.tag, self.weight, self.jump)) + + def __eq__(self, other): + return ((self.tag, self.weight, self.jump) + == (other.tag, other.weight, other.jump)) + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash((self.tag, self.weight, self.jump)) + + +# our core rbyd type +class Rbyd: + def __init__(self, blocks, trunk, weight, rev, eoff, cksum, data, *, + shrub=False, + gcksumdelta=None, + redund=0): + if isinstance(blocks, int): + self.blocks = (blocks,) + else: + self.blocks = blocks + self.trunk = trunk + self.weight = weight + self.rev = rev + self.eoff = eoff + self.cksum = cksum + self.data = data + + self.shrub = shrub + self.gcksumdelta = gcksumdelta + self.redund = redund + + @property + def block(self): + return self.blocks[0] + + @property + def corrupt(self): + # use redund=-1 to indicate corrupt rbyds + return self.redund >= 0 + + def addr(self): + if len(self.blocks) == 1: + return '0x%x.%x' % (self.block, self.trunk) + else: + return '0x{%s}.%x' % ( + ','.join('%x' % block for block in self.blocks), + self.trunk) + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, self.repr()) + + def repr(self): + return 'rbyd %s w%s' % (self.addr(), self.weight) + + def __bool__(self): + # use redund=-1 to indicate corrupt rbyds + return self.redund >= 0 + + def __eq__(self, other): + return ((frozenset(self.blocks), self.trunk) + == (frozenset(other.blocks), other.trunk)) + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash((frozenset(self.blocks), self.trunk)) + + @classmethod + def _fetch(cls, data, block, trunk=None): + # fetch the rbyd + rev = fromle32(data, 0) + cksum = 0 + cksum_ = crc32c(data[0:4]) + cksum__ = cksum_ + perturb = False + eoff = 0 + eoff_ = None + j_ = 4 + trunk_ = 0 + trunk__ = 0 + trunk___ = 0 + weight = 0 + weight_ = 0 + weight__ = 0 + gcksumdelta = None + gcksumdelta_ = None + while j_ < len(data) and (not trunk or eoff <= trunk): + # read next tag + v, tag, w, size, d = fromtag(data, j_) + if v != parity(cksum__): + break + cksum__ ^= 0x00000080 if v else 0 + cksum__ = crc32c(data[j_:j_+d], cksum__) + j_ += d + if not tag & TAG_ALT and j_ + size > len(data): + break + + # take care of cksums + if not tag & TAG_ALT: + if (tag & 0xff00) != TAG_CKSUM: + cksum__ = crc32c(data[j_:j_+size], cksum__) + + # found a gcksumdelta? + if (tag & 0xff00) == TAG_GCKSUMDELTA: + gcksumdelta_ = Rattr(tag, w, block, j_-d, + data[j_-d:j_], + data[j_:j_+size]) + + # found a cksum? + else: + # check cksum + cksum___ = fromle32(data, j_) + if cksum__ != cksum___: + break + # commit what we have + eoff = eoff_ if eoff_ else j_ + size + cksum = cksum_ + trunk_ = trunk__ + weight = weight_ + gcksumdelta = gcksumdelta_ + gcksumdelta_ = None + # update perturb bit + perturb = bool(tag & TAG_PERTURB) + # revert to data cksum and perturb + cksum__ = cksum_ ^ (0xfca42daf if perturb else 0) + + # evaluate trunks + if (tag & 0xf000) != TAG_CKSUM: + if not (trunk and j_-d > trunk and not trunk___): + # new trunk? + if not trunk___: + trunk___ = j_-d + weight__ = 0 + + # keep track of weight + weight__ += w + + # end of trunk? + if not tag & TAG_ALT: + # update trunk/weight unless we found a shrub or an + # explicit trunk (which may be a shrub) is requested + if not tag & TAG_SHRUB or trunk___ == trunk: + trunk__ = trunk___ + weight_ = weight__ + # keep track of eoff for best matching trunk + if trunk and j_ + size > trunk: + eoff_ = j_ + size + eoff = eoff_ + cksum = cksum__ ^ ( + 0xfca42daf if perturb else 0) + trunk_ = trunk__ + weight = weight_ + gcksumdelta = gcksumdelta_ + trunk___ = 0 + + # update canonical checksum, xoring out any perturb state + cksum_ = cksum__ ^ (0xfca42daf if perturb else 0) + + if not tag & TAG_ALT: + j_ += size + + return cls(block, trunk_, weight, rev, eoff, cksum, data, + gcksumdelta=gcksumdelta, + redund=0 if trunk_ else -1) + + @classmethod + def fetch(cls, bd, blocks, trunk=None): + # multiple blocks? + if not isinstance(blocks, int): + # fetch all blocks + rbyds = [cls.fetch(bd, block, trunk) for block in blocks] + + # determine most recent revision/trunk + rev, trunk = None, None + for rbyd in rbyds: + # compare with sequence arithmetic + if rbyd and ( + rev is None + or not ((rbyd.rev - rev) & 0x80000000) + or (rbyd.rev == rev and rbyd.trunk > trunk)): + rev, trunk = rbyd.rev, rbyd.trunk + # sort for reproducibility + rbyds.sort(key=lambda rbyd: ( + # prioritize valid redund blocks + 0 if rbyd and rbyd.rev == rev and rbyd.trunk == trunk + else 1, + # default to sorting by block + rbyd.block)) + + # choose an active rbyd + rbyd = rbyds[0] + # keep track of the other blocks + rbyd.blocks = tuple(rbyd.block for rbyd in rbyds) + # keep track of how many redund blocks are valid + rbyd.redund = -1 + sum(1 for rbyd in rbyds + if rbyd and rbyd.rev == rev and rbyd.trunk == trunk) + # and patch the gcksumdelta if we have one + if rbyd.gcksumdelta is not None: + rbyd.gcksumdelta.blocks = rbyd.blocks + return rbyd + + # seek/read the block + block = blocks + data = bd.readblock(block) + + # fetch the rbyd + return cls._fetch(data, block, trunk) + + @classmethod + def fetchck(cls, bd, blocks, trunk, weight, cksum): + # try to fetch the rbyd normally + rbyd = cls.fetch(bd, blocks, trunk) + + # cksum mismatch? trunk/weight mismatch? + if (rbyd.cksum != cksum + or rbyd.trunk != trunk + or rbyd.weight != weight): + # mark as corrupt and keep track of expected trunk/weight + rbyd.redund = -1 + rbyd.trunk = trunk + rbyd.weight = weight + + return rbyd + + @classmethod + def fetchshrub(cls, rbyd, trunk): + # steal the original rbyd's data + # + # this helps avoid race conditions with cksums and stuff + shrub = cls._fetch(rbyd.data, rbyd.block, trunk) + shrub.blocks = rbyd.blocks + shrub.shrub = True + return shrub + + def lookupnext(self, rid, tag=None, *, + path=False): + if not self or rid >= self.weight: + if path: + return None, None, [] + else: + return None, None + + tag = max(tag or 0, 0x1) + lower = 0 + upper = self.weight + path_ = [] + + # descend down tree + j = self.trunk + while True: + _, alt, w, jump, d = fromtag(self.data, j) + + # found an alt? + if alt & TAG_ALT: + # follow? + if ((rid, tag & 0xfff) > (upper-w-1, alt & 0xfff) + if alt & TAG_GT + else ((rid, tag & 0xfff) + <= (lower+w-1, alt & 0xfff))): + lower += upper-lower-w if alt & TAG_GT else 0 + upper -= upper-lower-w if not alt & TAG_GT else 0 + j = j - jump + + if path: + # figure out which color + if alt & TAG_R: + _, nalt, _, _, _ = fromtag(self.data, j+jump+d) + if nalt & TAG_R: + color = 'y' + else: + color = 'r' + else: + color = 'b' + + path_.append(Ralt( + alt, w, self.blocks, j+jump, + self.data[j+jump:j+jump+d], jump, + color=color, + followed=True)) + + # stay on path + else: + lower += w if not alt & TAG_GT else 0 + upper -= w if alt & TAG_GT else 0 + j = j + d + + if path: + # figure out which color + if alt & TAG_R: + _, nalt, _, _, _ = fromtag(self.data, j) + if nalt & TAG_R: + color = 'y' + else: + color = 'r' + else: + color = 'b' + + path_.append(Ralt( + alt, w, self.blocks, j-d, + self.data[j-d:j], jump, + color=color, + followed=False)) + + # found tag + else: + rid_ = upper-1 + tag_ = alt + w_ = upper-lower + + if not tag_ or (rid_, tag_) < (rid, tag): + if path: + return None, None, path_ + else: + return None, None + + rattr_ = Rattr(tag_, w_, self.blocks, j, + self.data[j:j+d], + self.data[j+d:j+d+jump]) + if path: + return rid_, rattr_, path_ + else: + return rid_, rattr_ + + def lookup(self, rid, tag=None, mask=None, *, + path=False): + if tag is None: + tag, mask = 0, 0xffff + if mask is None: + mask = 0 + + r = self.lookupnext(rid, tag & ~mask, + path=path) + if path: + rid_, rattr_, path_ = r + else: + rid_, rattr_ = r + if (rid_ is None + or rid_ != rid + or (rattr_.tag & ~mask & 0xfff) + != (tag & ~mask & 0xfff)): + if path: + return None, path_ + else: + return None + + if path: + return rattr_, path_ + else: + return rattr_ + + def rids(self, *, + path=False): + rid = -1 + while True: + r = self.lookupnext(rid, + path=path) + if path: + rid, name, path_ = r + else: + rid, name = r + # found end of tree? + if rid is None: + break + + if path: + yield rid, name, path_ + else: + yield rid, name + rid += 1 + + def rattrs(self, rid=None, tag=None, mask=None, *, + path=False): + if rid is None: + rid, tag = -1, 0 + while True: + r = self.lookupnext(rid, tag+0x1, + path=path) + if path: + rid, rattr, path_ = r + else: + rid, rattr = r + # found end of tree? + if rid is None: + break + + if path: + yield rid, rattr, path_ + else: + yield rid, rattr + tag = rattr.tag + else: + if tag is None: + tag, mask = 0, 0xffff + if mask is None: + mask = 0 + + tag_ = max((tag & ~mask) - 1, 0) + while True: + r = self.lookupnext(rid, tag_+0x1, + path=path) + if path: + rid_, rattr_, path_ = r + else: + rid_, rattr_ = r + # found end of tree? + if (rid_ is None + or rid_ != rid + or (rattr_.tag & ~mask & 0xfff) + != (tag & ~mask & 0xfff)): + break + + if path: + yield rattr_, path_ + else: + yield rattr_ + tag_ = rattr_.tag + + # lookup by name + def namelookup(self, did, name): + # binary search + best = None, None + lower = 0 + upper = self.weight + while lower < upper: + rid, name_ = self.lookupnext( + lower + (upper-1-lower)//2) + if rid is None: + break + + # bisect search space + if (name_.did, name_.name) > (did, name): + upper = rid-(name_.weight-1) + elif (name_.did, name_.name) < (did, name): + lower = rid + 1 + # keep track of best match + best = rid, name_ + else: + # found a match + return rid, name_ + + return best + + +# our rbyd btree type +class Btree: + def __init__(self, bd, rbyd): + self.bd = bd + self.rbyd = rbyd + + @property + def block(self): + return self.rbyd.block + + @property + def blocks(self): + return self.rbyd.blocks + + @property + def trunk(self): + return self.rbyd.trunk + + @property + def weight(self): + return self.rbyd.weight + + @property + def rev(self): + return self.rbyd.rev + + @property + def cksum(self): + return self.rbyd.cksum + + @property + def shrub(self): + return self.rbyd.shrub + + def addr(self): + return self.rbyd.addr() + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, self.repr()) + + def repr(self): + return 'btree %s w%s' % (self.addr(), self.weight) + + def __eq__(self, other): + return self.rbyd == other.rbyd + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash(self.rbyd) + + @classmethod + def fetch(cls, bd, blocks, trunk=None): + # rbyd fetch does most of the work here + rbyd = Rbyd.fetch(bd, blocks, trunk) + return cls(bd, rbyd) + + @classmethod + def fetchck(cls, bd, blocks, trunk, weight, cksum): + # rbyd fetchck does most of the work here + rbyd = Rbyd.fetchck(bd, blocks, trunk, weight, cksum) + return cls(bd, rbyd) + + @classmethod + def fetchshrub(cls, bd, rbyd, trunk): + shrub = Rbyd.fetchshrub(rbyd, trunk) + return cls(bd, shrub) + + def lookupnext_(self, bid, *, + path=False, + depth=None): + if not self or bid >= self.weight: + if path: + return None, None, None, None, [] + else: + return None, None, None, None + + rbyd = self.rbyd + rid = bid + depth_ = 1 + path_ = [] + + while True: + # corrupt branch? + if not rbyd: + if path: + return bid, rbyd, rid, None, path_ + else: + return bid, rbyd, rid, None + + # first tag indicates the branch's weight + rid_, name_ = rbyd.lookupnext(rid) + if rid_ is None: + if path: + return None, None, None, None, path_ + else: + return None, None, None, None + + # keep track of path + if path: + path_.append((bid + (rid_-rid), rbyd, rid_, name_)) + + # find branch tag if there is one + branch_ = rbyd.lookup(rid_, TAG_BRANCH, 0x3) + + # descend down branch? + if branch_ is not None and ( + not depth or depth_ < depth): + block, trunk, cksum, _ = frombranch(branch_.data) + rbyd = Rbyd.fetchck(self.bd, block, trunk, name_.weight, + cksum) + + rid -= (rid_-(name_.weight-1)) + depth_ += 1 + + else: + if path: + return bid + (rid_-rid), rbyd, rid_, name_, path_ + else: + return bid + (rid_-rid), rbyd, rid_, name_ + + # the non-leaf variants discard the rbyd info, these can be a bit + # more convenient, but at a performance cost + def lookupnext(self, bid, *, + path=False, + depth=None): + # just discard the rbyd info + r = self.lookupnext_(bid, + path=path, + depth=depth) + if path: + bid, rbyd, rid, name, path_ = r + else: + bid, rbyd, rid, name = r + + if path: + return bid, name, path_ + else: + return bid, name + + def lookup(self, bid, tag=None, mask=None, *, + path=False, + depth=None): + # lookup rbyd in btree + # + # note this function expects bid to be known, use lookupnext + # first if you don't care about the exact bid (or better yet, + # lookupnext_ and call lookup on the returned rbyd) + # + # this matches rbyd's lookup behavior, which needs a known rid + # to avoid a double lookup + r = self.lookupnext_(bid, + path=path, + depth=depth) + if path: + bid_, rbyd_, rid_, name_, path_ = r + else: + bid_, rbyd_, rid_, name_ = r + if bid_ is None or bid_ != bid: + if path: + return None, path_ + else: + return None + + # lookup tag in rbyd + rattr_ = rbyd_.lookup(rid_, tag, mask) + if rattr_ is None: + if path: + return None, path_ + else: + return None + + if path: + return rattr_, path_ + else: + return rattr_ + + # note leaves only iterates over leaf rbyds, whereas traverse + # traverses all rbyds + def leaves(self, *, + path=False, + depth=None): + # include our root rbyd even if the weight is zero + if self.weight == 0: + if path: + yield -1, self.rbyd, [] + else: + yield -1, self.rbyd + return + + bid = 0 + while True: + r = self.lookupnext_(bid, + path=path, + depth=depth) + if r: + bid, rbyd, rid, name, path_ = r + else: + bid, rbyd, rid, name = r + if bid is None: + break + + if path: + yield (bid-rid + (rbyd.weight-1), rbyd, + # path tail is usually redundant unless corrupt + path_[:-1] + if path_ and path_[-1][1] == rbyd + else path_) + else: + yield bid-rid + (rbyd.weight-1), rbyd + bid += rbyd.weight - rid + 1 + + def traverse(self, *, + path=False, + depth=None): + ptrunk_ = [] + for bid, rbyd, path_ in self.leaves( + path=True, + depth=depth): + # we only care about the rbyds here + trunk_ = ([(bid_-rid_ + (rbyd_.weight-1), rbyd_) + for bid_, rbyd_, rid_, name_ in path_] + + [(bid, rbyd)]) + for d, (bid_, rbyd_) in pathdelta( + trunk_, ptrunk_): + # but include branch rids in the path if requested + if path: + yield bid_, rbyd_, path_[:d] + else: + yield bid_, rbyd_ + ptrunk_ = trunk_ + + # note bids/rattrs do _not_ include corrupt btree nodes! + def bids(self, *, + leaves=False, + path=False, + depth=None): + for r in self.leaves( + path=path, + depth=depth): + if path: + bid, rbyd, path_ = r + else: + bid, rbyd = r + for rid, name in rbyd.rids(): + bid_ = bid-(rbyd.weight-1) + rid + if leaves: + if path: + yield (bid_, rbyd, rid, name, + path_+[(bid_, rbyd, rid, name)]) + else: + yield bid_, rbyd, rid, name + else: + if path: + yield (bid_, name, + path_+[(bid_, rbyd, rid, name)]) + else: + yield bid_, name + + def rattrs(self, bid=None, tag=None, mask=None, *, + leaves=False, + path=False, + depth=None): + if bid is None: + for r in self.leaves( + path=path, + depth=depth): + if path: + bid, rbyd, path_ = r + else: + bid, rbyd = r + for rid, name in rbyd.rids(): + bid_ = bid-(rbyd.weight-1) + rid + for rattr in rbyd.rattrs(rid): + if leaves: + if path: + yield (bid_, rbyd, rid, rattr, + path_+[(bid_, rbyd, rid, name)]) + else: + yield bid_, rbyd, rid, rattr + else: + if path: + yield (bid_, rattr, + path_+[(bid_, rbyd, rid, name)]) + else: + yield bid_, rattr + else: + r = self.lookupnext_(bid, + path=path, + depth=depth) + if path: + bid, rbyd, rid, name, path_ = r + else: + bid, rbyd, rid, name = r + if bid is None: + return + + for rattr in rbyd.rattrs(rid, tag, mask): + if leaves: + if path: + yield rbyd, rid, rattr, path_ + else: + yield rbyd, rid, rattr + else: + if path: + yield rattr, path_ + else: + yield rattr + + # lookup by name + def namelookup_(self, did, name, *, + path=False, + depth=None): + rbyd = self.rbyd + bid = 0 + depth_ = 1 + path_ = [] + + while True: + # corrupt branch? + if not rbyd: + bid_ = bid+(rbyd.weight-1) + if path: + return bid_, rbyd, rbyd.weight-1, None, path_ + else: + return bid_, rbyd, rbyd.weight-1, None + + rid_, name_ = rbyd.namelookup(did, name) + + # keep track of path + if path: + path_.append((bid + rid_, rbyd, rid_, name_)) + + # find branch tag if there is one + branch_ = rbyd.lookup(rid_, TAG_BRANCH, 0x3) + + # found another branch + if branch_ is not None and ( + not depth or depth_ < depth): + block, trunk, cksum, _ = frombranch(branch_.data) + rbyd = Rbyd.fetchck(self.bd, block, trunk, name_.weight, + cksum) + + # update our bid + bid += rid_ - (name_.weight-1) + depth_ += 1 + + # found best match + else: + if path: + return bid + rid_, rbyd, rid_, name_, path_ + else: + return bid + rid_, rbyd, rid_, name_ + + def namelookup(self, bid, *, + path=False, + depth=None): + # just discard the rbyd info + r = self.namelookup_(did, name, + path=path, + depth=depth) + if path: + bid, rbyd, rid, name, path_ = r + else: + bid, rbyd, rid, name = r + + if path: + return bid, name, path_ + else: + return bid, name + + +# a metadata id, this includes mbits for convenience +class Mid: + def __init__(self, mbid, mrid=None, *, + mbits=None): + # we need one of these to figure out mbits + if mbits is not None: + self.mbits = mbits + elif isinstance(mbid, Mid): + self.mbits = mbid.mbits + else: + assert mbits is not None, "mbits?" + + # accept other mids which can be useful for changing mrids + if isinstance(mbid, Mid): + mbid = mbid.mbid + + # accept either merged mid or separate mbid+mrid + if mrid is None: + mid = mbid + mbid = mid | ((1 << self.mbits) - 1) + mrid = mid & ((1 << self.mbits) - 1) + + # map mrid=-1 + if mrid == ((1 << self.mbits) - 1): + mrid = -1 + + self.mbid = mbid + self.mrid = mrid + + @property + def mid(self): + return ((self.mbid & ~((1 << self.mbits) - 1)) + | (self.mrid & ((1 << self.mbits) - 1))) + + def mbidrepr(self): + return str(self.mbid >> self.mbits) + + def mridrepr(self): + return str(self.mrid) + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, self.repr()) + + def repr(self): + return '%s.%s' % (self.mbidrepr(), self.mridrepr()) + + def __iter__(self): + return iter((self.mbid, self.mrid)) + + # note this is slightly different from mid order when mrid=-1 + def __eq__(self, other): + if isinstance(other, Mid): + return (self.mbid, self.mrid) == (other.mbid, other.mrid) + else: + return self.mid == other + + def __ne__(self, other): + if isinstance(other, Mid): + return (self.mbid, self.mrid) != (other.mbid, other.mrid) + else: + return self.mid != other + + def __hash__(self): + return hash((self.mbid, self.mrid)) + + def __lt__(self, other): + return (self.mbid, self.mrid) < (other.mbid, other.mrid) + + def __le__(self, other): + return (self.mbid, self.mrid) <= (other.mbid, other.mrid) + + def __gt__(self, other): + return (self.mbid, self.mrid) > (other.mbid, other.mrid) + + def __ge__(self, other): + return (self.mbid, self.mrid) >= (other.mbid, other.mrid) + +# mdirs, the gooey atomic center of littlefs +# +# really the only difference between this and our rbyd class is the +# implicit mbid associated with the mdir +class Mdir: + def __init__(self, mid, rbyd, *, + mbits=None): + # we need one of these to figure out mbits + if mbits is not None: + self.mbits = mbits + elif isinstance(mid, Mid): + self.mbits = mid.mbits + elif isinstance(rbyd, Mdir): + self.mbits = rbyd.mbits + else: + assert mbits is not None, "mbits?" + + # strip mrid, bugs will happen if caller relies on mrid here + self.mid = Mid(mid, -1, mbits=self.mbits) + + # accept either another mdir or rbyd + if isinstance(rbyd, Mdir): + self.rbyd = rbyd.rbyd + else: + self.rbyd = rbyd + + @property + def data(self): + return self.rbyd.data + + @property + def block(self): + return self.rbyd.block + + @property + def blocks(self): + return self.rbyd.blocks + + @property + def trunk(self): + return self.rbyd.trunk + + @property + def weight(self): + return self.rbyd.weight + + @property + def rev(self): + return self.rbyd.rev + + @property + def eoff(self): + return self.rbyd.eoff + + @property + def cksum(self): + return self.rbyd.cksum + + @property + def gcksumdelta(self): + return self.rbyd.gcksumdelta + + @property + def corrupt(self): + return self.rbyd.corrupt + + @property + def redund(self): + return self.rbyd.redund + + def addr(self): + if len(self.blocks) == 1: + return '0x%x' % self.block + else: + return '0x{%s}' % ( + ','.join('%x' % block for block in self.blocks)) + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, self.repr()) + + def repr(self): + return 'mdir %s %s w%s' % ( + self.mid.mbidrepr(), + self.addr(), + self.weight) + + def __bool__(self): + return bool(self.rbyd) + + # we _don't_ care about mid for equality, or trunk even + def __eq__(self, other): + return frozenset(self.blocks) == frozenset(other.blocks) + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash(frozenset(self.blocks)) + + @classmethod + def fetch(cls, bd, mid, blocks, trunk=None): + rbyd = Rbyd.fetch(bd, blocks, trunk) + return cls(mid, rbyd, mbits=Mtree.mbits_(bd)) + + def lookupnext(self, mid, tag=None, *, + path=False): + # this is similar to rbyd lookupnext, we just error if + # lookupnext changes mids + if not isinstance(mid, Mid): + mid = Mid(mid, mbits=self.mbits) + r = self.rbyd.lookupnext(mid.mrid, tag, + path=path) + if path: + rid, rattr, path_ = r + else: + rid, rattr = r + + if rid != mid.mrid: + if path: + return None, path_ + else: + return None + + if path: + return rattr, path_ + else: + return rattr + + def lookup(self, mid, tag=None, mask=None, *, + path=False): + if not isinstance(mid, Mid): + mid = Mid(mid, mbits=self.mbits) + return self.rbyd.lookup(mid.mrid, tag, mask, + path=path) + + def mids(self, *, + path=False): + for r in self.rbyd.rids( + path=path): + if path: + rid, name, path_ = r + else: + rid, name = r + + mid = Mid(self.mid, rid) + if path: + yield mid, name, path_ + else: + yield mid, name + + def rattrs(self, mid=None, tag=None, mask=None, *, + path=False): + if mid is None: + for r in self.rbyd.rattrs( + path=path): + if path: + rid, rattr, path_ = r + else: + rid, rattr = r + + mid = Mid(self.mid, rid) + if path: + yield mid, rattr, path_ + else: + yield mid, rattr + else: + if not isinstance(mid, Mid): + mid = Mid(mid, mbits=self.mbits) + yield from self.rbyd.rattrs(mid.mrid, tag, mask, + path=path) + + # lookup by name + def namelookup(self, did, name): + # unlike rbyd namelookup, we need an exact match here + rid, name_ = self.rbyd.namelookup(did, name) + if rid is None or (name_.did, name_.name) != (did, name): + return None, None + + return Mid(self.mid, rid), name_ + +# the mtree, the skeletal structure of littlefs +class Mtree: + def __init__(self, bd, mrootchain, mtree, *, + mrootpath=False, + mtreepath=False, + mbits=None): + if isinstance(mrootchain, Mdir): + mrootchain = [Mdir] + # we at least need the mrootanchor, even if it is corrupt + assert len(mrootchain) >= 1 + + self.bd = bd + if mbits is not None: + self.mbits = mbits + else: + self.mbits = Mtree.mbits_(self.bd) + + self.mrootchain = mrootchain + self.mrootanchor = mrootchain[0] + self.mroot = mrootchain[-1] + self.mtree = mtree + + # mbits is a static value derived from the block_size + @staticmethod + def mbits_(block_size): + if isinstance(block_size, Bd): + block_size = block_size.block_size + return mt.ceil(mt.log2(block_size)) - 3 + + # convenience function for creating mbits-dependent mids + def mid(self, mbid, mrid=None): + return Mid(mbid, mrid, mbits=self.mbits) + + @property + def block(self): + return self.mroot.block + + @property + def blocks(self): + return self.mroot.blocks + + @property + def trunk(self): + return self.mroot.trunk + + @property + def weight(self): + if self.mtree is None: + return 0 + else: + return self.mtree.weight + + @property + def mbweight(self): + return self.weight + + @property + def mrweight(self): + return 1 << self.mbits + + def mbweightrepr(self): + return str(self.mbweight >> self.mbits) + + def mrweightrepr(self): + return str(self.mrweight) + + @property + def rev(self): + return self.mroot.rev + + @property + def cksum(self): + return self.mroot.cksum + + def addr(self): + return self.mroot.addr() + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, self.repr()) + + def repr(self): + return 'mtree %s w%s.%s' % ( + self.addr(), + self.mbweightrepr(), self.mrweightrepr()) + + def __eq__(self, other): + return self.mrootanchor == other.mrootanchor + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash(self.mrootanchor) + + @classmethod + def fetch(cls, bd, blocks=None, trunk=None, *, + depth=None): + # default to blocks 0x{0,1} + if blocks is None: + blocks = [0, 1] + + # figure out mbits + mbits = Mtree.mbits_(bd) + + # fetch the mrootanchor + mrootanchor = Mdir.fetch(bd, -1, blocks, trunk) + + # follow the mroot chain to try to find the active mroot + mroot = mrootanchor + mrootchain = [mrootanchor] + mrootseen = set() + while True: + # corrupted? + if not mroot: + break + # cycle detected? + if mroot in mrootseen: + break + mrootseen.add(mroot) + + # stop here? + if depth and len(mrootchain) >= depth: + break + + # fetch the next mroot + rattr_ = mroot.lookup(-1, TAG_MROOT, 0x3) + if rattr_ is None: + break + blocks_, _ = frommdir(rattr_.data) + mroot = Mdir.fetch(bd, -1, blocks_) + mrootchain.append(mroot) + + # fetch the actual mtree, if there is one + mtree = None + if not depth or len(mrootchain) < depth: + rattr_ = mroot.lookup(-1, TAG_MTREE, 0x3) + if rattr_ is not None: + w_, block_, trunk_, cksum_, _ = frombtree(rattr_.data) + mtree = Btree.fetchck(bd, block_, trunk_, w_, cksum_) + + return cls(bd, mrootchain, mtree, + mbits=mbits) + + def _lookupnext_(self, mid, *, + path=False, + depth=None): + if not isinstance(mid, Mid): + mid = self.mid(mid) + + if path or depth: + # iterate over mrootchain + path_ = [] + for mroot in self.mrootchain: + # stop here? + if depth and len(path_) >= depth: + if path: + return mroot, path_ + else: + return mroot + + name = mroot.lookup(-1, TAG_MAGIC) + path_.append((mroot.mid, mroot, name)) + + # no mtree? must be inlined in mroot + if self.mtree is None: + if mid.mbid != -1: + if path: + return None, path_ + else: + return None + + if path: + return self.mroot, path_ + else: + return self.mroot + + # mtree? lookup in mtree + else: + # need to do two steps here in case lookupnext_ stops early + r = self.mtree.lookupnext_(mid.mid, + path=path or depth, + depth=depth-len(path_) if depth else None) + if path or depth: + bid_, rbyd_, rid_, name_, path__ = r + path_.extend(path__) + else: + bid_, rbyd_, rid_, name_ = r + if bid_ is None: + if path: + return None, path_ + else: + return None + + # corrupt btree node? + if not rbyd_: + if path: + return (bid_, rbyd_, rid_), path_ + else: + return (bid_, rbyd_, rid_) + + # stop here? it's not an mdir, but we only return btree nodes + # if explicitly requested + if depth and len(path_) >= depth: + if path: + return (bid_, rbyd_, rid_), path_ + else: + return (bid_, rbyd_, rid_) + + # fetch the mdir + rattr_ = rbyd_.lookup(rid_, TAG_MDIR, 0x3) + # mdir tag missing? weird + if rattr_ is None: + if path: + return (bid_, rbyd_, rid_), path_ + else: + return (bid_, rbyd_, rid_) + blocks_, _ = frommdir(rattr_.data) + mdir = Mdir.fetch(self.bd, mid, blocks_) + if path: + return mdir, path_ + else: + return mdir + + def lookupnext_(self, mid, *, + mdirs_only=True, + path=False, + depth=None): + # most of the logic is in _lookupnext_, this just helps + # deduplicate the mdirs_only logic + r = self._lookupnext_(mid, + path=path, + depth=depth) + if path: + mdir, path_ = r + else: + mdir = r + if mdir is None or ( + mdirs_only and not isinstance(mdir, Mdir)): + if path: + return None, path_ + else: + return None + + if path: + return mdir, path_ + else: + return mdir + + def lookup(self, mid, *, + path=False, + depth=None): + if not isinstance(mid, Mid): + mid = self.mid(mid) + + # lookup the relevant mdir + r = self.lookupnext_(mid, + path=path, + depth=depth) + if path: + mdir, path_ = r + else: + mdir = r + if mdir is None: + if path: + return None, None, path_ + else: + return None, None + + # not in mdir? + if mid.mrid >= mdir.weight: + if path: + return None, None, path_ + else: + return None, None + + # lookup mid in mdir + rattr = mdir.lookup(mid) + if path: + return mdir, rattr, path_+[(mid, mdir, rattr)] + else: + return mdir, rattr + + # iterate over all mdirs, this includes the mrootchain + def _leaves(self, *, + path=False, + depth=None): + # iterate over mrootchain + if path or depth: + path_ = [] + for mroot in self.mrootchain: + if path: + yield mroot, path_ + else: + yield mroot + + if path or depth: + # stop here? + if depth and len(path_) >= depth: + return + + name = mroot.lookup(-1, TAG_MAGIC) + path_.append((mroot.mid, mroot, name)) + + # do we even have an mtree? + if self.mtree is not None: + # include the mtree root even if the weight is zero + if self.mtree.weight == 0: + if path: + yield -1, self.mtree.rbyd, path_ + else: + yield -1, self.mtree.rbyd + return + + mid = self.mid(0) + while True: + r = self.lookupnext_(mid, + mdirs_only=False, + path=path, + depth=depth) + if path: + mdir, path_ = r + else: + mdir = r + if mdir is None: + break + + # mdir? + if isinstance(mdir, Mdir): + if path: + yield mdir, path_ + else: + yield mdir + mid = self.mid(mid.mbid+1) + # btree node? + else: + bid, rbyd, rid = mdir + if path: + yield ((bid-rid + (rbyd.weight-1), rbyd), + # path tail is usually redundant unless corrupt + path_[:-1] + if path_ + and isinstance(path_[-1][1], Rbyd) + and path_[-1][1] == rbyd + else path_) + else: + yield (bid-rid + (rbyd.weight-1), rbyd) + mid = self.mid(bid-rid + (rbyd.weight-1) + 1) + + def leaves(self, *, + mdirs_only=False, + path=False, + depth=None): + for r in self._leaves( + path=path, + depth=depth): + if path: + mdir, path_ = r + else: + mdir = r + if mdirs_only and not isinstance(mdir, Mdir): + continue + + if path: + yield mdir, path_ + else: + yield mdir + + # traverse over all mdirs and btree nodes + # - mdir => Mdir + # - btree node => (bid, rbyd) + def _traverse(self, *, + path=False, + depth=None): + ptrunk_ = [] + for mdir, path_ in self.leaves( + path=True, + depth=depth): + # we only care about the mdirs/rbyds here + trunk_ = ([(lambda mid_, mdir_, name_: mdir_)(*p) + if isinstance(p[1], Mdir) + else (lambda bid_, rbyd_, rid_, name_: + (bid_-rid_ + (rbyd_.weight-1), rbyd_))(*p) + for p in path_] + + [mdir]) + for d, mdir in pathdelta( + trunk_, ptrunk_): + # but include branch mids/rids in the path if requested + if path: + yield mdir, path_[:d] + else: + yield mdir + ptrunk_ = trunk_ + + def traverse(self, *, + mdirs_only=False, + path=False, + depth=None): + for r in self._traverse( + path=path, + depth=depth): + if path: + mdir, path_ = r + else: + mdir = r + if mdirs_only and not isinstance(mdir, Mdir): + continue + + if path: + yield mdir, path_ + else: + yield mdir + + # these are just aliases + + # the difference between mdirs and leaves is mdirs defaults to only + # mdirs, leaves can include btree nodes if corrupt + def mdirs(self, *, + mdirs_only=True, + path=False, + depth=None): + return self.leaves( + mdirs_only=mdirs_only, + path=path, + depth=depth) + + # note mids/rattrs do _not_ include corrupt btree nodes! + def mids(self, *, + mdirs_only=True, + path=False, + depth=None): + for r in self.mdirs( + mdirs_only=mdirs_only, + path=path, + depth=depth): + if path: + mdir, path_ = r + else: + mdir = r + if isinstance(mdir, Mdir): + for mid, name in mdir.mids(): + if path: + yield (mid, mdir, name, + path_+[(mid, mdir, name)]) + else: + yield mid, mdir, name + else: + bid, rbyd = mdir + for rid, name in rbyd.rids(): + bid_ = bid-(rbyd.weight-1) + rid + mid_ = self.mid(bid_) + mdir_ = (bid_, rbyd, rid) + if path: + yield (mid_, mdir_, name, + path_+[(bid_, rbyd, rid, name)]) + else: + yield mid_, mdir_, name + + def rattrs(self, mid=None, tag=None, mask=None, *, + mdirs_only=True, + path=False, + depth=None): + if mid is None: + for r in self.mdirs( + mdirs_only=mdirs_only, + path=path, + depth=depth): + if path: + mdir, path_ = r + else: + mdir = r + if isinstance(mdir, Mdir): + for mid, rattr in mdir.rattrs(): + if path: + yield (mid, mdir, rattr, + path_+[(mid, mdir, mdir.lookup(mid))]) + else: + yield mid, mdir, rattr + else: + bid, rbyd = mdir + for rid, name in rbyd.rids(): + bid_ = bid-(rbyd.weight-1) + rid + mid_ = self.mid(bid_) + mdir_ = (bid_, rbyd, rid) + for rattr in rbyd.rattrs(rid): + if path: + yield (mid_, mdir_, rattr, + path_+[(bid_, rbyd, rid, name)]) + else: + yield mid_, mdir_, rattr + else: + if not isinstance(mid, Mid): + mid = self.mid(mid) + + r = self.lookupnext_(mid, + path=path, + depth=depth) + if path: + mdir, path_ = r + else: + mdir = r + if mdir is None or ( + mdirs_only and not isinstance(mdir, Mdir)): + return + + if isinstance(mdir, Mdir): + for rattr in mdir.rattrs(mid, tag, mask): + if path: + yield rattr, path_ + else: + yield rattr + else: + bid, rbyd, rid = mdir + for rattr in rbyd.rattrs(rid, tag, mask): + if path: + yield rattr, path_ + else: + yield rattr + + # lookup by name + def _namelookup_(self, did, name, *, + path=False, + depth=None): + if path or depth: + # iterate over mrootchain + path_ = [] + for mroot in self.mrootchain: + # stop here? + if depth and len(path_) >= depth: + if path: + return mroot, path_ + else: + return mroot + + name = mroot.lookup(-1, TAG_MAGIC) + path_.append((mroot.mid, mroot, name)) + + # no mtree? must be inlined in mroot + if self.mtree is None: + if path: + return self.mroot, path_ + else: + return self.mroot + + # mtree? find name in mtree + else: + # need to do two steps here in case namelookup_ stops early + r = self.mtree.namelookup_(did, name, + path=path or depth, + depth=depth-len(path_) if depth else None) + if path or depth: + bid_, rbyd_, rid_, name_, path__ = r + path_.extend(path__) + else: + bid_, rbyd_, rid_, name_ = r + if bid_ is None: + if path: + return None, path_ + else: + return None + + # corrupt btree node? + if not rbyd_: + if path: + return (bid_, rbyd_, rid_), path_ + else: + return (bid_, rbyd_, rid_) + + # stop here? it's not an mdir, but we only return btree nodes + # if explicitly requested + if depth and len(path_) >= depth: + if path: + return (bid_, rbyd_, rid_), path_ + else: + return (bid_, rbyd_, rid_) + + # fetch the mdir + rattr_ = rbyd_.lookup(rid_, TAG_MDIR, 0x3) + # mdir tag missing? weird + if rattr_ is None: + if path: + return (bid_, rbyd_, rid_), path_ + else: + return (bid_, rbyd_, rid_) + blocks_, _ = frommdir(rattr_.data) + mdir = Mdir.fetch(self.bd, self.mid(bid_), blocks_) + if path: + return mdir, path_ + else: + return mdir + + def namelookup_(self, did, name, *, + mdirs_only=True, + path=False, + depth=None): + # most of the logic is in _namelookup_, this just helps + # deduplicate the mdirs_only logic + r = self._namelookup_(did, name, + path=path, + depth=depth) + if path: + mdir, path_ = r + else: + mdir = r + if mdir is None or ( + mdirs_only and not isinstance(mdir, Mdir)): + if path: + return None, path_ + else: + return None + + if path: + return mdir, path_ + else: + return mdir + + def namelookup(self, did, name, *, + path=False, + depth=None): + # lookup the relevant mdir + r = self.namelookup_(did, name, + path=path, + depth=depth) + if path: + mdir, path_ = r + else: + mdir = r + if mdir is None: + if path: + return None, None, None, path_ + else: + return None, None, None + + # find name in mdir + mid_, name_ = mdir.namelookup(did, name) + if mid_ is None: + if path: + return None, None, None, path_ + else: + return None, None, None + + if path: + return mid_, mdir, name_, path_+[(mid_, mdir, name_)] + else: + return mid_, mdir, name_ + + +# in-btree block pointers +class Bptr: + def __init__(self, rattr, block, off, size, cksize, cksum, ckdata, *, + corrupt=False): + self.rattr = rattr + self.block = block + self.off = off + self.size = size + self.cksize = cksize + self.cksum = cksum + self.ckdata = ckdata + + self.corrupt = corrupt + + @property + def tag(self): + return self.rattr.tag + + @property + def weight(self): + return self.rattr.weight + + # this is just for consistency with btrees, rbyds, etc + @property + def blocks(self): + return [self.block] + + # try to avoid unnecessary allocations + @ft.cached_property + def data(self): + return self.ckdata[self.off:self.off+self.size] + + def addr(self): + return '0x%x.%x' % (self.block, self.off) + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, self.repr()) + + def repr(self): + return '%sblock %s w%s %s' % ( + 'shrub' if self.tag & TAG_SHRUB else '', + self.addr(), + self.weight, + self.size) + + # lazily check the cksum + @ft.cached_property + def corrupt(self): + cksum_ = crc32c(self.ckdata) + return (cksum_ != self.cksum) + + @property + def redund(self): + return -1 if self.corrupt else 0 + + def __bool__(self): + return not self.corrupt + + @classmethod + def fetch(cls, bd, rattr, block, off, size, cksize, cksum): + # seek/read cksize bytes from the block, the actual data should + # always be a subset of cksize + ckdata = bd.read(block, 0, cksize) + + return cls(rattr, block, off, size, cksize, cksum, ckdata) + + @classmethod + def fetchck(cls, bd, rattr, blocks, off, size, cksize, cksum): + # fetch the bptr normally + bptr = cls.fetch(bd, rattr, blocks, off, size, cksize, cksum) + + # bit of a hack, but this exposes the lazy cksum checker + del bptr.corrupt + + return bptr + + # yeah, so, this doesn't catch mismatched cksizes, but at least the + # underlying data should be identical assuming no mutation + def __eq__(self, other): + return ((self.block, self.off, self.size) + == (other.block, other.off, other.size)) + + def __ne__(self, other): + return ((self.block, self.off, self.size) + != (other.block, other.off, other.size)) + + def __hash__(self): + return hash((self.block, self.off, self.size)) + + +# lazy config object +class Config: + def __init__(self, mroot): + self.mroot = mroot + + # lookup a specific tag + def lookup(self, tag=None, mask=None): + rattr = self.mroot.rbyd.lookup(-1, tag, mask) + if rattr is None: + return None + + return self._parse(rattr.tag, rattr) + + def __getitem__(self, key): + if not isinstance(key, tuple): + key = (key,) + + return self.lookup(*key) + + def __contains__(self, key): + if not isinstance(key, tuple): + key = (key,) + + return self.lookup(*key) is not None + + def __iter__(self): + for rattr in self.mroot.rbyd.rattrs(-1, TAG_CONFIG, 0xff): + yield self._parse(rattr.tag, rattr) + + # common config operations + class Config: + tag = None + mask = None + + def __init__(self, mroot, tag, rattr): + # replace tag with what we find + self.tag = tag + # and keep track of rattr + self.rattr = rattr + + @property + def block(self): + return self.rattr.block + + @property + def blocks(self): + return self.rattr.blocks + + @property + def toff(self): + return self.rattr.toff + + @property + def tdata(self): + return self.rattr.data + + @property + def off(self): + return self.rattr.off + + @property + def data(self): + return self.rattr.data + + @property + def size(self): + return self.rattr.size + + def __bytes__(self): + return self.data + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, self.repr()) + + def repr(self): + return self.rattr.repr() + + def __iter__(self): + return iter((self.tag, self.data)) + + def __eq__(self, other): + return (self.tag, self.data) == (other.tag, other.data) + + def __ne__(self, other): + return (self.tag, self.data) != (other.tag, other.data) + + def __hash__(self): + return hash((self.tag, self.data)) + + # marker class for unknown config + class Unknown(Config): + pass + + # special handling for known configs + + # the filesystem magic string + class Magic(Config): + tag = TAG_MAGIC + mask = 0x3 + + def repr(self): + return 'magic \"%s\"' % ( + ''.join(b if b >= ' ' and b <= '~' else '.' + for b in map(chr, self.data))) + + # version tuple + class Version(Config): + tag = TAG_VERSION + + def __init__(self, mroot, tag, rattr): + super().__init__(mroot, tag, rattr) + d = 0 + self.major, d_ = fromleb128(self.data, d); d += d_ + self.minor, d_ = fromleb128(self.data, d); d += d_ + + @property + def tuple(self): + return (self.major, self.minor) + + def repr(self): + return 'version v%s.%s' % (self.major, self.minor) + + # compat flags + class Rcompat(Config): + tag = TAG_RCOMPAT + + def __init__(self, mroot, tag, rattr): + super().__init__(mroot, tag, rattr) + self.flags = fromle32(self.data) + + def __int__(self): + return self.flags + + def repr(self): + return 'rcompat 0x%s' % ( + ''.join('%02x' % f for f in reversed(self.data))) + + class Wcompat(Config): + tag = TAG_WCOMPAT + + def __init__(self, mroot, tag, rattr): + super().__init__(mroot, tag, rattr) + self.flags = fromle32(self.data) + + def __int__(self): + return self.flags + + def repr(self): + return 'wcompat 0x%s' % ( + ''.join('%02x' % f for f in reversed(self.data))) + + class Ocompat(Config): + tag = TAG_OCOMPAT + + def __init__(self, mroot, tag, rattr): + super().__init__(mroot, tag, rattr) + self.flags = fromle32(self.data) + + def __int__(self): + return self.flags + + def repr(self): + return 'ocompat 0x%s' % ( + ''.join('%02x' % f for f in reversed(self.data))) + + # block device geometry + class Geometry(Config): + tag = TAG_GEOMETRY + + def __init__(self, mroot, tag, rattr): + super().__init__(mroot, tag, rattr) + d = 0 + block_size, d_ = fromleb128(self.data, d); d += d_ + block_count, d_ = fromleb128(self.data, d); d += d_ + # these are offset by 1 to avoid overflow issues + self.block_size = block_size + 1 + self.block_count = block_count + 1 + + def repr(self): + return 'geometry %sx%s' % (self.block_size, self.block_count) + + # file name limit + class NameLimit(Config): + tag = TAG_NAMELIMIT + + def __init__(self, mroot, tag, rattr): + super().__init__(mroot, tag, rattr) + self.limit, _ = fromleb128(self.data) + + def __int__(self): + return self.limit + + def repr(self): + return 'namelimit %s' % self.limit + + # file size limit + class FileLimit(Config): + tag = TAG_FILELIMIT + + def __init__(self, mroot, tag, rattr): + super().__init__(mroot, tag, rattr) + self.limit, _ = fromleb128(self.data) + + def __int__(self): + return self.limit + + def repr(self): + return 'filelimit %s' % self.limit + + # keep track of known configs + _known = [c for c in Config.__subclasses__() if c.tag is not None] + + # parse if known + def _parse(self, tag, rattr): + # known config? + for c in self._known: + if (c.tag & ~(c.mask or 0)) == (tag & ~(c.mask or 0)): + return c(self.mroot, tag, rattr) + # otherwise return a marker class + else: + return self.Unknown(self.mroot, tag, rattr) + + # create cached accessors for known config + def _parser(c): + def _parser(self): + return self.lookup(c.tag, c.mask) + return _parser + + for c in _known: + locals()[c.__name__.lower()] = ft.cached_property(_parser(c)) + +# lazy gstate object +class Gstate: + def __init__(self, mtree, config): + self.mtree = mtree + self.config = config + + # lookup a specific tag + def lookup(self, tag=None, mask=None): + # collect relevant gdeltas in the mtree + gdeltas = [] + for mdir in self.mtree.mdirs(): + # gcksumdelta is a bit special since it's outside the + # rbyd tree + if tag == TAG_GCKSUMDELTA: + gdelta = mdir.gcksumdelta + else: + gdelta = mdir.rbyd.lookup(-1, tag, mask) + if gdelta is not None: + gdeltas.append((mdir.mid, gdelta)) + + # xor to find gstate + return self._parse(tag, gdeltas) + + def __getitem__(self, key): + if not isinstance(key, tuple): + key = (key,) + + return self.lookup(*key) + + def __contains__(self, key): + # note gstate doesn't really "not exist" like normal attrs, + # missing gstate is equivalent to zero gstate, but we can + # still test if there are any gdeltas that match the given + # tag here + if not isinstance(key, tuple): + key = (key,) + + return any( + (mdir.gcksumdelta if tag == TAG_GCKSUMDELTA + else mdir.rbyd.lookup(-1, *key)) + is not None + for mdir in self.mtree.mdirs()) + + def __iter__(self): + # first figure out what gstate tags actually exist in the + # filesystem + gtags = set() + for mdir in self.mtree.mdirs(): + if mdir.gcksumdelta is not None: + gtags.add(TAG_GCKSUMDELTA) + + for rattr in mdir.rbyd.rattrs(-1): + if (rattr.tag & 0xff00) == TAG_GDELTA: + gtags.add(rattr.tag) + + # sort to keep things stable, moving gcksum to the front + gtags = sorted(gtags, key=lambda t: (-(t & 0xf000), t)) + + # compute all gstate in one pass (well, two technically) + gdeltas = {tag: [] for tag in gtags} + for mdir in self.mtree.mdirs(): + for tag in gtags: + # gcksumdelta is a bit special since it's outside the + # rbyd tree + if tag == TAG_GCKSUMDELTA: + gdelta = mdir.gcksumdelta + else: + gdelta = mdir.rbyd.lookup(-1, tag) + if gdelta is not None: + gdeltas[tag].append((mdir.mid, gdelta)) + + for tag in gtags: + # xor to find gstate + yield self._parse(tag, gdeltas[tag]) + + # common gstate operations + class Gstate: + tag = None + mask = None + rcompat = None + wcompat = None + ocompat = None + + def __init__(self, mtree, config, tag, gdeltas): + # replace tag with what we find + self.tag = tag + # keep track of gdeltas for debugging + self.gdeltas = gdeltas + + # xor together to build our gstate + data = bytes() + for mid, gdelta in gdeltas: + data = bytes( + a^b for a, b in it.zip_longest( + data, gdelta.data, + fillvalue=0)) + self.data = data + + # check compat flags while we can access config + if self.rcompat is not None: + self.rcompat = self.rcompat & ( + int(config.rcompat) if config.rcompat is not None + else 0) + if self.wcompat is not None: + self.wcompat = self.wcompat & ( + int(config.wcompat) if config.wcompat is not None + else 0) + if self.ocompat is not None: + self.ocompat = self.ocompat & ( + int(config.ocompat) if config.ocompat is not None + else 0) + + @property + def blocks(self): + return tuple(it.chain.from_iterable( + gdelta.blocks for _, gdelta in self.gdeltas)) + + # true unless compat flags are missing + def __bool__(self): + return (self.rcompat != 0 + and self.wcompat != 0 + and self.ocompat != 0) + + @property + def size(self): + return len(self.data) + + def __bytes__(self): + return self.data + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, self.repr()) + + def repr(self): + return Tag.repr(self.tag, 0, self.size, global_=True) + + def __iter__(self): + return iter((self.tag, self.data)) + + def __eq__(self, other): + return (self.tag, self.data) == (other.tag, other.data) + + def __ne__(self, other): + return (self.tag, self.data) != (other.tag, other.data) + + def __hash__(self): + return hash((self.tag, self.data)) + + # marker class for unknown gstate + class Unknown(Gstate): + pass + + # special handling for known gstate + + # the global-checksum, cubed + class Gcksum(Gstate): + tag = TAG_GCKSUMDELTA + wcompat = WCOMPAT_GCKSUM + + def __init__(self, mtree, config, tag, gdeltas): + super().__init__(mtree, config, tag, gdeltas) + self.gcksum = fromle32(self.data) + + def __int__(self): + return self.gcksum + + def repr(self): + return 'gcksum %08x' % self.gcksum + + # any global-removes + class Grm(Gstate): + tag = TAG_GRMDELTA + rcompat = RCOMPAT_GRM + + def __init__(self, mtree, config, tag, gdeltas): + super().__init__(mtree, config, tag, gdeltas) + queue = [] + d = 0 + for _ in range(2): + mid, d_ = fromleb128(self.data, d); d += d_ + # a null mid (mid=0.0) terminates the grm queue + if not mid: + break + mid = mtree.mid(mid) + # map mbids -> -1 if mroot-inlined + if mtree.mtree is None: + mid = mtree.mid(-1, mid.mrid) + queue.append(mid) + self.queue = queue + + def repr(self): + if self: + return 'grm [%s]' % ', '.join( + mid.repr() for mid in self.queue) + else: + return 'grm (unused)' + + # the global block map + class Gbmap(Gstate): + tag = TAG_GBMAPDELTA + wcompat = WCOMPAT_GBMAP + + def __init__(self, mtree, config, tag, gdeltas): + super().__init__(mtree, config, tag, gdeltas) + d = 0 + self.window, d_ = fromleb128(self.data, d); d += d_ + self.known, d_ = fromleb128(self.data, d); d += d_ + block, trunk, cksum, d_ = frombranch(self.data, d); d += d_ + self.btree = Btree.fetchck( + mtree.bd, block, trunk, + config.geometry.block_count + if config.geometry is not None else 0, + cksum) + + def repr(self): + if self: + return 'gbmap %s 0x%x %d' % ( + self.btree.addr(), + self.window, self.known) + else: + return 'gbmap (unused)' + + # keep track of known gstate + _known = [g for g in Gstate.__subclasses__() if g.tag is not None] + + # parse if known + def _parse(self, tag, gdeltas): + # known config? + for g in self._known: + if (g.tag & ~(g.mask or 0)) == (tag & ~(g.mask or 0)): + return g(self.mtree, self.config, tag, gdeltas) + # otherwise return a marker class + else: + return self.Unknown(self.mtree, self.config, tag, gdeltas) + + # create cached accessors for known gstate + def _parser(g): + def _parser(self): + return self.lookup(g.tag, g.mask) + return _parser + + for g in _known: + locals()[g.__name__.lower()] = ft.cached_property(_parser(g)) + + +# high-level littlefs representation +class Lfs3: + def __init__(self, bd, mtree, config=None, gstate=None, cksum=None, *, + corrupt=False): + self.bd = bd + self.mtree = mtree + + # create lazy config/gstate objects + self.config = config or Config(self.mroot) + self.gstate = gstate or Gstate(self.mtree, self.config) + + # go ahead and fetch some expected fields + self.version = self.config.version + self.rcompat = self.config.rcompat + self.wcompat = self.config.wcompat + self.ocompat = self.config.ocompat + if self.config.geometry is not None: + self.block_count = self.config.geometry.block_count + self.block_size = self.config.geometry.block_size + else: + self.block_count = self.bd.block_count + self.block_size = self.bd.block_size + + # calculate on-disk gcksum + if cksum is None: + cksum = 0 + for mdir in self.mtree.mdirs(): + cksum ^= mdir.cksum + self.cksum = cksum + + # is the filesystem corrupt? + self.corrupt = corrupt + + # create the root directory, this is a bit of a special case + self.root = self.Root(self) + + # mbits is a static value derived from the block_size + @staticmethod + def mbits_(block_size): + return Mtree.mbits_(block_size) + + @property + def mbits(self): + return self.mtree.mbits + + # convenience function for creating mbits-dependent mids + def mid(self, mbid, mrid=None): + return self.mtree.mid(mbid, mrid) + + # most of our fields map to the mtree + @property + def block(self): + return self.mroot.block + + @property + def blocks(self): + return self.mroot.blocks + + @property + def trunk(self): + return self.mroot.trunk + + @property + def rev(self): + return self.mroot.rev + + @property + def weight(self): + return self.mtree.weight + + @property + def mbweight(self): + return self.mtree.mbweight + + @property + def mrweight(self): + return self.mtree.mrweight + + def mbweightrepr(self): + return self.mtree.mbweightrepr() + + def mrweightrepr(self): + return self.mtree.mrweightrepr() + + @property + def mrootchain(self): + return self.mtree.mrootchain + + @property + def mrootanchor(self): + return self.mtree.mrootanchor + + @property + def mroot(self): + return self.mtree.mroot + + def addr(self): + return self.mroot.addr() + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, self.repr()) + + def repr(self): + return 'littlefs v%s.%s %sx%s %s w%s.%s' % ( + self.version.major if self.version is not None else '?', + self.version.minor if self.version is not None else '?', + self.block_size if self.block_size is not None else '?', + self.block_count if self.block_count is not None else '?', + self.addr(), + self.mbweightrepr(), self.mrweightrepr()) + + def __bool__(self): + return not self.corrupt + + def __eq__(self, other): + return self.mrootanchor == other.mrootanchor + + def __ne__(self, other): + return self.mrootanchor != other.mrootanchor + + def __hash__(self): + return hash(self.mrootanchor) + + @classmethod + def fetch(cls, bd, blocks=None, trunk=None, *, + depth=None, + no_ck=False, + no_ckmroot=False, + no_ckmagic=False, + no_ckgcksum=False): + # Mtree does most of the work here + mtree = Mtree.fetch(bd, blocks, trunk, + depth=depth) + + # create lfs object + lfs = cls(bd, mtree) + + # don't check anything? + if no_ck: + return lfs + + # check mroot + if (not no_ckmroot + and not lfs.corrupt + and not lfs.ckmroot()): + lfs.corrupt = True + + # check magic + if (not no_ckmagic + and not lfs.corrupt + and not lfs.ckmagic()): + lfs.corrupt = True + + # check gcksum + if (not no_ckgcksum + and not lfs.corrupt + and not lfs.ckgcksum()): + lfs.corrupt = True + + return lfs + + # check that the mroot is valid + def ckmroot(self): + return bool(self.mroot) + + # check that the magic string is littlefs + def ckmagic(self): + if self.config.magic is None: + return False + return self.config.magic.data == b'littlefs' + + # check that the gcksum checks out + def ckgcksum(self): + return crc32ccube(self.cksum) == int(self.gstate.gcksum) + + # read custom attrs + def uattrs(self): + return self.mroot.rattrs(-1, TAG_UATTR, 0xff) + + def sattrs(self): + return self.mroot.rattrs(-1, TAG_SATTR, 0xff) + + def attrs(self): + yield from self.uattrs() + yield from self.sattrs() + + # is file in grm queue? + def grmed(self, mid): + if not isinstance(mid, Mid): + mid = self.mid(mid) + + return mid in self.gstate.grm.queue + + # lookup operations + def lookup(self, mid, mdir=None, *, + all=False): + import builtins + all_, all = all, builtins.all + + # is this mid grmed? + if not all_ and self.grmed(mid): + return None + + if mdir is None: + mdir, name = self.mtree.lookup(mid) + if mdir is None: + return None + else: + name = mdir.lookup(mid) + + # stickynote? + if not all_ and name.tag == TAG_STICKYNOTE: + return None + + return self._open(mid, mdir, name.tag, name) + + def namelookup(self, did, name, *, + all=False): + import builtins + all_, all = all, builtins.all + + mid_, mdir_, name_ = self.mtree.namelookup(did, name) + if mid_ is None: + return None + + # is this mid grmed? + if not all_ and self.grmed(mid_): + return None + + # stickynote? + if not all_ and name_.tag == TAG_STICKYNOTE: + return None + + return self._open(mid_, mdir_, name_.tag, name_) + + class PathError(Exception): + pass + + # split a path into its components + # + # note this follows littlefs's internal logic, so dots and dotdot + # entries get resolved _before_ walking the path + @staticmethod + def pathsplit(path): + path_ = path + if isinstance(path_, str): + path_ = path_.encode('utf8') + + # empty path? + if path_ == b'': + raise Lfs3.PathError("invalid path: %r" % path) + + path__ = [] + for p in path_.split(b'/'): + # skip multiple slashes and dots + if p == b'' or p == b'.': + continue + path__.append(p) + path_ = path__ + + # resolve dotdots + path__ = [] + dotdots = 0 + for p in reversed(path_): + if p == b'..': + dotdots += 1 + elif dotdots: + dotdots -= 1 + else: + path__.append(p) + if dotdots: + raise Lfs3.PathError("invalid path: %r" % path) + path__.reverse() + path_ = path__ + + return path_ + + def pathlookup(self, did, path_=None, *, + all=False, + path=False, + depth=None): + import builtins + all_, all = all, builtins.all + + # default to the root directory + if path_ is None: + did, path_ = 0, did + # parse/split the path + if isinstance(path_, (bytes, str)): + path_ = self.pathsplit(path_) + + # start at the root dir + dir = self.root + did = did + if path or depth: + path__ = [] + + for p in path_: + # lookup the next file + file = self.namelookup(did, p, + all=all_) + if file is None: + if path: + return None, path__ + else: + return None + + # file? done? + if not file.recursable: + if path: + return file, path__ + else: + return file + + # recurse down the file tree + dir = file + did = dir.did + if path or depth: + path__.append(dir) + # stop here? + if depth and len(path__) >= depth: + if path: + return None, path__ + else: + return None + + if path: + return dir, path__ + else: + return dir + + def files(self, did=None, *, + all=False, + path=False, + depth=None): + import builtins + all_, all = all, builtins.all + + # default to the root directory + did = did or self.root.did + + # start with the bookmark entry + mid, mdir, name = self.mtree.namelookup(did, b'') + # no bookmark? weird + if mid is None: + return + + # iterate over files until we find a different did + while name.did == did: + # yield file, hiding grms, stickynotes, etc, by default + if all_ or (not self.grmed(mid) + and not name.tag == TAG_BOOKMARK + and not name.tag == TAG_STICKYNOTE): + file = self._open(mid, mdir, name.tag, name) + if path: + yield file, [] + else: + yield file + + # recurse? + if (file.recursable + and depth is not None + and (depth == 0 or depth > 1)): + for r in self.files(file.did, + all=all_, + path=path, + depth=depth-1 if depth else 0): + if path: + file_, path_ = r + yield file_, [file]+path_ + else: + file_ = r + yield file_ + + # increment mid and find the next mdir if needed + mbid, mrid = mid.mbid, mid.mrid + 1 + if mrid == mdir.weight: + mbid, mrid = mbid + (1 << self.mbits), 0 + mdir = self.mtree.lookupnext_(mbid) + if mdir is None: + break + # lookup name and adjust rid if necessary, you don't + # normally need to do this, but we don't want the iteration + # to terminate early on a corrupt filesystem + mrid, name = mdir.rbyd.lookupnext(mrid) + if mrid is None: + break + mid = self.mid(mbid, mrid) + + def orphans(self, + all=False): + import builtins + all_, all = all, builtins.all + + # first find all reachable dids + dids = {self.root.did} + for file in self.files(depth=mt.inf): + if file.recursable: + dids.add(file.did) + + # then iterate over all dids and yield any that aren't reachable + for mid, mdir, name in self.mtree.mids(): + # is this mid grmed? + if not all_ and self.grmed(mid): + continue + + # stickynote? + if not all_ and name.tag == TAG_STICKYNOTE: + continue + + # unreachable? note this lazily parses the did + if name.did not in dids: + file = self._open(mid, mdir, name.tag, name) + # mark as orphaned + file.orphaned = True + yield file + + # traverse the filesystem + def traverse(self, *, + mtree_only=False, + gstate=True, + shrubs=False, + fragments=False, + path=False): + # traverse the mtree + for r in self.mtree.traverse( + path=path): + if path: + mdir, path_ = r + else: + mdir = r + + # mdir? + if isinstance(mdir, Mdir): + if path: + yield mdir, path_ + else: + yield mdir + + # btree node? we only care about the rbyd for simplicity + else: + bid, rbyd = mdir + if path: + yield rbyd, path_ + else: + yield rbyd + + # traverse file bshrubs/btrees + if not mtree_only and isinstance(mdir, Mdir): + for mid, name in mdir.mids(): + file = self._open(mid, mdir, name.tag, name) + for r in file.traverse( + path=path): + if path: + pos, data, path__ = r + path__ = [(mid, mdir, name)]+path__ + else: + pos, data = r + + # inlined data? we usually ignore these + if isinstance(data, Rattr): + if fragments: + if path: + yield data, path_+path__ + else: + yield data + # block pointer? + elif isinstance(data, Bptr): + if path: + yield data, path_+path__ + else: + yield data + # bshrub/btree node? we only care about the rbyd + # for simplicity, we also usually ignore shrubs + # since these live the the parent mdir + else: + if shrubs or not data.shrub: + if path: + yield data, path_+path__ + else: + yield data + + # traverse any gstate + if not mtree_only and gstate: + for gstate_ in self.gstate: + if not gstate_ or getattr(gstate_, 'btree', None) is None: + continue + + for r in gstate_.btree.traverse( + path=path): + if path: + bid, rbyd, path_ = r + else: + bid, rbyd = r + + if path: + yield rbyd, [(self.mid(-1), gstate_)]+path_ + else: + yield rbyd + + # common file operations, note Reg extends this for regular files + class File: + tag = None + mask = None + internal = False + recursable = False + grmed = False + orphaned = False + + def __init__(self, lfs, mid, mdir, tag, name): + self.lfs = lfs + self.mid = mid + self.mdir = mdir + # replace tag with what we find + self.tag = tag + self.name = name + + # fetch the file structure if there is one + self.struct = mdir.lookup(mid, TAG_STRUCT, 0xff) + + # bshrub/btree? + self.bshrub = None + if (self.struct is not None + and (self.struct.tag & ~0x3) == TAG_BSHRUB): + weight, trunk, _ = fromshrub(self.struct.data) + self.bshrub = Btree.fetchshrub(lfs.bd, mdir.rbyd, trunk) + elif (self.struct is not None + and (self.struct.tag & ~0x3) == TAG_BTREE): + weight, block, trunk, cksum, _ = frombtree(self.struct.data) + self.bshrub = Btree.fetchck( + lfs.bd, block, trunk, weight, cksum) + + # did? + self.did = None + if (self.struct is not None + and self.struct.tag == TAG_DID): + self.did, _ = fromleb128(self.struct.data) + + # some other info that is useful for scripts + + # mark as grmed if grmed + if lfs.grmed(mid): + self.grmed = True + + @property + def size(self): + if self.bshrub is not None: + return self.bshrub.weight + else: + return 0 + + def structrepr(self): + if self.struct is not None: + # inlined bshrub? + if (self.struct.tag & ~0x3) == TAG_BSHRUB: + return 'bshrub %s' % self.bshrub.addr() + # btree? + elif (self.struct.tag & ~0x3) == TAG_BTREE: + return 'btree %s' % self.bshrub.addr() + # btree? + else: + return self.struct.repr() + else: + return '' + + def __repr__(self): + return '<%s %s.%s %s>' % ( + self.__class__.__name__, + self.mid.mbidrepr(), self.mid.mridrepr(), + self.repr()) + + def repr(self): + return 'type 0x%02x%s' % ( + self.tag & 0xff, + ', %s' % self.structrepr() + if self.struct is not None else '') + + def __eq__(self, other): + return self.mid == other.mid + + def __ne__(self, other): + return self.mid != other.mid + + def __hash__(self): + return hash(self.mid) + + # read attrs, note this includes _all_ attrs + def rattrs(self): + return self.mdir.rattrs(self.mid) + + # read custom attrs + def uattrs(self): + return self.mdir.rattrs(self.mid, TAG_UATTR, 0xff) + + def sattrs(self): + return self.mdir.rattrs(self.mid, TAG_SATTR, 0xff) + + def attrs(self): + yield from self.uattrs() + yield from self.sattrs() + + # lookup data in the underlying bshrub + def _lookupnext_(self, pos, *, + path=False, + depth=None): + # no bshrub? + if self.bshrub is None: + if path: + return None, None, [] + else: + return None, None + + # lookup data in our bshrub + r = self.bshrub.lookupnext_(pos, + path=path or depth, + depth=depth) + if path or depth: + bid, rbyd, rid, rattr, path_ = r + else: + bid, rbyd, rid, rattr = r + if bid is None: + if path: + return None, None, path_ + else: + return None, None + + # corrupt btree node? + if not rbyd: + if path: + return bid-(rbyd.weight-1), rbyd, path_ + else: + return bid-(rbyd.weight-1), rbyd + + # stop here? + if depth and len(path_) >= depth: + if path: + return bid-(rattr.weight-1), rbyd, path_ + else: + return bid-(rattr.weight-1), rbyd + + # inlined data? + if (rattr.tag & ~0x1003) == TAG_DATA: + if path: + return bid-(rattr.weight-1), rattr, path_ + else: + return bid-(rattr.weight-1), rattr + # block pointer? + elif (rattr.tag & ~0x1003) == TAG_BLOCK: + size, block, off, cksize, cksum, _ = frombptr(rattr.data) + bptr = Bptr.fetchck(self.lfs.bd, rattr, + block, off, size, cksize, cksum) + if path: + return bid-(rattr.weight-1), bptr, path_ + else: + return bid-(rattr.weight-1), bptr + # uh oh, something is broken + else: + if path: + return bid-(rattr.weight-1), rattr, path_ + else: + return bid-(rattr.weight-1), rattr + + def lookupnext_(self, pos, *, + data_only=True, + path=False, + depth=None): + r = self._lookupnext_(pos, + path=path, + depth=depth) + if path: + pos, data, path_ = r + else: + pos, data = r + if pos is None or ( + data_only and not isinstance(data, (Rattr, Bptr))): + if path: + return None, None, path_ + else: + return None, None + + if path: + return pos, data, path_ + else: + return pos, data + + def _leaves(self, *, + path=False, + depth=None): + pos = 0 + while True: + r = self.lookupnext_(pos, + data_only=False, + path=path, + depth=depth) + if path: + pos, data, path_ = r + else: + pos, data = r + if pos is None: + break + + # data? + if isinstance(data, (Rattr, Bptr)): + if path: + yield pos, data, path_ + else: + yield pos, data + pos += data.weight + # btree node? + else: + rbyd = data + if path: + yield (pos, rbyd, + # path tail is usually redundant unless corrupt + path_[:-1] + if path_ and path_[-1][1] == rbyd + else path_) + else: + yield pos, rbyd + pos += rbyd.weight + + def leaves(self, *, + data_only=False, + path=False, + depth=None): + for r in self._leaves( + path=path, + depth=depth): + if path: + pos, data, path_ = r + else: + pos, data = r + if data_only and not isinstance(data, (Rattr, Bptr)): + continue + + if path: + yield pos, data, path_ + else: + yield pos, data + + def _traverse(self, *, + path=False, + depth=None): + ptrunk_ = [] + for pos, data, path_ in self.leaves( + path=True, + depth=depth): + # we only care about the data/rbyds here + trunk_ = ([(bid_-rid_, rbyd_) + for bid_, rbyd_, rid_, name_ in path_] + + [(pos, data)]) + for d, (pos, data) in pathdelta( + trunk_, ptrunk_): + # but include branch rids in path if requested + if path: + yield pos, data, path_[:d] + else: + yield pos, data + ptrunk_ = trunk_ + + def traverse(self, *, + data_only=False, + path=False, + depth=None): + for r in self._traverse( + path=path, + depth=depth): + if path: + pos, data, path_ = r + else: + pos, data = r + if data_only and not isinstance(data, (Rattr, Bptr)): + continue + + if path: + yield pos, data, path_ + else: + yield pos, data + + def datas(self, *, + data_only=True, + path=False, + depth=None): + return self.leaves( + data_only=data_only, + path=path, + depth=depth) + + # some convience operations for reading data + def bytes(self, *, + depth=None): + for pos, data in self.datas(depth=depth): + if data.size > 0: + yield data.data + if data.weight > data.size: + yield b'\0' * (data.weight-data.size) + + def read(self, *, + depth=None): + return b''.join(self.bytes()) + + # bleh, with that out of the way, here are our known file types + + # regular files + class Reg(File): + tag = TAG_REG + + def repr(self): + return 'reg %s%s' % ( + self.size, + ', %s' % self.structrepr() + if self.struct is not None else '') + + # directories + class Dir(File): + tag = TAG_DIR + + def __init__(self, lfs, mid, mdir, tag, name): + super().__init__(lfs, mid, mdir, tag, name) + + # we're recursable if we're a non-grmed directory with a did + if (isinstance(self, Lfs3.Dir) + and not self.grmed + and self.did is not None): + self.recursable = True + + def repr(self): + return 'dir %s%s' % ( + '0x%x' % self.did + if self.did is not None else '?', + ', %s' % self.structrepr() + if self.struct is not None + and self.struct.tag != TAG_DID else '') + + # provide some convenient filesystem access relative to our did + def namelookup(self, name, **args): + if self.did is None: + return None + return self.lfs.namelookup(self.did, name, **args) + + def pathlookup(self, path_, **args): + if self.did is None: + if args.get('path'): + return None, [] + else: + return None + return self.lfs.pathlookup(self.did, path_, **args) + + def files(self, **args): + if self.did is None: + return iter(()) + return self.lfs.files(self.did, **args) + + # root is a bit special + class Root(Dir): + tag = None + + def __init__(self, lfs): + # root always has mid=-1 and did=0 + super().__init__(lfs, lfs.mid(-1), lfs.mroot, TAG_DIR, None) + self.did = 0 + self.recursable = True + + def repr(self): + return 'root' + + # bookmarks keep track of where directories start + class Bookmark(File): + tag = TAG_BOOKMARK + internal = True + + def repr(self): + return 'bookmark %s%s' % ( + '0x%x' % self.name.did + if self.name.did is not None else '?', + ', %s' % self.structrepr() + if self.struct is not None else '') + + # stickynotes, i.e. uncommitted files, behave the same as files + # for the most part + class Stickynote(File): + tag = TAG_STICKYNOTE + internal = True + + def repr(self): + return 'stickynote%s' % ( + ' %s, %s' % (self.size, self.structrepr()) + if self.struct is not None else '') + + # marker class for unknown file types + class Unknown(File): + pass + + # keep track of known file types + _known = [f for f in File.__subclasses__() if f.tag is not None] + + # fetch/parse state if known + def _open(self, mid, mdir, tag, name): + # known file type? + tag = name.tag + for f in self._known: + if (f.tag & ~(f.mask or 0)) == (tag & ~(f.mask or 0)): + return f(self, mid, mdir, tag, name) + # otherwise return a marker class + else: + return self.Unknown(self, mid, mdir, tag, name) + + + +# tree renderer +class TreeArt: + # tree branches are an abstract thing for tree rendering + class Branch(co.namedtuple('Branch', ['a', 'b', 'z', 'color'])): + __slots__ = () + def __new__(cls, a, b, z=0, color='b'): + # a and b are context specific + return super().__new__(cls, a, b, z, color) + + def __repr__(self): + return '%s(%s, %s, %s, %s)' % ( + self.__class__.__name__, + self.a, + self.b, + self.z, + self.color) + + # don't include color in branch comparisons, or else our tree + # renderings can end up with inconsistent colors between runs + def __eq__(self, other): + return (self.a, self.b, self.z) == (other.a, other.b, other.z) + + def __ne__(self, other): + return (self.a, self.b, self.z) != (other.a, other.b, other.z) + + def __hash__(self): + return hash((self.a, self.b, self.z)) + + # also order by z first, which can be useful for reproducibly + # prioritizing branches when simplifying trees + def __lt__(self, other): + return (self.z, self.a, self.b) < (other.z, other.a, other.b) + + def __le__(self, other): + return (self.z, self.a, self.b) <= (other.z, other.a, other.b) + + def __gt__(self, other): + return (self.z, self.a, self.b) > (other.z, other.a, other.b) + + def __ge__(self, other): + return (self.z, self.a, self.b) >= (other.z, other.a, other.b) + + # apply a function to a/b while trying to avoid copies + def map(self, filter_, map_=None): + if map_ is None: + filter_, map_ = None, filter_ + + a = self.a + if filter_ is None or filter_(a): + a = map_(a) + + b = self.b + if filter_ is None or filter_(b): + b = map_(b) + + if a != self.a or b != self.b: + return self.__class__( + a if a != self.a else self.a, + b if b != self.b else self.b, + self.z, + self.color) + else: + return self + + def __init__(self, tree): + self.tree = tree + self.depth = max((t.z+1 for t in tree), default=0) + if self.depth > 0: + self.width = 2*self.depth + 2 + else: + self.width = 0 + + def __iter__(self): + return iter(self.tree) + + def __bool__(self): + return bool(self.tree) + + def __len__(self): + return len(self.tree) + + # render an rbyd rbyd tree for debugging + @classmethod + def _fromrbydrtree(cls, rbyd, **args): + trunks = co.defaultdict(lambda: (-1, 0)) + alts = co.defaultdict(lambda: {}) + + for rid, rattr, path in rbyd.rattrs(path=True): + # keep track of trunks/alts + trunks[rattr.toff] = (rid, rattr.tag) + + for ralt in path: + if ralt.followed: + alts[ralt.toff] |= {'f': ralt.joff, 'c': ralt.color} + else: + alts[ralt.toff] |= {'nf': ralt.off, 'c': ralt.color} + + if args.get('tree_rbyd_all'): + # treat unreachable alts as converging paths + for j_, alt in alts.items(): + if 'f' not in alt: + alt['f'] = alt['nf'] + elif 'nf' not in alt: + alt['nf'] = alt['f'] + + else: + # prune any alts with unreachable edges + pruned = {} + for j, alt in alts.items(): + if 'f' not in alt: + pruned[j] = alt['nf'] + elif 'nf' not in alt: + pruned[j] = alt['f'] + for j in pruned.keys(): + del alts[j] + + for j, alt in alts.items(): + while alt['f'] in pruned: + alt['f'] = pruned[alt['f']] + while alt['nf'] in pruned: + alt['nf'] = pruned[alt['nf']] + + # find the trunk and depth of each alt + def rec_trunk(j): + if j not in alts: + return trunks[j] + else: + if 'nft' not in alts[j]: + alts[j]['nft'] = rec_trunk(alts[j]['nf']) + return alts[j]['nft'] + + for j in alts.keys(): + rec_trunk(j) + for j, alt in alts.items(): + if alt['f'] in alts: + alt['ft'] = alts[alt['f']]['nft'] + else: + alt['ft'] = trunks[alt['f']] + + def rec_height(j): + if j not in alts: + return 0 + else: + if 'h' not in alts[j]: + alts[j]['h'] = max( + rec_height(alts[j]['f']), + rec_height(alts[j]['nf'])) + 1 + return alts[j]['h'] + + for j in alts.keys(): + rec_height(j) + + t_depth = max((alt['h']+1 for alt in alts.values()), default=0) + + # convert to more general tree representation + tree = set() + for j, alt in alts.items(): + # note all non-trunk edges should be colored black + tree.add(cls.Branch( + alt['nft'], + alt['nft'], + t_depth-1 - alt['h'], + alt['c'])) + if alt['ft'] != alt['nft']: + tree.add(cls.Branch( + alt['nft'], + alt['ft'], + t_depth-1 - alt['h'], + 'b')) + + return cls(tree) + + # render an rbyd btree tree for debugging + @classmethod + def _fromrbydbtree(cls, rbyd, **args): + # for rbyds this is just a pointer to every rid + tree = set() + root = None + for rid, name in rbyd.rids(): + b = (rid, name.tag) + if root is None: + root = b + tree.add(cls.Branch(root, b)) + return cls(tree) + + # render an rbyd tree for debugging + @classmethod + def fromrbyd(cls, rbyd, **args): + if args.get('tree_btree'): + return cls._fromrbydbtree(rbyd, **args) + else: + return cls._fromrbydrtree(rbyd, **args) + + # render some nice ascii trees + def repr(self, x, color=False): + if self.depth == 0: + return '' + + def branchrepr(tree, x, d, was): + for t in tree: + if t.z == d and t.b == x: + if any(t.z == d and t.a == x + for t in tree): + return '+-', t.color, t.color + elif any(t.z == d + and x > min(t.a, t.b) + and x < max(t.a, t.b) + for t in tree): + return '|-', t.color, t.color + elif t.a < t.b: + return '\'-', t.color, t.color + else: + return '.-', t.color, t.color + for t in tree: + if t.z == d and t.a == x: + return '+ ', t.color, None + for t in tree: + if (t.z == d + and x > min(t.a, t.b) + and x < max(t.a, t.b)): + return '| ', t.color, was + if was: + return '--', was, was + return ' ', None, None + + trunk = [] + was = None + for d in range(self.depth): + t, c, was = branchrepr(self.tree, x, d, was) + + trunk.append('%s%s%s%s' % ( + '\x1b[33m' if color and c == 'y' + else '\x1b[31m' if color and c == 'r' + else '\x1b[1;30m' if color and c == 'b' + else '', + t, + ('>' if was else ' ') if d == self.depth-1 else '', + '\x1b[m' if color and c else '')) + + return '%s ' % ''.join(trunk) + +# some more renderers + +# render a btree rbyd tree for debugging +@classmethod +def _treeartfrombtreertree(cls, btree, *, + depth=None, + inner=False, + **args): + # precompute rbyd trees so we know the max depth at each layer + # to nicely align trees + rtrees = {} + rdepths = {} + for bid, rbyd, path in btree.traverse(path=True, depth=depth): + if not rbyd: + continue + + rtree = cls.fromrbyd(rbyd, **args) + rtrees[rbyd] = rtree + rdepths[len(path)] = max(rdepths.get(len(path), 0), rtree.depth) + + # map rbyd branches into our btree space + tree = set() + for bid, rbyd, path in btree.traverse(path=True, depth=depth): + if not rbyd: + continue + + # yes we can find new rbyds if disk is being mutated, just + # ignore these + if rbyd not in rtrees: + continue + + rtree = rtrees[rbyd] + rz = max((t.z+1 for t in rtree), default=0) + d = sum(rdepths[d]+1 for d in range(len(path))) + + # map into our btree space + for t in rtree: + # note we adjust our bid to be left-leaning, this allows + # a global order and makes tree rendering quite a bit easier + a_rid, a_tag = t.a + b_rid, b_tag = t.b + _, (_, a_w, _) = rbyd.lookupnext(a_rid) + _, (_, b_w, _) = rbyd.lookupnext(b_rid) + tree.add(cls.Branch( + (bid-(rbyd.weight-1)+a_rid-(a_w-1), len(path), a_tag), + (bid-(rbyd.weight-1)+b_rid-(b_w-1), len(path), b_tag), + d + rdepths[len(path)]-rz + t.z, + t.color)) + + # connect rbyd branches to rbyd roots + if path: + l_bid, l_rbyd, l_rid, l_name = path[-1] + l_branch = l_rbyd.lookup(l_rid, TAG_BRANCH, 0x3) + + if rtree: + r_rid, r_tag = min(rtree, key=lambda t: t.z).a + _, (_, r_w, _) = rbyd.lookupnext(r_rid) + else: + r_rid, (r_tag, r_w, _) = rbyd.lookupnext(-1) + + tree.add(cls.Branch( + (l_bid-(l_name.weight-1), len(path)-1, l_branch.tag), + (bid-(rbyd.weight-1)+r_rid-(r_w-1), len(path), r_tag), + d-1)) + + # remap branches to leaves if we aren't showing inner branches + if not inner: + # step through each btree layer backwards + b_depth = max((t.a[1]+1 for t in tree), default=0) + + for d in reversed(range(b_depth-1)): + # find bid ranges at this level + bids = set() + for t in tree: + if t.b[1] == d: + bids.add(t.b[0]) + bids = sorted(bids) + + # find the best root for each bid range + roots = {} + for i in range(len(bids)): + for t in tree: + if (t.a[1] > d + and t.a[0] >= bids[i] + and (i == len(bids)-1 or t.a[0] < bids[i+1]) + and (bids[i] not in roots + or t < roots[bids[i]])): + roots[bids[i]] = t + + # remap branches to leaf-roots + tree = {t.map( + lambda x: x[1] == d and x[0] in roots, + lambda x: roots[x[0]].a) + for t in tree} + + return cls(tree) + +# render a btree btree tree for debugging +@classmethod +def _treeartfrombtreebtree(cls, btree, *, + depth=None, + inner=False, + **args): + # find all branches + tree = set() + root = None + branches = {} + for bid, name, path in btree.bids( + path=True, + depth=depth): + # create branch for each jump in path + # + # note we adjust our bid to be left-leaning, this allows + # a global order and makes tree rendering quite a bit easier + a = root + for d, (bid_, rbyd_, rid_, name_) in enumerate(path): + # map into our btree space + bid__ = bid_-(name_.weight-1) + b = (bid__, d, name_.tag) + + # remap branches to leaves if we aren't showing inner + # branches + if not inner: + if b not in branches: + bid_, rbyd_, rid_, name_ = path[-1] + bid__ = bid_-(name_.weight-1) + branches[b] = (bid__, len(path)-1, name_.tag) + b = branches[b] + + # render the root path on first rid, this is arbitrary + if root is None: + root, a = b, b + + tree.add(cls.Branch(a, b, d)) + a = b + + return cls(tree) + +# render a btree tree for debugging +@classmethod +def treeartfrombtree(cls, btree, **args): + if args.get('tree_btree'): + return cls._frombtreebtree(btree, **args) + else: + return cls._frombtreertree(btree, **args) + +TreeArt._frombtreertree = _treeartfrombtreertree +TreeArt._frombtreebtree = _treeartfrombtreebtree +TreeArt.frombtree = treeartfrombtree + +# render a file tree for debugging +@classmethod +def treeartfromfile(cls, file, **args): + tree = cls.frombtree(file.bshrub, **args) + t_depth = tree.depth + + # connect bptr tags to bptrs + tree = set(tree) + bptrs = {} + for pos, data, path in file.datas( + path=True, + depth=args.get('depth')): + if isinstance(data, Bptr): + a = (pos, len(path)-1, data.tag) + b = (pos, len(path), data.tag) + bptrs[a] = b + tree.add(cls.Branch(a, b, t_depth)) + + # if we're not showing inner branches, nudge bptr tags to + # their bptrs + if not args.get('inner'): + tree = {t.map(lambda x: bptrs.get(x, x)) for t in tree} + + return cls(tree) + +TreeArt.fromfile = treeartfromfile + + + +# show the littlefs config +def dbg_config(lfs, *, + color=False, + w_width=2, + **args): + # show config + for i, config in enumerate(it.chain( + lfs.config, + lfs.attrs())): + # some special situations worth reporting + notes = [] + # magic corrupt? + if (config.tag == TAG_MAGIC + and config.data != b'littlefs'): + notes.append('magic!=littlefs') + + print('%s%12s %*s %-*s %s%s%s' % ( + '\x1b[31m' if color and notes else '', + '{%s}:' % ','.join('%04x' % block + for block in config.blocks) + if i == 0 else '', + 2*w_width+1, '%d.%d' % (-1, -1) + if i == 0 else '', + 21+w_width, config.repr(), + next(xxd(config.data, 8), '') + if not args.get('raw') + and not args.get('no_truncate') + else '', + ' (%s)' % ', '.join(notes) if notes else '', + '\x1b[m' if color and notes else '')) + + # show on-disk encoding + if args.get('raw') or args.get('no_truncate'): + for o, line in enumerate(xxd(config.data)): + print('%11s: %*s %s' % ( + '%04x' % (config.toff + o*16), + 2*w_width+1, '', + line)) + +# show the littlefs gstate +def dbg_gstate(lfs, *, + color=False, + w_width=2, + **args): + + # print gstate structures + def dbg_gstruct(gstate): + # not in use? + if not gstate: + return + # no tree? + if getattr(gstate, 'btree', None) is None: + return + + # precompute tree renderings + bt_width = 0 + if (args.get('tree_rbyd') + or args.get('tree_rbyd_all') + or args.get('tree_btree')): + treeart = TreeArt.frombtree(gstate.btree, **args) + bt_width = treeart.width + + # dynamically size the id field + bw_width = mt.ceil(mt.log10(max(1, gstate.btree.weight)+1)) + + # only show the rbyd address on rbyd change + prbyd = None + # recursively print btree branches + def dbg_branch(d, bid, rbyd, rid, name): + nonlocal prbyd + + # show human-readable representation + for rattr in rbyd.rattrs(rid): + print('%12s %*s %s%*s %-*s %s' % ( + '%04x.%04x:' % (rbyd.block, rbyd.trunk) + if prbyd is None or rbyd != prbyd + else '', + 2*w_width+1, '', + treeart.repr( + (bid-(name.weight-1), d, rattr.tag), + color) + if args.get('tree_rbyd') + or args.get('tree_rbyd_all') + or args.get('tree_btree') + else '', + 2*bw_width+1, '%d-%d' % (bid-(rattr.weight-1), bid) + if rattr.weight > 1 + else bid if rattr.weight > 0 + else '', + 21+2*bw_width+1, rattr.repr(), + next(xxd(rattr.data, 8), '') + if not args.get('raw') + and not args.get('no_truncate') + else '')) + prbyd = rbyd + + # show on-disk encoding of tags/data + if args.get('raw'): + for o, line in enumerate(xxd(rattr.tdata)): + print('%11s: %*s %*s%*s %s' % ( + '%04x' % (rattr.toff + o*16), + 2*w_width+1, '', + bt_width, '', + 2*bw_width+1, '', + line)) + if args.get('raw') or args.get('no_truncate'): + for o, line in enumerate(xxd(rattr.data)): + print('%11s: %*s %*s%*s %s' % ( + '%04x' % (rattr.off + o*16), + 2*w_width+1, '', + bt_width, '', + 2*bw_width+1, '', + line)) + + # traverse and print entries + ppath = [] + for bid, rbyd, path in gstate.btree.leaves( + path=True, + depth=args.get('depth')): + # print inner branches if requested + if args.get('inner'): + for d, (bid_, rbyd_, rid_, name_) in pathdelta( + path, ppath): + dbg_branch(d, bid_, rbyd_, rid_, name_) + ppath = path + + # corrupted? try to keep printing the tree + if not rbyd: + print('%s%11s: %*s %*s%s%s' % ( + '\x1b[31m' if color else '', + '%04x.%04x' % (rbyd.block, rbyd.trunk), + 2*w_width+1, '', + bt_width, '', + '(corrupted rbyd %s)' % rbyd.addr(), + '\x1b[m' if color else '')) + prbyd = None + continue + + for rid, name in rbyd.rids(): + bid_ = bid-(rbyd.weight-1) + rid + # show the leaf entry/branch + dbg_branch(len(path), bid_, rbyd, rid, name) + + # show gstate + for i, gstate in enumerate(lfs.gstate): + # some special situations worth reporting + notes = [] + # gcksum mismatch? + if (gstate.tag == TAG_GCKSUMDELTA + and int(gstate) != crc32ccube(lfs.cksum)): + notes.append('gcksum!=%08x' % crc32ccube(lfs.cksum)) + + print('%s%12s %*s %-*s %s%s%s' % ( + '\x1b[31m' if color and notes else '', + 'gstate:' + if i == 0 or args.get('gdelta') + else '', + 2*w_width+1, 'g.-1' + if i == 0 or args.get('gdelta') + else '', + 21+w_width, gstate.repr(), + next(xxd(gstate.data, 8), '') + if not args.get('raw') + and not args.get('no_truncate') + else '', + ' (%s)' % ', '.join(notes) if notes else '', + '\x1b[m' if color and notes else '')) + + # show on-disk encoding + if args.get('raw') or args.get('no_truncate'): + for o, line in enumerate(xxd(gstate.data)): + print('%11s: %*s %s' % ( + '%04x' % (o*16), + 2*w_width+1, '', + line)) + + # print gdeltas? + if args.get('gdelta'): + for mid, gdelta in gstate.gdeltas: + print('%s%12s %*s %-*s %s%s' % ( + '\x1b[1;30m' if color else '', + '{%s}:' % ','.join('%04x' % block + for block in gdelta.blocks), + 2*w_width+1, mid.repr(), + 21+w_width, gdelta.repr(), + next(xxd(gdelta.data, 8), '') + if not args.get('raw') + and not args.get('no_truncate') + else '', + '\x1b[m' if color else '')) + + # show on-disk encoding + if args.get('raw'): + for o, line in enumerate(xxd(gdelta.tdata)): + print('%11s: %*s %s' % ( + '%04x' % (gdelta.toff + o*16), + 2*w_width+1, '', + line)) + if args.get('raw') or args.get('no_truncate'): + for o, line in enumerate(xxd(gdelta.data)): + print('%11s: %*s %s' % ( + '%04x' % (gdelta.off + o*16), + 2*w_width+1, '', + line)) + + # print gstate structures? + if args.get('gstructs'): + dbg_gstruct(gstate) + +# show the littlefs file tree +def dbg_files(lfs, paths, *, + color=False, + w_width=2, + recurse=None, + all=False, + no_orphans=False, + **args): + import builtins + all_, all = all, builtins.all + + # parse all paths first, error if anything is malformed + dirs = [] + # default paths to the root dir + for path in (paths or ['%']): + try: + # skip leading % + if path == '%': + path = '/' + if path.startswith('%/'): + path = path[1:] + # lookup path + dir = lfs.pathlookup(path, + all=args.get('all')) + except Lfs3.PathError as e: + print("error: %s" % e, + file=sys.stderr) + sys.exit(-1) + + if dir is not None: + dirs.append(dir) + + # it's kinda tricky to iterate over everything we want to show, + # so create a reusable iterator + def iter_dir(dir, **args_): + if dir.recursable: + yield from dir.files(**args_) + else: + if args_.get('path'): + yield dir, [] + else: + yield dir + + # include any orphaned entries in the root directory to help + # debugging (these don't actually live in the root directory) + if not no_orphans and isinstance(dir, Lfs3.Root): + # finding orphans is expensive, so cache this + if not hasattr(iter_dir, 'orphans'): + iter_dir.orphans = dir.lfs.orphans() + for orphan in iter_dir.orphans: + if args_.get('path'): + yield orphan, [] + else: + yield orphan + + # do a pass to figure out the width+depth of the file tree + # and file names so we can format things nicely + f_depth, f_width = 0, 0 + for dir in dirs: + for file, path in iter_dir(dir, + all=all_, + depth=recurse, + path=True): + f_depth = max(f_depth, len(path)+1) + f_width = max(f_width, 4*len(path) + len(file.name.name)) + + # only show the mdir/rbyd/block address on mdir change + pmdir = None + # recursively print directories + def dbg_dir(dir, + depth, + prefixes=('', '', '', '')): + nonlocal pmdir + + # first figure out the dir length so we know when the dir ends + if prefixes != ('', '', '', ''): + len_ = sum(1 for _ in iter_dir(dir, all=all_)) + else: + len_ = 1 + + # print files + for i, file in enumerate(iter_dir(dir, all=all_)): + # some special situations worth reporting + notes = [] + # grmed? + if file.grmed: + notes.append('grmed') + # orphaned? + if file.orphaned: + notes.append('orphaned') + # missing bookmark/did? + if isinstance(file, Lfs3.Dir): + if file.did is None: + notes.append('missing did') + elif lfs.namelookup(file.did, b'') is None: + notes.append('missing bookmark') + + # print human readable file entry + print('%s%12s %*s %-*s %s%s%s' % ( + '\x1b[31m' + if color and not file.grmed and notes + else '\x1b[1;30m' + if color and (file.grmed or file.internal) + else '', + '{%s}:' % ','.join('%04x' % block + for block in file.mdir.blocks) + if not isinstance(pmdir, Mdir) or file.mdir != pmdir + else '', + 2*w_width+1, file.mid.repr(), + f_width, '%s%s' % ( + prefixes[0+(i==len_-1)], + file.name.name.decode('utf8', + errors='backslashreplace')), + file.repr(), + ' (%s)' % ', '.join(notes) if notes else '', + '\x1b[m' + if color and (notes or file.grmed or file.internal) + else '')) + pmdir = file.mdir + + # print attrs associated with each file? + if args.get('attrs'): + for rattr in file.rattrs(): + print('%12s %*s %-*s %s' % ( + '', + 2*w_width+1, '', + 21+w_width, rattr.repr(), + next(xxd(rattr.data, 8), '') + if not args.get('raw') + and not args.get('no_truncate') + else '')) + + # show on-disk encoding + if args.get('raw'): + for o, line in enumerate(xxd(rattr.tdata)): + print('%11s: %*s %s' % ( + '%04x' % (rattr.toff + o*16), + 2*w_width+1, '', + line)) + if args.get('raw') or args.get('no_truncate'): + for o, line in enumerate(xxd(rattr.data)): + print('%11s: %*s %s' % ( + '%04x' % (rattr.off + o*16), + 2*w_width+1, '', + line)) + + # print file structures? + if args.get('structs'): + dbg_struct(file) + + # recurse? + if (file.recursable + and depth is not None + and (depth == 0 or depth > 1)): + dbg_dir(file, + depth-1 if depth else 0, + (prefixes[2+(i==len_-1)] + "|-> ", + prefixes[2+(i==len_-1)] + "'-> ", + prefixes[2+(i==len_-1)] + "| ", + prefixes[2+(i==len_-1)] + " ")) + + # print file structures + def dbg_struct(file): + nonlocal pmdir + + # no tree? + if file.bshrub is None: + return + + # precompute tree renderings + bt_width = 0 + if (args.get('tree_rbyd') + or args.get('tree_rbyd_all') + or args.get('tree_btree')): + treeart = TreeArt.fromfile(file, **args) + bt_width = treeart.width + + # dynamically size the id field + bw_width = mt.ceil(mt.log10(max(1, file.size)+1)) + + # recursively print bshrub branches + def dbg_branch(d, bid, rbyd, rid, name): + nonlocal pmdir + + # show human-readable representation + for rattr in rbyd.rattrs(rid): + print('%12s %*s %s%*s %-*s %s' % ( + '%04x.%04x:' % (rbyd.block, rbyd.trunk) + if not isinstance(pmdir, Rbyd) or rbyd != pmdir + else '', + 2*w_width+1, '', + treeart.repr( + (bid-(name.weight-1), d, rattr.tag), + color) + if args.get('tree_rbyd') + or args.get('tree_rbyd_all') + or args.get('tree_btree') + else '', + 2*bw_width+1, '%d-%d' % (bid-(rattr.weight-1), bid) + if rattr.weight > 1 + else bid if rattr.weight > 0 + else '', + 21+2*bw_width+1, rattr.repr(), + next(xxd(rattr.data, 8), '') + if not args.get('raw') + and not args.get('no_truncate') + else '')) + pmdir = rbyd + + # show on-disk encoding of tags/data + if args.get('raw'): + for o, line in enumerate(xxd(rattr.tdata)): + print('%11s: %*s %*s%*s %s' % ( + '%04x' % (rattr.toff + o*16), + 2*w_width+1, '', + bt_width, '', + 2*bw_width+1, '', + line)) + if args.get('raw') or args.get('no_truncate'): + for o, line in enumerate(xxd(rattr.data)): + print('%11s: %*s %*s%*s %s' % ( + '%04x' % (rattr.off + o*16), + 2*w_width+1, '', + bt_width, '', + 2*bw_width+1, '', + line)) + + # print inlined data, block pointers, etc + def dbg_bptr(d, pos, bptr): + nonlocal pmdir + # some special situations worth reporting + notes = [] + # cksum mismatch? + cksum = crc32c(bptr.ckdata) + if cksum != bptr.cksum: + notes.append('cksum!=%08x' % bptr.cksum) + + print('%s%12s%s %*s %s%s%s%-*s%s%s' % ( + '\x1b[31m' if color and notes else '', + '%04x.%04x:' % (bptr.block, bptr.off) + if not isinstance(pmdir, Bptr) or bptr != pmdir + else '', + '\x1b[0m' if color and notes else '', + 2*w_width+1, '', + treeart.repr((pos, d, bptr.tag), color) + if args.get('tree_rbyd') + or args.get('tree_rbyd_all') + or args.get('tree_btree') + else '', + '\x1b[31m' if color and notes else '', + '%*s ' % ( + 2*bw_width+1, '%d-%d' % (pos, pos+(bptr.weight-1)) + if bptr.weight > 1 + else pos if bptr.weight > 0 + else ''), + 56+2*bw_width+1, '%-*s %s' % ( + 21+2*bw_width+1, bptr.repr(), + next(xxd(bptr.data, 8), '') + if not args.get('raw') + and not args.get('no_truncate') + else ''), + ' (%s)' % ', '.join(notes) if notes else '', + '\x1b[m' if color and notes else '')) + pmdir = bptr + + # show on-disk encoding of tag/bptr/data + if args.get('raw'): + for o, line in enumerate(xxd(bptr.rattr.tdata)): + print('%11s: %*s %*s%s%s' % ( + '%04x' % (bptr.rattr.toff + o*16), + 2*w_width+1, '', + bt_width, '', + '%*s ' % (2*bw_width+1, ''), + line)) + if args.get('raw'): + for o, line in enumerate(xxd(bptr.rattr.data)): + print('%11s: %*s %*s%s%s' % ( + '%04x' % (bptr.rattr.off + o*16), + 2*w_width+1, '', + bt_width, '', + '%*s ' % (2*bw_width+1, ''), + line)) + if args.get('raw') or args.get('no_truncate'): + for o, line in enumerate(xxd(bptr.data)): + print('%11s: %*s %*s%s%s' % ( + '%04x' % (bptr.off + o*16), + 2*w_width+1, '', + bt_width, '', + '%*s ' % (2*bw_width+1, ''), + line)) + + # traverse and print entries + ppath = [] + for pos, data, path in file.leaves( + path=True, + depth=args.get('depth')): + # print inner branches if requested + if args.get('inner'): + for d, (bid_, rbyd_, rid_, name_) in pathdelta( + path, ppath): + dbg_branch(d, bid_, rbyd_, rid_, name_) + ppath = path + + # inlined data? + if isinstance(data, Rattr): + # a bit of a hack + if not args.get('inner'): + bid_, rbyd_, rid_, name_ = path[-1] + dbg_branch(len(path)-1, bid_, rbyd_, rid_, data) + + # block pointer? + elif isinstance(data, Bptr): + # show the data + dbg_bptr(len(path), pos, data) + + # btree node? + else: + rbyd = data + bid = pos + (rbyd.weight-1) + + # corrupted? try to keep printing the tree + if not rbyd: + print('%s%11s: %*s %*s%s%s' % ( + '\x1b[31m' if color else '', + '%04x.%04x' % (rbyd.block, rbyd.trunk), + 2*w_width+1, '', + bt_width, '', + '(corrupted rbyd %s)' % rbyd.addr(), + '\x1b[m' if color else '')) + pmdir = None + continue + + for rid, name in rbyd.rids(): + bid_ = bid-(rbyd.weight-1) + rid + # show the leaf entry/branch + dbg_branch(len(path), bid_, rbyd, rid, name) + + # print stuff + for dir in dirs: + dbg_dir(dir, recurse) + +# common ck function +def dbg_ck(lfs, *, + meta=True, + data=True, + mtree_only=False, + quiet=False, + color=False, + **args): + # lfs traverse does most of the work here + corrupted = False + for child in lfs.traverse( + mtree_only=mtree_only): + # limit to metadata blocks? + if (((meta and isinstance(child, (Mdir, Rbyd))) + or (data and isinstance(child, Bptr))) + and not child): + if not quiet: + print('%s%11s: %s%s' % ( + '\x1b[31m' if color else '', + '{%s}' % ','.join('%04x' % block + for block in child.blocks) + if isinstance(child, Mdir) + else '%04x.%04x' % (child.block, child.trunk) + if isinstance(child, Rbyd) + else '%04x.%04x' % (child.block, child.off), + '(corrupted %s %s)' % ( + 'mroot' if isinstance(child, Mdir) + and child.mid == -1 + else 'mdir' if isinstance(child, Mdir) + else 'rbyd' if isinstance(child, Rbyd) + else 'bptr', + child.addr()), + '\x1b[m' if color else '')) + corrupted = True + + return not corrupted + +# check metadata blocks for errors +def dbg_ckmeta(lfs, **args): + return dbg_ck(lfs, meta=True, **args) + +# check metadata + data blocks for errors +def dbg_ckdata(lfs, **args): + return dbg_ck(lfs, meta=True, data=True, **args) + + +def main(disk, mroots=None, paths=None, *, + trunk=None, + block_size=None, + block_count=None, + quiet=False, + color='auto', + **args): + # figure out what color should be + if color == 'auto': + color = sys.stdout.isatty() + elif color == 'always': + color = True + else: + color = False + + # show files be default, but there's quite a few other things we + # can show if requested + show_config = args.get('config') + show_gstate = (args.get('gstate') + or args.get('gdelta') + or args.get('gstructs')) + show_files = (args.get('files') + or args.get('structs') + or args.get('attrs')) + show_ckmeta = args.get('ckmeta') + show_ckdata = args.get('ckdata') + + if (not show_config + and not show_gstate + and not show_files + and not show_ckmeta + and not show_ckdata): + show_files = True + + # is bd geometry specified? + if isinstance(block_size, tuple): + block_size, block_count_ = block_size + if block_count is None: + block_count = block_count_ + + # flatten mroots, default to 0x{0,1} + mroots = list(it.chain.from_iterable(mroots)) if mroots else [0, 1] + + # mroots may also encode trunks + mroots, trunk = ( + [block[0] if isinstance(block, tuple) + else block + for block in mroots], + trunk if trunk is not None + else ft.reduce( + lambda x, y: y, + (block[1] for block in mroots + if isinstance(block, tuple)), + None)) + + # we seek around a bunch, so just keep the disk open + with open(disk, 'rb') as f: + # if block_size is omitted, assume the block device is one big block + if block_size is None: + f.seek(0, os.SEEK_END) + block_size = f.tell() + + # fetch the filesystem + bd = Bd(f, block_size, block_count) + lfs = Lfs3.fetch(bd, mroots, trunk) + + # print some information about the filesystem + if not quiet: + print('littlefs%s v%s.%s %sx%s %s w%s.%s, ' + 'rev %08x, ' + 'cksum %08x%s' % ( + '' if lfs.ckmagic() else '?', + lfs.version.major if lfs.version is not None else '?', + lfs.version.minor if lfs.version is not None else '?', + lfs.block_size if lfs.block_size is not None else '?', + lfs.block_count if lfs.block_count is not None else '?', + lfs.addr(), + lfs.mbweightrepr(), lfs.mrweightrepr(), + lfs.rev, + lfs.cksum, + '' if lfs.ckgcksum() else '?')) + + # dynamically size the id field + w_width = max( + mt.ceil(mt.log10(max(1, lfs.mbweight >> lfs.mbits)+1)), + mt.ceil(mt.log10(max(1, max( + mdir.weight for mdir in lfs.mtree.mdirs())))+1), + # in case of -1.-1 + 2) + + # show the on-disk config? + if show_config and not quiet: + dbg_config(lfs, + color=color, + w_width=w_width, + **args) + + # show the on-disk gstate? + if show_gstate and not quiet: + dbg_gstate(lfs, + color=color, + w_width=w_width, + **args) + + # show the on-disk file tree? + if show_files and not quiet: + dbg_files(lfs, paths, + color=color, + w_width=w_width, + **args) + + # always check magic/gcksum + corrupted = not bool(lfs) + + # check metadata blocks for errors + if show_ckmeta and not show_ckdata: + if not dbg_ckmeta(lfs, + quiet=quiet, + color=color, + w_width=w_width, + **args): + corrupted = True + + # check metadata + data blocks for errors + if show_ckdata: + if not dbg_ckdata(lfs, + quiet=quiet, + color=color, + w_width=w_width, + **args): + corrupted = True + + # ckmeta/ckdata implies error_on_corrupt + if ((show_ckmeta or show_ckdata or args.get('error_on_corrupt')) + and corrupted): + sys.exit(2) + + +if __name__ == "__main__": + import argparse + import sys + parser = argparse.ArgumentParser( + description="Debug littlefs stuff.", + allow_abbrev=False) + parser.add_argument( + 'disk', + help="File containing the block device.") + class AppendMrootOrPath(argparse.Action): + def __call__(self, parser, namespace, values, option): + for value in values: + # mroot? + if not isinstance(value, str): + if getattr(namespace, 'mroots', None) is None: + namespace.mroots = [] + namespace.mroots.append(value) + # or path? + else: + if getattr(namespace, 'paths', None) is None: + namespace.paths = [] + namespace.paths.append(value) + parser.add_argument( + 'mroots', + nargs='*', + type=lambda x: rbydaddr(x) if not x.startswith('%') else x, + action=AppendMrootOrPath, + help="Block address of the mroots. Defaults to 0x{0,1}.") + parser.add_argument( + 'paths', + nargs='*', + type=lambda x: rbydaddr(x) if not x.startswith('%') else x, + action=AppendMrootOrPath, + help="Paths to show, must start with %% where %% indicates the " + "root littlefs directory. Defaults to the root littlefs " + "directory.") + parser.add_argument( + '--trunk', + type=lambda x: int(x, 0), + help="Use this offset as the trunk of the mroots.") + parser.add_argument( + '-b', '--block-size', + type=bdgeom, + help="Block size/geometry in bytes. Accepts x.") + parser.add_argument( + '--block-count', + type=lambda x: int(x, 0), + help="Block count in blocks.") + parser.add_argument( + '-q', '--quiet', + action='store_true', + help="Don't show anything, useful when checking for errors.") + parser.add_argument( + '--color', + choices=['never', 'always', 'auto'], + default='auto', + help="When to use terminal colors. Defaults to 'auto'.") + parser.add_argument( + '--config', + action='store_true', + help="Show the on-disk config.") + parser.add_argument( + '--gstate', + action='store_true', + help="Show the on-disk global-state.") + parser.add_argument( + '--gdelta', + action='store_true', + help="Show relevant gdeltas used to build the gstate. " + "Implies --gstate.") + parser.add_argument( + '--gstructs', + action='store_true', + help="Show gstate structures. Implies --gstate.") + parser.add_argument( + '--files', + action='store_true', + help="Show the file tree (the default).") + parser.add_argument( + '--structs', + action='store_true', + help="Show the internal structure of files. Implies --files.") + parser.add_argument( + '--attrs', + action='store_true', + help="Show custom attributes attached to files. Implies --files.") + parser.add_argument( + '--ckmeta', + action='store_true', + help="Check metadata blocks for errors.") + parser.add_argument( + '--ckdata', + action='store_true', + help="Check metadata + data blocks for errors.") + parser.add_argument( + '--mtree-only', + action='store_true', + help="Only traverse the mtree.") + parser.add_argument( + '-r', '--recurse', '--file-depth', + nargs='?', + type=lambda x: int(x, 0), + const=0, + help="Depth of the file tree to show. 0 shows all files in the " + "filesystem. Defaults to 1, which only shows the root " + "directory.") + parser.add_argument( + '-a', '--all', + action='store_true', + help="Show all files including bookmarks, stickynotes, grms, " + "etc.") + parser.add_argument( + '--no-orphans', + action='store_true', + help="Don't scan for orphaned files.") + parser.add_argument( + '-x', '--raw', + action='store_true', + help="Show the raw data including tag encodings.") + parser.add_argument( + '-T', '--no-truncate', + action='store_true', + help="Don't truncate, show the full contents.") + parser.add_argument( + '-R', '--tree', '--rbyd', '--tree-rbyd', + dest='tree_rbyd', + action='store_true', + help="Show the rbyd tree.") + parser.add_argument( + '-Y', '--rbyd-all', '--tree-rbyd-all', + dest='tree_rbyd_all', + action='store_true', + help="Show the full rbyd tree.") + parser.add_argument( + '-B', '--btree', '--tree-btree', + dest='tree_btree', + action='store_true', + help="Show a simplified btree tree.") + parser.add_argument( + '-i', '--inner', + action='store_true', + help="Show inner branches.") + parser.add_argument( + '-z', '--depth', '--tree-depth', + nargs='?', + type=lambda x: int(x, 0), + const=0, + help="Depth of trees to show. Defaults to 0, which shows full " + "trees.") + parser.add_argument( + '-e', '--error-on-corrupt', + action='store_true', + help="Error if the filesystem is corrupt.") + sys.exit(main(**{k: v + for k, v in vars(parser.parse_intermixed_args()).items() + if v is not None})) diff --git a/scripts/dbgmtree.py b/scripts/dbgmtree.py new file mode 100755 index 000000000..6d3c10aae --- /dev/null +++ b/scripts/dbgmtree.py @@ -0,0 +1,3171 @@ +#!/usr/bin/env python3 + +# prevent local imports +if __name__ == "__main__": + __import__('sys').path.pop(0) + +import bisect +import collections as co +import functools as ft +import itertools as it +import math as mt +import os +import struct + +try: + import crc32c as crc32c_lib +except ModuleNotFoundError: + crc32c_lib = None + + +TAG_NULL = 0x0000 ## v--- ---- +--- ---- +TAG_INTERNAL = 0x0000 ## v--- ---- +ttt tttt +TAG_CONFIG = 0x0100 ## v--- ---1 +ttt tttt +TAG_MAGIC = 0x0131 # v--- ---1 +-11 --rr +TAG_VERSION = 0x0134 # v--- ---1 +-11 -1-- +TAG_RCOMPAT = 0x0135 # v--- ---1 +-11 -1-1 +TAG_WCOMPAT = 0x0136 # v--- ---1 +-11 -11- +TAG_OCOMPAT = 0x0137 # v--- ---1 +-11 -111 +TAG_GEOMETRY = 0x0138 # v--- ---1 +-11 1--- +TAG_NAMELIMIT = 0x0139 # v--- ---1 +-11 1--1 +TAG_FILELIMIT = 0x013a # v--- ---1 +-11 1-1- +TAG_GDELTA = 0x0200 ## v--- --1- +ttt tttt +TAG_GRMDELTA = 0x0230 # v--- --1- +-11 --++ +TAG_GBMAPDELTA = 0x0234 # v--- --1- +-11 -1rr +TAG_NAME = 0x0300 ## v--- --11 +ttt tttt +TAG_BNAME = 0x0300 # v--- --11 +--- ---- +TAG_REG = 0x0301 # v--- --11 +--- ---1 +TAG_DIR = 0x0302 # v--- --11 +--- --1- +TAG_STICKYNOTE = 0x0303 # v--- --11 +--- --11 +TAG_BOOKMARK = 0x0304 # v--- --11 +--- -1-- +TAG_MNAME = 0x0330 # v--- --11 +-11 ---- +TAG_STRUCT = 0x0400 ## v--- -1-- +ttt tttt +TAG_BRANCH = 0x0400 # v--- -1-- +--- --rr +TAG_DATA = 0x0404 # v--- -1-- +--- -1rr +TAG_BLOCK = 0x0408 # v--- -1-- +--- 1err +TAG_DID = 0x0420 # v--- -1-- +-1- ---- +TAG_BSHRUB = 0x0428 # v--- -1-- +-1- 1-rr +TAG_BTREE = 0x042c # v--- -1-- +-1- 11rr +TAG_MROOT = 0x0431 # v--- -1-- +-11 --rr +TAG_MDIR = 0x0435 # v--- -1-- +-11 -1rr +TAG_MTREE = 0x043c # v--- -1-- +-11 11rr +TAG_BMRANGE = 0x0440 # v--- -1-- +1-- ++uu +TAG_BMFREE = 0x0440 # v--- -1-- +1-- ---- +TAG_BMINUSE = 0x0441 # v--- -1-- +1-- ---1 +TAG_BMERASED = 0x0442 # v--- -1-- +1-- --1- +TAG_BMBAD = 0x0443 # v--- -1-- +1-- --11 +TAG_ATTR = 0x0600 ## v--- -11a +aaa aaaa +TAG_UATTR = 0x0600 # v--- -11- +aaa aaaa +TAG_SATTR = 0x0700 # v--- -111 +aaa aaaa +TAG_SHRUB = 0x1000 ## v--1 kkkk +kkk kkkk +TAG_ALT = 0x4000 ## v1cd kkkk +kkk kkkk +TAG_B = 0x0000 +TAG_R = 0x2000 +TAG_LE = 0x0000 +TAG_GT = 0x1000 +TAG_CKSUM = 0x3000 ## v-11 ---- ++++ +pqq +TAG_PHASE = 0x0003 +TAG_PERTURB = 0x0004 +TAG_NOTE = 0x3100 ## v-11 ---1 ++++ ++++ +TAG_ECKSUM = 0x3200 ## v-11 --1- ++++ ++++ +TAG_GCKSUMDELTA = 0x3300 ## v-11 --11 ++++ ++++ + + +# self-parsing tag repr +class Tag: + def __init__(self, name, tag, encoding, help): + self.name = name + self.tag = tag + self.encoding = encoding + self.help = help + # derive mask from encoding + self.mask = sum( + (1 if x in 'v-01' else 0) << len(self.encoding)-1-i + for i, x in enumerate(self.encoding)) + + def __repr__(self): + return 'Tag(%r, %r, %r)' % ( + self.name, + self.tag, + self.encoding) + + def __eq__(self, other): + return self.name == other.name + + def __ne__(self, other): + return self.name != other.name + + def __hash__(self): + return hash(self.name) + + def line(self): + # substitute mask chars when zero + tag = '0x%s' % ''.join( + n if n != '0' else next( + (x for x in self.encoding[i*4:i*4+4] + if x not in 'v-01+'), + '0') + for i, n in enumerate('%04x' % self.tag)) + # group into nibbles + encoding = ' '.join(self.encoding[i*4:i*4+4] + for i in range(len(self.encoding)//4)) + return ('LFS3_%s' % self.name, tag, encoding) + + def specificity(self): + return sum(1 for x in self.encoding if x in 'v-01') + + def matches(self, tag): + return (tag & self.mask) == (self.tag & self.mask) + + def get(self, chars, tag): + return sum( + tag & ((1 if x in chars else 0) << len(self.encoding)-1-i) + for i, x in enumerate(self.encoding)) + + def max(self, chars): + return max(len(self.encoding)-1-i + for i, x in enumerate(self.encoding) if x in chars) + + def min(self, chars): + return min(len(self.encoding)-1-i + for i, x in enumerate(self.encoding) if x in chars) + + def width(self, chars): + return self.max(chars) - self.min(chars) + + def __contains__(self, chars): + return any(x in self.encoding for x in chars) + + @staticmethod + @ft.cache + def tags(): + # parse our script's source to figure out tags + import inspect + import re + tags = [] + tag_pattern = re.compile( + '^(?PTAG_[^ ]*) *= *(?P[^#]*?) *' + '#+ *(?P(?:[^ ] *?){16}) *(?P.*)$') + for line in (inspect.getsource( + inspect.getmodule(inspect.currentframe())) + .replace('\\\n', '') + .splitlines()): + m = tag_pattern.match(line) + if m: + tags.append(Tag( + m.group('name'), + globals()[m.group('name')], + m.group('encoding').replace(' ', ''), + m.group('help'))) + return tags + + # find best matching tag + @staticmethod + def find(tag): + # find tags, note this is cached + tags__ = Tag.tags() + + # find the most specific matching tag, ignoring valid bits + return max((t for t in tags__ if t.matches(tag & 0x7fff)), + key=lambda t: t.specificity(), + default=None) + + # human readable tag repr + @staticmethod + def repr(tag, weight=None, size=None, *, + global_=False, + toff=None): + # find the most specific matching tag, ignoring the shrub bit + t = Tag.find(tag & ~(TAG_SHRUB if tag & 0x7000 == TAG_SHRUB else 0)) + + # build repr + r = [] + # normal tag? + if not tag & TAG_ALT: + if t is not None: + # prefix shrub tags with shrub + if tag & 0x7000 == TAG_SHRUB: + r.append('shrub') + # lowercase name + r.append(t.name.split('_', 1)[1].lower()) + # gstate tag? + if global_: + if r[-1] == 'gdelta': + r[-1] = 'gstate' + elif r[-1].endswith('delta'): + r[-1] = r[-1][:-len('delta')] + # include perturb/phase bits + if 'q' in t: + r.append('q%d' % t.get('q', tag)) + if 'p' in t and tag & TAG_PERTURB: + r.append('p') + + # include unmatched fields, but not just redund, and + # only reserved bits if non-zero + if 'tua' in t or ('+' in t and t.get('+', tag) != 0): + r.append(' 0x%0*x' % ( + (t.width('tuar+')+4-1)//4, + t.get('tuar+', tag))) + # unknown tag? + else: + r.append('0x%04x' % tag) + + # weight? + if weight: + r.append(' w%d' % weight) + # size? don't include if null + if size is not None and (size or tag & 0x7fff): + r.append(' %d' % size) + + # alt pointer? + else: + r.append('alt') + r.append('r' if tag & TAG_R else 'b') + r.append('gt' if tag & TAG_GT else 'le') + r.append(' 0x%0*x' % ( + (t.width('k')+4-1)//4, + t.get('k', tag))) + + # weight? + if weight is not None: + r.append(' w%d' % weight) + # jump? + if size and toff is not None: + r.append(' 0x%x' % (0xffffffff & (toff-size))) + elif size: + r.append(' -%d' % size) + + return ''.join(r) + + +# some ways of block geometry representations +# 512 -> 512 +# 512x16 -> (512, 16) +# 0x200x10 -> (512, 16) +def bdgeom(s): + s = s.strip() + b = 10 + if s.startswith('0x') or s.startswith('0X'): + s = s[2:] + b = 16 + elif s.startswith('0o') or s.startswith('0O'): + s = s[2:] + b = 8 + elif s.startswith('0b') or s.startswith('0B'): + s = s[2:] + b = 2 + + if 'x' in s: + s, s_ = s.split('x', 1) + return (int(s, b), int(s_, b)) + else: + return int(s, b) + +# parse some rbyd addr encodings +# 0xa -> (0xa,) +# 0xa.c -> ((0xa, 0xc),) +# 0x{a,b} -> (0xa, 0xb) +# 0x{a,b}.c -> ((0xa, 0xc), (0xb, 0xc)) +def rbydaddr(s): + s = s.strip() + b = 10 + if s.startswith('0x') or s.startswith('0X'): + s = s[2:] + b = 16 + elif s.startswith('0o') or s.startswith('0O'): + s = s[2:] + b = 8 + elif s.startswith('0b') or s.startswith('0B'): + s = s[2:] + b = 2 + + trunk = None + if '.' in s: + s, s_ = s.split('.', 1) + trunk = int(s_, b) + + if s.startswith('{') and '}' in s: + ss = s[1:s.find('}')].split(',') + else: + ss = [s] + + addr = [] + for s in ss: + if trunk is not None: + addr.append((int(s, b), trunk)) + else: + addr.append(int(s, b)) + + return tuple(addr) + +def crc32c(data, crc=0): + if crc32c_lib is not None: + return crc32c_lib.crc32c(data, crc) + else: + crc ^= 0xffffffff + for b in data: + crc ^= b + for j in range(8): + crc = (crc >> 1) ^ ((crc & 1) * 0x82f63b78) + return 0xffffffff ^ crc + +def popc(x): + return bin(x).count('1') + +def parity(x): + return popc(x) & 1 + +def fromle32(data, j=0): + return struct.unpack('H', data[j:j+2].ljust(2, b'\0'))[0]; d += 2 + weight, d_ = fromleb128(data, j+d); d += d_ + size, d_ = fromleb128(data, j+d); d += d_ + return tag>>15, tag&0x7fff, weight, size, d + +def frombranch(data, j=0): + d = 0 + block, d_ = fromleb128(data, j+d); d += d_ + trunk, d_ = fromleb128(data, j+d); d += d_ + cksum = fromle32(data, j+d); d += 4 + return block, trunk, cksum, d + +def frombtree(data, j=0): + d = 0 + w, d_ = fromleb128(data, j+d); d += d_ + block, trunk, cksum, d_ = frombranch(data, j+d); d += d_ + return w, block, trunk, cksum, d + +def frommdir(data, j=0): + blocks = [] + d = 0 + while j+d < len(data): + block, d_ = fromleb128(data, j+d) + blocks.append(block) + d += d_ + return tuple(blocks), d + +def xxd(data, width=16): + for i in range(0, len(data), width): + yield '%-*s %-*s' % ( + 3*width, + ' '.join('%02x' % b for b in data[i:i+width]), + width, + ''.join( + b if b >= ' ' and b <= '~' else '.' + for b in map(chr, data[i:i+width]))) + +# compute the difference between two paths, returning everything +# in a after the paths diverge, as well as the relevant index +def pathdelta(a, b): + if not isinstance(a, list): + a = list(a) + i = 0 + for a_, b_ in zip(a, b): + try: + if type(a_) == type(b_) and a_ == b_: + i += 1 + else: + break + # treat exceptions here as failure to match, most likely + # the compared types are incompatible, it's the caller's + # problem + except Exception: + break + + return [(i+j, a_) for j, a_ in enumerate(a[i:])] + + +# a simple wrapper over an open file with bd geometry +class Bd: + def __init__(self, f, block_size=None, block_count=None): + self.f = f + self.block_size = block_size + self.block_count = block_count + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, self.repr()) + + def repr(self): + return 'bd %sx%s' % (self.block_size, self.block_count) + + def read(self, block, off, size): + self.f.seek(block*self.block_size + off) + return self.f.read(size) + + def readblock(self, block): + self.f.seek(block*self.block_size) + return self.f.read(self.block_size) + +# tagged data in an rbyd +class Rattr: + def __init__(self, tag, weight, blocks, toff, tdata, data): + self.tag = tag + self.weight = weight + if isinstance(blocks, int): + self.blocks = (blocks,) + else: + self.blocks = blocks + self.toff = toff + self.tdata = tdata + self.data = data + + @property + def block(self): + return self.blocks[0] + + @property + def tsize(self): + return len(self.tdata) + + @property + def off(self): + return self.toff + len(self.tdata) + + @property + def size(self): + return len(self.data) + + def __bytes__(self): + return self.data + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, self.repr()) + + def repr(self): + return Tag.repr(self.tag, self.weight, self.size) + + def __iter__(self): + return iter((self.tag, self.weight, self.data)) + + def __eq__(self, other): + return ((self.tag, self.weight, self.data) + == (other.tag, other.weight, other.data)) + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash((self.tag, self.weight, self.data)) + + # convenience for did/name access + def _parse_name(self): + # note we return a null name for non-name tags, this is so + # vestigial names in btree nodes act as a catch-all + if (self.tag & 0xff00) != TAG_NAME: + did = 0 + name = b'' + else: + did, d = fromleb128(self.data) + name = self.data[d:] + + # cache both + self.did = did + self.name = name + + @ft.cached_property + def did(self): + self._parse_name() + return self.did + + @ft.cached_property + def name(self): + self._parse_name() + return self.name + +class Ralt: + def __init__(self, tag, weight, blocks, toff, tdata, jump, + color=None, followed=None): + self.tag = tag + self.weight = weight + if isinstance(blocks, int): + self.blocks = (blocks,) + else: + self.blocks = blocks + self.toff = toff + self.tdata = tdata + self.jump = jump + + if color is not None: + self.color = color + else: + self.color = 'r' if tag & TAG_R else 'b' + self.followed = followed + + @property + def block(self): + return self.blocks[0] + + @property + def tsize(self): + return len(self.tdata) + + @property + def off(self): + return self.toff + len(self.tdata) + + @property + def joff(self): + return self.toff - self.jump + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, self.repr()) + + def repr(self): + return Tag.repr(self.tag, self.weight, self.jump, toff=self.toff) + + def __iter__(self): + return iter((self.tag, self.weight, self.jump)) + + def __eq__(self, other): + return ((self.tag, self.weight, self.jump) + == (other.tag, other.weight, other.jump)) + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash((self.tag, self.weight, self.jump)) + + +# our core rbyd type +class Rbyd: + def __init__(self, blocks, trunk, weight, rev, eoff, cksum, data, *, + shrub=False, + gcksumdelta=None, + redund=0): + if isinstance(blocks, int): + self.blocks = (blocks,) + else: + self.blocks = blocks + self.trunk = trunk + self.weight = weight + self.rev = rev + self.eoff = eoff + self.cksum = cksum + self.data = data + + self.shrub = shrub + self.gcksumdelta = gcksumdelta + self.redund = redund + + @property + def block(self): + return self.blocks[0] + + @property + def corrupt(self): + # use redund=-1 to indicate corrupt rbyds + return self.redund >= 0 + + def addr(self): + if len(self.blocks) == 1: + return '0x%x.%x' % (self.block, self.trunk) + else: + return '0x{%s}.%x' % ( + ','.join('%x' % block for block in self.blocks), + self.trunk) + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, self.repr()) + + def repr(self): + return 'rbyd %s w%s' % (self.addr(), self.weight) + + def __bool__(self): + # use redund=-1 to indicate corrupt rbyds + return self.redund >= 0 + + def __eq__(self, other): + return ((frozenset(self.blocks), self.trunk) + == (frozenset(other.blocks), other.trunk)) + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash((frozenset(self.blocks), self.trunk)) + + @classmethod + def _fetch(cls, data, block, trunk=None): + # fetch the rbyd + rev = fromle32(data, 0) + cksum = 0 + cksum_ = crc32c(data[0:4]) + cksum__ = cksum_ + perturb = False + eoff = 0 + eoff_ = None + j_ = 4 + trunk_ = 0 + trunk__ = 0 + trunk___ = 0 + weight = 0 + weight_ = 0 + weight__ = 0 + gcksumdelta = None + gcksumdelta_ = None + while j_ < len(data) and (not trunk or eoff <= trunk): + # read next tag + v, tag, w, size, d = fromtag(data, j_) + if v != parity(cksum__): + break + cksum__ ^= 0x00000080 if v else 0 + cksum__ = crc32c(data[j_:j_+d], cksum__) + j_ += d + if not tag & TAG_ALT and j_ + size > len(data): + break + + # take care of cksums + if not tag & TAG_ALT: + if (tag & 0xff00) != TAG_CKSUM: + cksum__ = crc32c(data[j_:j_+size], cksum__) + + # found a gcksumdelta? + if (tag & 0xff00) == TAG_GCKSUMDELTA: + gcksumdelta_ = Rattr(tag, w, block, j_-d, + data[j_-d:j_], + data[j_:j_+size]) + + # found a cksum? + else: + # check cksum + cksum___ = fromle32(data, j_) + if cksum__ != cksum___: + break + # commit what we have + eoff = eoff_ if eoff_ else j_ + size + cksum = cksum_ + trunk_ = trunk__ + weight = weight_ + gcksumdelta = gcksumdelta_ + gcksumdelta_ = None + # update perturb bit + perturb = bool(tag & TAG_PERTURB) + # revert to data cksum and perturb + cksum__ = cksum_ ^ (0xfca42daf if perturb else 0) + + # evaluate trunks + if (tag & 0xf000) != TAG_CKSUM: + if not (trunk and j_-d > trunk and not trunk___): + # new trunk? + if not trunk___: + trunk___ = j_-d + weight__ = 0 + + # keep track of weight + weight__ += w + + # end of trunk? + if not tag & TAG_ALT: + # update trunk/weight unless we found a shrub or an + # explicit trunk (which may be a shrub) is requested + if not tag & TAG_SHRUB or trunk___ == trunk: + trunk__ = trunk___ + weight_ = weight__ + # keep track of eoff for best matching trunk + if trunk and j_ + size > trunk: + eoff_ = j_ + size + eoff = eoff_ + cksum = cksum__ ^ ( + 0xfca42daf if perturb else 0) + trunk_ = trunk__ + weight = weight_ + gcksumdelta = gcksumdelta_ + trunk___ = 0 + + # update canonical checksum, xoring out any perturb state + cksum_ = cksum__ ^ (0xfca42daf if perturb else 0) + + if not tag & TAG_ALT: + j_ += size + + return cls(block, trunk_, weight, rev, eoff, cksum, data, + gcksumdelta=gcksumdelta, + redund=0 if trunk_ else -1) + + @classmethod + def fetch(cls, bd, blocks, trunk=None): + # multiple blocks? + if not isinstance(blocks, int): + # fetch all blocks + rbyds = [cls.fetch(bd, block, trunk) for block in blocks] + + # determine most recent revision/trunk + rev, trunk = None, None + for rbyd in rbyds: + # compare with sequence arithmetic + if rbyd and ( + rev is None + or not ((rbyd.rev - rev) & 0x80000000) + or (rbyd.rev == rev and rbyd.trunk > trunk)): + rev, trunk = rbyd.rev, rbyd.trunk + # sort for reproducibility + rbyds.sort(key=lambda rbyd: ( + # prioritize valid redund blocks + 0 if rbyd and rbyd.rev == rev and rbyd.trunk == trunk + else 1, + # default to sorting by block + rbyd.block)) + + # choose an active rbyd + rbyd = rbyds[0] + # keep track of the other blocks + rbyd.blocks = tuple(rbyd.block for rbyd in rbyds) + # keep track of how many redund blocks are valid + rbyd.redund = -1 + sum(1 for rbyd in rbyds + if rbyd and rbyd.rev == rev and rbyd.trunk == trunk) + # and patch the gcksumdelta if we have one + if rbyd.gcksumdelta is not None: + rbyd.gcksumdelta.blocks = rbyd.blocks + return rbyd + + # seek/read the block + block = blocks + data = bd.readblock(block) + + # fetch the rbyd + return cls._fetch(data, block, trunk) + + @classmethod + def fetchck(cls, bd, blocks, trunk, weight, cksum): + # try to fetch the rbyd normally + rbyd = cls.fetch(bd, blocks, trunk) + + # cksum mismatch? trunk/weight mismatch? + if (rbyd.cksum != cksum + or rbyd.trunk != trunk + or rbyd.weight != weight): + # mark as corrupt and keep track of expected trunk/weight + rbyd.redund = -1 + rbyd.trunk = trunk + rbyd.weight = weight + + return rbyd + + @classmethod + def fetchshrub(cls, rbyd, trunk): + # steal the original rbyd's data + # + # this helps avoid race conditions with cksums and stuff + shrub = cls._fetch(rbyd.data, rbyd.block, trunk) + shrub.blocks = rbyd.blocks + shrub.shrub = True + return shrub + + def lookupnext(self, rid, tag=None, *, + path=False): + if not self or rid >= self.weight: + if path: + return None, None, [] + else: + return None, None + + tag = max(tag or 0, 0x1) + lower = 0 + upper = self.weight + path_ = [] + + # descend down tree + j = self.trunk + while True: + _, alt, w, jump, d = fromtag(self.data, j) + + # found an alt? + if alt & TAG_ALT: + # follow? + if ((rid, tag & 0xfff) > (upper-w-1, alt & 0xfff) + if alt & TAG_GT + else ((rid, tag & 0xfff) + <= (lower+w-1, alt & 0xfff))): + lower += upper-lower-w if alt & TAG_GT else 0 + upper -= upper-lower-w if not alt & TAG_GT else 0 + j = j - jump + + if path: + # figure out which color + if alt & TAG_R: + _, nalt, _, _, _ = fromtag(self.data, j+jump+d) + if nalt & TAG_R: + color = 'y' + else: + color = 'r' + else: + color = 'b' + + path_.append(Ralt( + alt, w, self.blocks, j+jump, + self.data[j+jump:j+jump+d], jump, + color=color, + followed=True)) + + # stay on path + else: + lower += w if not alt & TAG_GT else 0 + upper -= w if alt & TAG_GT else 0 + j = j + d + + if path: + # figure out which color + if alt & TAG_R: + _, nalt, _, _, _ = fromtag(self.data, j) + if nalt & TAG_R: + color = 'y' + else: + color = 'r' + else: + color = 'b' + + path_.append(Ralt( + alt, w, self.blocks, j-d, + self.data[j-d:j], jump, + color=color, + followed=False)) + + # found tag + else: + rid_ = upper-1 + tag_ = alt + w_ = upper-lower + + if not tag_ or (rid_, tag_) < (rid, tag): + if path: + return None, None, path_ + else: + return None, None + + rattr_ = Rattr(tag_, w_, self.blocks, j, + self.data[j:j+d], + self.data[j+d:j+d+jump]) + if path: + return rid_, rattr_, path_ + else: + return rid_, rattr_ + + def lookup(self, rid, tag=None, mask=None, *, + path=False): + if tag is None: + tag, mask = 0, 0xffff + if mask is None: + mask = 0 + + r = self.lookupnext(rid, tag & ~mask, + path=path) + if path: + rid_, rattr_, path_ = r + else: + rid_, rattr_ = r + if (rid_ is None + or rid_ != rid + or (rattr_.tag & ~mask & 0xfff) + != (tag & ~mask & 0xfff)): + if path: + return None, path_ + else: + return None + + if path: + return rattr_, path_ + else: + return rattr_ + + def rids(self, *, + path=False): + rid = -1 + while True: + r = self.lookupnext(rid, + path=path) + if path: + rid, name, path_ = r + else: + rid, name = r + # found end of tree? + if rid is None: + break + + if path: + yield rid, name, path_ + else: + yield rid, name + rid += 1 + + def rattrs(self, rid=None, tag=None, mask=None, *, + path=False): + if rid is None: + rid, tag = -1, 0 + while True: + r = self.lookupnext(rid, tag+0x1, + path=path) + if path: + rid, rattr, path_ = r + else: + rid, rattr = r + # found end of tree? + if rid is None: + break + + if path: + yield rid, rattr, path_ + else: + yield rid, rattr + tag = rattr.tag + else: + if tag is None: + tag, mask = 0, 0xffff + if mask is None: + mask = 0 + + tag_ = max((tag & ~mask) - 1, 0) + while True: + r = self.lookupnext(rid, tag_+0x1, + path=path) + if path: + rid_, rattr_, path_ = r + else: + rid_, rattr_ = r + # found end of tree? + if (rid_ is None + or rid_ != rid + or (rattr_.tag & ~mask & 0xfff) + != (tag & ~mask & 0xfff)): + break + + if path: + yield rattr_, path_ + else: + yield rattr_ + tag_ = rattr_.tag + + # lookup by name + def namelookup(self, did, name): + # binary search + best = None, None + lower = 0 + upper = self.weight + while lower < upper: + rid, name_ = self.lookupnext( + lower + (upper-1-lower)//2) + if rid is None: + break + + # bisect search space + if (name_.did, name_.name) > (did, name): + upper = rid-(name_.weight-1) + elif (name_.did, name_.name) < (did, name): + lower = rid + 1 + # keep track of best match + best = rid, name_ + else: + # found a match + return rid, name_ + + return best + + +# our rbyd btree type +class Btree: + def __init__(self, bd, rbyd): + self.bd = bd + self.rbyd = rbyd + + @property + def block(self): + return self.rbyd.block + + @property + def blocks(self): + return self.rbyd.blocks + + @property + def trunk(self): + return self.rbyd.trunk + + @property + def weight(self): + return self.rbyd.weight + + @property + def rev(self): + return self.rbyd.rev + + @property + def cksum(self): + return self.rbyd.cksum + + @property + def shrub(self): + return self.rbyd.shrub + + def addr(self): + return self.rbyd.addr() + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, self.repr()) + + def repr(self): + return 'btree %s w%s' % (self.addr(), self.weight) + + def __eq__(self, other): + return self.rbyd == other.rbyd + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash(self.rbyd) + + @classmethod + def fetch(cls, bd, blocks, trunk=None): + # rbyd fetch does most of the work here + rbyd = Rbyd.fetch(bd, blocks, trunk) + return cls(bd, rbyd) + + @classmethod + def fetchck(cls, bd, blocks, trunk, weight, cksum): + # rbyd fetchck does most of the work here + rbyd = Rbyd.fetchck(bd, blocks, trunk, weight, cksum) + return cls(bd, rbyd) + + @classmethod + def fetchshrub(cls, bd, rbyd, trunk): + shrub = Rbyd.fetchshrub(rbyd, trunk) + return cls(bd, shrub) + + def lookupnext_(self, bid, *, + path=False, + depth=None): + if not self or bid >= self.weight: + if path: + return None, None, None, None, [] + else: + return None, None, None, None + + rbyd = self.rbyd + rid = bid + depth_ = 1 + path_ = [] + + while True: + # corrupt branch? + if not rbyd: + if path: + return bid, rbyd, rid, None, path_ + else: + return bid, rbyd, rid, None + + # first tag indicates the branch's weight + rid_, name_ = rbyd.lookupnext(rid) + if rid_ is None: + if path: + return None, None, None, None, path_ + else: + return None, None, None, None + + # keep track of path + if path: + path_.append((bid + (rid_-rid), rbyd, rid_, name_)) + + # find branch tag if there is one + branch_ = rbyd.lookup(rid_, TAG_BRANCH, 0x3) + + # descend down branch? + if branch_ is not None and ( + not depth or depth_ < depth): + block, trunk, cksum, _ = frombranch(branch_.data) + rbyd = Rbyd.fetchck(self.bd, block, trunk, name_.weight, + cksum) + + rid -= (rid_-(name_.weight-1)) + depth_ += 1 + + else: + if path: + return bid + (rid_-rid), rbyd, rid_, name_, path_ + else: + return bid + (rid_-rid), rbyd, rid_, name_ + + # the non-leaf variants discard the rbyd info, these can be a bit + # more convenient, but at a performance cost + def lookupnext(self, bid, *, + path=False, + depth=None): + # just discard the rbyd info + r = self.lookupnext_(bid, + path=path, + depth=depth) + if path: + bid, rbyd, rid, name, path_ = r + else: + bid, rbyd, rid, name = r + + if path: + return bid, name, path_ + else: + return bid, name + + def lookup(self, bid, tag=None, mask=None, *, + path=False, + depth=None): + # lookup rbyd in btree + # + # note this function expects bid to be known, use lookupnext + # first if you don't care about the exact bid (or better yet, + # lookupnext_ and call lookup on the returned rbyd) + # + # this matches rbyd's lookup behavior, which needs a known rid + # to avoid a double lookup + r = self.lookupnext_(bid, + path=path, + depth=depth) + if path: + bid_, rbyd_, rid_, name_, path_ = r + else: + bid_, rbyd_, rid_, name_ = r + if bid_ is None or bid_ != bid: + if path: + return None, path_ + else: + return None + + # lookup tag in rbyd + rattr_ = rbyd_.lookup(rid_, tag, mask) + if rattr_ is None: + if path: + return None, path_ + else: + return None + + if path: + return rattr_, path_ + else: + return rattr_ + + # note leaves only iterates over leaf rbyds, whereas traverse + # traverses all rbyds + def leaves(self, *, + path=False, + depth=None): + # include our root rbyd even if the weight is zero + if self.weight == 0: + if path: + yield -1, self.rbyd, [] + else: + yield -1, self.rbyd + return + + bid = 0 + while True: + r = self.lookupnext_(bid, + path=path, + depth=depth) + if r: + bid, rbyd, rid, name, path_ = r + else: + bid, rbyd, rid, name = r + if bid is None: + break + + if path: + yield (bid-rid + (rbyd.weight-1), rbyd, + # path tail is usually redundant unless corrupt + path_[:-1] + if path_ and path_[-1][1] == rbyd + else path_) + else: + yield bid-rid + (rbyd.weight-1), rbyd + bid += rbyd.weight - rid + 1 + + def traverse(self, *, + path=False, + depth=None): + ptrunk_ = [] + for bid, rbyd, path_ in self.leaves( + path=True, + depth=depth): + # we only care about the rbyds here + trunk_ = ([(bid_-rid_ + (rbyd_.weight-1), rbyd_) + for bid_, rbyd_, rid_, name_ in path_] + + [(bid, rbyd)]) + for d, (bid_, rbyd_) in pathdelta( + trunk_, ptrunk_): + # but include branch rids in the path if requested + if path: + yield bid_, rbyd_, path_[:d] + else: + yield bid_, rbyd_ + ptrunk_ = trunk_ + + # note bids/rattrs do _not_ include corrupt btree nodes! + def bids(self, *, + leaves=False, + path=False, + depth=None): + for r in self.leaves( + path=path, + depth=depth): + if path: + bid, rbyd, path_ = r + else: + bid, rbyd = r + for rid, name in rbyd.rids(): + bid_ = bid-(rbyd.weight-1) + rid + if leaves: + if path: + yield (bid_, rbyd, rid, name, + path_+[(bid_, rbyd, rid, name)]) + else: + yield bid_, rbyd, rid, name + else: + if path: + yield (bid_, name, + path_+[(bid_, rbyd, rid, name)]) + else: + yield bid_, name + + def rattrs(self, bid=None, tag=None, mask=None, *, + leaves=False, + path=False, + depth=None): + if bid is None: + for r in self.leaves( + path=path, + depth=depth): + if path: + bid, rbyd, path_ = r + else: + bid, rbyd = r + for rid, name in rbyd.rids(): + bid_ = bid-(rbyd.weight-1) + rid + for rattr in rbyd.rattrs(rid): + if leaves: + if path: + yield (bid_, rbyd, rid, rattr, + path_+[(bid_, rbyd, rid, name)]) + else: + yield bid_, rbyd, rid, rattr + else: + if path: + yield (bid_, rattr, + path_+[(bid_, rbyd, rid, name)]) + else: + yield bid_, rattr + else: + r = self.lookupnext_(bid, + path=path, + depth=depth) + if path: + bid, rbyd, rid, name, path_ = r + else: + bid, rbyd, rid, name = r + if bid is None: + return + + for rattr in rbyd.rattrs(rid, tag, mask): + if leaves: + if path: + yield rbyd, rid, rattr, path_ + else: + yield rbyd, rid, rattr + else: + if path: + yield rattr, path_ + else: + yield rattr + + # lookup by name + def namelookup_(self, did, name, *, + path=False, + depth=None): + rbyd = self.rbyd + bid = 0 + depth_ = 1 + path_ = [] + + while True: + # corrupt branch? + if not rbyd: + bid_ = bid+(rbyd.weight-1) + if path: + return bid_, rbyd, rbyd.weight-1, None, path_ + else: + return bid_, rbyd, rbyd.weight-1, None + + rid_, name_ = rbyd.namelookup(did, name) + + # keep track of path + if path: + path_.append((bid + rid_, rbyd, rid_, name_)) + + # find branch tag if there is one + branch_ = rbyd.lookup(rid_, TAG_BRANCH, 0x3) + + # found another branch + if branch_ is not None and ( + not depth or depth_ < depth): + block, trunk, cksum, _ = frombranch(branch_.data) + rbyd = Rbyd.fetchck(self.bd, block, trunk, name_.weight, + cksum) + + # update our bid + bid += rid_ - (name_.weight-1) + depth_ += 1 + + # found best match + else: + if path: + return bid + rid_, rbyd, rid_, name_, path_ + else: + return bid + rid_, rbyd, rid_, name_ + + def namelookup(self, bid, *, + path=False, + depth=None): + # just discard the rbyd info + r = self.namelookup_(did, name, + path=path, + depth=depth) + if path: + bid, rbyd, rid, name, path_ = r + else: + bid, rbyd, rid, name = r + + if path: + return bid, name, path_ + else: + return bid, name + + +# a metadata id, this includes mbits for convenience +class Mid: + def __init__(self, mbid, mrid=None, *, + mbits=None): + # we need one of these to figure out mbits + if mbits is not None: + self.mbits = mbits + elif isinstance(mbid, Mid): + self.mbits = mbid.mbits + else: + assert mbits is not None, "mbits?" + + # accept other mids which can be useful for changing mrids + if isinstance(mbid, Mid): + mbid = mbid.mbid + + # accept either merged mid or separate mbid+mrid + if mrid is None: + mid = mbid + mbid = mid | ((1 << self.mbits) - 1) + mrid = mid & ((1 << self.mbits) - 1) + + # map mrid=-1 + if mrid == ((1 << self.mbits) - 1): + mrid = -1 + + self.mbid = mbid + self.mrid = mrid + + @property + def mid(self): + return ((self.mbid & ~((1 << self.mbits) - 1)) + | (self.mrid & ((1 << self.mbits) - 1))) + + def mbidrepr(self): + return str(self.mbid >> self.mbits) + + def mridrepr(self): + return str(self.mrid) + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, self.repr()) + + def repr(self): + return '%s.%s' % (self.mbidrepr(), self.mridrepr()) + + def __iter__(self): + return iter((self.mbid, self.mrid)) + + # note this is slightly different from mid order when mrid=-1 + def __eq__(self, other): + if isinstance(other, Mid): + return (self.mbid, self.mrid) == (other.mbid, other.mrid) + else: + return self.mid == other + + def __ne__(self, other): + if isinstance(other, Mid): + return (self.mbid, self.mrid) != (other.mbid, other.mrid) + else: + return self.mid != other + + def __hash__(self): + return hash((self.mbid, self.mrid)) + + def __lt__(self, other): + return (self.mbid, self.mrid) < (other.mbid, other.mrid) + + def __le__(self, other): + return (self.mbid, self.mrid) <= (other.mbid, other.mrid) + + def __gt__(self, other): + return (self.mbid, self.mrid) > (other.mbid, other.mrid) + + def __ge__(self, other): + return (self.mbid, self.mrid) >= (other.mbid, other.mrid) + +# mdirs, the gooey atomic center of littlefs +# +# really the only difference between this and our rbyd class is the +# implicit mbid associated with the mdir +class Mdir: + def __init__(self, mid, rbyd, *, + mbits=None): + # we need one of these to figure out mbits + if mbits is not None: + self.mbits = mbits + elif isinstance(mid, Mid): + self.mbits = mid.mbits + elif isinstance(rbyd, Mdir): + self.mbits = rbyd.mbits + else: + assert mbits is not None, "mbits?" + + # strip mrid, bugs will happen if caller relies on mrid here + self.mid = Mid(mid, -1, mbits=self.mbits) + + # accept either another mdir or rbyd + if isinstance(rbyd, Mdir): + self.rbyd = rbyd.rbyd + else: + self.rbyd = rbyd + + @property + def data(self): + return self.rbyd.data + + @property + def block(self): + return self.rbyd.block + + @property + def blocks(self): + return self.rbyd.blocks + + @property + def trunk(self): + return self.rbyd.trunk + + @property + def weight(self): + return self.rbyd.weight + + @property + def rev(self): + return self.rbyd.rev + + @property + def eoff(self): + return self.rbyd.eoff + + @property + def cksum(self): + return self.rbyd.cksum + + @property + def gcksumdelta(self): + return self.rbyd.gcksumdelta + + @property + def corrupt(self): + return self.rbyd.corrupt + + @property + def redund(self): + return self.rbyd.redund + + def addr(self): + if len(self.blocks) == 1: + return '0x%x' % self.block + else: + return '0x{%s}' % ( + ','.join('%x' % block for block in self.blocks)) + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, self.repr()) + + def repr(self): + return 'mdir %s %s w%s' % ( + self.mid.mbidrepr(), + self.addr(), + self.weight) + + def __bool__(self): + return bool(self.rbyd) + + # we _don't_ care about mid for equality, or trunk even + def __eq__(self, other): + return frozenset(self.blocks) == frozenset(other.blocks) + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash(frozenset(self.blocks)) + + @classmethod + def fetch(cls, bd, mid, blocks, trunk=None): + rbyd = Rbyd.fetch(bd, blocks, trunk) + return cls(mid, rbyd, mbits=Mtree.mbits_(bd)) + + def lookupnext(self, mid, tag=None, *, + path=False): + # this is similar to rbyd lookupnext, we just error if + # lookupnext changes mids + if not isinstance(mid, Mid): + mid = Mid(mid, mbits=self.mbits) + r = self.rbyd.lookupnext(mid.mrid, tag, + path=path) + if path: + rid, rattr, path_ = r + else: + rid, rattr = r + + if rid != mid.mrid: + if path: + return None, path_ + else: + return None + + if path: + return rattr, path_ + else: + return rattr + + def lookup(self, mid, tag=None, mask=None, *, + path=False): + if not isinstance(mid, Mid): + mid = Mid(mid, mbits=self.mbits) + return self.rbyd.lookup(mid.mrid, tag, mask, + path=path) + + def mids(self, *, + path=False): + for r in self.rbyd.rids( + path=path): + if path: + rid, name, path_ = r + else: + rid, name = r + + mid = Mid(self.mid, rid) + if path: + yield mid, name, path_ + else: + yield mid, name + + def rattrs(self, mid=None, tag=None, mask=None, *, + path=False): + if mid is None: + for r in self.rbyd.rattrs( + path=path): + if path: + rid, rattr, path_ = r + else: + rid, rattr = r + + mid = Mid(self.mid, rid) + if path: + yield mid, rattr, path_ + else: + yield mid, rattr + else: + if not isinstance(mid, Mid): + mid = Mid(mid, mbits=self.mbits) + yield from self.rbyd.rattrs(mid.mrid, tag, mask, + path=path) + + # lookup by name + def namelookup(self, did, name): + # unlike rbyd namelookup, we need an exact match here + rid, name_ = self.rbyd.namelookup(did, name) + if rid is None or (name_.did, name_.name) != (did, name): + return None, None + + return Mid(self.mid, rid), name_ + +# the mtree, the skeletal structure of littlefs +class Mtree: + def __init__(self, bd, mrootchain, mtree, *, + mrootpath=False, + mtreepath=False, + mbits=None): + if isinstance(mrootchain, Mdir): + mrootchain = [Mdir] + # we at least need the mrootanchor, even if it is corrupt + assert len(mrootchain) >= 1 + + self.bd = bd + if mbits is not None: + self.mbits = mbits + else: + self.mbits = Mtree.mbits_(self.bd) + + self.mrootchain = mrootchain + self.mrootanchor = mrootchain[0] + self.mroot = mrootchain[-1] + self.mtree = mtree + + # mbits is a static value derived from the block_size + @staticmethod + def mbits_(block_size): + if isinstance(block_size, Bd): + block_size = block_size.block_size + return mt.ceil(mt.log2(block_size)) - 3 + + # convenience function for creating mbits-dependent mids + def mid(self, mbid, mrid=None): + return Mid(mbid, mrid, mbits=self.mbits) + + @property + def block(self): + return self.mroot.block + + @property + def blocks(self): + return self.mroot.blocks + + @property + def trunk(self): + return self.mroot.trunk + + @property + def weight(self): + if self.mtree is None: + return 0 + else: + return self.mtree.weight + + @property + def mbweight(self): + return self.weight + + @property + def mrweight(self): + return 1 << self.mbits + + def mbweightrepr(self): + return str(self.mbweight >> self.mbits) + + def mrweightrepr(self): + return str(self.mrweight) + + @property + def rev(self): + return self.mroot.rev + + @property + def cksum(self): + return self.mroot.cksum + + def addr(self): + return self.mroot.addr() + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, self.repr()) + + def repr(self): + return 'mtree %s w%s.%s' % ( + self.addr(), + self.mbweightrepr(), self.mrweightrepr()) + + def __eq__(self, other): + return self.mrootanchor == other.mrootanchor + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash(self.mrootanchor) + + @classmethod + def fetch(cls, bd, blocks=None, trunk=None, *, + depth=None): + # default to blocks 0x{0,1} + if blocks is None: + blocks = [0, 1] + + # figure out mbits + mbits = Mtree.mbits_(bd) + + # fetch the mrootanchor + mrootanchor = Mdir.fetch(bd, -1, blocks, trunk) + + # follow the mroot chain to try to find the active mroot + mroot = mrootanchor + mrootchain = [mrootanchor] + mrootseen = set() + while True: + # corrupted? + if not mroot: + break + # cycle detected? + if mroot in mrootseen: + break + mrootseen.add(mroot) + + # stop here? + if depth and len(mrootchain) >= depth: + break + + # fetch the next mroot + rattr_ = mroot.lookup(-1, TAG_MROOT, 0x3) + if rattr_ is None: + break + blocks_, _ = frommdir(rattr_.data) + mroot = Mdir.fetch(bd, -1, blocks_) + mrootchain.append(mroot) + + # fetch the actual mtree, if there is one + mtree = None + if not depth or len(mrootchain) < depth: + rattr_ = mroot.lookup(-1, TAG_MTREE, 0x3) + if rattr_ is not None: + w_, block_, trunk_, cksum_, _ = frombtree(rattr_.data) + mtree = Btree.fetchck(bd, block_, trunk_, w_, cksum_) + + return cls(bd, mrootchain, mtree, + mbits=mbits) + + def _lookupnext_(self, mid, *, + path=False, + depth=None): + if not isinstance(mid, Mid): + mid = self.mid(mid) + + if path or depth: + # iterate over mrootchain + path_ = [] + for mroot in self.mrootchain: + # stop here? + if depth and len(path_) >= depth: + if path: + return mroot, path_ + else: + return mroot + + name = mroot.lookup(-1, TAG_MAGIC) + path_.append((mroot.mid, mroot, name)) + + # no mtree? must be inlined in mroot + if self.mtree is None: + if mid.mbid != -1: + if path: + return None, path_ + else: + return None + + if path: + return self.mroot, path_ + else: + return self.mroot + + # mtree? lookup in mtree + else: + # need to do two steps here in case lookupnext_ stops early + r = self.mtree.lookupnext_(mid.mid, + path=path or depth, + depth=depth-len(path_) if depth else None) + if path or depth: + bid_, rbyd_, rid_, name_, path__ = r + path_.extend(path__) + else: + bid_, rbyd_, rid_, name_ = r + if bid_ is None: + if path: + return None, path_ + else: + return None + + # corrupt btree node? + if not rbyd_: + if path: + return (bid_, rbyd_, rid_), path_ + else: + return (bid_, rbyd_, rid_) + + # stop here? it's not an mdir, but we only return btree nodes + # if explicitly requested + if depth and len(path_) >= depth: + if path: + return (bid_, rbyd_, rid_), path_ + else: + return (bid_, rbyd_, rid_) + + # fetch the mdir + rattr_ = rbyd_.lookup(rid_, TAG_MDIR, 0x3) + # mdir tag missing? weird + if rattr_ is None: + if path: + return (bid_, rbyd_, rid_), path_ + else: + return (bid_, rbyd_, rid_) + blocks_, _ = frommdir(rattr_.data) + mdir = Mdir.fetch(self.bd, mid, blocks_) + if path: + return mdir, path_ + else: + return mdir + + def lookupnext_(self, mid, *, + mdirs_only=True, + path=False, + depth=None): + # most of the logic is in _lookupnext_, this just helps + # deduplicate the mdirs_only logic + r = self._lookupnext_(mid, + path=path, + depth=depth) + if path: + mdir, path_ = r + else: + mdir = r + if mdir is None or ( + mdirs_only and not isinstance(mdir, Mdir)): + if path: + return None, path_ + else: + return None + + if path: + return mdir, path_ + else: + return mdir + + def lookup(self, mid, *, + path=False, + depth=None): + if not isinstance(mid, Mid): + mid = self.mid(mid) + + # lookup the relevant mdir + r = self.lookupnext_(mid, + path=path, + depth=depth) + if path: + mdir, path_ = r + else: + mdir = r + if mdir is None: + if path: + return None, None, path_ + else: + return None, None + + # not in mdir? + if mid.mrid >= mdir.weight: + if path: + return None, None, path_ + else: + return None, None + + # lookup mid in mdir + rattr = mdir.lookup(mid) + if path: + return mdir, rattr, path_+[(mid, mdir, rattr)] + else: + return mdir, rattr + + # iterate over all mdirs, this includes the mrootchain + def _leaves(self, *, + path=False, + depth=None): + # iterate over mrootchain + if path or depth: + path_ = [] + for mroot in self.mrootchain: + if path: + yield mroot, path_ + else: + yield mroot + + if path or depth: + # stop here? + if depth and len(path_) >= depth: + return + + name = mroot.lookup(-1, TAG_MAGIC) + path_.append((mroot.mid, mroot, name)) + + # do we even have an mtree? + if self.mtree is not None: + # include the mtree root even if the weight is zero + if self.mtree.weight == 0: + if path: + yield -1, self.mtree.rbyd, path_ + else: + yield -1, self.mtree.rbyd + return + + mid = self.mid(0) + while True: + r = self.lookupnext_(mid, + mdirs_only=False, + path=path, + depth=depth) + if path: + mdir, path_ = r + else: + mdir = r + if mdir is None: + break + + # mdir? + if isinstance(mdir, Mdir): + if path: + yield mdir, path_ + else: + yield mdir + mid = self.mid(mid.mbid+1) + # btree node? + else: + bid, rbyd, rid = mdir + if path: + yield ((bid-rid + (rbyd.weight-1), rbyd), + # path tail is usually redundant unless corrupt + path_[:-1] + if path_ + and isinstance(path_[-1][1], Rbyd) + and path_[-1][1] == rbyd + else path_) + else: + yield (bid-rid + (rbyd.weight-1), rbyd) + mid = self.mid(bid-rid + (rbyd.weight-1) + 1) + + def leaves(self, *, + mdirs_only=False, + path=False, + depth=None): + for r in self._leaves( + path=path, + depth=depth): + if path: + mdir, path_ = r + else: + mdir = r + if mdirs_only and not isinstance(mdir, Mdir): + continue + + if path: + yield mdir, path_ + else: + yield mdir + + # traverse over all mdirs and btree nodes + # - mdir => Mdir + # - btree node => (bid, rbyd) + def _traverse(self, *, + path=False, + depth=None): + ptrunk_ = [] + for mdir, path_ in self.leaves( + path=True, + depth=depth): + # we only care about the mdirs/rbyds here + trunk_ = ([(lambda mid_, mdir_, name_: mdir_)(*p) + if isinstance(p[1], Mdir) + else (lambda bid_, rbyd_, rid_, name_: + (bid_-rid_ + (rbyd_.weight-1), rbyd_))(*p) + for p in path_] + + [mdir]) + for d, mdir in pathdelta( + trunk_, ptrunk_): + # but include branch mids/rids in the path if requested + if path: + yield mdir, path_[:d] + else: + yield mdir + ptrunk_ = trunk_ + + def traverse(self, *, + mdirs_only=False, + path=False, + depth=None): + for r in self._traverse( + path=path, + depth=depth): + if path: + mdir, path_ = r + else: + mdir = r + if mdirs_only and not isinstance(mdir, Mdir): + continue + + if path: + yield mdir, path_ + else: + yield mdir + + # these are just aliases + + # the difference between mdirs and leaves is mdirs defaults to only + # mdirs, leaves can include btree nodes if corrupt + def mdirs(self, *, + mdirs_only=True, + path=False, + depth=None): + return self.leaves( + mdirs_only=mdirs_only, + path=path, + depth=depth) + + # note mids/rattrs do _not_ include corrupt btree nodes! + def mids(self, *, + mdirs_only=True, + path=False, + depth=None): + for r in self.mdirs( + mdirs_only=mdirs_only, + path=path, + depth=depth): + if path: + mdir, path_ = r + else: + mdir = r + if isinstance(mdir, Mdir): + for mid, name in mdir.mids(): + if path: + yield (mid, mdir, name, + path_+[(mid, mdir, name)]) + else: + yield mid, mdir, name + else: + bid, rbyd = mdir + for rid, name in rbyd.rids(): + bid_ = bid-(rbyd.weight-1) + rid + mid_ = self.mid(bid_) + mdir_ = (bid_, rbyd, rid) + if path: + yield (mid_, mdir_, name, + path_+[(bid_, rbyd, rid, name)]) + else: + yield mid_, mdir_, name + + def rattrs(self, mid=None, tag=None, mask=None, *, + mdirs_only=True, + path=False, + depth=None): + if mid is None: + for r in self.mdirs( + mdirs_only=mdirs_only, + path=path, + depth=depth): + if path: + mdir, path_ = r + else: + mdir = r + if isinstance(mdir, Mdir): + for mid, rattr in mdir.rattrs(): + if path: + yield (mid, mdir, rattr, + path_+[(mid, mdir, mdir.lookup(mid))]) + else: + yield mid, mdir, rattr + else: + bid, rbyd = mdir + for rid, name in rbyd.rids(): + bid_ = bid-(rbyd.weight-1) + rid + mid_ = self.mid(bid_) + mdir_ = (bid_, rbyd, rid) + for rattr in rbyd.rattrs(rid): + if path: + yield (mid_, mdir_, rattr, + path_+[(bid_, rbyd, rid, name)]) + else: + yield mid_, mdir_, rattr + else: + if not isinstance(mid, Mid): + mid = self.mid(mid) + + r = self.lookupnext_(mid, + path=path, + depth=depth) + if path: + mdir, path_ = r + else: + mdir = r + if mdir is None or ( + mdirs_only and not isinstance(mdir, Mdir)): + return + + if isinstance(mdir, Mdir): + for rattr in mdir.rattrs(mid, tag, mask): + if path: + yield rattr, path_ + else: + yield rattr + else: + bid, rbyd, rid = mdir + for rattr in rbyd.rattrs(rid, tag, mask): + if path: + yield rattr, path_ + else: + yield rattr + + # lookup by name + def _namelookup_(self, did, name, *, + path=False, + depth=None): + if path or depth: + # iterate over mrootchain + path_ = [] + for mroot in self.mrootchain: + # stop here? + if depth and len(path_) >= depth: + if path: + return mroot, path_ + else: + return mroot + + name = mroot.lookup(-1, TAG_MAGIC) + path_.append((mroot.mid, mroot, name)) + + # no mtree? must be inlined in mroot + if self.mtree is None: + if path: + return self.mroot, path_ + else: + return self.mroot + + # mtree? find name in mtree + else: + # need to do two steps here in case namelookup_ stops early + r = self.mtree.namelookup_(did, name, + path=path or depth, + depth=depth-len(path_) if depth else None) + if path or depth: + bid_, rbyd_, rid_, name_, path__ = r + path_.extend(path__) + else: + bid_, rbyd_, rid_, name_ = r + if bid_ is None: + if path: + return None, path_ + else: + return None + + # corrupt btree node? + if not rbyd_: + if path: + return (bid_, rbyd_, rid_), path_ + else: + return (bid_, rbyd_, rid_) + + # stop here? it's not an mdir, but we only return btree nodes + # if explicitly requested + if depth and len(path_) >= depth: + if path: + return (bid_, rbyd_, rid_), path_ + else: + return (bid_, rbyd_, rid_) + + # fetch the mdir + rattr_ = rbyd_.lookup(rid_, TAG_MDIR, 0x3) + # mdir tag missing? weird + if rattr_ is None: + if path: + return (bid_, rbyd_, rid_), path_ + else: + return (bid_, rbyd_, rid_) + blocks_, _ = frommdir(rattr_.data) + mdir = Mdir.fetch(self.bd, self.mid(bid_), blocks_) + if path: + return mdir, path_ + else: + return mdir + + def namelookup_(self, did, name, *, + mdirs_only=True, + path=False, + depth=None): + # most of the logic is in _namelookup_, this just helps + # deduplicate the mdirs_only logic + r = self._namelookup_(did, name, + path=path, + depth=depth) + if path: + mdir, path_ = r + else: + mdir = r + if mdir is None or ( + mdirs_only and not isinstance(mdir, Mdir)): + if path: + return None, path_ + else: + return None + + if path: + return mdir, path_ + else: + return mdir + + def namelookup(self, did, name, *, + path=False, + depth=None): + # lookup the relevant mdir + r = self.namelookup_(did, name, + path=path, + depth=depth) + if path: + mdir, path_ = r + else: + mdir = r + if mdir is None: + if path: + return None, None, None, path_ + else: + return None, None, None + + # find name in mdir + mid_, name_ = mdir.namelookup(did, name) + if mid_ is None: + if path: + return None, None, None, path_ + else: + return None, None, None + + if path: + return mid_, mdir, name_, path_+[(mid_, mdir, name_)] + else: + return mid_, mdir, name_ + + + +# tree renderer +class TreeArt: + # tree branches are an abstract thing for tree rendering + class Branch(co.namedtuple('Branch', ['a', 'b', 'z', 'color'])): + __slots__ = () + def __new__(cls, a, b, z=0, color='b'): + # a and b are context specific + return super().__new__(cls, a, b, z, color) + + def __repr__(self): + return '%s(%s, %s, %s, %s)' % ( + self.__class__.__name__, + self.a, + self.b, + self.z, + self.color) + + # don't include color in branch comparisons, or else our tree + # renderings can end up with inconsistent colors between runs + def __eq__(self, other): + return (self.a, self.b, self.z) == (other.a, other.b, other.z) + + def __ne__(self, other): + return (self.a, self.b, self.z) != (other.a, other.b, other.z) + + def __hash__(self): + return hash((self.a, self.b, self.z)) + + # also order by z first, which can be useful for reproducibly + # prioritizing branches when simplifying trees + def __lt__(self, other): + return (self.z, self.a, self.b) < (other.z, other.a, other.b) + + def __le__(self, other): + return (self.z, self.a, self.b) <= (other.z, other.a, other.b) + + def __gt__(self, other): + return (self.z, self.a, self.b) > (other.z, other.a, other.b) + + def __ge__(self, other): + return (self.z, self.a, self.b) >= (other.z, other.a, other.b) + + # apply a function to a/b while trying to avoid copies + def map(self, filter_, map_=None): + if map_ is None: + filter_, map_ = None, filter_ + + a = self.a + if filter_ is None or filter_(a): + a = map_(a) + + b = self.b + if filter_ is None or filter_(b): + b = map_(b) + + if a != self.a or b != self.b: + return self.__class__( + a if a != self.a else self.a, + b if b != self.b else self.b, + self.z, + self.color) + else: + return self + + def __init__(self, tree): + self.tree = tree + self.depth = max((t.z+1 for t in tree), default=0) + if self.depth > 0: + self.width = 2*self.depth + 2 + else: + self.width = 0 + + def __iter__(self): + return iter(self.tree) + + def __bool__(self): + return bool(self.tree) + + def __len__(self): + return len(self.tree) + + # render an rbyd rbyd tree for debugging + @classmethod + def _fromrbydrtree(cls, rbyd, **args): + trunks = co.defaultdict(lambda: (-1, 0)) + alts = co.defaultdict(lambda: {}) + + for rid, rattr, path in rbyd.rattrs(path=True): + # keep track of trunks/alts + trunks[rattr.toff] = (rid, rattr.tag) + + for ralt in path: + if ralt.followed: + alts[ralt.toff] |= {'f': ralt.joff, 'c': ralt.color} + else: + alts[ralt.toff] |= {'nf': ralt.off, 'c': ralt.color} + + if args.get('tree_rbyd_all'): + # treat unreachable alts as converging paths + for j_, alt in alts.items(): + if 'f' not in alt: + alt['f'] = alt['nf'] + elif 'nf' not in alt: + alt['nf'] = alt['f'] + + else: + # prune any alts with unreachable edges + pruned = {} + for j, alt in alts.items(): + if 'f' not in alt: + pruned[j] = alt['nf'] + elif 'nf' not in alt: + pruned[j] = alt['f'] + for j in pruned.keys(): + del alts[j] + + for j, alt in alts.items(): + while alt['f'] in pruned: + alt['f'] = pruned[alt['f']] + while alt['nf'] in pruned: + alt['nf'] = pruned[alt['nf']] + + # find the trunk and depth of each alt + def rec_trunk(j): + if j not in alts: + return trunks[j] + else: + if 'nft' not in alts[j]: + alts[j]['nft'] = rec_trunk(alts[j]['nf']) + return alts[j]['nft'] + + for j in alts.keys(): + rec_trunk(j) + for j, alt in alts.items(): + if alt['f'] in alts: + alt['ft'] = alts[alt['f']]['nft'] + else: + alt['ft'] = trunks[alt['f']] + + def rec_height(j): + if j not in alts: + return 0 + else: + if 'h' not in alts[j]: + alts[j]['h'] = max( + rec_height(alts[j]['f']), + rec_height(alts[j]['nf'])) + 1 + return alts[j]['h'] + + for j in alts.keys(): + rec_height(j) + + t_depth = max((alt['h']+1 for alt in alts.values()), default=0) + + # convert to more general tree representation + tree = set() + for j, alt in alts.items(): + # note all non-trunk edges should be colored black + tree.add(cls.Branch( + alt['nft'], + alt['nft'], + t_depth-1 - alt['h'], + alt['c'])) + if alt['ft'] != alt['nft']: + tree.add(cls.Branch( + alt['nft'], + alt['ft'], + t_depth-1 - alt['h'], + 'b')) + + return cls(tree) + + # render an rbyd btree tree for debugging + @classmethod + def _fromrbydbtree(cls, rbyd, **args): + # for rbyds this is just a pointer to every rid + tree = set() + root = None + for rid, name in rbyd.rids(): + b = (rid, name.tag) + if root is None: + root = b + tree.add(cls.Branch(root, b)) + return cls(tree) + + # render an rbyd tree for debugging + @classmethod + def fromrbyd(cls, rbyd, **args): + if args.get('tree_btree'): + return cls._fromrbydbtree(rbyd, **args) + else: + return cls._fromrbydrtree(rbyd, **args) + + # render some nice ascii trees + def repr(self, x, color=False): + if self.depth == 0: + return '' + + def branchrepr(tree, x, d, was): + for t in tree: + if t.z == d and t.b == x: + if any(t.z == d and t.a == x + for t in tree): + return '+-', t.color, t.color + elif any(t.z == d + and x > min(t.a, t.b) + and x < max(t.a, t.b) + for t in tree): + return '|-', t.color, t.color + elif t.a < t.b: + return '\'-', t.color, t.color + else: + return '.-', t.color, t.color + for t in tree: + if t.z == d and t.a == x: + return '+ ', t.color, None + for t in tree: + if (t.z == d + and x > min(t.a, t.b) + and x < max(t.a, t.b)): + return '| ', t.color, was + if was: + return '--', was, was + return ' ', None, None + + trunk = [] + was = None + for d in range(self.depth): + t, c, was = branchrepr(self.tree, x, d, was) + + trunk.append('%s%s%s%s' % ( + '\x1b[33m' if color and c == 'y' + else '\x1b[31m' if color and c == 'r' + else '\x1b[1;30m' if color and c == 'b' + else '', + t, + ('>' if was else ' ') if d == self.depth-1 else '', + '\x1b[m' if color and c else '')) + + return '%s ' % ''.join(trunk) + +# some more renderers + +# render a btree rbyd tree for debugging +@classmethod +def _treeartfrombtreertree(cls, btree, *, + depth=None, + inner=False, + **args): + # precompute rbyd trees so we know the max depth at each layer + # to nicely align trees + rtrees = {} + rdepths = {} + for bid, rbyd, path in btree.traverse(path=True, depth=depth): + if not rbyd: + continue + + rtree = cls.fromrbyd(rbyd, **args) + rtrees[rbyd] = rtree + rdepths[len(path)] = max(rdepths.get(len(path), 0), rtree.depth) + + # map rbyd branches into our btree space + tree = set() + for bid, rbyd, path in btree.traverse(path=True, depth=depth): + if not rbyd: + continue + + # yes we can find new rbyds if disk is being mutated, just + # ignore these + if rbyd not in rtrees: + continue + + rtree = rtrees[rbyd] + rz = max((t.z+1 for t in rtree), default=0) + d = sum(rdepths[d]+1 for d in range(len(path))) + + # map into our btree space + for t in rtree: + # note we adjust our bid to be left-leaning, this allows + # a global order and makes tree rendering quite a bit easier + a_rid, a_tag = t.a + b_rid, b_tag = t.b + _, (_, a_w, _) = rbyd.lookupnext(a_rid) + _, (_, b_w, _) = rbyd.lookupnext(b_rid) + tree.add(cls.Branch( + (bid-(rbyd.weight-1)+a_rid-(a_w-1), len(path), a_tag), + (bid-(rbyd.weight-1)+b_rid-(b_w-1), len(path), b_tag), + d + rdepths[len(path)]-rz + t.z, + t.color)) + + # connect rbyd branches to rbyd roots + if path: + l_bid, l_rbyd, l_rid, l_name = path[-1] + l_branch = l_rbyd.lookup(l_rid, TAG_BRANCH, 0x3) + + if rtree: + r_rid, r_tag = min(rtree, key=lambda t: t.z).a + _, (_, r_w, _) = rbyd.lookupnext(r_rid) + else: + r_rid, (r_tag, r_w, _) = rbyd.lookupnext(-1) + + tree.add(cls.Branch( + (l_bid-(l_name.weight-1), len(path)-1, l_branch.tag), + (bid-(rbyd.weight-1)+r_rid-(r_w-1), len(path), r_tag), + d-1)) + + # remap branches to leaves if we aren't showing inner branches + if not inner: + # step through each btree layer backwards + b_depth = max((t.a[1]+1 for t in tree), default=0) + + for d in reversed(range(b_depth-1)): + # find bid ranges at this level + bids = set() + for t in tree: + if t.b[1] == d: + bids.add(t.b[0]) + bids = sorted(bids) + + # find the best root for each bid range + roots = {} + for i in range(len(bids)): + for t in tree: + if (t.a[1] > d + and t.a[0] >= bids[i] + and (i == len(bids)-1 or t.a[0] < bids[i+1]) + and (bids[i] not in roots + or t < roots[bids[i]])): + roots[bids[i]] = t + + # remap branches to leaf-roots + tree = {t.map( + lambda x: x[1] == d and x[0] in roots, + lambda x: roots[x[0]].a) + for t in tree} + + return cls(tree) + +# render a btree btree tree for debugging +@classmethod +def _treeartfrombtreebtree(cls, btree, *, + depth=None, + inner=False, + **args): + # find all branches + tree = set() + root = None + branches = {} + for bid, name, path in btree.bids( + path=True, + depth=depth): + # create branch for each jump in path + # + # note we adjust our bid to be left-leaning, this allows + # a global order and makes tree rendering quite a bit easier + a = root + for d, (bid_, rbyd_, rid_, name_) in enumerate(path): + # map into our btree space + bid__ = bid_-(name_.weight-1) + b = (bid__, d, name_.tag) + + # remap branches to leaves if we aren't showing inner + # branches + if not inner: + if b not in branches: + bid_, rbyd_, rid_, name_ = path[-1] + bid__ = bid_-(name_.weight-1) + branches[b] = (bid__, len(path)-1, name_.tag) + b = branches[b] + + # render the root path on first rid, this is arbitrary + if root is None: + root, a = b, b + + tree.add(cls.Branch(a, b, d)) + a = b + + return cls(tree) + +# render a btree tree for debugging +@classmethod +def treeartfrombtree(cls, btree, **args): + if args.get('tree_btree'): + return cls._frombtreebtree(btree, **args) + else: + return cls._frombtreertree(btree, **args) + +TreeArt._frombtreertree = _treeartfrombtreertree +TreeArt._frombtreebtree = _treeartfrombtreebtree +TreeArt.frombtree = treeartfrombtree + +# render an mtree tree for debugging +@classmethod +def _treeartfrommtreertree(cls, mtree, *, + depth=None, + inner=False, + **args): + # precompute rbyd trees so we know the max depth at each layer + # to nicely align trees + rtrees = {} + rdepths = {} + for mdir, path in mtree.traverse(path=True, depth=depth): + if isinstance(mdir, Mdir): + if not mdir: + continue + rbyd = mdir.rbyd + else: + bid, rbyd = mdir + if not rbyd: + continue + + rtree = cls.fromrbyd(rbyd, **args) + rtrees[rbyd] = rtree + rdepths[len(path)] = max(rdepths.get(len(path), 0), rtree.depth) + + # map rbyd branches into our mtree space + tree = set() + branches = {} + for mdir, path in mtree.traverse(path=True, depth=depth): + if isinstance(mdir, Mdir): + if not mdir: + continue + rbyd = mdir.rbyd + else: + bid, rbyd = mdir + if not rbyd: + continue + + # yes we can find new rbyds if disk is being mutated, just + # ignore these + if rbyd not in rtrees: + continue + + rtree = rtrees[rbyd] + rz = max((t.z+1 for t in rtree), default=0) + d = sum(rdepths[d]+1 for d, p in enumerate(path)) + + # map into our mtree space + for t in rtree: + # note we adjust our mid/bid to be left-leaning, this allows + # a global order and makes tree rendering quite a bit easier + # + # we also need to give btree nodes mrid=-1 so they come + # before and mrid=-1 mdir attrs + a_rid, a_tag = t.a + b_rid, b_tag = t.b + _, (_, a_w, _) = rbyd.lookupnext(a_rid) + _, (_, b_w, _) = rbyd.lookupnext(b_rid) + if isinstance(mdir, Mdir): + a_mid = mtree.mid(mdir.mid, a_rid) + b_mid = mtree.mid(mdir.mid, b_rid) + else: + a_mid = mtree.mid(bid-(rbyd.weight-1)+a_rid-(a_w-1), -1) + b_mid = mtree.mid(bid-(rbyd.weight-1)+b_rid-(b_w-1), -1) + + tree.add(cls.Branch( + (a_mid, len(path), a_tag), + (b_mid, len(path), b_tag), + d + rdepths[len(path)]-rz + t.z, + t.color)) + + # connect rbyd branches to rbyd roots + if path: + # figure out branch mid/attr + if isinstance(path[-1][1], Mdir): + l_mid, l_mdir, l_name = path[-1] + l_branch = (l_mdir.lookup(l_mid, TAG_MROOT, 0x3) + or l_mdir.lookup(l_mid, TAG_MTREE, 0x3)) + else: + l_bid, l_rbyd, l_rid, l_name = path[-1] + l_mid = mtree.mid(l_bid-(l_name.weight-1), -1) + l_branch = (l_rbyd.lookup(l_rid, TAG_BRANCH, 0x3) + or l_rbyd.lookup(l_rid, TAG_MDIR, 0x3)) + + # figure out root mid/rattr + if rtree: + r_rid, r_tag = min(rtree, key=lambda t: t.z).a + _, (_, r_w, _) = rbyd.lookupnext(r_rid) + else: + r_rid, (r_tag, r_w, _) = rbyd.lookupnext(-1) + + if isinstance(mdir, Mdir): + r_mid = mtree.mid(mdir.mid, r_rid) + else: + r_mid = mtree.mid(bid-(rbyd.weight-1)+r_rid-(r_w-1), -1) + + tree.add(cls.Branch( + (l_mid, len(path)-1, l_branch.tag), + (r_mid, len(path), r_tag), + d-1)) + + # remap branches to leaves if we aren't showing inner branches + if not inner: + # step through each btree layer backwards + b_depth = max((t.a[1]+1 for t in tree), default=0) + + for d in reversed(range(len(mtree.mrootchain), b_depth-1)): + # find mid ranges at this level + mids = set() + for t in tree: + if t.b[1] == d: + mids.add(t.b[0]) + mids = sorted(mids) + + # find the best root for each mid range + roots = {} + for i in range(len(mids)): + for t in tree: + if (t.a[1] > d + and t.a[0] >= mids[i] + and (i == len(mids)-1 or t.a[0] < mids[i+1]) + and (mids[i] not in roots + or t < roots[mids[i]])): + roots[mids[i]] = t + + # remap branches to leaf-roots + tree = {t.map( + lambda x: x[1] == d and x[0] in roots, + lambda x: roots[x[0]].a) + for t in tree} + + return cls(tree) + +# render an mtree tree for debugging +@classmethod +def _treeartfrommtreebtree(cls, mtree, *, + depth=None, + inner=False, + **args): + tree = set() + root = None + branches = {} + for mid, mdir, name, path in mtree.mids( + mdirs_only=False, + path=True, + depth=depth): + # create branch for each jump in path + # + # note we adjust our mid/bid to be left-leaning, this allows + # a global order and makes tree rendering quite a bit easier + # + # we also need to give btree nodes mrid=-1 so they come + # before and mrid=-1 mdir attrs + a = root + for d, p in enumerate(path): + # map into our mtree space + if isinstance(p[1], Mdir): + mid_, mdir_, name_ = p + else: + bid_, rbyd_, rid_, name_ = p + mid_ = mtree.mid(bid_-(name_.weight-1), -1) + b = (mid_, d, name_.tag) + + # remap branches to leaves if we aren't showing inner + # branches + if not inner: + if b not in branches: + if isinstance(path[-1][1], Mdir): + mid_, mdir_, name_ = path[-1] + else: + bid_, rbyd_, rid_, name_ = path[-1] + mid_ = mtree.mid(bid_-(name_.weight-1), -1) + branches[b] = (mid_, len(path)-1, name_.tag) + b = branches[b] + + # render the root path on first rid, this is arbitrary + if root is None: + root, a = b, b + + tree.add(cls.Branch(a, b, d)) + a = b + + return cls(tree) + +# render an mtree tree for debugging +@classmethod +def treeartfrommtree(cls, mtree, **args): + if args.get('tree_btree'): + return cls._frommtreebtree(mtree, **args) + else: + return cls._frommtreertree(mtree, **args) + +TreeArt._frommtreertree = _treeartfrommtreertree +TreeArt._frommtreebtree = _treeartfrommtreebtree +TreeArt.frommtree = treeartfrommtree + + + +def main(disk, mroots=None, *, + trunk=None, + block_size=None, + block_count=None, + quiet=False, + color='auto', + **args): + # figure out what color should be + if color == 'auto': + color = sys.stdout.isatty() + elif color == 'always': + color = True + else: + color = False + + # is bd geometry specified? + if isinstance(block_size, tuple): + block_size, block_count_ = block_size + if block_count is None: + block_count = block_count_ + + # flatten mroots, default to 0x{0,1} + mroots = list(it.chain.from_iterable(mroots)) if mroots else [0, 1] + + # mroots may also encode trunks + mroots, trunk = ( + [block[0] if isinstance(block, tuple) + else block + for block in mroots], + trunk if trunk is not None + else ft.reduce( + lambda x, y: y, + (block[1] for block in mroots + if isinstance(block, tuple)), + None)) + + # we seek around a bunch, so just keep the disk open + with open(disk, 'rb') as f: + # if block_size is omitted, assume the block device is one big block + if block_size is None: + f.seek(0, os.SEEK_END) + block_size = f.tell() + + # fetch the mtree + bd = Bd(f, block_size, block_count) + mtree = Mtree.fetch(bd, mroots, trunk, + depth=args.get('depth')) + + # print some information about the mtree + if not quiet: + print('mtree %s w%s.%s, rev %08x, cksum %08x' % ( + mtree.addr(), + mtree.mbweightrepr(), mtree.mrweightrepr(), + mtree.rev, + mtree.cksum)) + + # precompute tree renderings + t_width = 0 + if (args.get('tree_rbyd') + or args.get('tree_rbyd_all') + or args.get('tree_btree')): + treeart = TreeArt.frommtree(mtree, **args) + t_width = treeart.width + + # dynamically size the id field + w_width = max( + mt.ceil(mt.log10(max(1, mtree.mbweight >> mtree.mbits)+1)), + mt.ceil(mt.log10(max(1, max( + mdir.weight for mdir in mtree.mdirs( + depth=args.get('depth')))))+1), + # in case of -1.-1 + 2) + + # pmdir keeps track of the last rendered mdir/rbyd, we update + # this in dbg_mdir/dbg_branch to always print interleaved + # addresses + pmdir = None + def dbg_mdir(d, mdir): + nonlocal pmdir + + # show human-readable tag representation + for i, (mid, rattr) in enumerate(mdir.rattrs()): + print('%12s %s%s' % ( + '{%s}:' % ','.join('%04x' % block + for block in mdir.blocks) + if not isinstance(pmdir, Mdir) or mdir != pmdir + else '', + treeart.repr((mid, d, rattr.tag), color) + if args.get('tree_rbyd') + or args.get('tree_rbyd_all') + or args.get('tree_btree') + else '', + '%*s %-*s%s' % ( + 2*w_width+1, '%d.%d-%d' % ( + mid.mbid >> mtree.mbits, + mid.mrid-(rattr.weight-1), + mid.mrid) + if rattr.weight > 1 + else '%d.%d' % ( + mid.mbid >> mtree.mbits, + mid.mrid) + if rattr.weight > 0 or i == 0 + else '', + 21+w_width, rattr.repr(), + ' %s' % next(xxd(rattr.data, 8), '') + if not args.get('raw') + and not args.get('no_truncate') + else ''))) + pmdir = mdir + + # show on-disk encoding of tags + if args.get('raw'): + for o, line in enumerate(xxd(rattr.tdata)): + print('%11s: %*s%*s %s' % ( + '%04x' % (rattr.toff + o*16), + t_width, '', + 2*w_width+1, '', + line)) + if args.get('raw') or args.get('no_truncate'): + for o, line in enumerate(xxd(rattr.data)): + print('%11s: %*s%*s %s' % ( + '%04x' % (rattr.off + o*16), + t_width, '', + 2*w_width+1, '', + line)) + + def dbg_branch(d, bid, rbyd, rid, name): + nonlocal pmdir + + # show human-readable representation + for rattr in rbyd.rattrs(rid): + print('%12s %s%*s %-*s %s' % ( + '%04x.%04x:' % (rbyd.block, rbyd.trunk) + if not isinstance(pmdir, Rbyd) or rbyd != pmdir + else '', + treeart.repr( + (mtree.mid(bid-(name.weight-1), -1), + d, rattr.tag), + color) + if args.get('tree_rbyd') + or args.get('tree_rbyd_all') + or args.get('tree_btree') + else '', + 2*w_width+1, '%d-%d' % ( + (bid-(rattr.weight-1)) >> mtree.mbits, + bid >> mtree.mbits) + if (rattr.weight >> mtree.mbits) > 1 + else bid >> mtree.mbits if rattr.weight > 0 + else '', + 21+w_width, rattr.repr(), + next(xxd(rattr.data, 8), '') + if not args.get('raw') + and not args.get('no_truncate') + else '')) + pmdir = rbyd + + # show on-disk encoding of tags/data + if args.get('raw'): + for o, line in enumerate(xxd( + rbyd.data[rattr.toff:rattr.off])): + print('%11s: %*s%*s %s' % ( + '%04x' % (rattr.toff + o*16), + t_width, '', + 2*w_width+1, '', + line)) + if args.get('raw') or args.get('no_truncate'): + for o, line in enumerate(xxd(rattr.data)): + print('%11s: %*s%*s %s' % ( + '%04x' % (rattr.off + o*16), + t_width, '', + 2*w_width+1, '', + line)) + + # traverse and print entries + prbyd = None + ppath = [] + mrootseen = set() + corrupted = False + for mdir, path in mtree.leaves( + path=True, + depth=args.get('depth')): + # print inner branches if requested + if args.get('inner') and not quiet: + for d, (bid_, rbyd_, rid_, name_) in pathdelta( + # skip the mrootchain + path[len(mtree.mrootchain):], + ppath[len(mtree.mrootchain):]): + dbg_branch(len(mtree.mrootchain)+d, + bid_, rbyd_, rid_, name_) + ppath = path + + # mdir? + if isinstance(mdir, Mdir): + # corrupted? + if not mdir: + if not quiet: + print('%s{%s}: %s%s' % ( + '\x1b[31m' if color else '', + ','.join('%04x' % block + for block in mdir.blocks), + '(corrupted %s %s)' % ( + 'mroot' if mdir.mid == -1 else 'mdir', + mdir.addr()), + '\x1b[m' if color else '')) + pmdir = None + corrupted = True + continue + + # cycle detected? + if mdir.mid == -1: + if mdir in mrootseen: + if not quiet: + print('%s{%s}: %s%s' % ( + '\x1b[31m' if color else '', + ','.join('%04x' % block + for block in mdir.blocks), + '(mroot cycle detected %s)' % mdir.addr(), + '\x1b[m' if color else '')) + pmdir = None + corrupted = True + continue + mrootseen.add(mdir) + + # show the mdir + if not quiet: + dbg_mdir(len(path), mdir) + + # btree node? + else: + bid, rbyd = mdir + # corrupted? try to keep printing the tree + if not rbyd: + if not quiet: + print('%s%11s: %*s%s%s' % ( + '\x1b[31m' if color else '', + '%04x.%04x' % (rbyd.block, rbyd.trunk), + t_width, '', + '(corrupted rbyd %s)' % rbyd.addr(), + '\x1b[m' if color else '')) + pmdir = None + corrupted = True + continue + + if not quiet: + for rid, name in rbyd.rids(): + bid_ = bid-(rbyd.weight-1) + rid + # show the leaf entry/branch + dbg_branch(len(path), bid_, rbyd, rid, name) + + if args.get('error_on_corrupt') and corrupted: + sys.exit(2) + + +if __name__ == "__main__": + import argparse + import sys + parser = argparse.ArgumentParser( + description="Debug littlefs's metadata tree.", + allow_abbrev=False) + parser.add_argument( + 'disk', + help="File containing the block device.") + parser.add_argument( + 'mroots', + nargs='*', + type=rbydaddr, + help="Block address of the mroots. Defaults to 0x{0,1}.") + parser.add_argument( + '--trunk', + type=lambda x: int(x, 0), + help="Use this offset as the trunk of the mroots.") + parser.add_argument( + '-b', '--block-size', + type=bdgeom, + help="Block size/geometry in bytes. Accepts x.") + parser.add_argument( + '--block-count', + type=lambda x: int(x, 0), + help="Block count in blocks.") + parser.add_argument( + '-q', '--quiet', + action='store_true', + help="Don't show anything, useful when checking for errors.") + parser.add_argument( + '--color', + choices=['never', 'always', 'auto'], + default='auto', + help="When to use terminal colors. Defaults to 'auto'.") + parser.add_argument( + '-x', '--raw', + action='store_true', + help="Show the raw data including tag encodings.") + parser.add_argument( + '-T', '--no-truncate', + action='store_true', + help="Don't truncate, show the full contents.") + parser.add_argument( + '-R', '--tree', '--rbyd', '--tree-rbyd', + dest='tree_rbyd', + action='store_true', + help="Show the rbyd tree.") + parser.add_argument( + '-Y', '--rbyd-all', '--tree-rbyd-all', + dest='tree_rbyd_all', + action='store_true', + help="Show the full rbyd tree.") + parser.add_argument( + '-B', '--btree', '--tree-btree', + dest='tree_btree', + action='store_true', + help="Show a simplified btree tree.") + parser.add_argument( + '-i', '--inner', + action='store_true', + help="Show inner branches.") + parser.add_argument( + '-z', '--depth', + nargs='?', + type=lambda x: int(x, 0), + const=0, + help="Depth of mtree to show.") + parser.add_argument( + '-e', '--error-on-corrupt', + action='store_true', + help="Error if the filesystem is corrupt.") + sys.exit(main(**{k: v + for k, v in vars(parser.parse_intermixed_args()).items() + if v is not None})) diff --git a/scripts/dbgrbyd.py b/scripts/dbgrbyd.py new file mode 100755 index 000000000..88eccba28 --- /dev/null +++ b/scripts/dbgrbyd.py @@ -0,0 +1,1898 @@ +#!/usr/bin/env python3 + +# prevent local imports +if __name__ == "__main__": + __import__('sys').path.pop(0) + +import bisect +import collections as co +import functools as ft +import itertools as it +import math as mt +import os +import struct + +try: + import crc32c as crc32c_lib +except ModuleNotFoundError: + crc32c_lib = None + + +COLORS = [ + '34', # blue + '31', # red + '32', # green + '35', # purple + '33', # yellow + '36', # cyan +] + + +TAG_NULL = 0x0000 ## v--- ---- +--- ---- +TAG_INTERNAL = 0x0000 ## v--- ---- +ttt tttt +TAG_CONFIG = 0x0100 ## v--- ---1 +ttt tttt +TAG_MAGIC = 0x0131 # v--- ---1 +-11 --rr +TAG_VERSION = 0x0134 # v--- ---1 +-11 -1-- +TAG_RCOMPAT = 0x0135 # v--- ---1 +-11 -1-1 +TAG_WCOMPAT = 0x0136 # v--- ---1 +-11 -11- +TAG_OCOMPAT = 0x0137 # v--- ---1 +-11 -111 +TAG_GEOMETRY = 0x0138 # v--- ---1 +-11 1--- +TAG_NAMELIMIT = 0x0139 # v--- ---1 +-11 1--1 +TAG_FILELIMIT = 0x013a # v--- ---1 +-11 1-1- +TAG_GDELTA = 0x0200 ## v--- --1- +ttt tttt +TAG_GRMDELTA = 0x0230 # v--- --1- +-11 --++ +TAG_GBMAPDELTA = 0x0234 # v--- --1- +-11 -1rr +TAG_NAME = 0x0300 ## v--- --11 +ttt tttt +TAG_BNAME = 0x0300 # v--- --11 +--- ---- +TAG_REG = 0x0301 # v--- --11 +--- ---1 +TAG_DIR = 0x0302 # v--- --11 +--- --1- +TAG_STICKYNOTE = 0x0303 # v--- --11 +--- --11 +TAG_BOOKMARK = 0x0304 # v--- --11 +--- -1-- +TAG_MNAME = 0x0330 # v--- --11 +-11 ---- +TAG_STRUCT = 0x0400 ## v--- -1-- +ttt tttt +TAG_BRANCH = 0x0400 # v--- -1-- +--- --rr +TAG_DATA = 0x0404 # v--- -1-- +--- -1rr +TAG_BLOCK = 0x0408 # v--- -1-- +--- 1err +TAG_DID = 0x0420 # v--- -1-- +-1- ---- +TAG_BSHRUB = 0x0428 # v--- -1-- +-1- 1-rr +TAG_BTREE = 0x042c # v--- -1-- +-1- 11rr +TAG_MROOT = 0x0431 # v--- -1-- +-11 --rr +TAG_MDIR = 0x0435 # v--- -1-- +-11 -1rr +TAG_MTREE = 0x043c # v--- -1-- +-11 11rr +TAG_BMRANGE = 0x0440 # v--- -1-- +1-- ++uu +TAG_BMFREE = 0x0440 # v--- -1-- +1-- ---- +TAG_BMINUSE = 0x0441 # v--- -1-- +1-- ---1 +TAG_BMERASED = 0x0442 # v--- -1-- +1-- --1- +TAG_BMBAD = 0x0443 # v--- -1-- +1-- --11 +TAG_ATTR = 0x0600 ## v--- -11a +aaa aaaa +TAG_UATTR = 0x0600 # v--- -11- +aaa aaaa +TAG_SATTR = 0x0700 # v--- -111 +aaa aaaa +TAG_SHRUB = 0x1000 ## v--1 kkkk +kkk kkkk +TAG_ALT = 0x4000 ## v1cd kkkk +kkk kkkk +TAG_B = 0x0000 +TAG_R = 0x2000 +TAG_LE = 0x0000 +TAG_GT = 0x1000 +TAG_CKSUM = 0x3000 ## v-11 ---- ++++ +pqq +TAG_PHASE = 0x0003 +TAG_PERTURB = 0x0004 +TAG_NOTE = 0x3100 ## v-11 ---1 ++++ ++++ +TAG_ECKSUM = 0x3200 ## v-11 --1- ++++ ++++ +TAG_GCKSUMDELTA = 0x3300 ## v-11 --11 ++++ ++++ + + +# self-parsing tag repr +class Tag: + def __init__(self, name, tag, encoding, help): + self.name = name + self.tag = tag + self.encoding = encoding + self.help = help + # derive mask from encoding + self.mask = sum( + (1 if x in 'v-01' else 0) << len(self.encoding)-1-i + for i, x in enumerate(self.encoding)) + + def __repr__(self): + return 'Tag(%r, %r, %r)' % ( + self.name, + self.tag, + self.encoding) + + def __eq__(self, other): + return self.name == other.name + + def __ne__(self, other): + return self.name != other.name + + def __hash__(self): + return hash(self.name) + + def line(self): + # substitute mask chars when zero + tag = '0x%s' % ''.join( + n if n != '0' else next( + (x for x in self.encoding[i*4:i*4+4] + if x not in 'v-01+'), + '0') + for i, n in enumerate('%04x' % self.tag)) + # group into nibbles + encoding = ' '.join(self.encoding[i*4:i*4+4] + for i in range(len(self.encoding)//4)) + return ('LFS3_%s' % self.name, tag, encoding) + + def specificity(self): + return sum(1 for x in self.encoding if x in 'v-01') + + def matches(self, tag): + return (tag & self.mask) == (self.tag & self.mask) + + def get(self, chars, tag): + return sum( + tag & ((1 if x in chars else 0) << len(self.encoding)-1-i) + for i, x in enumerate(self.encoding)) + + def max(self, chars): + return max(len(self.encoding)-1-i + for i, x in enumerate(self.encoding) if x in chars) + + def min(self, chars): + return min(len(self.encoding)-1-i + for i, x in enumerate(self.encoding) if x in chars) + + def width(self, chars): + return self.max(chars) - self.min(chars) + + def __contains__(self, chars): + return any(x in self.encoding for x in chars) + + @staticmethod + @ft.cache + def tags(): + # parse our script's source to figure out tags + import inspect + import re + tags = [] + tag_pattern = re.compile( + '^(?PTAG_[^ ]*) *= *(?P[^#]*?) *' + '#+ *(?P(?:[^ ] *?){16}) *(?P.*)$') + for line in (inspect.getsource( + inspect.getmodule(inspect.currentframe())) + .replace('\\\n', '') + .splitlines()): + m = tag_pattern.match(line) + if m: + tags.append(Tag( + m.group('name'), + globals()[m.group('name')], + m.group('encoding').replace(' ', ''), + m.group('help'))) + return tags + + # find best matching tag + @staticmethod + def find(tag): + # find tags, note this is cached + tags__ = Tag.tags() + + # find the most specific matching tag, ignoring valid bits + return max((t for t in tags__ if t.matches(tag & 0x7fff)), + key=lambda t: t.specificity(), + default=None) + + # human readable tag repr + @staticmethod + def repr(tag, weight=None, size=None, *, + global_=False, + toff=None): + # find the most specific matching tag, ignoring the shrub bit + t = Tag.find(tag & ~(TAG_SHRUB if tag & 0x7000 == TAG_SHRUB else 0)) + + # build repr + r = [] + # normal tag? + if not tag & TAG_ALT: + if t is not None: + # prefix shrub tags with shrub + if tag & 0x7000 == TAG_SHRUB: + r.append('shrub') + # lowercase name + r.append(t.name.split('_', 1)[1].lower()) + # gstate tag? + if global_: + if r[-1] == 'gdelta': + r[-1] = 'gstate' + elif r[-1].endswith('delta'): + r[-1] = r[-1][:-len('delta')] + # include perturb/phase bits + if 'q' in t: + r.append('q%d' % t.get('q', tag)) + if 'p' in t and tag & TAG_PERTURB: + r.append('p') + + # include unmatched fields, but not just redund, and + # only reserved bits if non-zero + if 'tua' in t or ('+' in t and t.get('+', tag) != 0): + r.append(' 0x%0*x' % ( + (t.width('tuar+')+4-1)//4, + t.get('tuar+', tag))) + # unknown tag? + else: + r.append('0x%04x' % tag) + + # weight? + if weight: + r.append(' w%d' % weight) + # size? don't include if null + if size is not None and (size or tag & 0x7fff): + r.append(' %d' % size) + + # alt pointer? + else: + r.append('alt') + r.append('r' if tag & TAG_R else 'b') + r.append('gt' if tag & TAG_GT else 'le') + r.append(' 0x%0*x' % ( + (t.width('k')+4-1)//4, + t.get('k', tag))) + + # weight? + if weight is not None: + r.append(' w%d' % weight) + # jump? + if size and toff is not None: + r.append(' 0x%x' % (0xffffffff & (toff-size))) + elif size: + r.append(' -%d' % size) + + return ''.join(r) + + +# some ways of block geometry representations +# 512 -> 512 +# 512x16 -> (512, 16) +# 0x200x10 -> (512, 16) +def bdgeom(s): + s = s.strip() + b = 10 + if s.startswith('0x') or s.startswith('0X'): + s = s[2:] + b = 16 + elif s.startswith('0o') or s.startswith('0O'): + s = s[2:] + b = 8 + elif s.startswith('0b') or s.startswith('0B'): + s = s[2:] + b = 2 + + if 'x' in s: + s, s_ = s.split('x', 1) + return (int(s, b), int(s_, b)) + else: + return int(s, b) + +# parse some rbyd addr encodings +# 0xa -> (0xa,) +# 0xa.c -> ((0xa, 0xc),) +# 0x{a,b} -> (0xa, 0xb) +# 0x{a,b}.c -> ((0xa, 0xc), (0xb, 0xc)) +def rbydaddr(s): + s = s.strip() + b = 10 + if s.startswith('0x') or s.startswith('0X'): + s = s[2:] + b = 16 + elif s.startswith('0o') or s.startswith('0O'): + s = s[2:] + b = 8 + elif s.startswith('0b') or s.startswith('0B'): + s = s[2:] + b = 2 + + trunk = None + if '.' in s: + s, s_ = s.split('.', 1) + trunk = int(s_, b) + + if s.startswith('{') and '}' in s: + ss = s[1:s.find('}')].split(',') + else: + ss = [s] + + addr = [] + for s in ss: + if trunk is not None: + addr.append((int(s, b), trunk)) + else: + addr.append(int(s, b)) + + return tuple(addr) + +def crc32c(data, crc=0): + if crc32c_lib is not None: + return crc32c_lib.crc32c(data, crc) + else: + crc ^= 0xffffffff + for b in data: + crc ^= b + for j in range(8): + crc = (crc >> 1) ^ ((crc & 1) * 0x82f63b78) + return 0xffffffff ^ crc + +def popc(x): + return bin(x).count('1') + +def parity(x): + return popc(x) & 1 + +def fromle32(data, j=0): + return struct.unpack('H', data[j:j+2].ljust(2, b'\0'))[0]; d += 2 + weight, d_ = fromleb128(data, j+d); d += d_ + size, d_ = fromleb128(data, j+d); d += d_ + return tag>>15, tag&0x7fff, weight, size, d + +def xxd(data, width=16): + for i in range(0, len(data), width): + yield '%-*s %-*s' % ( + 3*width, + ' '.join('%02x' % b for b in data[i:i+width]), + width, + ''.join( + b if b >= ' ' and b <= '~' else '.' + for b in map(chr, data[i:i+width]))) + + +# a simple wrapper over an open file with bd geometry +class Bd: + def __init__(self, f, block_size=None, block_count=None): + self.f = f + self.block_size = block_size + self.block_count = block_count + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, self.repr()) + + def repr(self): + return 'bd %sx%s' % (self.block_size, self.block_count) + + def read(self, block, off, size): + self.f.seek(block*self.block_size + off) + return self.f.read(size) + + def readblock(self, block): + self.f.seek(block*self.block_size) + return self.f.read(self.block_size) + +# tagged data in an rbyd +class Rattr: + def __init__(self, tag, weight, blocks, toff, tdata, data): + self.tag = tag + self.weight = weight + if isinstance(blocks, int): + self.blocks = (blocks,) + else: + self.blocks = blocks + self.toff = toff + self.tdata = tdata + self.data = data + + @property + def block(self): + return self.blocks[0] + + @property + def tsize(self): + return len(self.tdata) + + @property + def off(self): + return self.toff + len(self.tdata) + + @property + def size(self): + return len(self.data) + + def __bytes__(self): + return self.data + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, self.repr()) + + def repr(self): + return Tag.repr(self.tag, self.weight, self.size) + + def __iter__(self): + return iter((self.tag, self.weight, self.data)) + + def __eq__(self, other): + return ((self.tag, self.weight, self.data) + == (other.tag, other.weight, other.data)) + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash((self.tag, self.weight, self.data)) + + # convenience for did/name access + def _parse_name(self): + # note we return a null name for non-name tags, this is so + # vestigial names in btree nodes act as a catch-all + if (self.tag & 0xff00) != TAG_NAME: + did = 0 + name = b'' + else: + did, d = fromleb128(self.data) + name = self.data[d:] + + # cache both + self.did = did + self.name = name + + @ft.cached_property + def did(self): + self._parse_name() + return self.did + + @ft.cached_property + def name(self): + self._parse_name() + return self.name + +class Ralt: + def __init__(self, tag, weight, blocks, toff, tdata, jump, + color=None, followed=None): + self.tag = tag + self.weight = weight + if isinstance(blocks, int): + self.blocks = (blocks,) + else: + self.blocks = blocks + self.toff = toff + self.tdata = tdata + self.jump = jump + + if color is not None: + self.color = color + else: + self.color = 'r' if tag & TAG_R else 'b' + self.followed = followed + + @property + def block(self): + return self.blocks[0] + + @property + def tsize(self): + return len(self.tdata) + + @property + def off(self): + return self.toff + len(self.tdata) + + @property + def joff(self): + return self.toff - self.jump + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, self.repr()) + + def repr(self): + return Tag.repr(self.tag, self.weight, self.jump, toff=self.toff) + + def __iter__(self): + return iter((self.tag, self.weight, self.jump)) + + def __eq__(self, other): + return ((self.tag, self.weight, self.jump) + == (other.tag, other.weight, other.jump)) + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash((self.tag, self.weight, self.jump)) + + +# our core rbyd type +class Rbyd: + def __init__(self, blocks, trunk, weight, rev, eoff, cksum, data, *, + shrub=False, + gcksumdelta=None, + redund=0): + if isinstance(blocks, int): + self.blocks = (blocks,) + else: + self.blocks = blocks + self.trunk = trunk + self.weight = weight + self.rev = rev + self.eoff = eoff + self.cksum = cksum + self.data = data + + self.shrub = shrub + self.gcksumdelta = gcksumdelta + self.redund = redund + + @property + def block(self): + return self.blocks[0] + + @property + def corrupt(self): + # use redund=-1 to indicate corrupt rbyds + return self.redund >= 0 + + def addr(self): + if len(self.blocks) == 1: + return '0x%x.%x' % (self.block, self.trunk) + else: + return '0x{%s}.%x' % ( + ','.join('%x' % block for block in self.blocks), + self.trunk) + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, self.repr()) + + def repr(self): + return 'rbyd %s w%s' % (self.addr(), self.weight) + + def __bool__(self): + # use redund=-1 to indicate corrupt rbyds + return self.redund >= 0 + + def __eq__(self, other): + return ((frozenset(self.blocks), self.trunk) + == (frozenset(other.blocks), other.trunk)) + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash((frozenset(self.blocks), self.trunk)) + + @classmethod + def _fetch(cls, data, block, trunk=None): + # fetch the rbyd + rev = fromle32(data, 0) + cksum = 0 + cksum_ = crc32c(data[0:4]) + cksum__ = cksum_ + perturb = False + eoff = 0 + eoff_ = None + j_ = 4 + trunk_ = 0 + trunk__ = 0 + trunk___ = 0 + weight = 0 + weight_ = 0 + weight__ = 0 + gcksumdelta = None + gcksumdelta_ = None + while j_ < len(data) and (not trunk or eoff <= trunk): + # read next tag + v, tag, w, size, d = fromtag(data, j_) + if v != parity(cksum__): + break + cksum__ ^= 0x00000080 if v else 0 + cksum__ = crc32c(data[j_:j_+d], cksum__) + j_ += d + if not tag & TAG_ALT and j_ + size > len(data): + break + + # take care of cksums + if not tag & TAG_ALT: + if (tag & 0xff00) != TAG_CKSUM: + cksum__ = crc32c(data[j_:j_+size], cksum__) + + # found a gcksumdelta? + if (tag & 0xff00) == TAG_GCKSUMDELTA: + gcksumdelta_ = Rattr(tag, w, block, j_-d, + data[j_-d:j_], + data[j_:j_+size]) + + # found a cksum? + else: + # check cksum + cksum___ = fromle32(data, j_) + if cksum__ != cksum___: + break + # commit what we have + eoff = eoff_ if eoff_ else j_ + size + cksum = cksum_ + trunk_ = trunk__ + weight = weight_ + gcksumdelta = gcksumdelta_ + gcksumdelta_ = None + # update perturb bit + perturb = bool(tag & TAG_PERTURB) + # revert to data cksum and perturb + cksum__ = cksum_ ^ (0xfca42daf if perturb else 0) + + # evaluate trunks + if (tag & 0xf000) != TAG_CKSUM: + if not (trunk and j_-d > trunk and not trunk___): + # new trunk? + if not trunk___: + trunk___ = j_-d + weight__ = 0 + + # keep track of weight + weight__ += w + + # end of trunk? + if not tag & TAG_ALT: + # update trunk/weight unless we found a shrub or an + # explicit trunk (which may be a shrub) is requested + if not tag & TAG_SHRUB or trunk___ == trunk: + trunk__ = trunk___ + weight_ = weight__ + # keep track of eoff for best matching trunk + if trunk and j_ + size > trunk: + eoff_ = j_ + size + eoff = eoff_ + cksum = cksum__ ^ ( + 0xfca42daf if perturb else 0) + trunk_ = trunk__ + weight = weight_ + gcksumdelta = gcksumdelta_ + trunk___ = 0 + + # update canonical checksum, xoring out any perturb state + cksum_ = cksum__ ^ (0xfca42daf if perturb else 0) + + if not tag & TAG_ALT: + j_ += size + + return cls(block, trunk_, weight, rev, eoff, cksum, data, + gcksumdelta=gcksumdelta, + redund=0 if trunk_ else -1) + + @classmethod + def fetch(cls, bd, blocks, trunk=None): + # multiple blocks? + if not isinstance(blocks, int): + # fetch all blocks + rbyds = [cls.fetch(bd, block, trunk) for block in blocks] + + # determine most recent revision/trunk + rev, trunk = None, None + for rbyd in rbyds: + # compare with sequence arithmetic + if rbyd and ( + rev is None + or not ((rbyd.rev - rev) & 0x80000000) + or (rbyd.rev == rev and rbyd.trunk > trunk)): + rev, trunk = rbyd.rev, rbyd.trunk + # sort for reproducibility + rbyds.sort(key=lambda rbyd: ( + # prioritize valid redund blocks + 0 if rbyd and rbyd.rev == rev and rbyd.trunk == trunk + else 1, + # default to sorting by block + rbyd.block)) + + # choose an active rbyd + rbyd = rbyds[0] + # keep track of the other blocks + rbyd.blocks = tuple(rbyd.block for rbyd in rbyds) + # keep track of how many redund blocks are valid + rbyd.redund = -1 + sum(1 for rbyd in rbyds + if rbyd and rbyd.rev == rev and rbyd.trunk == trunk) + # and patch the gcksumdelta if we have one + if rbyd.gcksumdelta is not None: + rbyd.gcksumdelta.blocks = rbyd.blocks + return rbyd + + # seek/read the block + block = blocks + data = bd.readblock(block) + + # fetch the rbyd + return cls._fetch(data, block, trunk) + + @classmethod + def fetchck(cls, bd, blocks, trunk, weight, cksum): + # try to fetch the rbyd normally + rbyd = cls.fetch(bd, blocks, trunk) + + # cksum mismatch? trunk/weight mismatch? + if (rbyd.cksum != cksum + or rbyd.trunk != trunk + or rbyd.weight != weight): + # mark as corrupt and keep track of expected trunk/weight + rbyd.redund = -1 + rbyd.trunk = trunk + rbyd.weight = weight + + return rbyd + + @classmethod + def fetchshrub(cls, rbyd, trunk): + # steal the original rbyd's data + # + # this helps avoid race conditions with cksums and stuff + shrub = cls._fetch(rbyd.data, rbyd.block, trunk) + shrub.blocks = rbyd.blocks + shrub.shrub = True + return shrub + + def lookupnext(self, rid, tag=None, *, + path=False): + if not self or rid >= self.weight: + if path: + return None, None, [] + else: + return None, None + + tag = max(tag or 0, 0x1) + lower = 0 + upper = self.weight + path_ = [] + + # descend down tree + j = self.trunk + while True: + _, alt, w, jump, d = fromtag(self.data, j) + + # found an alt? + if alt & TAG_ALT: + # follow? + if ((rid, tag & 0xfff) > (upper-w-1, alt & 0xfff) + if alt & TAG_GT + else ((rid, tag & 0xfff) + <= (lower+w-1, alt & 0xfff))): + lower += upper-lower-w if alt & TAG_GT else 0 + upper -= upper-lower-w if not alt & TAG_GT else 0 + j = j - jump + + if path: + # figure out which color + if alt & TAG_R: + _, nalt, _, _, _ = fromtag(self.data, j+jump+d) + if nalt & TAG_R: + color = 'y' + else: + color = 'r' + else: + color = 'b' + + path_.append(Ralt( + alt, w, self.blocks, j+jump, + self.data[j+jump:j+jump+d], jump, + color=color, + followed=True)) + + # stay on path + else: + lower += w if not alt & TAG_GT else 0 + upper -= w if alt & TAG_GT else 0 + j = j + d + + if path: + # figure out which color + if alt & TAG_R: + _, nalt, _, _, _ = fromtag(self.data, j) + if nalt & TAG_R: + color = 'y' + else: + color = 'r' + else: + color = 'b' + + path_.append(Ralt( + alt, w, self.blocks, j-d, + self.data[j-d:j], jump, + color=color, + followed=False)) + + # found tag + else: + rid_ = upper-1 + tag_ = alt + w_ = upper-lower + + if not tag_ or (rid_, tag_) < (rid, tag): + if path: + return None, None, path_ + else: + return None, None + + rattr_ = Rattr(tag_, w_, self.blocks, j, + self.data[j:j+d], + self.data[j+d:j+d+jump]) + if path: + return rid_, rattr_, path_ + else: + return rid_, rattr_ + + def lookup(self, rid, tag=None, mask=None, *, + path=False): + if tag is None: + tag, mask = 0, 0xffff + if mask is None: + mask = 0 + + r = self.lookupnext(rid, tag & ~mask, + path=path) + if path: + rid_, rattr_, path_ = r + else: + rid_, rattr_ = r + if (rid_ is None + or rid_ != rid + or (rattr_.tag & ~mask & 0xfff) + != (tag & ~mask & 0xfff)): + if path: + return None, path_ + else: + return None + + if path: + return rattr_, path_ + else: + return rattr_ + + def rids(self, *, + path=False): + rid = -1 + while True: + r = self.lookupnext(rid, + path=path) + if path: + rid, name, path_ = r + else: + rid, name = r + # found end of tree? + if rid is None: + break + + if path: + yield rid, name, path_ + else: + yield rid, name + rid += 1 + + def rattrs(self, rid=None, tag=None, mask=None, *, + path=False): + if rid is None: + rid, tag = -1, 0 + while True: + r = self.lookupnext(rid, tag+0x1, + path=path) + if path: + rid, rattr, path_ = r + else: + rid, rattr = r + # found end of tree? + if rid is None: + break + + if path: + yield rid, rattr, path_ + else: + yield rid, rattr + tag = rattr.tag + else: + if tag is None: + tag, mask = 0, 0xffff + if mask is None: + mask = 0 + + tag_ = max((tag & ~mask) - 1, 0) + while True: + r = self.lookupnext(rid, tag_+0x1, + path=path) + if path: + rid_, rattr_, path_ = r + else: + rid_, rattr_ = r + # found end of tree? + if (rid_ is None + or rid_ != rid + or (rattr_.tag & ~mask & 0xfff) + != (tag & ~mask & 0xfff)): + break + + if path: + yield rattr_, path_ + else: + yield rattr_ + tag_ = rattr_.tag + + # lookup by name + def namelookup(self, did, name): + # binary search + best = None, None + lower = 0 + upper = self.weight + while lower < upper: + rid, name_ = self.lookupnext( + lower + (upper-1-lower)//2) + if rid is None: + break + + # bisect search space + if (name_.did, name_.name) > (did, name): + upper = rid-(name_.weight-1) + elif (name_.did, name_.name) < (did, name): + lower = rid + 1 + # keep track of best match + best = rid, name_ + else: + # found a match + return rid, name_ + + return best + + + +# jump renderer +class JumpArt: + # abstract thing for jump rendering + class Jump(co.namedtuple('Jump', ['a', 'b', 'x', 'color'])): + __slots__ = () + def __new__(cls, a, b, x=0, color='b'): + return super().__new__(cls, a, b, x, color) + + def __repr__(self): + return '%s(%s, %s, %s, %s)' % ( + self.__class__.__name__, + self.a, + self.b, + self.x, + self.color) + + # don't include color in branch comparisons, or else our tree + # renderings can end up with inconsistent colors between runs + def __eq__(self, other): + return (self.a, self.b, self.x) == (other.a, other.b, other.x) + + def __ne__(self, other): + return (self.a, self.b, self.x) != (other.a, other.b, other.x) + + def __hash__(self): + return hash((self.a, self.b, self.x)) + + def __init__(self, jumps): + self.jumps = jumps + self.width = 2*max((x for _, _, x, _ in jumps), default=0) + + @classmethod + def collide(cls, jumps): + # figure out x-offsets to avoid collisions between jumps + for j in range(len(jumps)): + a, b, _, c = jumps[j] + x = 0 + while any( + max(a, b) >= min(a_, b_) + and max(a_, b_) >= min(a, b) + and x == x_ + for a_, b_, x_, _ in jumps[:j]): + x += 1 + jumps[j] = cls.Jump(a, b, x, c) + + return jumps + + @classmethod + def fromrbyd(cls, rbyd, all=False): + import builtins + all_, all = all, builtins.all + + jumps = [] + j_ = 4 + while j_ < (len(rbyd.data) if all_ else rbyd.eoff): + j = j_ + v, tag, w, size, d = fromtag(rbyd.data, j_) + j_ += d + if not tag & TAG_ALT: + j_ += size + + if tag & TAG_ALT and size: + # figure out which alt color + if tag & TAG_R: + _, ntag, _, _, _ = fromtag(rbyd.data, j_) + if ntag & TAG_R: + jumps.append(cls.Jump(j, j-size, 0, 'y')) + else: + jumps.append(cls.Jump(j, j-size, 0, 'r')) + else: + jumps.append(cls.Jump(j, j-size, 0, 'b')) + + jumps = cls.collide(jumps) + return cls(jumps) + + def repr(self, j, color=False): + # render jumps + chars = {} + for a, b, x, c in self.jumps: + c_start = ( + '\x1b[33m' if color and c == 'y' + else '\x1b[31m' if color and c == 'r' + else '\x1b[1;30m' if color + else '') + c_stop = '\x1b[m' if color else '' + + if j == a: + for x_ in range(2*x+1): + chars[x_] = '%s-%s' % (c_start, c_stop) + chars[2*x+1] = '%s\'%s' % (c_start, c_stop) + elif j == b: + for x_ in range(2*x+1): + chars[x_] = '%s-%s' % (c_start, c_stop) + chars[2*x+1] = '%s.%s' % (c_start, c_stop) + chars[0] = '%s<%s' % (c_start, c_stop) + elif j >= min(a, b) and j <= max(a, b): + chars[2*x+1] = '%s|%s' % (c_start, c_stop) + + return ''.join(chars.get(x, ' ') + for x in range(max(chars.keys(), default=0)+1)) + + +# lifetime renderer +class LifetimeArt: + # abstract things for lifetime rendering + class Lifetime(co.namedtuple('Lifetime', ['id', 'origin', 'tags'])): + __slots__ = () + def __new__(cls, id, origin, tags=None): + return super().__new__(cls, id, origin, + set(tags) if tags is not None else set()) + + def __repr__(self): + return '%s(%s, %s, %s)' % ( + self.__class__.__name__, + self.id, + self.origin, + self.tags) + + def add(self, j): + self.tags.add(j) + + def __bool__(self): + return bool(self.tags) + + # define equality by id + def __eq__(self, other): + return self.id == other.id + + def __ne__(self, other): + return self.id != other.id + + def __hash__(self): + return hash((self.id)) + + def __lt__(self, other): + return self.id < other.id + + def __le__(self, other): + return self.id <= other.id + + def __gt__(self, other): + return self.id > other.id + + def __ge__(self, other): + return self.id >= other.id + + class Checkpoint(co.namedtuple('Checkpoint', [ + 'j', 'weights', 'lifetimes', 'grows', 'shrinks', 'tags'])): + __slots__ = () + def __new__(cls, j, weights, lifetimes, + grows=None, shrinks=None, tags=None): + return super().__new__(cls, j, + # note we rely on tuple making frozen copies here + tuple(weights), + tuple(lifetimes), + frozenset(grows) if grows is not None else frozenset(), + frozenset(shrinks) if shrinks is not None else frozenset(), + frozenset(tags) if tags is not None else frozenset()) + + # define equality by checkpoint offset + def __eq__(self, other): + return self.j == other.j + + def __ne__(self, other): + return self.j != other.j + + def __hash__(self): + return hash((self.j)) + + def __lt__(self, other): + return self.j < other.j + + def __le__(self, other): + return self.j <= other.j + + def __gt__(self, other): + return self.j > other.j + + def __ge__(self, other): + return self.j >= other.j + + def __init__(self, checkpoints): + self.lifetimes = sorted(set( + lifetime + for checkpoint in checkpoints + for lifetime in checkpoint.lifetimes)) + self.checkpoints = checkpoints + self.width = 2*max( + (sum(1 for lifetime in checkpoint.lifetimes if lifetime) + for checkpoint in checkpoints), + default=0) + + @staticmethod + def index(weights, rid): + for i, w in enumerate(weights): + if rid < w: + return i, rid + rid -= w + return len(weights), 0 + + @classmethod + def fromrbyd(cls, rbyd, all=False): + import builtins + all_, all = all, builtins.all + + # first figure out where each rid comes from + id = 0 + weights = [] + lifetimes = [] + checkpoints = [cls.Checkpoint(0, [], [])] + + lower_, upper_ = 0, 0 + weight_ = 0 + trunk_ = 0 + j_ = 4 + while j_ < (len(rbyd.data) if all_ else rbyd.eoff): + j = j_ + v, tag, w, size, d = fromtag(rbyd.data, j_) + j_ += d + if not tag & TAG_ALT: + j_ += size + + # evaluate trunks + if (tag & 0xf000) != TAG_CKSUM: + if not trunk_: + trunk_ = j_-d + lower_, upper_ = 0, 0 + + if tag & TAG_ALT and not tag & TAG_GT: + lower_ += w + else: + upper_ += w + + if not tag & TAG_ALT: + # derive the current tag's rid from alt weights + delta = (lower_+upper_) - weight_ + weight_ = lower_+upper_ + rid = lower_ + w-1 + trunk_ = 0 + + if (tag & 0xf000) != TAG_CKSUM and not tag & TAG_ALT: + # note we ignore out-of-bounds here for debugging + if delta > 0: + # grow lifetimes + l = cls.Lifetime(id, j) + id += 1 + i, rid_ = cls.index(weights, lower_) + if rid_ > 0: + weights[i:i+1] = [rid_, delta, weights[i]-rid_] + lifetimes[i:i+1] = [lifetimes[i], l, lifetimes[i]] + else: + weights[i:i] = [delta] + lifetimes[i:i] = [l] + + checkpoints.append(cls.Checkpoint( + j, weights, lifetimes, + grows={i}, + tags={i})) + + elif delta < 0: + # shrink lifetimes + i, rid_ = cls.index(weights, lower_) + delta_ = -delta + weights_ = weights.copy() + lifetimes_ = lifetimes.copy() + shrinks = set() + while delta_ > 0 and i < len(weights_): + if weights_[i] > delta_: + delta__ = min(delta_, weights_[i]-rid_) + delta_ -= delta__ + weights_[i] -= delta__ + i += 1 + rid_ = 0 + else: + delta_ -= weights_[i] + weights_[i:i+1] = [] + lifetimes_[i:i+1] = [] + shrinks.add(i + len(shrinks)) + + checkpoints.append(cls.Checkpoint( + j, weights, lifetimes, + shrinks=shrinks, + tags={i})) + weights = weights_ + lifetimes = lifetimes_ + + if rid >= 0: + # attach tag to lifetime + i, rid_ = cls.index(weights, rid) + if i < len(weights): + lifetimes[i].add(j) + + if delta == 0: + checkpoints.append(cls.Checkpoint( + j, weights, lifetimes, + tags={i})) + + return cls(checkpoints) + + def repr(self, j, color=False): + i = bisect.bisect(self.checkpoints, j, + key=lambda checkpoint: checkpoint.j) - 1 + j_, weights, lifetimes, grows, shrinks, tags = self.checkpoints[i] + + reprs = [] + colors = [] + was = None + for i, (w, lifetime) in enumerate(zip(weights, lifetimes)): + # skip lifetimes with no tags and shrinks + if not lifetime or (j != j_ and i in shrinks): + if i in grows or i in shrinks or i in tags: + tags = tags | {i+1} + continue + + if j == j_ and i in grows: + reprs.append('.') + was = 'grow' + elif j == j_ and i in shrinks: + reprs.append('\'') + was = 'shrink' + elif j == j_ and i in tags: + reprs.append('* ') + elif was == 'grow': + reprs.append('\\ ') + elif was == 'shrink': + reprs.append('/ ') + else: + reprs.append('| ') + + colors.append(COLORS[lifetime.id % len(COLORS)]) + + return '%s%*s' % ( + ''.join('%s%s%s' % ( + '\x1b[%sm' % c if color else '', + r, + '\x1b[m' if color else '') + for r, c in zip(reprs, colors)), + self.width - sum(len(r) for r in reprs), '') + + +# tree renderer +class TreeArt: + # tree branches are an abstract thing for tree rendering + class Branch(co.namedtuple('Branch', ['a', 'b', 'z', 'color'])): + __slots__ = () + def __new__(cls, a, b, z=0, color='b'): + # a and b are context specific + return super().__new__(cls, a, b, z, color) + + def __repr__(self): + return '%s(%s, %s, %s, %s)' % ( + self.__class__.__name__, + self.a, + self.b, + self.z, + self.color) + + # don't include color in branch comparisons, or else our tree + # renderings can end up with inconsistent colors between runs + def __eq__(self, other): + return (self.a, self.b, self.z) == (other.a, other.b, other.z) + + def __ne__(self, other): + return (self.a, self.b, self.z) != (other.a, other.b, other.z) + + def __hash__(self): + return hash((self.a, self.b, self.z)) + + # also order by z first, which can be useful for reproducibly + # prioritizing branches when simplifying trees + def __lt__(self, other): + return (self.z, self.a, self.b) < (other.z, other.a, other.b) + + def __le__(self, other): + return (self.z, self.a, self.b) <= (other.z, other.a, other.b) + + def __gt__(self, other): + return (self.z, self.a, self.b) > (other.z, other.a, other.b) + + def __ge__(self, other): + return (self.z, self.a, self.b) >= (other.z, other.a, other.b) + + # apply a function to a/b while trying to avoid copies + def map(self, filter_, map_=None): + if map_ is None: + filter_, map_ = None, filter_ + + a = self.a + if filter_ is None or filter_(a): + a = map_(a) + + b = self.b + if filter_ is None or filter_(b): + b = map_(b) + + if a != self.a or b != self.b: + return self.__class__( + a if a != self.a else self.a, + b if b != self.b else self.b, + self.z, + self.color) + else: + return self + + def __init__(self, tree): + self.tree = tree + self.depth = max((t.z+1 for t in tree), default=0) + if self.depth > 0: + self.width = 2*self.depth + 2 + else: + self.width = 0 + + def __iter__(self): + return iter(self.tree) + + def __bool__(self): + return bool(self.tree) + + def __len__(self): + return len(self.tree) + + # render an rbyd rbyd tree for debugging + @classmethod + def _fromrbydrtree(cls, rbyd, **args): + trunks = co.defaultdict(lambda: (-1, 0)) + alts = co.defaultdict(lambda: {}) + + for rid, rattr, path in rbyd.rattrs(path=True): + # keep track of trunks/alts + trunks[rattr.toff] = (rid, rattr.tag) + + for ralt in path: + if ralt.followed: + alts[ralt.toff] |= {'f': ralt.joff, 'c': ralt.color} + else: + alts[ralt.toff] |= {'nf': ralt.off, 'c': ralt.color} + + if args.get('tree_rbyd_all'): + # treat unreachable alts as converging paths + for j_, alt in alts.items(): + if 'f' not in alt: + alt['f'] = alt['nf'] + elif 'nf' not in alt: + alt['nf'] = alt['f'] + + else: + # prune any alts with unreachable edges + pruned = {} + for j, alt in alts.items(): + if 'f' not in alt: + pruned[j] = alt['nf'] + elif 'nf' not in alt: + pruned[j] = alt['f'] + for j in pruned.keys(): + del alts[j] + + for j, alt in alts.items(): + while alt['f'] in pruned: + alt['f'] = pruned[alt['f']] + while alt['nf'] in pruned: + alt['nf'] = pruned[alt['nf']] + + # find the trunk and depth of each alt + def rec_trunk(j): + if j not in alts: + return trunks[j] + else: + if 'nft' not in alts[j]: + alts[j]['nft'] = rec_trunk(alts[j]['nf']) + return alts[j]['nft'] + + for j in alts.keys(): + rec_trunk(j) + for j, alt in alts.items(): + if alt['f'] in alts: + alt['ft'] = alts[alt['f']]['nft'] + else: + alt['ft'] = trunks[alt['f']] + + def rec_height(j): + if j not in alts: + return 0 + else: + if 'h' not in alts[j]: + alts[j]['h'] = max( + rec_height(alts[j]['f']), + rec_height(alts[j]['nf'])) + 1 + return alts[j]['h'] + + for j in alts.keys(): + rec_height(j) + + t_depth = max((alt['h']+1 for alt in alts.values()), default=0) + + # convert to more general tree representation + tree = set() + for j, alt in alts.items(): + # note all non-trunk edges should be colored black + tree.add(cls.Branch( + alt['nft'], + alt['nft'], + t_depth-1 - alt['h'], + alt['c'])) + if alt['ft'] != alt['nft']: + tree.add(cls.Branch( + alt['nft'], + alt['ft'], + t_depth-1 - alt['h'], + 'b')) + + return cls(tree) + + # render an rbyd btree tree for debugging + @classmethod + def _fromrbydbtree(cls, rbyd, **args): + # for rbyds this is just a pointer to every rid + tree = set() + root = None + for rid, name in rbyd.rids(): + b = (rid, name.tag) + if root is None: + root = b + tree.add(cls.Branch(root, b)) + return cls(tree) + + # render an rbyd tree for debugging + @classmethod + def fromrbyd(cls, rbyd, **args): + if args.get('tree_btree'): + return cls._fromrbydbtree(rbyd, **args) + else: + return cls._fromrbydrtree(rbyd, **args) + + # render some nice ascii trees + def repr(self, x, color=False): + if self.depth == 0: + return '' + + def branchrepr(tree, x, d, was): + for t in tree: + if t.z == d and t.b == x: + if any(t.z == d and t.a == x + for t in tree): + return '+-', t.color, t.color + elif any(t.z == d + and x > min(t.a, t.b) + and x < max(t.a, t.b) + for t in tree): + return '|-', t.color, t.color + elif t.a < t.b: + return '\'-', t.color, t.color + else: + return '.-', t.color, t.color + for t in tree: + if t.z == d and t.a == x: + return '+ ', t.color, None + for t in tree: + if (t.z == d + and x > min(t.a, t.b) + and x < max(t.a, t.b)): + return '| ', t.color, was + if was: + return '--', was, was + return ' ', None, None + + trunk = [] + was = None + for d in range(self.depth): + t, c, was = branchrepr(self.tree, x, d, was) + + trunk.append('%s%s%s%s' % ( + '\x1b[33m' if color and c == 'y' + else '\x1b[31m' if color and c == 'r' + else '\x1b[1;30m' if color and c == 'b' + else '', + t, + ('>' if was else ' ') if d == self.depth-1 else '', + '\x1b[m' if color and c else '')) + + return '%s ' % ''.join(trunk) + + + +# show the rbyd log +def dbg_log(rbyd, *, + color=False, + **args): + data = rbyd.data + + # preprocess jumps + if args.get('jumps'): + jumpart = JumpArt.fromrbyd(rbyd, + all=args.get('all')) + + # preprocess lifetimes + l_width = 0 + if args.get('lifetimes'): + lifetimeart = LifetimeArt.fromrbyd(rbyd, all=args.get('all')) + l_width = lifetimeart.width + + # dynamically size the id field + # + # we need to do an additional pass to find this since our rbyd weight + # does not include any shrub trees + data = rbyd.data + weight_ = 0 + weight__ = 0 + trunk_ = 0 + j_ = 4 + while j_ < (len(data) if args.get('all') else rbyd.eoff): + j = j_ + v, tag, w, size, d = fromtag(data, j_) + j_ += d + + if not tag & TAG_ALT: + j_ += size + + # evaluate trunks + if (tag & 0xf000) != TAG_CKSUM: + if not trunk_: + trunk_ = j_-d + weight__ = 0 + + weight__ += w + + if not tag & TAG_ALT: + # found new weight? + weight_ = max(weight_, weight__) + trunk_ = 0 + + w_width = mt.ceil(mt.log10(max(1, weight_)+1)) + + # print revision count + if args.get('raw'): + print('%8s: %*s%*s %s' % ( + '%04x' % 0, + l_width, '', + 2*w_width+1, '', + next(xxd(data[0:4])))) + + # print tags + cksum = crc32c(data[0:4]) + cksum_ = cksum + perturb = False + lower_, upper_ = 0, 0 + trunk_ = 0 + j_ = 4 + while j_ < (len(data) if args.get('all') else rbyd.eoff): + notes = [] + + # read next tag + j = j_ + v, tag, w, size, d = fromtag(data, j_) + # note if parity doesn't match + if v != parity(cksum_): + notes.append('v!=%x' % parity(cksum_)) + cksum_ ^= 0x00000080 if v else 0 + cksum_ = crc32c(data[j_:j_+d], cksum_) + j_ += d + + # take care of cksums + if not tag & TAG_ALT: + if (tag & 0xff00) != TAG_CKSUM: + cksum_ = crc32c(data[j_:j_+size], cksum_) + # found a cksum? + else: + # note if phase doesn't match + if (tag & 0x3) != (rbyd.block & 0x3): + notes.append('q!=%x' % (rbyd.block & 0x3)) + # check cksum + cksum__ = fromle32(data, j_) + # note if cksum doesn't match + if cksum_ != cksum__: + notes.append('cksum!=%08x' % cksum__) + # update perturb bit + perturb = bool(tag & TAG_PERTURB) + # revert to data cksum and perturb + cksum_ = cksum ^ (0xfca42daf if perturb else 0) + j_ += size + + # evaluate trunks + if (tag & 0xf000) != TAG_CKSUM: + if not trunk_: + trunk_ = j_-d + lower_, upper_ = 0, 0 + + if tag & TAG_ALT and not tag & TAG_GT: + lower_ += w + else: + upper_ += w + + # end of trunk? + if not tag & TAG_ALT: + # derive the current tag's rid from alt weights + rid = lower_ + w-1 + trunk_ = 0 + + # update canonical checksum, xoring out any perturb state + cksum = cksum_ ^ (0xfca42daf if perturb else 0) + + # show human-readable tag representation + print('%s%08x:%s %*s%s%*s %-*s%s%s%s' % ( + '\x1b[1;30m' if color and j >= rbyd.eoff else '', + j, + '\x1b[m' if color and j >= rbyd.eoff else '', + l_width, lifetimeart.repr(j, color) + if args.get('lifetimes') + else '', + '\x1b[1;30m' if color and j >= rbyd.eoff else '', + 2*w_width+1, '' if (tag & 0xe000) != 0x0000 + else '%d-%d' % (rid-(w-1), rid) if w > 1 + else rid, + 56+w_width, '%-*s %s' % ( + 21+w_width, Tag.repr(tag, w, size, toff=j), + next(xxd(data[j+d:j+d+min(size, 8)], 8), '') + if not args.get('raw') + and not args.get('no_truncate') + and not tag & TAG_ALT + else ''), + ' (%s)' % ', '.join(notes) if notes else '', + '\x1b[m' if color and j >= rbyd.eoff else '', + ' %s' % jumpart.repr(j, color) + if args.get('jumps') and not notes + else '')) + + # show on-disk encoding of tags + if args.get('raw'): + for o, line in enumerate(xxd(data[j:j+d])): + print('%s%8s: %*s%*s %s%s' % ( + '\x1b[1;30m' if color and j >= rbyd.eoff else '', + '%04x' % (j + o*16), + l_width, '', + 2*w_width+1, '', + line, + '\x1b[m' if color and j >= rbyd.eoff else '')) + if args.get('raw') or args.get('no_truncate'): + if not tag & TAG_ALT: + for o, line in enumerate(xxd(data[j+d:j+d+size])): + print('%s%8s: %*s%*s %s%s' % ( + '\x1b[1;30m' if color and j >= rbyd.eoff else '', + '%04x' % (j+d + o*16), + l_width, '', + 2*w_width+1, '', + line, + '\x1b[m' if color and j >= rbyd.eoff else '')) + +# show the rbyd tree +def dbg_tree(rbyd, *, + color=False, + **args): + if not rbyd: + return + + # precompute tree renderings + t_width = 0 + if (args.get('tree_rbyd') + or args.get('tree_rbyd_all') + or args.get('tree_btree')): + tree = TreeArt.fromrbyd(rbyd, **args) + t_width = tree.width + + # dynamically size the id field + w_width = mt.ceil(mt.log10(max(1, rbyd.weight)+1)) + + for i, (rid, rattr) in enumerate(rbyd.rattrs()): + # show human-readable tag representation + print('%08x: %s%*s %-*s %s' % ( + rattr.toff, + tree.repr((rid, rattr.tag), color) + if (args.get('tree_rbyd') + or args.get('tree_rbyd_all') + or args.get('tree_btree')) + else '', + 2*w_width+1, '%d-%d' % (rid-(rattr.weight-1), rid) + if rattr.weight > 1 + else rid if rattr.weight > 0 or i == 0 + else '', + 21+w_width, rattr.repr(), + next(xxd(rattr.data[:8], 8), '') + if not args.get('raw') + and not args.get('no_truncate') + and not rattr.tag & TAG_ALT + else '')) + + # show on-disk encoding of tags + if args.get('raw'): + for o, line in enumerate(xxd(rattr.tdata)): + print('%8s: %*s%*s %s' % ( + '%04x' % (rattr.toff + o*16), + t_width, '', + 2*w_width+1, '', + line)) + if args.get('raw') or args.get('no_truncate'): + if not rattr.tag & TAG_ALT: + for o, line in enumerate(xxd(rattr.data)): + print('%8s: %*s%*s %s' % ( + '%04x' % (rattr.off + o*16), + t_width, '', + 2*w_width+1, '', + line)) + + +def main(disk, blocks=None, *, + trunk=None, + block_size=None, + block_count=None, + quiet=False, + color='auto', + **args): + # figure out what color should be + if color == 'auto': + color = sys.stdout.isatty() + elif color == 'always': + color = True + else: + color = False + + # is bd geometry specified? + if isinstance(block_size, tuple): + block_size, block_count_ = block_size + if block_count is None: + block_count = block_count_ + + # flatten blocks, default to block 0 + blocks = list(it.chain.from_iterable(blocks)) if blocks else [0] + + # blocks may also encode trunks + blocks, trunk = ( + [block[0] if isinstance(block, tuple) + else block + for block in blocks], + trunk if trunk is not None + else ft.reduce( + lambda x, y: y, + (block[1] for block in blocks + if isinstance(block, tuple)), + None)) + + with open(disk, 'rb') as f: + # if block_size is omitted, assume the block device is one big block + if block_size is None: + f.seek(0, os.SEEK_END) + block_size = f.tell() + + # fetch the rbyd + bd = Bd(f, block_size, block_count) + rbyd = Rbyd.fetch(bd, blocks, trunk) + + # print some information about the rbyd + if not quiet: + print('rbyd %s w%d, rev %08x, size %d, cksum %08x' % ( + rbyd.addr(), + rbyd.weight, + rbyd.rev, + rbyd.eoff, + rbyd.cksum)) + + if args.get('log') and not quiet: + dbg_log(rbyd, + color=color, + **args) + elif not quiet: + dbg_tree(rbyd, + color=color, + **args) + + if args.get('error_on_corrupt') and not rbyd: + sys.exit(2) + + +if __name__ == "__main__": + import argparse + import sys + parser = argparse.ArgumentParser( + description="Debug rbyd metadata.", + allow_abbrev=False) + parser.add_argument( + 'disk', + help="File containing the block device.") + parser.add_argument( + 'blocks', + nargs='*', + type=rbydaddr, + help="Block address of metadata blocks.") + parser.add_argument( + '--trunk', + type=lambda x: int(x, 0), + help="Use this offset as the trunk of the tree.") + parser.add_argument( + '-b', '--block-size', + type=bdgeom, + help="Block size/geometry in bytes. Accepts x.") + parser.add_argument( + '--block-count', + type=lambda x: int(x, 0), + help="Block count in blocks.") + parser.add_argument( + '-q', '--quiet', + action='store_true', + help="Don't show anything, useful when checking for errors.") + parser.add_argument( + '--color', + choices=['never', 'always', 'auto'], + default='auto', + help="When to use terminal colors. Defaults to 'auto'.") + parser.add_argument( + '-a', '--all', + action='store_true', + help="Don't stop parsing on bad commits.") + parser.add_argument( + '-l', '--log', + action='store_true', + help="Show the raw tags as they appear in the log.") + parser.add_argument( + '-x', '--raw', + action='store_true', + help="Show the raw data including tag encodings.") + parser.add_argument( + '-T', '--no-truncate', + action='store_true', + help="Don't truncate, show the full contents.") + parser.add_argument( + '-R', '--tree', '--rbyd', '--tree-rbyd', + dest='tree_rbyd', + action='store_true', + help="Show the rbyd tree.") + parser.add_argument( + '-Y', '--rbyd-all', '--tree-rbyd-all', + dest='tree_rbyd_all', + action='store_true', + help="Show the full rbyd tree.") + parser.add_argument( + '-B', '--btree', '--tree-btree', + dest='tree_btree', + action='store_true', + help="Show a simplified btree tree.") + parser.add_argument( + '-j', '--jumps', + action='store_true', + help="Show alt pointer jumps in the margin.") + parser.add_argument( + '-g', '--lifetimes', + action='store_true', + help="Show inserts/deletes of ids in the margin.") + parser.add_argument( + '-e', '--error-on-corrupt', + action='store_true', + help="Error if no valid commit is found.") + sys.exit(main(**{k: v + for k, v in vars(parser.parse_intermixed_args()).items() + if v is not None})) diff --git a/scripts/dbgtag.py b/scripts/dbgtag.py new file mode 100755 index 000000000..4220b94d3 --- /dev/null +++ b/scripts/dbgtag.py @@ -0,0 +1,394 @@ +#!/usr/bin/env python3 + +# prevent local imports +if __name__ == "__main__": + __import__('sys').path.pop(0) + +import functools as ft +import io +import math as mt +import os +import struct +import sys + + +TAG_NULL = 0x0000 ## v--- ---- +--- ---- +TAG_INTERNAL = 0x0000 ## v--- ---- +ttt tttt +TAG_CONFIG = 0x0100 ## v--- ---1 +ttt tttt +TAG_MAGIC = 0x0131 # v--- ---1 +-11 --rr +TAG_VERSION = 0x0134 # v--- ---1 +-11 -1-- +TAG_RCOMPAT = 0x0135 # v--- ---1 +-11 -1-1 +TAG_WCOMPAT = 0x0136 # v--- ---1 +-11 -11- +TAG_OCOMPAT = 0x0137 # v--- ---1 +-11 -111 +TAG_GEOMETRY = 0x0138 # v--- ---1 +-11 1--- +TAG_NAMELIMIT = 0x0139 # v--- ---1 +-11 1--1 +TAG_FILELIMIT = 0x013a # v--- ---1 +-11 1-1- +TAG_GDELTA = 0x0200 ## v--- --1- +ttt tttt +TAG_GRMDELTA = 0x0230 # v--- --1- +-11 --++ +TAG_GBMAPDELTA = 0x0234 # v--- --1- +-11 -1rr +TAG_NAME = 0x0300 ## v--- --11 +ttt tttt +TAG_BNAME = 0x0300 # v--- --11 +--- ---- +TAG_REG = 0x0301 # v--- --11 +--- ---1 +TAG_DIR = 0x0302 # v--- --11 +--- --1- +TAG_STICKYNOTE = 0x0303 # v--- --11 +--- --11 +TAG_BOOKMARK = 0x0304 # v--- --11 +--- -1-- +TAG_MNAME = 0x0330 # v--- --11 +-11 ---- +TAG_STRUCT = 0x0400 ## v--- -1-- +ttt tttt +TAG_BRANCH = 0x0400 # v--- -1-- +--- --rr +TAG_DATA = 0x0404 # v--- -1-- +--- -1rr +TAG_BLOCK = 0x0408 # v--- -1-- +--- 1err +TAG_DID = 0x0420 # v--- -1-- +-1- ---- +TAG_BSHRUB = 0x0428 # v--- -1-- +-1- 1-rr +TAG_BTREE = 0x042c # v--- -1-- +-1- 11rr +TAG_MROOT = 0x0431 # v--- -1-- +-11 --rr +TAG_MDIR = 0x0435 # v--- -1-- +-11 -1rr +TAG_MTREE = 0x043c # v--- -1-- +-11 11rr +TAG_BMRANGE = 0x0440 # v--- -1-- +1-- ++uu +TAG_BMFREE = 0x0440 # v--- -1-- +1-- ---- +TAG_BMINUSE = 0x0441 # v--- -1-- +1-- ---1 +TAG_BMERASED = 0x0442 # v--- -1-- +1-- --1- +TAG_BMBAD = 0x0443 # v--- -1-- +1-- --11 +TAG_ATTR = 0x0600 ## v--- -11a +aaa aaaa +TAG_UATTR = 0x0600 # v--- -11- +aaa aaaa +TAG_SATTR = 0x0700 # v--- -111 +aaa aaaa +TAG_SHRUB = 0x1000 ## v--1 kkkk +kkk kkkk +TAG_ALT = 0x4000 ## v1cd kkkk +kkk kkkk +TAG_B = 0x0000 +TAG_R = 0x2000 +TAG_LE = 0x0000 +TAG_GT = 0x1000 +TAG_CKSUM = 0x3000 ## v-11 ---- ++++ +pqq +TAG_PHASE = 0x0003 +TAG_PERTURB = 0x0004 +TAG_NOTE = 0x3100 ## v-11 ---1 ++++ ++++ +TAG_ECKSUM = 0x3200 ## v-11 --1- ++++ ++++ +TAG_GCKSUMDELTA = 0x3300 ## v-11 --11 ++++ ++++ + + +# self-parsing tag repr +class Tag: + def __init__(self, name, tag, encoding, help): + self.name = name + self.tag = tag + self.encoding = encoding + self.help = help + # derive mask from encoding + self.mask = sum( + (1 if x in 'v-01' else 0) << len(self.encoding)-1-i + for i, x in enumerate(self.encoding)) + + def __repr__(self): + return 'Tag(%r, %r, %r)' % ( + self.name, + self.tag, + self.encoding) + + def __eq__(self, other): + return self.name == other.name + + def __ne__(self, other): + return self.name != other.name + + def __hash__(self): + return hash(self.name) + + def line(self): + # substitute mask chars when zero + tag = '0x%s' % ''.join( + n if n != '0' else next( + (x for x in self.encoding[i*4:i*4+4] + if x not in 'v-01+'), + '0') + for i, n in enumerate('%04x' % self.tag)) + # group into nibbles + encoding = ' '.join(self.encoding[i*4:i*4+4] + for i in range(len(self.encoding)//4)) + return ('LFS3_%s' % self.name, tag, encoding) + + def specificity(self): + return sum(1 for x in self.encoding if x in 'v-01') + + def matches(self, tag): + return (tag & self.mask) == (self.tag & self.mask) + + def get(self, chars, tag): + return sum( + tag & ((1 if x in chars else 0) << len(self.encoding)-1-i) + for i, x in enumerate(self.encoding)) + + def max(self, chars): + return max(len(self.encoding)-1-i + for i, x in enumerate(self.encoding) if x in chars) + + def min(self, chars): + return min(len(self.encoding)-1-i + for i, x in enumerate(self.encoding) if x in chars) + + def width(self, chars): + return self.max(chars) - self.min(chars) + + def __contains__(self, chars): + return any(x in self.encoding for x in chars) + + @staticmethod + @ft.cache + def tags(): + # parse our script's source to figure out tags + import inspect + import re + tags = [] + tag_pattern = re.compile( + '^(?PTAG_[^ ]*) *= *(?P[^#]*?) *' + '#+ *(?P(?:[^ ] *?){16}) *(?P.*)$') + for line in (inspect.getsource( + inspect.getmodule(inspect.currentframe())) + .replace('\\\n', '') + .splitlines()): + m = tag_pattern.match(line) + if m: + tags.append(Tag( + m.group('name'), + globals()[m.group('name')], + m.group('encoding').replace(' ', ''), + m.group('help'))) + return tags + + # find best matching tag + @staticmethod + def find(tag): + # find tags, note this is cached + tags__ = Tag.tags() + + # find the most specific matching tag, ignoring valid bits + return max((t for t in tags__ if t.matches(tag & 0x7fff)), + key=lambda t: t.specificity(), + default=None) + + # human readable tag repr + @staticmethod + def repr(tag, weight=None, size=None, *, + global_=False, + toff=None): + # find the most specific matching tag, ignoring the shrub bit + t = Tag.find(tag & ~(TAG_SHRUB if tag & 0x7000 == TAG_SHRUB else 0)) + + # build repr + r = [] + # normal tag? + if not tag & TAG_ALT: + if t is not None: + # prefix shrub tags with shrub + if tag & 0x7000 == TAG_SHRUB: + r.append('shrub') + # lowercase name + r.append(t.name.split('_', 1)[1].lower()) + # gstate tag? + if global_: + if r[-1] == 'gdelta': + r[-1] = 'gstate' + elif r[-1].endswith('delta'): + r[-1] = r[-1][:-len('delta')] + # include perturb/phase bits + if 'q' in t: + r.append('q%d' % t.get('q', tag)) + if 'p' in t and tag & TAG_PERTURB: + r.append('p') + + # include unmatched fields, but not just redund, and + # only reserved bits if non-zero + if 'tua' in t or ('+' in t and t.get('+', tag) != 0): + r.append(' 0x%0*x' % ( + (t.width('tuar+')+4-1)//4, + t.get('tuar+', tag))) + # unknown tag? + else: + r.append('0x%04x' % tag) + + # weight? + if weight: + r.append(' w%d' % weight) + # size? don't include if null + if size is not None and (size or tag & 0x7fff): + r.append(' %d' % size) + + # alt pointer? + else: + r.append('alt') + r.append('r' if tag & TAG_R else 'b') + r.append('gt' if tag & TAG_GT else 'le') + r.append(' 0x%0*x' % ( + (t.width('k')+4-1)//4, + t.get('k', tag))) + + # weight? + if weight is not None: + r.append(' w%d' % weight) + # jump? + if size and toff is not None: + r.append(' 0x%x' % (0xffffffff & (toff-size))) + elif size: + r.append(' -%d' % size) + + return ''.join(r) + + +# open with '-' for stdin/stdout +def openio(path, mode='r', buffering=-1): + import os + if path == '-': + if 'r' in mode: + return os.fdopen(os.dup(sys.stdin.fileno()), mode, buffering) + else: + return os.fdopen(os.dup(sys.stdout.fileno()), mode, buffering) + else: + return open(path, mode, buffering) + +def fromleb128(data, j=0): + word = 0 + d = 0 + while j+d < len(data): + b = data[j+d] + word |= (b & 0x7f) << 7*d + word &= 0xffffffff + if not b & 0x80: + return word, d+1 + d += 1 + return word, d + +def fromtag(data, j=0): + d = 0 + tag = struct.unpack('>H', data[j:j+2].ljust(2, b'\0'))[0]; d += 2 + weight, d_ = fromleb128(data, j+d); d += d_ + size, d_ = fromleb128(data, j+d); d += d_ + return tag>>15, tag&0x7fff, weight, size, d + + +def list_tags(): + # find tags + tags__ = Tag.tags() + + # list + lines = [] + for t in tags__: + lines.append(t.line()) + + # figure out widths + w = [0, 0] + for l in lines: + w[0] = max(w[0], len(l[0])) + w[1] = max(w[1], len(l[1])) + + # then print results + for l in lines: + print('%-*s %-*s %s' % ( + w[0], l[0], + w[1], l[1], + l[2])) + +def dbg_tags(data, *, + word_bits=32): + # figure out tag size in bytes + if word_bits != 0: + n = 2 + 2*mt.ceil(word_bits / 7) + + lines = [] + # interpret as ints? + if not isinstance(data, bytes): + for tag in data: + lines.append(( + ' '.join('%02x' % b for b in struct.pack('>H', tag)), + Tag.repr(tag))) + + # interpret as bytes? + else: + j = 0 + while j < len(data): + # bounded tags? + if word_bits != 0: + v, tag, w, size, d = fromtag(data[j:j+n]) + # unbounded? + else: + v, tag, w, size, d = fromtag(data, j) + + lines.append(( + ' '.join('%02x' % b for b in data[j:j+d]), + Tag.repr(tag, w, size))) + j += d + + # skip attached data if there is any + if not tag & TAG_ALT: + j += size + + # figure out widths + w = [0] + for l in lines: + w[0] = max(w[0], len(l[0])) + + # then print results + for l in lines: + print('%-*s %s' % ( + w[0], l[0], + l[1])) + +def main(tags, *, + list=False, + hex=False, + input=None, + word_bits=32): + import builtins + list_, list = list, builtins.list + hex_, hex = hex, builtins.hex + + # list all known tags + if list_: + list_tags() + + # interpret as a sequence of hex bytes + elif hex_: + bytes_ = [b for tag in tags for b in tag.split()] + dbg_tags(bytes(int(b, 16) for b in bytes_), + word_bits=word_bits) + + # parse tags in a file + elif input: + with openio(input, 'rb') as f: + dbg_tags(f.read(), + word_bits=word_bits) + + # default to interpreting as ints + else: + dbg_tags((int(tag, 0) for tag in tags), + word_bits=word_bits) + + +if __name__ == "__main__": + import argparse + import sys + parser = argparse.ArgumentParser( + description="Decode littlefs tags.", + allow_abbrev=False) + parser.add_argument( + 'tags', + nargs='*', + help="Tags to decode.") + parser.add_argument( + '-l', '--list', + action='store_true', + help="List all known tags.") + parser.add_argument( + '-x', '--hex', + action='store_true', + help="Interpret as a sequence of hex bytes.") + parser.add_argument( + '-i', '--input', + help="Read tags from this file. Can use - for stdin.") + parser.add_argument( + '-w', '--word', '--word-bits', + dest='word_bits', + nargs='?', + type=lambda x: int(x, 0), + const=0, + help="Word size in bits. 0 is unbounded. Defaults to 32.") + sys.exit(main(**{k: v + for k, v in vars(parser.parse_intermixed_args()).items() + if v is not None})) diff --git a/scripts/dbgtrace.py b/scripts/dbgtrace.py new file mode 100755 index 000000000..d8f1698dc --- /dev/null +++ b/scripts/dbgtrace.py @@ -0,0 +1,1931 @@ +#!/usr/bin/env python3 +# +# Render operations on block devices based on trace output +# +# Example: +# ./scripts/dbgtrace.py trace +# +# Copyright (c) 2022, The littlefs authors. +# SPDX-License-Identifier: BSD-3-Clause +# + +# prevent local imports +if __name__ == "__main__": + __import__('sys').path.pop(0) + +import bisect +import collections as co +import fnmatch +import functools as ft +import io +import itertools as it +import math as mt +import os +import re +import shlex +import shutil +import sys +import threading as th +import time + + +# assign chars/colors to specific bd operations +CHARS = { + 'read': 'r', + 'prog': 'p', + 'erase': 'e', + 'noop': '-', +} +COLORS = { + 'read': '32', + 'prog': '35', + 'erase': '34', + 'noop': '1;30', +} + +# assign chars/colors to varying levels of wear +WEAR_CHARS = '0123456789' +WEAR_COLORS = ['1;30', '1;30', '1;30', '', '', '', '', '31', '31', '1;31'] + +# give more interesting operations a higher priority +# +# note that while progs always subset erases, erases are much rarer, +# which is why we give them priority +Z_ORDER = ['erase', 'prog', 'read', 'wear', 'noop'] + +CHARS_DOTS = " .':" +CHARS_BRAILLE = ( + '⠀⢀⡀⣀⠠⢠⡠⣠⠄⢄⡄⣄⠤⢤⡤⣤' '⠐⢐⡐⣐⠰⢰⡰⣰⠔⢔⡔⣔⠴⢴⡴⣴' + '⠂⢂⡂⣂⠢⢢⡢⣢⠆⢆⡆⣆⠦⢦⡦⣦' '⠒⢒⡒⣒⠲⢲⡲⣲⠖⢖⡖⣖⠶⢶⡶⣶' + '⠈⢈⡈⣈⠨⢨⡨⣨⠌⢌⡌⣌⠬⢬⡬⣬' '⠘⢘⡘⣘⠸⢸⡸⣸⠜⢜⡜⣜⠼⢼⡼⣼' + '⠊⢊⡊⣊⠪⢪⡪⣪⠎⢎⡎⣎⠮⢮⡮⣮' '⠚⢚⡚⣚⠺⢺⡺⣺⠞⢞⡞⣞⠾⢾⡾⣾' + '⠁⢁⡁⣁⠡⢡⡡⣡⠅⢅⡅⣅⠥⢥⡥⣥' '⠑⢑⡑⣑⠱⢱⡱⣱⠕⢕⡕⣕⠵⢵⡵⣵' + '⠃⢃⡃⣃⠣⢣⡣⣣⠇⢇⡇⣇⠧⢧⡧⣧' '⠓⢓⡓⣓⠳⢳⡳⣳⠗⢗⡗⣗⠷⢷⡷⣷' + '⠉⢉⡉⣉⠩⢩⡩⣩⠍⢍⡍⣍⠭⢭⡭⣭' '⠙⢙⡙⣙⠹⢹⡹⣹⠝⢝⡝⣝⠽⢽⡽⣽' + '⠋⢋⡋⣋⠫⢫⡫⣫⠏⢏⡏⣏⠯⢯⡯⣯' '⠛⢛⡛⣛⠻⢻⡻⣻⠟⢟⡟⣟⠿⢿⡿⣿') + +SI_PREFIXES = { + 18: 'E', + 15: 'P', + 12: 'T', + 9: 'G', + 6: 'M', + 3: 'K', + 0: '', + -3: 'm', + -6: 'u', + -9: 'n', + -12: 'p', + -15: 'f', + -18: 'a', +} + +SI2_PREFIXES = { + 60: 'Ei', + 50: 'Pi', + 40: 'Ti', + 30: 'Gi', + 20: 'Mi', + 10: 'Ki', + 0: '', + -10: 'mi', + -20: 'ui', + -30: 'ni', + -40: 'pi', + -50: 'fi', + -60: 'ai', +} + + +# open with '-' for stdin/stdout +def openio(path, mode='r', buffering=-1): + import os + if path == '-': + if 'r' in mode: + return os.fdopen(os.dup(sys.stdin.fileno()), mode, buffering) + else: + return os.fdopen(os.dup(sys.stdout.fileno()), mode, buffering) + else: + return open(path, mode, buffering) + +# some ways of block geometry representations +# 512 -> 512 +# 512x16 -> (512, 16) +# 0x200x10 -> (512, 16) +def bdgeom(s): + s = s.strip() + b = 10 + if s.startswith('0x') or s.startswith('0X'): + s = s[2:] + b = 16 + elif s.startswith('0o') or s.startswith('0O'): + s = s[2:] + b = 8 + elif s.startswith('0b') or s.startswith('0B'): + s = s[2:] + b = 2 + + if 'x' in s: + s, s_ = s.split('x', 1) + return (int(s, b), int(s_, b)) + else: + return int(s, b) + +# parse some rbyd addr encodings +# 0xa -> (0xa,) +# 0xa.c -> ((0xa, 0xc),) +# 0x{a,b} -> (0xa, 0xb) +# 0x{a,b}.c -> ((0xa, 0xc), (0xb, 0xc)) +def rbydaddr(s): + s = s.strip() + b = 10 + if s.startswith('0x') or s.startswith('0X'): + s = s[2:] + b = 16 + elif s.startswith('0o') or s.startswith('0O'): + s = s[2:] + b = 8 + elif s.startswith('0b') or s.startswith('0B'): + s = s[2:] + b = 2 + + trunk = None + if '.' in s: + s, s_ = s.split('.', 1) + trunk = int(s_, b) + + if s.startswith('{') and '}' in s: + ss = s[1:s.find('}')].split(',') + else: + ss = [s] + + addr = [] + for s in ss: + if trunk is not None: + addr.append((int(s, b), trunk)) + else: + addr.append(int(s, b)) + + return tuple(addr) + + +# a pseudo-stdout ring buffer +class RingIO: + def __init__(self, maxlen=None, head=False): + self.maxlen = maxlen + self.head = head + self.lines = co.deque( + maxlen=max(maxlen, 0) if maxlen is not None else None) + self.tail = io.StringIO() + + # trigger automatic sizing + self.resize(self.maxlen) + + @property + def width(self): + # just fetch this on demand, we don't actually use width + return shutil.get_terminal_size((80, 5))[0] + + @property + def height(self): + # calculate based on terminal height? + if self.maxlen is None or self.maxlen <= 0: + return max( + shutil.get_terminal_size((80, 5))[1] + + (self.maxlen or 0), + 0) + # limit to maxlen + else: + return self.maxlen + + def resize(self, maxlen): + self.maxlen = maxlen + if maxlen is not None and maxlen <= 0: + maxlen = self.height + if maxlen != self.lines.maxlen: + self.lines = co.deque(self.lines, maxlen=maxlen) + + def __len__(self): + return len(self.lines) + + def write(self, s): + # note using split here ensures the trailing string has no newline + lines = s.split('\n') + + if len(lines) > 1 and self.tail.getvalue(): + self.tail.write(lines[0]) + lines[0] = self.tail.getvalue() + self.tail = io.StringIO() + + self.lines.extend(lines[:-1]) + + if lines[-1]: + self.tail.write(lines[-1]) + + # keep track of maximum drawn canvas + canvas_lines = 1 + + def draw(self): + # did terminal size change? + self.resize(self.maxlen) + + # copy lines + lines = self.lines.copy() + # pad to fill any existing canvas, but truncate to terminal size + h = shutil.get_terminal_size((80, 5))[1] + lines.extend('' for _ in range( + len(lines), + min(RingIO.canvas_lines, h))) + while len(lines) > h: + if self.head: + lines.pop() + else: + lines.popleft() + + # build up the redraw in memory first and render in a single + # write call, this minimizes flickering caused by the cursor + # jumping around + canvas = [] + + # hide the cursor + canvas.append('\x1b[?25l') + + # give ourself a canvas + while RingIO.canvas_lines < len(lines): + canvas.append('\n') + RingIO.canvas_lines += 1 + + # write lines from top to bottom so later lines overwrite earlier + # lines, note xA/xB stop at terminal boundaries + for i, line in enumerate(lines): + # move to col 0 + canvas.append('\r') + # move up to line + if len(lines)-1-i > 0: + canvas.append('\x1b[%dA' % (len(lines)-1-i)) + # clear line + canvas.append('\x1b[K') + # disable line wrap + canvas.append('\x1b[?7l') + # print the line + canvas.append(line) + # enable line wrap + canvas.append('\x1b[?7h') # enable line wrap + # move back down + if len(lines)-1-i > 0: + canvas.append('\x1b[%dB' % (len(lines)-1-i)) + + # show the cursor again + canvas.append('\x1b[?25h') + + # write to stdout and flush + sys.stdout.write(''.join(canvas)) + sys.stdout.flush() + +# a representation of optionally key-mapped attrs +class CsvAttr: + def __init__(self, attrs, defaults=None): + if attrs is None: + attrs = [] + if isinstance(attrs, dict): + attrs = attrs.items() + + # normalize + self.attrs = [] + self.keyed = co.OrderedDict() + for attr in attrs: + if not isinstance(attr, tuple): + attr = ((), attr) + if attr[0] in {None, (), (None,), ('*',)}: + attr = ((), attr[1]) + if not isinstance(attr[0], tuple): + attr = ((attr[0],), attr[1]) + + self.attrs.append(attr) + if attr[0] not in self.keyed: + self.keyed[attr[0]] = [] + self.keyed[attr[0]].append(attr[1]) + + # create attrs object for defaults + if isinstance(defaults, CsvAttr): + self.defaults = defaults + elif defaults is not None: + self.defaults = CsvAttr(defaults) + else: + self.defaults = None + + def __repr__(self): + if self.defaults is None: + return 'CsvAttr(%r)' % ( + [(','.join(attr[0]), attr[1]) + for attr in self.attrs]) + else: + return 'CsvAttr(%r, %r)' % ( + [(','.join(attr[0]), attr[1]) + for attr in self.attrs], + [(','.join(attr[0]), attr[1]) + for attr in self.defaults.attrs]) + + def __iter__(self): + if () in self.keyed: + return it.cycle(self.keyed[()]) + elif self.defaults is not None: + return iter(self.defaults) + else: + return iter(()) + + def __bool__(self): + return bool(self.attrs) + + def __getitem__(self, key): + if isinstance(key, tuple): + if len(key) > 0 and not isinstance(key[0], str): + i, key = key + if not isinstance(key, tuple): + key = (key,) + else: + i, key = 0, key + elif isinstance(key, str): + i, key = 0, (key,) + else: + i, key = key, () + + # try to lookup by key + best = None + for ks, vs in self.keyed.items(): + prefix = [] + for j, k in enumerate(ks): + if j < len(key) and fnmatch.fnmatchcase(key[j], k): + prefix.append(k) + else: + prefix = None + break + + if prefix is not None and ( + best is None or len(prefix) >= len(best[0])): + best = (prefix, vs) + + if best is not None: + # cycle based on index + return best[1][i % len(best[1])] + + # fallback to defaults? + if self.defaults is not None: + return self.defaults[i, key] + + raise KeyError(i, key) + + def get(self, key, default=None): + try: + return self.__getitem__(key) + except KeyError: + return default + + def __contains__(self, key): + try: + self.__getitem__(key) + return True + except KeyError: + return False + + # get all results for a given key + def getall(self, key, default=None): + if not isinstance(key, tuple): + key = (key,) + + # try to lookup by key + best = None + for ks, vs in self.keyed.items(): + prefix = [] + for j, k in enumerate(ks): + if j < len(key) and fnmatch.fnmatchcase(key[j], k): + prefix.append(k) + else: + prefix = None + break + + if prefix is not None and ( + best is None or len(prefix) >= len(best[0])): + best = (prefix, vs) + + if best is not None: + return best[1] + + # fallback to defaults? + if self.defaults is not None: + return self.defaults.getall(key, default) + + raise default + + # a key function for sorting by key order + def key(self, key): + if not isinstance(key, tuple): + key = (key,) + + best = None + for i, ks in enumerate(self.keyed.keys()): + prefix = [] + for j, k in enumerate(ks): + if j < len(key) and (not k or key[j] == k): + prefix.append(k) + else: + prefix = None + break + + if prefix is not None and ( + best is None or len(prefix) >= len(best[0])): + best = (prefix, i) + + if best is not None: + return best[1] + + # fallback to defaults? + if self.defaults is not None: + return len(self.keyed) + self.defaults.key(key) + + return len(self.keyed) + +# SI-prefix formatter +def si(x): + if x == 0: + return '0' + # figure out prefix and scale + p = 3*mt.floor(mt.log(abs(x), 10**3)) + p = min(18, max(-18, p)) + # format with 3 digits of precision + s = '%.3f' % (abs(x) / (10.0**p)) + s = s[:3+1] + # truncate but only digits that follow the dot + if '.' in s: + s = s.rstrip('0') + s = s.rstrip('.') + return '%s%s%s' % ('-' if x < 0 else '', s, SI_PREFIXES[p]) + +# SI-prefix formatter for powers-of-two +def si2(x): + if x == 0: + return '0' + # figure out prefix and scale + p = 10*mt.floor(mt.log(abs(x), 2**10)) + p = min(30, max(-30, p)) + # format with 3 digits of precision + s = '%.3f' % (abs(x) / (2.0**p)) + s = s[:3+1] + # truncate but only digits that follow the dot + if '.' in s: + s = s.rstrip('0') + s = s.rstrip('.') + return '%s%s%s' % ('-' if x < 0 else '', s, SI2_PREFIXES[p]) + +# parse %-escaped strings +# +# attrs can override __getitem__ for lazy attr generation +def punescape(s, attrs=None): + pattern = re.compile( + '%[%n]' + '|' '%x..' + '|' '%u....' + '|' '%U........' + '|' '%\((?P[^)]*)\)' + '(?P[+\- #0-9\.]*[siIdboxXfFeEgG])') + def unescape(m): + if m.group()[1] == '%': return '%' + elif m.group()[1] == 'n': return '\n' + elif m.group()[1] == 'x': return chr(int(m.group()[2:], 16)) + elif m.group()[1] == 'u': return chr(int(m.group()[2:], 16)) + elif m.group()[1] == 'U': return chr(int(m.group()[2:], 16)) + elif m.group()[1] == '(': + if attrs is not None: + try: + v = attrs[m.group('field')] + except KeyError: + return m.group() + else: + return m.group() + f = m.group('format') + if f[-1] in 'dboxX': + if isinstance(v, str): + v = dat(v, 0) + v = int(v) + elif f[-1] in 'iIfFeEgG': + if isinstance(v, str): + v = dat(v, 0) + v = float(v) + if f[-1] in 'iI': + v = (si if 'i' in f[-1] else si2)(v) + f = f.replace('i', 's').replace('I', 's') + if '+' in f and not v.startswith('-'): + v = '+'+v + f = f.replace('+', '').replace('-', '') + else: + f = ('<' if '-' in f else '>') + f.replace('-', '') + v = str(v) + # note we need Python's new format syntax for binary + return ('{:%s}' % f).format(v) + else: assert False + + return re.sub(pattern, unescape, s) + +# split %-escaped strings into chars +def psplit(s): + pattern = re.compile( + '%[%n]' + '|' '%x..' + '|' '%u....' + '|' '%U........' + '|' '%\((?P[^)]*)\)' + '(?P[+\- #0-9\.]*[siIdboxXfFeEgG])') + return [m.group() for m in re.finditer(pattern.pattern + '|.', s)] + + +# a little ascii renderer +class Canvas: + def __init__(self, width, height, *, + color=False, + dots=False, + braille=False): + # scale if we're printing with dots or braille + if braille: + xscale, yscale = 2, 4 + elif dots: + xscale, yscale = 1, 2 + else: + xscale, yscale = 1, 1 + + self.width_ = width + self.height_ = height + self.width = xscale*width + self.height = yscale*height + self.xscale = xscale + self.yscale = yscale + self.color_ = color + self.dots = dots + self.braille = braille + + # create initial canvas + self.chars = [0] * (width*height) + self.colors = [''] * (width*height) + + def char(self, x, y, char=None): + # ignore out of bounds + if x < 0 or y < 0 or x >= self.width or y >= self.height: + return False + + x_ = x // self.xscale + y_ = y // self.yscale + if char is not None: + c = self.chars[x_ + y_*self.width_] + # mask in sub-char pixel? + if isinstance(char, bool): + if not isinstance(c, int): + c = 0 + self.chars[x_ + y_*self.width_] = (c + | (1 + << ((y%self.yscale)*self.xscale + + (self.xscale-1)-(x%self.xscale)))) + else: + self.chars[x_ + y_*self.width_] = char + else: + c = self.chars[x_ + y_*self.width_] + if isinstance(c, int): + return ((c + >> ((y%self.yscale)*self.xscale + + (self.xscale-1)-(x%self.xscale))) + & 1) == 1 + else: + return c + + def color(self, x, y, color=None): + # ignore out of bounds + if x < 0 or y < 0 or x >= self.width or y >= self.height: + return '' + + x_ = x // self.xscale + y_ = y // self.yscale + if color is not None: + self.colors[x_ + y_*self.width_] = color + else: + return self.colors[x_ + y_*self.width_] + + def __getitem__(self, xy): + x, y = xy + return self.char(x, y) + + def __setitem__(self, xy, char): + x, y = xy + self.char(x, y, char) + + def point(self, x, y, *, + char=True, + color=''): + self.char(x, y, char) + self.color(x, y, color) + + def line(self, x1, y1, x2, y2, *, + char=True, + color=''): + # incremental error line algorithm + ex = abs(x2 - x1) + ey = -abs(y2 - y1) + dx = +1 if x1 < x2 else -1 + dy = +1 if y1 < y2 else -1 + e = ex + ey + + while True: + self.point(x1, y1, char=char, color=color) + e2 = 2*e + + if x1 == x2 and y1 == y2: + break + + if e2 > ey: + e += ey + x1 += dx + + if x1 == x2 and y1 == y2: + break + + if e2 < ex: + e += ex + y1 += dy + + self.point(x2, y2, char=char, color=color) + + def rect(self, x, y, w, h, *, + char=True, + color=''): + for j in range(h): + for i in range(w): + self.point(x+i, y+j, char=char, color=color) + + def label(self, x, y, label, width=None, height=None, *, + color=''): + x_ = x + y_ = y + for char in label: + if char == '\n': + x_ = x + y_ -= self.yscale + else: + if ((width is None or x_ < x+width) + and (height is None or y_ > y-height)): + self.point(x_, y_, char=char, color=color) + x_ += self.xscale + + def draw(self, row): + y_ = self.height_-1 - row + row_ = [] + for x_ in range(self.width_): + # char? + c = self.chars[x_ + y_*self.width_] + if isinstance(c, int): + if self.braille: + assert c < 256 + c = CHARS_BRAILLE[c] + elif self.dots: + assert c < 4 + c = CHARS_DOTS[c] + else: + assert c < 2 + c = '.' if c else ' ' + + # color? + if self.color_: + color = self.colors[x_ + y_*self.width_] + if color: + c = '\x1b[%sm%s\x1b[m' % (color, c) + + row_.append(c) + + return ''.join(row_) + + +# naive space filling curve (the default) +def naive_curve(width, height): + for y in range(height): + for x in range(width): + yield x, y + +# space filling Hilbert-curve +def hilbert_curve(width, height): + # based on generalized Hilbert curves: + # https://github.com/jakubcerveny/gilbert + # + def hilbert_(x, y, a_x, a_y, b_x, b_y): + w = abs(a_x+a_y) + h = abs(b_x+b_y) + a_dx = -1 if a_x < 0 else +1 if a_x > 0 else 0 + a_dy = -1 if a_y < 0 else +1 if a_y > 0 else 0 + b_dx = -1 if b_x < 0 else +1 if b_x > 0 else 0 + b_dy = -1 if b_y < 0 else +1 if b_y > 0 else 0 + + # trivial row + if h == 1: + for _ in range(w): + yield x, y + x, y = x+a_dx, y+a_dy + return + + # trivial column + if w == 1: + for _ in range(h): + yield x, y + x, y = x+b_dx, y+b_dy + return + + a_x_, a_y_ = a_x//2, a_y//2 + b_x_, b_y_ = b_x//2, b_y//2 + w_ = abs(a_x_+a_y_) + h_ = abs(b_x_+b_y_) + + if 2*w > 3*h: + # prefer even steps + if w_ % 2 != 0 and w > 2: + a_x_, a_y_ = a_x_+a_dx, a_y_+a_dy + + # split in two + yield from hilbert_( + x, y, + a_x_, a_y_, b_x, b_y) + yield from hilbert_( + x+a_x_, y+a_y_, + a_x-a_x_, a_y-a_y_, b_x, b_y) + else: + # prefer even steps + if h_ % 2 != 0 and h > 2: + b_x_, b_y_ = b_x_+b_dx, b_y_+b_dy + + # split in three + yield from hilbert_( + x, y, + b_x_, b_y_, a_x_, a_y_) + yield from hilbert_( + x+b_x_, y+b_y_, + a_x, a_y, b_x-b_x_, b_y-b_y_) + yield from hilbert_( + x+(a_x-a_dx)+(b_x_-b_dx), y+(a_y-a_dy)+(b_y_-b_dy), + -b_x_, -b_y_, -(a_x-a_x_), -(a_y-a_y_)) + + if width >= height: + yield from hilbert_(0, 0, +width, 0, 0, +height) + else: + yield from hilbert_(0, 0, 0, +height, +width, 0) + +# space filling Z-curve/Lebesgue-curve +def lebesgue_curve(width, height): + # we create a truncated Z-curve by simply filtering out the + # points that are outside our region + for i in range(2**(2*mt.ceil(mt.log2(max(width, height))))): + # we just operate on binary strings here because it's easier + b = '{:0{}b}'.format(i, 2*mt.ceil(mt.log2(i+1)/2)) + x = int(b[1::2], 2) if b[1::2] else 0 + y = int(b[0::2], 2) if b[0::2] else 0 + if x < width and y < height: + yield x, y + + +# a mergable range set type +class RangeSet: + def __init__(self, ranges=None): + self._ranges = [] + + if ranges is not None: + # using add here makes sure all ranges are merged/sorted + # correctly + for r in ranges: + self.add(r) + + def __repr__(self): + return 'RangeSet(%r)' % self._ranges + + def __contains__(self, k): + i = bisect.bisect(self._ranges, k, + key=lambda r: r.start) - 1 + if i > -1: + return k in self._ranges[i] + else: + return False + + def __bool__(self): + return bool(self._ranges) + + def ranges(self): + yield from self._ranges + + def __iter__(self): + for r in self._ranges: + yield from r + + def add(self, r): + assert isinstance(r, range) + # trivial range? + if not r: + return + + # find earliest possible merge point + ranges = self._ranges + i = bisect.bisect_left(ranges, r.start, + key=lambda r: r.stop) + + # copy ranges < merge + merged = ranges[:i] + + # merge ranges and append + while i < len(ranges) and ranges[i].start <= r.stop: + r = range( + min(ranges[i].start, r.start), + max(ranges[i].stop, r.stop)) + i += 1 + merged.append(r) + + # copy ranges > merge + merged.extend(ranges[i:]) + + self._ranges = merged + + def remove(self, r): + assert isinstance(r, range) + # trivial range? + if not r: + return + + # find earliest possible carve point + ranges = self._ranges + i = bisect.bisect_left(ranges, r.start, + key=lambda r: r.stop) + + # copy ranges < carve + carved = ranges[:i] + + # carve overlapping ranges, note this can split ranges + while i < len(ranges) and ranges[i].start <= r.stop: + if ranges[i].start < r.start: + carved.append(range(ranges[i].start, r.start)) + if ranges[i].stop > r.stop: + carved.append(range(r.stop, ranges[i].stop)) + i += 1 + + # copy ranges > carve + carved.extend(ranges[i:]) + + self._ranges = carved + + @property + def start(self): + if not self._ranges: + return 0 + else: + return self._ranges[0].start + + @property + def stop(self): + if not self._ranges: + return 0 + else: + return self._ranges[-1].stop + + def __len__(self): + return self.stop + + def copy(self): + # create a shallow copy + ranges = RangeSet() + ranges._ranges = self._ranges.copy() + return ranges + + def __getitem__(self, slice_): + assert isinstance(slice_, slice) + + # create a copy + ranges = self.copy() + + # just use remove to do the carving, it's good enough probably + if slice_.stop is not None: + ranges.remove(range(slice_.stop, len(self))) + if slice_.start is not None: + ranges.remove(range(0, slice_.start)) + ranges._ranges = [range( + r.start - slice_.start, + r.stop - slice_.start) + for r in ranges._ranges] + + return ranges + + def __ior__(self, other): + for r in other.ranges(): + self.add(r) + return self + + def __or__(self, other): + ranges = self.copy() + ranges |= other + return ranges + + +# an abstract block representation +class TraceBlock: + def __init__(self, block, *, + readed=None, proged=None, erased=None, wear=0, + x=None, y=None, width=None, height=None): + self.block = block + self.readed = readed if readed is not None else RangeSet() + self.proged = proged if proged is not None else RangeSet() + self.erased = erased if erased is not None else RangeSet() + self.wear = wear + self.x = x + self.y = y + self.width = width + self.height = height + + def __repr__(self): + return 'TraceBlock(0x%x, x=%s, y=%s, width=%s, height=%s)' % ( + self.block, + self.x, self.y, self.width, self.height) + + def __eq__(self, other): + return self.block == other.block + + def __ne__(self, other): + return self.block != other.block + + def __hash__(self): + return hash(self.block) + + def __lt__(self, other): + return self.block < other.block + + def __le__(self, other): + return self.block <= other.block + + def __gt__(self, other): + return self.block > other.block + + def __ge__(self, other): + return self.block >= other.block + + # align to pixel boundaries + def align(self): + # this extra +0.1 and using points instead of width/height is + # to help minimize rounding errors + x0 = int(self.x+0.1) + y0 = int(self.y+0.1) + x1 = int(self.x+self.width+0.1) + y1 = int(self.y+self.height+0.1) + self.x = x0 + self.y = y0 + self.width = x1 - x0 + self.height = y1 - y0 + + # generate attrs for punescaping + @ft.cached_property + def attrs(self): + # really the only reasonable attrs are block and wear + return { + 'block': self.block, + 'wear': self.wear, + } + + # some simulated bd operations + def read(self, off, size): + self.readed.add(range(off, off+size)) + + def prog(self, off, size): + self.proged.add(range(off, off+size)) + + def erase(self, off, size, *, + wear=0): + self.erased.add(range(off, off+size)) + self.wear += wear + + def clear(self): + self.readed = RangeSet() + self.proged = RangeSet() + self.erased = RangeSet() + + + +def main(path='-', *, + block_size=None, + block_count=None, + blocks=None, + reads=False, + progs=False, + erases=False, + wear=False, + wear_only=False, + block_cycles=None, + volatile=False, + chars=[], + wear_chars=[], + colors=[], + wear_colors=[], + color='auto', + dots=False, + braille=False, + width=None, + height=None, + block_cols=None, + block_rows=None, + block_ratio=None, + no_header=False, + hilbert=False, + lebesgue=False, + contiguous=False, + to_scale=None, + to_ratio=1/1, + tiny=False, + title=None, + lines=None, + head=False, + cat=False, + coalesce=None, + wait=None, + keep_open=False, + **args): + # figure out what color should be + if color == 'auto': + color = sys.stdout.isatty() + elif color == 'always': + color = True + else: + color = False + + # if not specified default to all ops + if not reads and not progs and not erases and not wear_only: + progs = True + reads = True + erases = True + + # wear_only implies only wear + if wear_only: + wear = True + + # block_cycles implies wear + if block_cycles is not None: + wear = True + + # tiny mode? + if tiny: + if block_ratio is None: + block_ratio = 1 + if to_scale is None: + to_scale = 1 + no_header = True + + if block_ratio is None: + # try to align block_ratio to chars, even in braille/dots + # mode (we can't color sub-chars) + if braille or dots: + block_ratio = 1/2 + else: + block_ratio = 1 + + # what chars/colors/labels to use? + chars_ = [] + for char in chars: + if isinstance(char, tuple): + chars_.extend((char[0], c) for c in psplit(char[1])) + else: + chars_.extend(psplit(char)) + chars_ = CsvAttr(chars_, + defaults=[True] if braille or dots else CHARS) + + wear_chars_ = [] + for char in wear_chars: + if isinstance(char, tuple): + wear_chars_.extend((char[0], c) for c in psplit(char[1])) + else: + wear_chars_.extend(psplit(char)) + wear_chars_ = CsvAttr(wear_chars_, + defaults=[True] if braille or dots else WEAR_CHARS) + + colors_ = CsvAttr(colors, defaults=COLORS) + + wear_colors_ = CsvAttr(wear_colors, defaults=WEAR_COLORS) + + # is bd geometry specified? + if isinstance(block_size, tuple): + block_size, block_count_ = block_size + if block_count is None: + block_count = block_count_ + + # keep track of block_size/block_count and block map state + block_size_ = block_size + block_count_ = block_count + bmap = None + # keep track of some extra info + readed = 0 + proged = 0 + erased = 0 + + def bmap_init(block_size__, block_count__): + nonlocal block_size_ + nonlocal block_count_ + nonlocal bmap + nonlocal readed + nonlocal proged + nonlocal erased + + # keep track of block_size/block_count + if block_size is None: + block_size_ = block_size__ + if block_count is None: + block_count_ = block_count__ + + # flatten blocks, default to all blocks + blocks_ = list( + range(blocks.start or 0, blocks.stop or block_count_) + if isinstance(blocks, slice) + else range(blocks, blocks+1) + if blocks + else range(block_count_)) + + # create a new block map? + if bmap is None or volatile: + bmap = {b: TraceBlock(b) for b in blocks_} + readed = 0 + proged = 0 + erased = 0 + + # just resize block map + else: + bmap = {b: bmap[b] if b in bmap else TraceBlock(b) + for b in blocks_} + + # if we know block_count, go ahead and flatten blocks + create + # a block map, otherwise we need to wait for first bd init + if block_size is not None and block_count is not None: + bmap_init(block_size, block_count) + + + ## trace parser + + # precompute trace regexes + init_pattern = re.compile( + '^(?P[^ :]*):(?P[0-9]+):trace:' + '.*?bd_createcfg\(' + '\s*(?P\w+)' + '(?:' + 'block_size=(?P\w+)' + '|' 'block_count=(?P\w+)' + '|' '.*?' ')*' '\)') + read_pattern = re.compile( + '^(?P[^ :]*):(?P[0-9]+):trace:' + '.*?bd_read\(' + '\s*(?P\w+)' '\s*,' + '\s*(?P\w+)' '\s*,' + '\s*(?P\w+)' '\s*,' + '\s*(?P\w+)' '\s*,' + '\s*(?P\w+)' '\s*\)') + prog_pattern = re.compile( + '^(?P[^ :]*):(?P[0-9]+):trace:' + '.*?bd_prog\(' + '\s*(?P\w+)' '\s*,' + '\s*(?P\w+)' '\s*,' + '\s*(?P\w+)' '\s*,' + '\s*(?P\w+)' '\s*,' + '\s*(?P\w+)' '\s*\)') + erase_pattern = re.compile( + '^(?P[^ :]*):(?P[0-9]+):trace:' + '.*?bd_erase\(' + '\s*(?P\w+)' '\s*,' + '\s*(?P\w+)' + '(?:\s*\(\s*(?P\w+)\s*\))?' '\s*\)') + sync_pattern = re.compile( + '^(?P[^ :]*):(?P[0-9]+):trace:' + '.*?bd_sync\(' + '\s*(?P\w+)' '\s*\)') + + def trace__(line): + nonlocal readed + nonlocal proged + nonlocal erased + + # string searching is much faster than the regex here, this + # actually has a big impact given the sheer quantity of how much + # trace output we have to deal with + if ('trace' not in line + # ignore return trace statements + or '->' in line): + return False + + # note we can't do most ops until we know block_count/block_size + + # bd init? + if 'bd_createcfg(' in line: + m = init_pattern.match(line) + if not m: + return False + # block_size/block_count missing? + if not m.group('block_size') or not m.group('block_count'): + return False + block_size__ = int(m.group('block_size'), 0) + block_count__ = int(m.group('block_count'), 0) + + bmap_init(block_size__, block_count__) + return True + + # bd read? + elif reads and bmap is not None and 'bd_read(' in line: + m = read_pattern.match(line) + if not m: + return False + block = int(m.group('block'), 0) + off = int(m.group('off'), 0) + size = int(m.group('size'), 0) + + if block not in bmap: + return False + else: + bmap[block].read(off, size) + readed += size + return True + + # bd prog? + elif progs and bmap is not None and 'bd_prog(' in line: + m = prog_pattern.match(line) + if not m: + return False + block = int(m.group('block'), 0) + off = int(m.group('off'), 0) + size = int(m.group('size'), 0) + + if block not in bmap: + return False + else: + bmap[block].prog(off, size) + proged += size + return True + + # bd erase? + elif (erases or wear) and bmap is not None and 'bd_erase(' in line: + m = erase_pattern.match(line) + if not m: + return False + + block = int(m.group('block'), 0) + if block not in bmap: + return False + else: + bmap[block].erase(0, block_size_, + wear=+1 if wear else 0) + erased += block_count_ + return True + + else: + return False + + + ## bmap renderer + + # these curves are expensive to calculate, so memoize these + if hilbert: + curve = ft.lru_cache(16)(lambda w, h: list(hilbert_curve(w, h))) + elif lebesgue: + curve = ft.lru_cache(16)(lambda w, h: list(lebesgue_curve(w, h))) + else: + curve = ft.lru_cache(16)(lambda w, h: list(naive_curve(w, h))) + + def draw__(ring, width, height): + nonlocal bmap + # still waiting on bd init + if bmap is None: + return + + # compute total ops + total = readed + proged + erased + + # if we're showing wear, find min/max/avg/etc + if wear: + wear_min = min(b.wear for b in bmap.values()) + wear_max = max(b.wear for b in bmap.values()) + wear_avg = (sum(b.wear for b in bmap.values()) + / max(len(bmap), 1)) + wear_stddev = mt.sqrt( + sum((b.wear - wear_avg)**2 for b in bmap.values()) + / max(len(bmap), 1)) + + # if block_cycles isn't provided or is zero, scale based on + # max wear + if block_cycles: + block_cycles_ = block_cycles + else: + block_cycles_ = wear_max + + # build a title + if title: + title_ = punescape(title, { + 'geometry': '%sx%s' % (block_size_, block_count_), + 'block_size': block_size_, + 'block_count': block_count_, + 'total': total, + 'read': readed, + 'read_percent': 100*readed / max(total, 1), + 'prog': proged, + 'prog_percent': 100*proged / max(total, 1), + 'erase': erased, + 'erase_percent': 100*erased / max(total, 1), + 'wear_min': wear_min if wear else '?', + 'wear_min_percent': + 100*wear_min / max(block_cycles_, 1) if wear else '?', + 'wear_max': wear_max if wear else '?', + 'wear_max_percent': + 100*wear_max / max(block_cycles_, 1) if wear else '?', + 'wear_avg': wear_avg if wear else '?', + 'wear_avg_percent': + 100*wear_avg / max(block_cycles_, 1) if wear else '?', + 'wear_stddev': wear_stddev if wear else '?', + 'wear_stddev_percent': + 100*wear_stddev / max(block_cycles_, 1) if wear else '?', + }) + else: + title_ = ('bd %dx%d%s%s%s%s' % ( + block_size_, block_count_, + ', %s read' % ('%.1f%%' % (100*readed / max(total, 1))) + if reads else '', + ', %s prog' % ('%.1f%%' % (100*proged / max(total, 1))) + if progs else '', + ', %s erase' % ('%.1f%%' % (100*erased / max(total, 1))) + if erases else '', + ', %s wear' % ( + '%.1f%% +-%.1fσ' % ( + 100*wear_avg / max(block_cycles_, 1), + 100*wear_stddev / max(block_cycles_, 1))) + if wear else '')) + + # give ring a writeln function + def writeln(self, s=''): + self.write(s) + self.write('\n') + ring.writeln = writeln.__get__(ring) + + # figure out width/height + if width is None: + width_ = min(80, shutil.get_terminal_size((80, 5))[0]) + elif width > 0: + width_ = width + else: + width_ = max(0, shutil.get_terminal_size((80, 5))[0] + width) + + if height is None: + height_ = 2 if not no_header else 1 + elif height > 0: + height_ = height + else: + height_ = max(0, shutil.get_terminal_size((80, 5))[1] + height) + + # scale width/height if requested + if (to_scale is not None + and (width is None or height is None)): + # don't include header in scale + width__ = width_ + height__ = height_ - (1 if not no_header else 0) + + # scale width only + if height is not None: + width__ = mt.ceil((len(bmap) * to_scale) / max(height__, 1)) + # scale height only + elif width is not None: + height__ = mt.ceil((len(bmap) * to_scale) / max(width__, 1)) + # scale based on aspect-ratio + else: + width__ = mt.ceil(mt.sqrt(len(bmap) * to_scale * to_ratio)) + height__ = mt.ceil((len(bmap) * to_scale) / max(width__, 1)) + + width_ = width__ + height_ = height__ + (1 if not no_header else 0) + + # create a canvas + canvas = Canvas( + width_, + height_ - (1 if not no_header else 0), + color=color, + dots=dots, + braille=braille) + + # if contiguous, compute the global curve + if contiguous: + global_block = min(bmap.keys(), default=0) + global_curve = list(curve(canvas.width, canvas.height)) + + # if blocky, figure out block sizes/locations + else: + # figure out block_cols_/block_rows_ + if block_cols is not None and block_rows is not None: + block_cols_ = block_cols + block_rows_ = block_rows + elif block_rows is not None: + block_cols_ = mt.ceil(len(bmap) / block_rows) + block_rows_ = block_rows + elif block_cols is not None: + block_cols_ = block_cols + block_rows_ = mt.ceil(len(bmap) / block_cols) + else: + # divide by 2 until we hit our target ratio, this works + # well for things that are often powers-of-two + block_cols_ = 1 + block_rows_ = len(bmap) + while (abs(((canvas.width/(block_cols_*2)) + / max(canvas.height/mt.ceil(block_rows_/2), 1)) + - block_ratio) + < abs(((canvas.width/block_cols_) + / max(canvas.height/block_rows_, 1))) + - block_ratio): + block_cols_ *= 2 + block_rows_ = mt.ceil(block_rows_ / 2) + + block_width_ = canvas.width / block_cols_ + block_height_ = canvas.height / block_rows_ + + # assign block locations based on block_rows_/block_cols_ and + # the requested space filling curve + for (x, y), b in zip( + curve(block_cols_, block_rows_), + sorted(bmap.values())): + b.x = x * block_width_ + b.y = y * block_height_ + b.width = block_width_ + b.height = block_height_ + + # align to pixel boundaries + b.align() + + # bump up to at least one pixel for every block, dont't + # worry about out-of-bounds, Canvas handles this for us + b.width = max(b.width, 1) + b.height = max(b.height, 1) + + # assign chars based on op + block + for b in bmap.values(): + b.chars = {} + for op in ((['read'] if reads else []) + + (['prog'] if progs else []) + + (['erase'] if erases else []) + + ['noop']): + char__ = chars_.get((b.block, (op, '0x%x' % b.block))) + if char__ is not None: + if isinstance(char__, str): + # don't punescape unless we have to + if '%' in char__: + char__ = punescape(char__, b.attrs) + char__ = char__[0] # limit to 1 char + b.chars[op] = char__ + + # assign colors based on op + block + for b in bmap.values(): + b.colors = {} + for op in ((['read'] if reads else []) + + (['prog'] if progs else []) + + (['erase'] if erases else []) + + ['noop']): + color__ = colors_.get((b.block, (op, '0x%x' % b.block))) + if color__ is not None: + # don't punescape unless we have to + if '%' in color__: + color__ = punescape(color__, b.attrs) + b.colors[op] = color__ + + # assign wear chars based on block + if wear: + for b in bmap.values(): + b.wear_chars = [] + for char__ in wear_chars_.getall((b.block, '0x%x' % b.block)): + if isinstance(char__, str): + # don't punescape unless we have to + if '%' in char__: + char__ = punescape(char__, b.attrs) + char__ = char__[0] # limit to 1 char + b.wear_chars.append(char__) + + # assign wear colors based on block + if wear: + for b in bmap.values(): + b.wear_colors = [] + for color__ in wear_colors_.getall((b.block, '0x%x' % b.block)): + # don't punescape unless we have to + if '%' in color__: + color__ = punescape(color__, b.attrs) + b.wear_colors.append(color__) + + # render to canvas in a specific z-order that prioritizes + # interesting ops + for op in reversed(Z_ORDER): + # don't render noops in braille/dots mode + if (braille or dots) and op == 'noop': + continue + # skip ops we're not interested in + if ((not reads and op == 'read') + or (not progs and op == 'prog') + or (not erases and op == 'erase') + or (not wear and op == 'wear')): + continue + + for b in bmap.values(): + if op == 'read': + ranges__ = b.readed + char__ = b.chars['read'] + color__ = b.colors['read'] + elif op == 'prog': + ranges__ = b.proged + char__ = b.chars['prog'] + color__ = b.colors['prog'] + elif op == 'erase': + ranges__ = b.erased + char__ = b.chars['erase'] + color__ = b.colors['erase'] + elif op == 'wear': + # _no_ wear? + if b.wear == 0: + continue + ranges__ = RangeSet([range(block_size_)]) + # scale char/color based on either block_cycles + # or wear_avg + if block_cycles: + wear__ = min(b.wear / max(block_cycles, 1), 1.0) + else: + wear__ = min(b.wear / max(2*wear_avg, 1), 1.0) + char__ = b.wear_chars[int(wear__*(len(b.wear_chars)-1))] + color__ = b.wear_colors[int(wear__*(len(b.wear_colors)-1))] + else: + ranges__ = RangeSet([range(block_size_)]) + char__ = b.chars['noop'] + color__ = b.colors['noop'] + + if not ranges__: + continue + + # contiguous? + if contiguous: + for range__ in ranges__.ranges(): + # where are we in the curve? + block__ = b.block - global_block + range__ = range( + mt.floor(((block__*block_size_ + range__.start) + / (block_size_ * len(bmap))) + * len(global_curve)), + mt.ceil(((block__*block_size_ + range__.stop) + / (block_size_ * len(bmap))) + * len(global_curve))) + + # map to global curve + for i in range__: + if i >= len(global_curve): + continue + x__, y__ = global_curve[i] + + # flip y + y__ = canvas.height - (y__+1) + + canvas.point(x__, y__, + char=char__, + color=color__) + + # blocky? + else: + x__ = b.x + y__ = b.y + width__ = b.width + height__ = b.height + + # flip y + y__ = canvas.height - (y__+height__) + + for range__ in ranges__.ranges(): + # scale from bytes -> pixels + range__ = range( + mt.floor((range__.start/block_size_) + * (width__*height__)), + mt.ceil((range__.stop/block_size_) + * (width__*height__))) + # map to in-block curve + for i, (dx, dy) in enumerate(curve(width__, height__)): + if i in range__: + # flip y + canvas.point(x__+dx, y__+(height__-(dy+1)), + char=char__, + color=color__) + + # print some summary info + if not no_header: + ring.writeln(title_) + + # draw canvas + for row in range(canvas.height//canvas.yscale): + line = canvas.draw(row) + ring.writeln(line) + + # clear bmap + for b in bmap.values(): + b.clear() + + + ## main loop + lock = th.Lock() + event = th.Event() + def main_(): + try: + while True: + with openio(path) as f: + count = 0 + for line in f: + with lock: + count += trace__(line) + + # always redraw if we're sleeping, otherwise + # wait for coalesce number of operations + if wait is not None or count >= (coalesce or 1): + event.set() + count = 0 + + if not keep_open: + break + # don't just flood open calls + time.sleep(wait or 2) + + except FileNotFoundError as e: + print("error: file not found %r" % path, + file=sys.stderr) + sys.exit(-1) + + except KeyboardInterrupt: + pass + + # keep track of history if lines specified + if lines is not None: + ring = RingIO(lines+1 + if not no_header and lines > 0 + else lines) + def draw_(): + # cat? write directly to stdout + if cat: + draw__(sys.stdout, + width=width, + # make space for shell prompt + height=-1 if height is ... else height) + # not cat? write to a bounded ring + else: + ring_ = RingIO(head=head) + draw__(ring_, + width=width, + height=0 if height is ... else height) + # no history? draw immediately + if lines is None: + ring_.draw() + # history? merge with previous lines + else: + # write header separately? + if not no_header: + if not ring.lines: + ring.lines.append('') + ring.lines.extend(it.islice(ring_.lines, 1, None)) + ring.lines[0] = ring_.lines[0] + else: + ring.lines.extend(ring_.lines) + ring.draw() + + # print in a background thread + done = False + def background(): + while not done: + event.wait() + event.clear() + with lock: + draw_() + # sleep a minimum amount of time to avoid flickering + time.sleep(wait or 0.01) + th.Thread(target=background, daemon=True).start() + + main_() + + done = True + lock.acquire() # avoids https://bugs.python.org/issue42717 + if not cat: + # give ourselves one last draw, helps if background is + # never triggered + draw_() + sys.stdout.write('\n') + + +if __name__ == "__main__": + import sys + import argparse + parser = argparse.ArgumentParser( + description="Render operations on block devices based on " + "trace output.", + allow_abbrev=False) + parser.add_argument( + 'path', + nargs='?', + help="Path to read from.") + parser.add_argument( + '-b', '--block-size', + type=bdgeom, + help="Block size/geometry in bytes. Accepts x.") + parser.add_argument( + '--block-count', + type=lambda x: int(x, 0), + help="Block count in blocks.") + parser.add_argument( + '-@', '--blocks', + type=lambda x: ( + slice(*(int(x, 0) if x.strip() else None + for x in x.split(',', 1))) + if ',' in x + else int(x, 0)), + help="Show a specific block, may be a range.") + parser.add_argument( + '--reads', + action='store_true', + help="Render reads.") + parser.add_argument( + '--progs', + action='store_true', + help="Render progs.") + parser.add_argument( + '--erases', + action='store_true', + help="Render erases.") + parser.add_argument( + '--wear', + action='store_true', + help="Render wear.") + parser.add_argument( + '--wear-only', + action='store_true', + help="Only render wear, don't render bd ops. Implies --wear.") + parser.add_argument( + '--block-cycles', + type=lambda x: int(x, 0), + help="Assumed maximum number of erase cycles when measuring " + "wear. Defaults to the maximum wear on any single block. " + "Implies --wear.") + parser.add_argument( + '--volatile', + action='store_true', + help="Reset wear on block device initialization.") + parser.add_argument( + '-.', '--add-char', '--chars', + dest='chars', + action='append', + type=lambda x: ( + lambda ks, v: ( + tuple(k.strip() for k in ks.split(',')), + v.strip()) + )(*x.split('=', 1)) + if '=' in x else x.strip(), + help="Add characters to use. Can be assigned to a specific " + "operation/block. Accepts %% modifiers.") + parser.add_argument( + '-,', '--add-wear-char', '--wear-chars', + dest='wear_chars', + action='append', + type=lambda x: ( + lambda ks, v: ( + tuple(k.strip() for k in ks.split(',')), + v.strip()) + )(*x.split('=', 1)) + if '=' in x else x.strip(), + help="Add wear characters to use. Can be assigned to a specific " + "operation/block. Accepts %% modifiers.") + parser.add_argument( + '-C', '--add-color', + dest='colors', + action='append', + type=lambda x: ( + lambda ks, v: ( + tuple(k.strip() for k in ks.split(',')), + v.strip()) + )(*x.split('=', 1)) + if '=' in x else x.strip(), + help="Add a color to use. Can be assigned to a specific " + "block type/block. Accepts %% modifiers.") + parser.add_argument( + '-G', '--add-wear-color', + dest='wear_colors', + action='append', + type=lambda x: ( + lambda ks, v: ( + tuple(k.strip() for k in ks.split(',')), + v.strip()) + )(*x.split('=', 1)) + if '=' in x else x.strip(), + help="Add a wear color to use. Can be assigned to a specific " + "operation/block. Accepts %% modifiers.") + parser.add_argument( + '--color', + choices=['never', 'always', 'auto'], + default='auto', + help="When to use terminal colors. Defaults to 'auto'.") + parser.add_argument( + '-:', '--dots', + action='store_true', + help="Use 1x2 ascii dot characters.") + parser.add_argument( + '-⣿', '--braille', + action='store_true', + help="Use 2x4 unicode braille characters. Note that braille " + "characters sometimes suffer from inconsistent widths.") + parser.add_argument( + '-W', '--width', + nargs='?', + type=lambda x: int(x, 0), + const=0, + help="Width in columns. <=0 uses the terminal width. Defaults " + "to min(terminal, 80).") + parser.add_argument( + '-H', '--height', + nargs='?', + type=lambda x: int(x, 0), + const=..., # handles shell prompt spacing, which is a bit subtle + help="Height in rows. <=0 uses the terminal height. Defaults " + "to 1.") + parser.add_argument( + '-X', '--block-cols', + type=lambda x: int(x, 0), + help="Number of blocks on the x-axis. Guesses from --block-count " + "and --block-ratio by default.") + parser.add_argument( + '-Y', '--block-rows', + type=lambda x: int(x, 0), + help="Number of blocks on the y-axis. Guesses from --block-count " + "and --block-ratio by default.") + parser.add_argument( + '--block-ratio', + dest='block_ratio', + type=lambda x: ( + (lambda a, b: a / b)(*(float(v) for v in x.split(':', 1))) + if ':' in x else float(x)), + help="Target ratio for block sizes. Defaults to 1:1 or 1:2 " + "for -:/--dots and -⣿/--braille.") + parser.add_argument( + '--no-header', + action='store_true', + help="Don't show the header.") + parser.add_argument( + '-U', '--hilbert', + action='store_true', + help="Render as a space-filling Hilbert curve.") + parser.add_argument( + '-Z', '--lebesgue', + action='store_true', + help="Render as a space-filling Z-curve.") + parser.add_argument( + '-u', '--contiguous', + action='store_true', + help="Render as one contiguous curve instead of organizing by " + "blocks first.") + parser.add_argument( + '--to-scale', + nargs='?', + type=lambda x: ( + (lambda a, b: a / b)(*(float(v) for v in x.split(':', 1))) + if ':' in x else float(x)), + const=1, + help="Scale the resulting map such that 1 char ~= 1/scale " + "blocks. Defaults to scale=1. ") + parser.add_argument( + '--to-ratio', + type=lambda x: ( + (lambda a, b: a / b)(*(float(v) for v in x.split(':', 1))) + if ':' in x else float(x)), + help="Aspect ratio to use with --to-scale. Defaults to 1:1.") + parser.add_argument( + '--tiny', + action='store_true', + help="Tiny mode, alias for --block-ratio=1, --to-scale=1, " + "and --no-header.") + parser.add_argument( + '--title', + help="Add a title. Accepts %% modifiers.") + parser.add_argument( + '-n', '--lines', + nargs='?', + type=lambda x: int(x, 0), + const=0, + help="Show this many lines of history. <=0 uses the terminal " + "height. Defaults to 1.") + parser.add_argument( + '-^', '--head', + action='store_true', + help="Show the first n lines.") + parser.add_argument( + '-c', '--cat', + action='store_true', + help="Pipe directly to stdout.") + parser.add_argument( + '-+', '--coalesce', + type=lambda x: int(x, 0), + help="Number of operations to coalesce together.") + parser.add_argument( + '-w', '--wait', + type=float, + help="Seconds to sleep between draws, coalescing operations " + "in between.") + parser.add_argument( + '-k', '--keep-open', + action='store_true', + help="Reopen the pipe on EOF, useful when multiple " + "processes are writing.") + sys.exit(main(**{k: v + for k, v in vars(parser.parse_intermixed_args()).items() + if v is not None})) diff --git a/scripts/parity.py b/scripts/parity.py new file mode 100755 index 000000000..a204231e8 --- /dev/null +++ b/scripts/parity.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 + +# prevent local imports +if __name__ == "__main__": + __import__('sys').path.pop(0) + +import io +import os +import struct +import sys +import functools as ft +import operator as op + +try: + import crc32c as crc32c_lib +except ModuleNotFoundError: + crc32c_lib = None + + +# open with '-' for stdin/stdout +def openio(path, mode='r', buffering=-1): + import os + if path == '-': + if 'r' in mode: + return os.fdopen(os.dup(sys.stdin.fileno()), mode, buffering) + else: + return os.fdopen(os.dup(sys.stdout.fileno()), mode, buffering) + else: + return open(path, mode, buffering) + +def crc32c(data, crc=0): + if crc32c_lib is not None: + return crc32c_lib.crc32c(data, crc) + else: + crc ^= 0xffffffff + for b in data: + crc ^= b + for j in range(8): + crc = (crc >> 1) ^ ((crc & 1) * 0x82f63b78) + return 0xffffffff ^ crc + +def popc(x): + return bin(x).count('1') + +def parity(x): + return popc(x) & 1 + + +def main(paths, *, + hex=False, + string=False): + import builtins + hex_, hex = hex, builtins.hex + + # interpret as sequence of hex bytes + if hex_: + bytes_ = [b for path in paths for b in path.split()] + print('%01x' % parity(crc32c(bytes(int(b, 16) for b in bytes_)))) + + # interpret as strings + elif string: + for path in paths: + print('%01x %s' % (parity(crc32c(path.encode('utf8'))), path)) + + # default to interpreting as paths + else: + if not paths: + paths = [None] + + for path in paths: + with openio(path or '-', 'rb') as f: + # calculate crc, crc32c preserves parity + crc = 0 + while True: + block = f.read(io.DEFAULT_BUFFER_SIZE) + if not block: + break + + crc = crc32c(block, crc) + + # print what we found + if path is not None: + print('%01x %s' % (parity(crc), path)) + else: + print('%01x' % parity(crc)) + + +if __name__ == "__main__": + import argparse + import sys + parser = argparse.ArgumentParser( + description="Calculates parity.", + allow_abbrev=False) + parser.add_argument( + 'paths', + nargs='*', + help="Paths to read. Reads stdin by default.") + parser.add_argument( + '-x', '--hex', + action='store_true', + help="Interpret as a sequence of hex bytes.") + parser.add_argument( + '-s', '--string', + action='store_true', + help="Interpret as strings.") + sys.exit(main(**{k: v + for k, v in vars(parser.parse_intermixed_args()).items() + if v is not None})) diff --git a/scripts/perf.py b/scripts/perf.py index 2ee006c0b..ca193d1d2 100755 --- a/scripts/perf.py +++ b/scripts/perf.py @@ -3,32 +3,39 @@ # Script to aggregate and report Linux perf results. # # Example: -# ./scripts/perf.py -R -obench.perf ./runners/bench_runner +# ./scripts/perf.py --record -obench.perf ./runners/bench_runner # ./scripts/perf.py bench.perf -j -Flfs.c -Flfs_util.c -Scycles # # Copyright (c) 2022, The littlefs authors. # SPDX-License-Identifier: BSD-3-Clause # +# prevent local imports +if __name__ == "__main__": + __import__('sys').path.pop(0) + import bisect import collections as co import csv import errno import fcntl +import fnmatch import functools as ft +import io import itertools as it -import math as m +import math as mt import multiprocessing as mp import os import re import shlex import shutil import subprocess as sp +import sys import tempfile import zipfile -# TODO support non-zip perf results? +# TODO support non-zip perf results? PERF_PATH = ['perf'] PERF_EVENTS = 'cycles,branch-misses,branches,cache-misses,cache-references' @@ -38,118 +45,155 @@ # integer fields -class Int(co.namedtuple('Int', 'x')): +class CsvInt(co.namedtuple('CsvInt', 'a')): __slots__ = () - def __new__(cls, x=0): - if isinstance(x, Int): - return x - if isinstance(x, str): + def __new__(cls, a=0): + if isinstance(a, CsvInt): + return a + if isinstance(a, str): try: - x = int(x, 0) + a = int(a, 0) except ValueError: # also accept +-∞ and +-inf - if re.match('^\s*\+?\s*(?:∞|inf)\s*$', x): - x = m.inf - elif re.match('^\s*-\s*(?:∞|inf)\s*$', x): - x = -m.inf + if re.match('^\s*\+?\s*(?:∞|inf)\s*$', a): + a = mt.inf + elif re.match('^\s*-\s*(?:∞|inf)\s*$', a): + a = -mt.inf else: raise - assert isinstance(x, int) or m.isinf(x), x - return super().__new__(cls, x) + if not (isinstance(a, int) or mt.isinf(a)): + a = int(a) + return super().__new__(cls, a) + + def __repr__(self): + return '%s(%r)' % (self.__class__.__name__, self.a) def __str__(self): - if self.x == m.inf: + if self.a == mt.inf: return '∞' - elif self.x == -m.inf: + elif self.a == -mt.inf: return '-∞' else: - return str(self.x) + return str(self.a) + + def __csv__(self): + if self.a == mt.inf: + return 'inf' + elif self.a == -mt.inf: + return '-inf' + else: + return repr(self.a) + + def __bool__(self): + return bool(self.a) def __int__(self): - assert not m.isinf(self.x) - return self.x + assert not mt.isinf(self.a) + return self.a def __float__(self): - return float(self.x) + return float(self.a) none = '%7s' % '-' def table(self): return '%7s' % (self,) - diff_none = '%7s' % '-' - diff_table = table - - def diff_diff(self, other): - new = self.x if self else 0 - old = other.x if other else 0 + def diff(self, other): + new = self.a if self else 0 + old = other.a if other else 0 diff = new - old - if diff == +m.inf: + if diff == +mt.inf: return '%7s' % '+∞' - elif diff == -m.inf: + elif diff == -mt.inf: return '%7s' % '-∞' else: return '%+7d' % diff def ratio(self, other): - new = self.x if self else 0 - old = other.x if other else 0 - if m.isinf(new) and m.isinf(old): + new = self.a if self else 0 + old = other.a if other else 0 + if mt.isinf(new) and mt.isinf(old): return 0.0 - elif m.isinf(new): - return +m.inf - elif m.isinf(old): - return -m.inf + elif mt.isinf(new): + return +mt.inf + elif mt.isinf(old): + return -mt.inf elif not old and not new: return 0.0 elif not old: - return 1.0 + return +mt.inf else: return (new-old) / old + def __pos__(self): + return self.__class__(+self.a) + + def __neg__(self): + return self.__class__(-self.a) + + def __abs__(self): + return self.__class__(abs(self.a)) + def __add__(self, other): - return self.__class__(self.x + other.x) + return self.__class__(self.a + other.a) def __sub__(self, other): - return self.__class__(self.x - other.x) + return self.__class__(self.a - other.a) def __mul__(self, other): - return self.__class__(self.x * other.x) + return self.__class__(self.a * other.a) + + def __truediv__(self, other): + if not other: + if self >= self.__class__(0): + return self.__class__(+mt.inf) + else: + return self.__class__(-mt.inf) + return self.__class__(self.a // other.a) + + def __mod__(self, other): + return self.__class__(self.a % other.a) # perf results class PerfResult(co.namedtuple('PerfResult', [ - 'file', 'function', 'line', + 'z', 'file', 'function', 'line', 'cycles', 'bmisses', 'branches', 'cmisses', 'caches', 'children'])): - _by = ['file', 'function', 'line'] + _prefix = 'perf' + _by = ['z', 'file', 'function', 'line'] _fields = ['cycles', 'bmisses', 'branches', 'cmisses', 'caches'] _sort = ['cycles', 'bmisses', 'cmisses', 'branches', 'caches'] _types = { - 'cycles': Int, - 'bmisses': Int, 'branches': Int, - 'cmisses': Int, 'caches': Int} + 'cycles': CsvInt, + 'bmisses': CsvInt, 'branches': CsvInt, + 'cmisses': CsvInt, 'caches': CsvInt} + _children = 'children' __slots__ = () - def __new__(cls, file='', function='', line=0, + def __new__(cls, z=0, file='', function='', line=0, cycles=0, bmisses=0, branches=0, cmisses=0, caches=0, - children=[]): - return super().__new__(cls, file, function, int(Int(line)), - Int(cycles), Int(bmisses), Int(branches), Int(cmisses), Int(caches), - children) + children=None): + return super().__new__(cls, z, file, function, int(CsvInt(line)), + CsvInt(cycles), + CsvInt(bmisses), CsvInt(branches), + CsvInt(cmisses), CsvInt(caches), + children if children is not None else []) def __add__(self, other): - return PerfResult(self.file, self.function, self.line, - self.cycles + other.cycles, - self.bmisses + other.bmisses, - self.branches + other.branches, - self.cmisses + other.cmisses, - self.caches + other.caches, - self.children + other.children) + return PerfResult(self.z, self.file, self.function, self.line, + self.cycles + other.cycles, + self.bmisses + other.bmisses, + self.branches + other.branches, + self.cmisses + other.cmisses, + self.caches + other.caches, + self.children + other.children) +# open with '-' for stdin/stdout def openio(path, mode='r', buffering=-1): - # allow '-' for stdin/stdout + import os if path == '-': - if mode == 'r': + if 'r' in mode: return os.fdopen(os.dup(sys.stdin.fileno()), mode, buffering) else: return os.fdopen(os.dup(sys.stdout.fileno()), mode, buffering) @@ -169,17 +213,17 @@ def record(command, *, with tempfile.NamedTemporaryFile('rb') as f: # figure out our perf invocation perf = perf_path + list(filter(None, [ - 'record', - '-F%s' % perf_freq - if perf_freq is not None - and perf_period is None else None, - '-c%s' % perf_period - if perf_period is not None else None, - '-B', - '-g', - '--all-user', - '-e%s' % perf_events, - '-o%s' % f.name])) + 'record', + '-F%s' % perf_freq + if perf_freq is not None + and perf_period is None else None, + '-c%s' % perf_period + if perf_period is not None else None, + '-B', + '-g', + '--all-user', + '-e%s' % perf_events, + '-o%s' % f.name])) # run our command try: @@ -206,7 +250,7 @@ def record(command, *, return err -# try to only process each dso onceS +# try to only process each dso once # # note this only caches with the non-keyword arguments def multiprocessing_cache(f): @@ -233,83 +277,259 @@ def multiprocessing_cache(*args, **kwargs): return multiprocessing_cache +class Sym(co.namedtuple('Sym', [ + 'name', 'global_', 'section', 'addr', 'size'])): + __slots__ = () + def __new__(cls, name, global_, section, addr, size): + return super().__new__(cls, name, global_, section, addr, size) + + def __repr__(self): + return '%s(%r, %r, %r, 0x%x, 0x%x)' % ( + self.__class__.__name__, + self.name, + self.global_, + self.section, + self.addr, + self.size) + +class SymInfo: + def __init__(self, syms): + self.syms = syms + + def get(self, k, d=None): + # allow lookup by both symbol and address + if isinstance(k, str): + # organize by symbol, note multiple symbols can share a name + if not hasattr(self, '_by_sym'): + by_sym = {} + for sym in self.syms: + if sym.name not in by_sym: + by_sym[sym.name] = [] + if sym not in by_sym[sym.name]: + by_sym[sym.name].append(sym) + self._by_sym = by_sym + + return self._by_sym.get(k, d) + + else: + import bisect + + # organize by address + if not hasattr(self, '_by_addr'): + # sort and keep largest/first when duplicates + syms = self.syms.copy() + syms.sort(key=lambda x: (x.addr, -x.size)) + + by_addr = [] + for sym in syms: + if (len(by_addr) == 0 + or by_addr[-1].addr != sym.addr): + by_addr.append(sym) + self._by_addr = by_addr + + # find sym by range + i = bisect.bisect(self._by_addr, k, + key=lambda x: x.addr) - 1 + # check that we're actually in this sym's size + if i > -1 and k < self._by_addr[i].addr+self._by_addr[i].size: + return self._by_addr[i] + else: + return d + + def __getitem__(self, k): + v = self.get(k) + if v is None: + raise KeyError(k) + return v + + def __contains__(self, k): + return self.get(k) is not None + + def __bool__(self): + return bool(self.syms) + + def __len__(self): + return len(self.syms) + + def __iter__(self): + return iter(self.syms) + + def globals(self): + return SymInfo([sym for sym in self.syms + if sym.global_]) + + def section(self, section): + return SymInfo([sym for sym in self.syms + # note we accept prefixes + if s.startswith(section)]) + @multiprocessing_cache -def collect_syms_and_lines(obj_path, *, - objdump_path=None, +def collect_syms(obj_path, global_=False, sections=None, *, + objdump_path=OBJDUMP_PATH, **args): symbol_pattern = re.compile( - '^(?P[0-9a-fA-F]+)' - '\s+.*' - '\s+(?P[0-9a-fA-F]+)' - '\s+(?P[^\s]+)\s*$') - line_pattern = re.compile( - '^\s+(?:' - # matches dir/file table - '(?P[0-9]+)' - '(?:\s+(?P[0-9]+))?' - '\s+.*' - '\s+(?P[^\s]+)' - # matches line opcodes - '|' '\[[^\]]*\]\s+' - '(?:' - '(?PSpecial)' - '|' '(?PCopy)' - '|' '(?PEnd of Sequence)' - '|' 'File .*?to (?:entry )?(?P\d+)' - '|' 'Line .*?to (?P[0-9]+)' - '|' '(?:Address|PC) .*?to (?P[0x0-9a-fA-F]+)' - '|' '.' ')*' - ')$', re.IGNORECASE) - - # figure out symbol addresses and file+line ranges - syms = {} - sym_at = [] - cmd = objdump_path + ['-t', obj_path] + '^(?P[0-9a-fA-F]+)' + ' (?P.).*' + '\s+(?P
[^\s]+)' + '\s+(?P[0-9a-fA-F]+)' + '\s+(?P[^\s]+)\s*$') + + # find symbol addresses and sizes + syms = [] + cmd = objdump_path + ['--syms', obj_path] if args.get('verbose'): print(' '.join(shlex.quote(c) for c in cmd)) proc = sp.Popen(cmd, - stdout=sp.PIPE, - stderr=sp.PIPE if not args.get('verbose') else None, - universal_newlines=True, - errors='replace', - close_fds=False) + stdout=sp.PIPE, + universal_newlines=True, + errors='replace', + close_fds=False) for line in proc.stdout: m = symbol_pattern.match(line) if m: name = m.group('name') + scope = m.group('scope') + section = m.group('section') addr = int(m.group('addr'), 16) size = int(m.group('size'), 16) - # ignore zero-sized symbols + # skip non-globals? + # l => local + # g => global + # u => unique global + # => neither + # ! => local + global + global__ = scope not in 'l ' + if global_ and not global__: + continue + # filter by section? note we accept prefixes + if (sections is not None + and not any(section.startswith(prefix) + for prefix in sections)): + continue + # skip zero sized symbols if not size: continue # note multiple symbols can share a name - if name not in syms: - syms[name] = set() - syms[name].add((addr, size)) - sym_at.append((addr, name, size)) + syms.append(Sym(name, global__, section, addr, size)) proc.wait() if proc.returncode != 0: - if not args.get('verbose'): - for line in proc.stderr: - sys.stdout.write(line) - # assume no debug-info on failure - pass - - # sort and keep largest/first when duplicates - sym_at.sort(key=lambda x: (x[0], -x[2], x[1])) - sym_at_ = [] - for addr, name, size in sym_at: - if len(sym_at_) == 0 or sym_at_[-1][0] != addr: - sym_at_.append((addr, name, size)) - sym_at = sym_at_ + raise sp.CalledProcessError(proc.returncode, proc.args) + + return SymInfo(syms) + +class Line(co.namedtuple('Line', ['file', 'line', 'addr'])): + __slots__ = () + def __new__(cls, file, line, addr): + return super().__new__(cls, file, line, addr) + + def __repr__(self): + return '%s(%r, %r, 0x%x)' % ( + self.__class__.__name__, + self.file, + self.line, + self.addr) + +class LineInfo: + def __init__(self, lines): + self.lines = lines + + def get(self, k, d=None): + # allow lookup by both address and file+line tuple + if not isinstance(k, tuple): + import bisect + + # organize by address + if not hasattr(self, '_by_addr'): + # sort and keep first when duplicates + lines = self.lines.copy() + lines.sort(key=lambda x: (x.addr, x.file, x.line)) + + by_addr = [] + for line in lines: + if (len(by_addr) == 0 + or by_addr[-1].addr != line.addr): + by_addr.append(line) + self._by_addr = by_addr + + # find file+line by addr + i = bisect.bisect(self._by_addr, k, + key=lambda x: x.addr) - 1 + if i > -1: + return self._by_addr[i] + else: + return d + + else: + import bisect + + # organize by file+line + if not hasattr(self, '_by_line'): + # sort and keep first when duplicates + lines = self.lines.copy() + lines.sort() + + by_line = [] + for line in lines: + if (len(by_line) == 0 + or by_line[-1].file != line.file + or by_line[-1].line != line.line): + by_line.append(line) + self._by_line = by_line + + # find addr by file+line tuple + i = bisect.bisect(self._by_line, k, + key=lambda x: (x.file, x.line)) - 1 + # make sure file at least matches! + if i > -1 and self._by_line[i].file == k[0]: + return self._by_line[i] + else: + return d + + def __getitem__(self, k): + v = self.get(k) + if v is None: + raise KeyError(k) + return v + + def __contains__(self, k): + return self.get(k) is not None + + def __bool__(self): + return bool(self.lines) + + def __len__(self): + return len(self.lines) + + def __iter__(self): + return iter(self.lines) + +@multiprocessing_cache +def collect_dwarf_lines(obj_path, *, + objdump_path=OBJDUMP_PATH, + **args): + line_pattern = re.compile( + # matches dir/file table + '^\s*(?P[0-9]+)' + '(?:\s+(?P[0-9]+))?' + '.*\s+(?P[^\s]+)\s*$' + # matches line opcodes + '|' '^\s*\[[^\]]*\]' '(?:' + '\s+(?PSpecial)' + '|' '\s+(?PCopy)' + '|' '\s+(?PEnd of Sequence)' + '|' '\s+File.*?to.*?(?P[0-9]+)' + '|' '\s+Line.*?to.*?(?P[0-9]+)' + '|' '\s+(?:Address|PC)' + '\s+.*?to.*?(?P[0xX0-9a-fA-F]+)' + '|' '\s+[^\s]+' ')+\s*$', + re.IGNORECASE) # state machine for dwarf line numbers, note that objdump's # decodedline seems to have issues with multiple dir/file # tables, which is why we need this lines = [] - line_at = [] - dirs = {} - files = {} + dirs = co.OrderedDict() + files = co.OrderedDict() op_file = 1 op_line = 1 op_addr = 0 @@ -317,11 +537,10 @@ def collect_syms_and_lines(obj_path, *, if args.get('verbose'): print(' '.join(shlex.quote(c) for c in cmd)) proc = sp.Popen(cmd, - stdout=sp.PIPE, - stderr=sp.PIPE if not args.get('verbose') else None, - universal_newlines=True, - errors='replace', - close_fds=False) + stdout=sp.PIPE, + universal_newlines=True, + errors='replace', + close_fds=False) for line in proc.stdout: m = line_pattern.match(line) if m: @@ -333,8 +552,8 @@ def collect_syms_and_lines(obj_path, *, dir = int(m.group('dir')) if dir in dirs: files[int(m.group('no'))] = os.path.join( - dirs[dir], - m.group('path')) + dirs[dir], + m.group('path')) else: files[int(m.group('no'))] = m.group('path') else: @@ -350,8 +569,7 @@ def collect_syms_and_lines(obj_path, *, or m.group('op_copy') or m.group('op_end')): file = os.path.abspath(files.get(op_file, '?')) - lines.append((file, op_line, op_addr)) - line_at.append((op_addr, file, op_line)) + lines.append(Line(file, op_line, op_addr)) if m.group('op_end'): op_file = 1 @@ -359,64 +577,44 @@ def collect_syms_and_lines(obj_path, *, op_addr = 0 proc.wait() if proc.returncode != 0: - if not args.get('verbose'): - for line in proc.stderr: - sys.stdout.write(line) - # assume no debug-info on failure - pass - - # sort and keep first when duplicates - lines.sort() - lines_ = [] - for file, line, addr in lines: - if len(lines_) == 0 or lines_[-1][0] != file or lines[-1][1] != line: - lines_.append((file, line, addr)) - lines = lines_ - - # sort and keep first when duplicates - line_at.sort() - line_at_ = [] - for addr, file, line in line_at: - if len(line_at_) == 0 or line_at_[-1][0] != addr: - line_at_.append((addr, file, line)) - line_at = line_at_ - - return syms, sym_at, lines, line_at + raise sp.CalledProcessError(proc.returncode, proc.args) + + return LineInfo(lines) def collect_decompressed(path, *, perf_path=PERF_PATH, sources=None, everything=False, + no_strip=False, propagate=0, depth=1, **args): sample_pattern = re.compile( - '(?P\w+)' - '\s+(?P\w+)' - '\s+(?P