diff --git a/tests/.gitignore b/tests/.gitignore index a1f53dd..6969312 100644 --- a/tests/.gitignore +++ b/tests/.gitignore @@ -69,6 +69,7 @@ tmp.* /supernode_atdepth_offset /sw_tree1 /sw_states +/treegen /truncated_property /truncated_string /truncated_memrsv diff --git a/tests/Makefile.tests b/tests/Makefile.tests index bed2f36..de12f6e 100644 --- a/tests/Makefile.tests +++ b/tests/Makefile.tests @@ -54,13 +54,13 @@ TESTS_DEPFILES = $(TESTS:%=%.d) \ TESTS_CLEANFILES_L = $(STD_CLEANFILES) \ *.dtb *.test.dts *.test.dt.yaml *.dtsv1 tmp.* *.bak \ - dumptrees + dumptrees treegen TESTS_CLEANFILES = $(TESTS) $(TESTS_CLEANFILES_L:%=$(TESTS_PREFIX)%) -TESTS_CLEANDIRS_L = fs +TESTS_CLEANDIRS_L = fs treegen-out TESTS_CLEANDIRS = $(TESTS_CLEANDIRS_L:%=$(TESTS_PREFIX)%) .PHONY: tests -tests: $(TESTS) $(TESTS_TREES) +tests: $(TESTS) $(TESTS_TREES) $(TESTS_PREFIX)treegen $(LIB_TESTS): %: $(TESTS_PREFIX)testutils.o util.o $(LIBFDT_dep) @@ -71,6 +71,8 @@ $(DL_LIB_TESTS): %: %.o $(TESTS_PREFIX)testutils.o util.o $(LIBFDT_dep) @$(VECHO) LD [libdl] $@ $(LINK.c) -o $@ $^ $(LIBDL) +$(TESTS_PREFIX)treegen: + $(TESTS_PREFIX)dumptrees: $(TESTS_PREFIX)trees.o $(TESTS_TREES): $(TESTS_PREFIX)dumptrees diff --git a/tests/meson.build b/tests/meson.build index 571c42f..32297c0 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -6,6 +6,10 @@ dumptrees = executable('dumptrees', files('dumptrees.c'), build_by_default: false, link_with: trees, dependencies: libfdt_dep) +treegen = executable('treegen', files('treegen.c'), + build_by_default: false, + dependencies: [util_dep]) + dumptrees_dtb = custom_target( 'dumptrees', command: [dumptrees, meson.current_build_dir()], @@ -139,10 +143,11 @@ run_test_types = [ 'fdtget', 'fdtput', 'fdtdump', - 'fdtoverlay' + 'fdtoverlay', + 'treegen' ] run_test_deps = [ - dtc_tools, dumptrees_dtb, tests_exe + dtc_tools, dumptrees_dtb, tests_exe, treegen ] if pylibfdt_enabled run_test_types += 'pylibfdt' diff --git a/tests/run_tests.sh b/tests/run_tests.sh index 842b543..df63a18 100755 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -1091,6 +1091,16 @@ fdtoverlay_tests() { run_wrap_test test "$bd" = "$pd" } +treegen_tests () { + mkdir -p treegen-out + run_wrap_test ./treegen treegen-out + for tree in test_tree1 bad_node_char bad_node_format bad_prop_char \ + ovf_size_strings truncated_property truncated_string \ + truncated_memrsv unterminated_memrsv two_roots named_root; do + run_wrap_test cmp $tree.dtb treegen-out/$tree.dtb + done +} + pylibfdt_tests () { run_dtc_test -I dts -O dtb -o test_props.dtb "$SRCDIR/test_props.dts" TMP=/tmp/tests.stderr.$$ @@ -1130,7 +1140,7 @@ while getopts "vt:me" ARG ; do done if [ -z "$TESTSETS" ]; then - TESTSETS="libfdt utilfdt dtc dtbs_equal fdtget fdtput fdtdump fdtoverlay" + TESTSETS="libfdt utilfdt dtc dtbs_equal fdtget fdtput fdtdump fdtoverlay treegen" # Test pylibfdt if the libfdt Python module is available. if ! $no_python; then @@ -1170,6 +1180,9 @@ for set in $TESTSETS; do "fdtoverlay") fdtoverlay_tests ;; + "treegen") + treegen_tests + ;; esac done diff --git a/tests/treegen.c b/tests/treegen.c new file mode 100644 index 0000000..6891bfd --- /dev/null +++ b/tests/treegen.c @@ -0,0 +1,756 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * treegen - generate test device tree blobs + * + * Build example device tree blobs for test. Note that some of the generated + * blobs are deliberately malformed, in order to test error conditions. + */ +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "tests.h" +#include "testdata.h" + +/* + * Buffer with write cursor. Emit helpers write at the cursor, advance it, and + * return the offset at which data was written. + */ +struct buf { + char *data; + size_t size, cursor; +}; + +#define CHUNKSIZE 1024 + +static struct buf buf_init(void) +{ + struct buf b = { NULL, 0, 0 }; + + return b; +} + +/* Low-level helpers */ + +static size_t emit_bytes(struct buf *b, const void *data, int len) +{ + size_t off = b->cursor; + + if (!len) + return off; + + if (b->cursor + len > b->size) { + size_t newsize = b->cursor + len + CHUNKSIZE; + + b->data = xrealloc(b->data, newsize); + memset(b->data + b->size, 0, newsize - b->size); + b->size = newsize; + } + memcpy(b->data + b->cursor, data, len); + b->cursor += len; + return off; +} + +static size_t emit_u32(struct buf *b, uint32_t val) +{ + fdt32_t beval = cpu_to_fdt32(val); + + return emit_bytes(b, &beval, sizeof(beval)); +} + +static size_t emit_u64(struct buf *b, uint64_t val) +{ + fdt64_t beval = cpu_to_fdt64(val); + + return emit_bytes(b, &beval, sizeof(beval)); +} + +static size_t emit_string(struct buf *b, const char *str) +{ + return emit_bytes(b, str, strlen(str) + 1); +} + +#define MAX_ALIGNMENT 8 + +static size_t emit_align(struct buf *b, size_t alignment) +{ + size_t pad = ((b->cursor + alignment - 1) & ~(alignment - 1)) - b->cursor; + static const char zeros[MAX_ALIGNMENT]; + + assert(alignment <= MAX_ALIGNMENT); + assert(pad < alignment); + + return emit_bytes(b, zeros, pad); +} + +static size_t start_block(struct buf *b) +{ + return b->cursor; +} + +static void fill_prop_name(struct buf *b, size_t strs, size_t prop, size_t stroff) +{ + struct fdt_property *fp = (struct fdt_property *)(b->data + prop); + + fp->nameoff = cpu_to_fdt32(stroff - strs); +} + +/* FDT structure helpers */ + +static size_t emit_fdt_header(struct buf *b) +{ + size_t off = emit_u32(b, FDT_MAGIC); + + emit_u32(b, 0); /* totalsize */ + emit_u32(b, 0); /* off_dt_struct */ + emit_u32(b, 0); /* off_dt_strings */ + emit_u32(b, 0); /* off_mem_rsvmap */ + emit_u32(b, 0x11); /* version */ + emit_u32(b, 0x10); /* last_comp_version */ + emit_u32(b, 0); /* boot_cpuid_phys */ + emit_u32(b, 0); /* size_dt_strings */ + emit_u32(b, 0); /* size_dt_struct */ + return off; +} + +static void finish_rsvmap(struct buf *b, size_t hdr, size_t rsvmap) +{ + struct fdt_header *fh = (struct fdt_header *)(b->data + hdr); + + fh->off_mem_rsvmap = cpu_to_fdt32(rsvmap - hdr); +} + +static void finish_struct_block(struct buf *b, size_t hdr, size_t start) +{ + struct fdt_header *fh = (struct fdt_header *)(b->data + hdr); + + fh->off_dt_struct = cpu_to_fdt32(start - hdr); + fh->size_dt_struct = cpu_to_fdt32(b->cursor - start); +} + +static void finish_strings_block(struct buf *b, size_t hdr, size_t start) +{ + struct fdt_header *fh = (struct fdt_header *)(b->data + hdr); + + fh->off_dt_strings = cpu_to_fdt32(start - hdr); + fh->size_dt_strings = cpu_to_fdt32(b->cursor - start); +} + +static void finish_totalsize(struct buf *b, size_t hdr) +{ + struct fdt_header *fh = (struct fdt_header *)(b->data + hdr); + + fh->totalsize = cpu_to_fdt32(b->cursor - hdr); +} + +static size_t emit_rsvmap_entry(struct buf *b, uint64_t addr, uint64_t size) +{ + size_t off = emit_u64(b, addr); + + emit_u64(b, size); + return off; +} + +static size_t emit_rsvmap_term(struct buf *b) +{ + return emit_rsvmap_entry(b, 0, 0); +} + +static void emit_rsvmap_empty(struct buf *b, size_t hdr) +{ + size_t rsvmap = start_block(b); + + emit_rsvmap_term(b); + finish_rsvmap(b, hdr, rsvmap); +} + +static size_t emit_begin_node(struct buf *b, const char *name) +{ + size_t off = emit_u32(b, FDT_BEGIN_NODE); + + emit_string(b, name); + emit_align(b, FDT_TAGSIZE); + return off; +} + +static size_t emit_end_node(struct buf *b) +{ + return emit_u32(b, FDT_END_NODE); +} + +static size_t emit_fdt_end(struct buf *b) +{ + return emit_u32(b, FDT_END); +} + +static size_t emit_prop_header(struct buf *b, int nameoff, int len) +{ + size_t off = emit_u32(b, FDT_PROP); + + emit_u32(b, len); + emit_u32(b, nameoff); + return off; +} + +static size_t emit_prop_nil(struct buf *b, int nameoff) +{ + return emit_prop_header(b, nameoff, 0); +} + +static size_t emit_prop_u32(struct buf *b, int nameoff, uint32_t val) +{ + size_t off = emit_prop_header(b, nameoff, 4); + + emit_u32(b, val); + return off; +} + +static size_t emit_prop_u64(struct buf *b, int nameoff, uint64_t val) +{ + size_t off = emit_prop_header(b, nameoff, 8); + + emit_u64(b, val); + return off; +} + +static size_t emit_prop_str(struct buf *b, int nameoff, const char *str) +{ + int len = strlen(str) + 1; + size_t off = emit_prop_header(b, nameoff, len); + + emit_bytes(b, str, len); + emit_align(b, FDT_TAGSIZE); + return off; +} + +#define emit_prop_strings(b, nameoff, ...) \ + emit_prop_strings_(b, nameoff, __VA_ARGS__, NULL) + +static size_t emit_prop_strings_(struct buf *b, int nameoff, ...) +{ + size_t off = emit_prop_header(b, nameoff, 0); + size_t start = b->cursor; + va_list ap; + const char *s; + + va_start(ap, nameoff); + while ((s = va_arg(ap, const char *)) != NULL) + emit_string(b, s); + va_end(ap); + + ((struct fdt_property *)(b->data + off))->len = + cpu_to_fdt32(b->cursor - start); + emit_align(b, FDT_TAGSIZE); + return off; +} + +/* + * Tree generators + * + * Each function builds a complete DTB in a buffer. + */ + +static struct buf make_test_tree1(void) +{ + struct buf b = buf_init(); + size_t hdr; + size_t p_compatible, p_prop_int, p_prop_int64, p_prop_str, + p_address_cells, p_size_cells; + size_t p_s1_compatible, p_s1_reg, p_s1_prop_int; + size_t p_s1ss_compatible, p_s1ss_placeholder, p_s1ss_prop_int; + size_t p_s2_reg, p_s2_linux_phandle, p_s2_prop_int, + p_s2_address_cells, p_s2_size_cells; + size_t p_s2ss0_reg, p_s2ss0_phandle, p_s2ss0_compatible, + p_s2ss0_prop_int; + + hdr = emit_fdt_header(&b); + emit_align(&b, 8); + + { + size_t rsvmap = start_block(&b); + + emit_rsvmap_entry(&b, TEST_ADDR_1, TEST_SIZE_1); + emit_rsvmap_entry(&b, TEST_ADDR_2, TEST_SIZE_2); + emit_rsvmap_term(&b); + finish_rsvmap(&b, hdr, rsvmap); + } + + { + size_t ss = start_block(&b); + + emit_begin_node(&b, ""); + p_compatible = emit_prop_str(&b, 0, "test_tree1"); + p_prop_int = emit_prop_u32(&b, 0, TEST_VALUE_1); + p_prop_int64 = emit_prop_u64(&b, 0, TEST_VALUE64_1); + p_prop_str = emit_prop_str(&b, 0, TEST_STRING_1); + p_address_cells = emit_prop_u32(&b, 0, 1); + p_size_cells = emit_prop_u32(&b, 0, 0); + + emit_begin_node(&b, "subnode@1"); + p_s1_compatible = emit_prop_str(&b, 0, "subnode1"); + p_s1_reg = emit_prop_u32(&b, 0, 1); + p_s1_prop_int = emit_prop_u32(&b, 0, TEST_VALUE_1); + + emit_begin_node(&b, "subsubnode"); + p_s1ss_compatible = emit_prop_strings(&b, 0, + "subsubnode1", "subsubnode"); + p_s1ss_placeholder = emit_prop_strings(&b, 0, + "this is a placeholder string", + "string2"); + p_s1ss_prop_int = emit_prop_u32(&b, 0, TEST_VALUE_1); + emit_end_node(&b); + + emit_begin_node(&b, "ss1"); + emit_end_node(&b); + + emit_end_node(&b); + + emit_begin_node(&b, "subnode@2"); + p_s2_reg = emit_prop_u32(&b, 0, 2); + p_s2_linux_phandle = emit_prop_u32(&b, 0, PHANDLE_1); + p_s2_prop_int = emit_prop_u32(&b, 0, TEST_VALUE_2); + p_s2_address_cells = emit_prop_u32(&b, 0, 1); + p_s2_size_cells = emit_prop_u32(&b, 0, 0); + + emit_begin_node(&b, "subsubnode@0"); + p_s2ss0_reg = emit_prop_u32(&b, 0, 0); + p_s2ss0_phandle = emit_prop_u32(&b, 0, PHANDLE_2); + p_s2ss0_compatible = emit_prop_strings(&b, 0, + "subsubnode2", "subsubnode"); + p_s2ss0_prop_int = emit_prop_u32(&b, 0, TEST_VALUE_2); + emit_end_node(&b); + + emit_begin_node(&b, "ss2"); + emit_end_node(&b); + + emit_end_node(&b); + + emit_end_node(&b); + emit_fdt_end(&b); + finish_struct_block(&b, hdr, ss); + } + + { + size_t strs = start_block(&b); + size_t s; + + s = emit_string(&b, "compatible"); + fill_prop_name(&b, strs, p_compatible, s); + fill_prop_name(&b, strs, p_s1_compatible, s); + fill_prop_name(&b, strs, p_s1ss_compatible, s); + fill_prop_name(&b, strs, p_s2ss0_compatible, s); + + s = emit_string(&b, "prop-int"); + fill_prop_name(&b, strs, p_prop_int, s); + fill_prop_name(&b, strs, p_s1_prop_int, s); + fill_prop_name(&b, strs, p_s1ss_prop_int, s); + fill_prop_name(&b, strs, p_s2_prop_int, s); + fill_prop_name(&b, strs, p_s2ss0_prop_int, s); + + fill_prop_name(&b, strs, p_prop_int64, + emit_string(&b, "prop-int64")); + + fill_prop_name(&b, strs, p_prop_str, + emit_string(&b, "prop-str")); + + fill_prop_name(&b, strs, p_s2_linux_phandle, + emit_string(&b, "linux,phandle")); + + fill_prop_name(&b, strs, p_s2ss0_phandle, + emit_string(&b, "phandle")); + + s = emit_string(&b, "reg"); + fill_prop_name(&b, strs, p_s1_reg, s); + fill_prop_name(&b, strs, p_s2_reg, s); + fill_prop_name(&b, strs, p_s2ss0_reg, s); + + fill_prop_name(&b, strs, p_s1ss_placeholder, + emit_string(&b, "placeholder")); + + s = emit_string(&b, "#address-cells"); + fill_prop_name(&b, strs, p_address_cells, s); + fill_prop_name(&b, strs, p_s2_address_cells, s); + + s = emit_string(&b, "#size-cells"); + fill_prop_name(&b, strs, p_size_cells, s); + fill_prop_name(&b, strs, p_s2_size_cells, s); + + finish_strings_block(&b, hdr, strs); + } + + finish_totalsize(&b, hdr); + + return b; +} + +static struct buf make_truncated_property(void) +{ + struct buf b = buf_init(); + size_t hdr; + size_t p; + + hdr = emit_fdt_header(&b); + emit_align(&b, 8); + + emit_rsvmap_empty(&b, hdr); + + { + size_t ss = start_block(&b); + + emit_begin_node(&b, ""); + p = emit_prop_header(&b, 0, 4); + finish_struct_block(&b, hdr, ss); + } + + { + size_t strs = start_block(&b); + + emit_string(&b, "truncated"); + fill_prop_name(&b, strs, p, strs); + finish_strings_block(&b, hdr, strs); + } + + finish_totalsize(&b, hdr); + + return b; +} + +static struct buf make_bad_node_char(void) +{ + struct buf b = buf_init(); + size_t hdr; + + hdr = emit_fdt_header(&b); + emit_align(&b, 8); + + emit_rsvmap_empty(&b, hdr); + + { + size_t ss = start_block(&b); + + emit_begin_node(&b, ""); + emit_begin_node(&b, "sub$node"); + emit_end_node(&b); + emit_end_node(&b); + emit_fdt_end(&b); + finish_struct_block(&b, hdr, ss); + } + + finish_strings_block(&b, hdr, start_block(&b)); + + finish_totalsize(&b, hdr); + + return b; +} + +static struct buf make_bad_node_format(void) +{ + struct buf b = buf_init(); + size_t hdr; + + hdr = emit_fdt_header(&b); + emit_align(&b, 8); + + emit_rsvmap_empty(&b, hdr); + + { + size_t ss = start_block(&b); + + emit_begin_node(&b, ""); + emit_begin_node(&b, "subnode@1@2"); + emit_end_node(&b); + emit_end_node(&b); + emit_fdt_end(&b); + finish_struct_block(&b, hdr, ss); + } + + finish_strings_block(&b, hdr, start_block(&b)); + + finish_totalsize(&b, hdr); + + return b; +} + +static struct buf make_bad_prop_char(void) +{ + struct buf b = buf_init(); + size_t hdr; + size_t p; + + hdr = emit_fdt_header(&b); + emit_align(&b, 8); + + emit_rsvmap_empty(&b, hdr); + + { + size_t ss = start_block(&b); + + emit_begin_node(&b, ""); + p = emit_prop_u32(&b, 0, TEST_VALUE_1); + emit_end_node(&b); + emit_fdt_end(&b); + finish_struct_block(&b, hdr, ss); + } + + { + size_t strs = start_block(&b); + + fill_prop_name(&b, strs, p, + emit_string(&b, "prop$erty")); + finish_strings_block(&b, hdr, strs); + } + + finish_totalsize(&b, hdr); + + return b; +} + +static struct buf make_ovf_size_strings(void) +{ + struct buf b = buf_init(); + size_t hdr; + + hdr = emit_fdt_header(&b); + emit_align(&b, 8); + + emit_rsvmap_empty(&b, hdr); + + { + size_t ss = start_block(&b); + + emit_begin_node(&b, ""); + emit_prop_u32(&b, 0x10000000, 0); + emit_end_node(&b); + emit_fdt_end(&b); + finish_struct_block(&b, hdr, ss); + } + + { + size_t strs = start_block(&b); + + emit_string(&b, "x"); + finish_strings_block(&b, hdr, strs); + } + + ((struct fdt_header *)(b.data + hdr))->size_dt_strings = + cpu_to_fdt32(0xffffffff); + + finish_totalsize(&b, hdr); + + return b; +} + +static struct buf make_truncated_string(void) +{ + struct buf b = buf_init(); + size_t hdr; + size_t p_good, p_bad; + + hdr = emit_fdt_header(&b); + emit_align(&b, 8); + + emit_rsvmap_empty(&b, hdr); + + { + size_t ss = start_block(&b); + + emit_begin_node(&b, ""); + p_good = emit_prop_nil(&b, 0); + p_bad = emit_prop_nil(&b, 0); + emit_end_node(&b); + emit_fdt_end(&b); + finish_struct_block(&b, hdr, ss); + } + + { + size_t strs = start_block(&b); + + fill_prop_name(&b, strs, p_good, emit_string(&b, "good")); + fill_prop_name(&b, strs, p_bad, emit_bytes(&b, "bad", 3)); + finish_strings_block(&b, hdr, strs); + } + + finish_totalsize(&b, hdr); + + return b; +} + +static struct buf make_truncated_memrsv(void) +{ + struct buf b = buf_init(); + size_t hdr; + + hdr = emit_fdt_header(&b); + + { + size_t ss = start_block(&b); + + emit_begin_node(&b, ""); + emit_end_node(&b); + emit_fdt_end(&b); + finish_struct_block(&b, hdr, ss); + } + + finish_strings_block(&b, hdr, start_block(&b)); + + { + size_t rsvmap; + + emit_align(&b, 8); + rsvmap = start_block(&b); + emit_rsvmap_entry(&b, TEST_ADDR_1, TEST_SIZE_1); + finish_rsvmap(&b, hdr, rsvmap); + } + + finish_totalsize(&b, hdr); + + return b; +} + +static struct buf make_unterminated_memrsv(void) +{ + struct buf b = buf_init(); + size_t hdr; + + hdr = emit_fdt_header(&b); + + { + size_t rsvmap; + + emit_align(&b, 8); + rsvmap = start_block(&b); + emit_rsvmap_entry(&b, TEST_ADDR_1, TEST_SIZE_1); + finish_rsvmap(&b, hdr, rsvmap); + } + + { + size_t ss = start_block(&b); + + emit_begin_node(&b, ""); + emit_end_node(&b); + emit_fdt_end(&b); + finish_struct_block(&b, hdr, ss); + } + + finish_strings_block(&b, hdr, start_block(&b)); + + finish_totalsize(&b, hdr); + + return b; +} + +static struct buf make_two_roots(void) +{ + struct buf b = buf_init(); + size_t hdr; + + hdr = emit_fdt_header(&b); + emit_align(&b, 8); + + emit_rsvmap_empty(&b, hdr); + + { + size_t ss = start_block(&b); + + emit_begin_node(&b, ""); + emit_end_node(&b); + emit_begin_node(&b, ""); + emit_end_node(&b); + emit_fdt_end(&b); + finish_struct_block(&b, hdr, ss); + } + + finish_strings_block(&b, hdr, start_block(&b)); + + finish_totalsize(&b, hdr); + + return b; +} + +static struct buf make_named_root(void) +{ + struct buf b = buf_init(); + size_t hdr; + + hdr = emit_fdt_header(&b); + emit_align(&b, 8); + + emit_rsvmap_empty(&b, hdr); + + { + size_t ss = start_block(&b); + + emit_begin_node(&b, "fake"); + emit_end_node(&b); + emit_fdt_end(&b); + finish_struct_block(&b, hdr, ss); + } + + finish_strings_block(&b, hdr, start_block(&b)); + + finish_totalsize(&b, hdr); + + return b; +} + +/* Tree table and main */ + +static struct { + struct buf (*generator)(void); + const char *filename; +} trees[] = { +#define TREE(name) { make_##name, #name ".dtb" } + TREE(test_tree1), + TREE(bad_node_char), TREE(bad_node_format), TREE(bad_prop_char), + TREE(ovf_size_strings), + TREE(truncated_property), TREE(truncated_string), + TREE(truncated_memrsv), + TREE(unterminated_memrsv), + TREE(two_roots), + TREE(named_root), +}; + +int main(int argc, char *argv[]) +{ + unsigned int i; + + if (argc != 2) { + fprintf(stderr, "Usage: %s \n", argv[0]); + return 1; + } + + if (chdir(argv[1]) != 0) { + perror("chdir()"); + return 1; + } + + for (i = 0; i < ARRAY_SIZE(trees); i++) { + struct buf b = trees[i].generator(); + const char *filename = trees[i].filename; + int fd; + ssize_t ret; + + printf("Tree \"%s\", %zu bytes\n", filename, b.cursor); + + fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0666); + if (fd < 0) + perror("open()"); + + ret = write(fd, b.data, b.cursor); + if (ret < 0 || (size_t)ret != b.cursor) + perror("write()"); + + close(fd); + free(b.data); + } + + return 0; +}