From a041d523de389b65b98a5373a8034041db2a8d83 Mon Sep 17 00:00:00 2001 From: Runxi Yu Date: Thu, 2 Apr 2026 06:23:30 +0000 Subject: *: Remove --- BENCHMARKS.md | 22 - CONTRIBUTING.md | 18 +- README.md | 15 +- ROADMAP.md | 212 - TODO | 53 - cmd/doc.go | 2 - cmd/receivepack9418/conn.go | 122 - cmd/receivepack9418/errpkt.go | 18 - cmd/receivepack9418/gitproto.go | 23 - cmd/receivepack9418/main.go | 61 - cmd/receivepack9418/profile.go | 58 - cmd/receivepack9418/request.go | 61 - cmd/receivepack9418/run.go | 70 - cmd/receivepack9418/server.go | 12 - cmd/show-object/main.go | 23 - cmd/show-object/print.go | 74 - cmd/show-object/resolve.go | 22 - cmd/show-object/run.go | 45 - commitquery/commit_data.go | 17 - commitquery/doc.go | 6 - commitquery/errors.go | 6 - commitquery/mark_bits.go | 17 - commitquery/node.go | 25 - commitquery/node_commit_time.go | 6 - commitquery/node_compare.go | 25 - commitquery/node_generation.go | 45 - commitquery/node_id.go | 8 - commitquery/node_index.go | 4 - commitquery/node_new.go | 14 - commitquery/node_parents.go | 6 - commitquery/node_populate.go | 42 - commitquery/parent_ref.go | 13 - commitquery/queries.go | 26 - commitquery/queries_acquire.go | 17 - commitquery/queries_is_ancestor.go | 14 - .../queries_is_ancestor_integration_test.go | 133 - commitquery/queries_is_ancestor_unit_test.go | 166 - commitquery/queries_merge_base.go | 11 - commitquery/queries_merge_bases.go | 13 - .../queries_merge_bases_integration_test.go | 312 - commitquery/queries_merge_bases_unit_test.go | 485 -- commitquery/queries_new.go | 22 - commitquery/queries_release.go | 15 - commitquery/query.go | 23 - commitquery/query_collect_marked_results.go | 20 - commitquery/query_ensure_loaded.go | 14 - commitquery/query_has_marks.go | 11 - commitquery/query_is_ancestor.go | 49 - commitquery/query_load_by_graph_pos.go | 8 - commitquery/query_load_by_oid.go | 41 - commitquery/query_load_commit_at_graph_pos.go | 64 - commitquery/query_mark_phase.go | 36 - commitquery/query_marks_get.go | 6 - commitquery/query_merge_base.go | 17 - commitquery/query_merge_bases.go | 45 - commitquery/query_merge_bases_internal.go | 34 - commitquery/query_new.go | 19 - commitquery/query_paint_down_to_common.go | 67 - commitquery/query_reduce.go | 166 - commitquery/query_reset.go | 10 - commitquery/query_resolve_commitish.go | 13 - commitquery/query_resolve_graph_pos.go | 40 - commitquery/query_resolve_oid.go | 28 - commitquery/query_resolve_parent.go | 10 - commitquery/query_set_clear_marks.go | 22 - common/doc.go | 2 - common/iowrap/doc.go | 2 - common/iowrap/nop_flush.go | 20 - common/iowrap/write_flusher.go | 9 - config/bom.go | 56 - config/char.go | 52 - config/config.go | 14 - config/config_test.go | 606 -- config/entry.go | 25 - config/extended_section.go | 76 - config/key_value.go | 119 - config/lookup.go | 45 - config/parser.go | 88 - config/result.go | 68 - config/section.go | 41 - config/testdata/fuzz/FuzzConfig/86abac337c758b6b | 3 - config/testdata/fuzz/FuzzConfig/a76c07b1ae70ed94 | 3 - config/testdata/fuzz/FuzzConfig/c0718ca6bc57e0e2 | 3 - config/testdata/fuzz/FuzzConfig/dc6f7dcd8aaa1cf7 | 3 - config/typed.go | 170 - config/value.go | 111 - diff/diff.go | 2 - diff/lines/chunk.go | 20 - diff/lines/diff.go | 231 - diff/lines/diff_test.go | 333 - diff/trees/diff.go | 22 - diff/trees/diff_recursive.go | 176 - diff/trees/diff_test.go | 255 - diff/trees/entry.go | 15 - diff/trees/kind.go | 15 - diff/trees/path.go | 17 - errors/doc.go | 2 - errors/missing.go | 18 - errors/type.go | 31 - format/commitgraph/TODO | 6 - format/commitgraph/bloom/bloom.go | 3 - format/commitgraph/bloom/constants.go | 8 - format/commitgraph/bloom/contain.go | 25 - format/commitgraph/bloom/errors.go | 5 - format/commitgraph/bloom/filter.go | 26 - format/commitgraph/bloom/key.go | 117 - format/commitgraph/bloom/murmur.go | 127 - format/commitgraph/bloom/settings.go | 50 - format/commitgraph/constants.go | 32 - format/commitgraph/doc.go | 2 - format/commitgraph/read/bloom.go | 119 - format/commitgraph/read/close.go | 20 - format/commitgraph/read/commitat.go | 87 - format/commitgraph/read/commits.go | 20 - format/commitgraph/read/doc.go | 2 - format/commitgraph/read/edges.go | 48 - format/commitgraph/read/errors.go | 58 - format/commitgraph/read/generation.go | 43 - format/commitgraph/read/hash.go | 79 - format/commitgraph/read/iterators.go | 49 - format/commitgraph/read/layer.go | 28 - format/commitgraph/read/layer_close.go | 33 - format/commitgraph/read/layer_lookup.go | 53 - format/commitgraph/read/layer_open.go | 81 - format/commitgraph/read/layer_parse.go | 276 - format/commitgraph/read/layer_pos.go | 21 - format/commitgraph/read/layerinfo.go | 25 - format/commitgraph/read/lookup.go | 29 - format/commitgraph/read/mode.go | 11 - format/commitgraph/read/oidat.go | 38 - format/commitgraph/read/open.go | 26 - format/commitgraph/read/open_chain.go | 133 - format/commitgraph/read/open_single.go | 32 - format/commitgraph/read/parents.go | 67 - format/commitgraph/read/position.go | 38 - format/commitgraph/read/read_test.go | 322 - format/commitgraph/read/reader.go | 14 - .../fixtures/sha1/chain_changed/repo.git/HEAD | 1 - .../fixtures/sha1/chain_changed/repo.git/config | 4 - .../objects/info/commit-graphs/commit-graph-chain | 2 - ...-bf985c21612a52070d8b008e6ef51edf8b609401.graph | Bin 4810 -> 0 bytes ...-dd7578d5216ca76c25b19631ba90f7498aeabbe7.graph | Bin 7088 -> 0 bytes .../sha1/chain_changed/repo.git/objects/info/packs | 2 - ...15b064d6a8ef8cff520565f6db8c006b2e6f7f2f.bitmap | Bin 8234 -> 0 bytes ...ck-15b064d6a8ef8cff520565f6db8c006b2e6f7f2f.idx | Bin 13252 -> 0 bytes ...k-15b064d6a8ef8cff520565f6db8c006b2e6f7f2f.pack | Bin 34730 -> 0 bytes ...ck-15b064d6a8ef8cff520565f6db8c006b2e6f7f2f.rev | Bin 1792 -> 0 bytes .../sha1/chain_changed/repo.git/refs/heads/master | 1 - .../fixtures/sha1/single_changed/repo.git/HEAD | 1 - .../fixtures/sha1/single_changed/repo.git/config | 4 - .../repo.git/objects/info/commit-graph | Bin 9068 -> 0 bytes .../single_changed/repo.git/objects/info/packs | 2 - ...34e9e132566989e2abfe8821731236c77f9bcbe9.bitmap | Bin 7780 -> 0 bytes ...ck-34e9e132566989e2abfe8821731236c77f9bcbe9.idx | Bin 11152 -> 0 bytes ...k-34e9e132566989e2abfe8821731236c77f9bcbe9.pack | Bin 28664 -> 0 bytes ...ck-34e9e132566989e2abfe8821731236c77f9bcbe9.rev | Bin 1492 -> 0 bytes .../sha1/single_changed/repo.git/refs/heads/main | 1 - .../fixtures/sha1/single_nochanged/repo.git/HEAD | 1 - .../fixtures/sha1/single_nochanged/repo.git/config | 4 - .../repo.git/objects/info/commit-graph | Bin 5912 -> 0 bytes .../single_nochanged/repo.git/objects/info/packs | 2 - ...a3da595034c94bb16b6829d757a66b7d259b9ffc.bitmap | Bin 5452 -> 0 bytes ...ck-a3da595034c94bb16b6829d757a66b7d259b9ffc.idx | Bin 7792 -> 0 bytes ...k-a3da595034c94bb16b6829d757a66b7d259b9ffc.pack | Bin 18969 -> 0 bytes ...ck-a3da595034c94bb16b6829d757a66b7d259b9ffc.rev | Bin 1012 -> 0 bytes .../single_nochanged/repo.git/refs/heads/master | 1 - .../fixtures/sha256/chain_changed/repo.git/HEAD | 1 - .../fixtures/sha256/chain_changed/repo.git/config | 6 - .../objects/info/commit-graphs/commit-graph-chain | 2 - ...97943112739236c6bcd19ed4d1f7c6b830cab4f62.graph | Bin 9260 -> 0 bytes ...717a5823c0cb4b5ee336a14959678e060d674ffb6.graph | Bin 6154 -> 0 bytes .../chain_changed/repo.git/objects/info/packs | 2 - ...f045957e44ccee06d812b5e531ae666014a26ed1.bitmap | Bin 8234 -> 0 bytes ...fbcf045957e44ccee06d812b5e531ae666014a26ed1.idx | Bin 18496 -> 0 bytes ...bcf045957e44ccee06d812b5e531ae666014a26ed1.pack | Bin 41482 -> 0 bytes ...fbcf045957e44ccee06d812b5e531ae666014a26ed1.rev | Bin 1816 -> 0 bytes .../chain_changed/repo.git/refs/heads/master | 1 - .../fixtures/sha256/single_changed/repo.git/HEAD | 1 - .../fixtures/sha256/single_changed/repo.git/config | 6 - .../repo.git/objects/info/commit-graph | Bin 11960 -> 0 bytes .../single_changed/repo.git/objects/info/packs | 2 - ...a0c55b76387cbf1fd2a117ab3d7ca0d854a031c9.bitmap | Bin 7804 -> 0 bytes ...40da0c55b76387cbf1fd2a117ab3d7ca0d854a031c9.idx | Bin 15496 -> 0 bytes ...0da0c55b76387cbf1fd2a117ab3d7ca0d854a031c9.pack | Bin 34252 -> 0 bytes ...40da0c55b76387cbf1fd2a117ab3d7ca0d854a031c9.rev | Bin 1516 -> 0 bytes .../sha256/single_changed/repo.git/refs/heads/main | 1 - .../fixtures/sha256/single_nochanged/repo.git/HEAD | 1 - .../sha256/single_nochanged/repo.git/config | 6 - .../repo.git/objects/info/commit-graph | Bin 7844 -> 0 bytes .../single_nochanged/repo.git/objects/info/packs | 2 - ...0ec9bf0e5dd596c0ee1ac6310136067c4f13438b.bitmap | Bin 5476 -> 0 bytes ...d780ec9bf0e5dd596c0ee1ac6310136067c4f13438b.idx | Bin 10696 -> 0 bytes ...780ec9bf0e5dd596c0ee1ac6310136067c4f13438b.pack | Bin 22569 -> 0 bytes ...d780ec9bf0e5dd596c0ee1ac6310136067c4f13438b.rev | Bin 1036 -> 0 bytes .../single_nochanged/repo.git/refs/heads/master | 1 - format/doc.go | 5 - format/packfile/delta/apply/apply.go | 160 - format/packfile/delta/apply/header.go | 47 - format/packfile/delta/doc.go | 2 - format/packfile/doc.go | 5 - format/packfile/entry.go | 76 - format/packfile/entry_header.go | 52 - format/packfile/header.go | 9 - format/packfile/ofs.go | 26 - furgit.go | 71 - go.mod | 3 - internal/adler32/LICENSE | 30 - internal/adler32/LICENSE.ZLIB | 17 - internal/adler32/README | 1 - internal/adler32/adler32_amd64.go | 89 - internal/adler32/adler32_avx2.go | 6 - internal/adler32/adler32_avx2.s | 251 - internal/adler32/adler32_fallback.go | 19 - internal/adler32/adler32_generic.go | 49 - internal/adler32/bench_test.go | 26 - internal/adler32/doc.go | 2 - internal/bufpool/append.go | 16 - internal/bufpool/borrow.go | 31 - internal/bufpool/buffer.go | 24 - internal/bufpool/buffers_test.go | 97 - internal/bufpool/bytes.go | 7 - internal/bufpool/capacity.go | 37 - internal/bufpool/class.go | 24 - internal/bufpool/consts.go | 12 - internal/bufpool/doc.go | 3 - internal/bufpool/from_owned.go | 8 - internal/bufpool/index.go | 7 - internal/bufpool/pool.go | 18 - internal/bufpool/release.go | 17 - internal/bufpool/resize.go | 15 - internal/bufpool/return.go | 10 - internal/compress/LICENSE | 29 - internal/compress/doc.go | 2 - internal/compress/flate/_gen/gen_inflate.go | 303 - internal/compress/flate/deflate.go | 996 --- internal/compress/flate/deflate_test.go | 708 -- internal/compress/flate/dict_decoder.go | 181 - internal/compress/flate/dict_decoder_test.go | 284 - internal/compress/flate/example_test.go | 240 - internal/compress/flate/fast_encoder.go | 189 - internal/compress/flate/flate_test.go | 370 - internal/compress/flate/fuzz_test.go | 176 - internal/compress/flate/huffman_bit_writer.go | 1174 --- internal/compress/flate/huffman_bit_writer_test.go | 381 - internal/compress/flate/huffman_code.go | 419 - internal/compress/flate/huffman_sortByFreq.go | 159 - internal/compress/flate/huffman_sortByLiteral.go | 203 - internal/compress/flate/inflate.go | 867 -- internal/compress/flate/inflate_gen.go | 1283 --- internal/compress/flate/inflate_test.go | 301 - internal/compress/flate/level1.go | 215 - internal/compress/flate/level2.go | 214 - internal/compress/flate/level3.go | 242 - internal/compress/flate/level4.go | 221 - internal/compress/flate/level5.go | 705 -- internal/compress/flate/level6.go | 325 - internal/compress/flate/matchlen_generic.go | 34 - internal/compress/flate/reader_test.go | 108 - internal/compress/flate/regmask_amd64.go | 37 - internal/compress/flate/regmask_other.go | 39 - internal/compress/flate/stateless.go | 325 - .../compress/flate/testdata/fuzz/FuzzEncoding.zip | Bin 1213291 -> 0 bytes .../flate/testdata/fuzz/encode-raw-corpus.zip | Bin 683330 -> 0 bytes .../flate/testdata/huffman-null-max.dyn.expect | Bin 78 -> 0 bytes .../testdata/huffman-null-max.dyn.expect-noinput | Bin 78 -> 0 bytes .../flate/testdata/huffman-null-max.golden | Bin 8204 -> 0 bytes .../compress/flate/testdata/huffman-null-max.in | Bin 65535 -> 0 bytes .../flate/testdata/huffman-null-max.sync.expect | Bin 78 -> 0 bytes .../testdata/huffman-null-max.sync.expect-noinput | Bin 78 -> 0 bytes .../flate/testdata/huffman-null-max.wb.expect | Bin 78 -> 0 bytes .../testdata/huffman-null-max.wb.expect-noinput | Bin 78 -> 0 bytes .../compress/flate/testdata/huffman-pi.dyn.expect | Bin 1696 -> 0 bytes .../flate/testdata/huffman-pi.dyn.expect-noinput | Bin 1696 -> 0 bytes internal/compress/flate/testdata/huffman-pi.golden | Bin 1606 -> 0 bytes internal/compress/flate/testdata/huffman-pi.in | 1 - .../compress/flate/testdata/huffman-pi.sync.expect | Bin 1696 -> 0 bytes .../flate/testdata/huffman-pi.sync.expect-noinput | Bin 1696 -> 0 bytes .../compress/flate/testdata/huffman-pi.wb.expect | Bin 1696 -> 0 bytes .../flate/testdata/huffman-pi.wb.expect-noinput | Bin 1696 -> 0 bytes .../flate/testdata/huffman-rand-1k.dyn.expect | Bin 1005 -> 0 bytes .../testdata/huffman-rand-1k.dyn.expect-noinput | Bin 1054 -> 0 bytes .../compress/flate/testdata/huffman-rand-1k.golden | Bin 1005 -> 0 bytes .../compress/flate/testdata/huffman-rand-1k.in | Bin 1000 -> 0 bytes .../flate/testdata/huffman-rand-1k.sync.expect | Bin 1005 -> 0 bytes .../testdata/huffman-rand-1k.sync.expect-noinput | Bin 1054 -> 0 bytes .../flate/testdata/huffman-rand-1k.wb.expect | Bin 1005 -> 0 bytes .../testdata/huffman-rand-1k.wb.expect-noinput | Bin 1054 -> 0 bytes .../flate/testdata/huffman-rand-limit.dyn.expect | Bin 186 -> 0 bytes .../testdata/huffman-rand-limit.dyn.expect-noinput | Bin 186 -> 0 bytes .../flate/testdata/huffman-rand-limit.golden | Bin 246 -> 0 bytes .../compress/flate/testdata/huffman-rand-limit.in | 4 - .../flate/testdata/huffman-rand-limit.sync.expect | Bin 186 -> 0 bytes .../huffman-rand-limit.sync.expect-noinput | Bin 186 -> 0 bytes .../flate/testdata/huffman-rand-limit.wb.expect | Bin 186 -> 0 bytes .../testdata/huffman-rand-limit.wb.expect-noinput | Bin 186 -> 0 bytes .../flate/testdata/huffman-rand-max.golden | Bin 65540 -> 0 bytes .../compress/flate/testdata/huffman-rand-max.in | Bin 65535 -> 0 bytes .../flate/testdata/huffman-shifts.dyn.expect | Bin 32 -> 0 bytes .../testdata/huffman-shifts.dyn.expect-noinput | Bin 32 -> 0 bytes .../compress/flate/testdata/huffman-shifts.golden | Bin 1812 -> 0 bytes internal/compress/flate/testdata/huffman-shifts.in | 2 - .../flate/testdata/huffman-shifts.sync.expect | Bin 32 -> 0 bytes .../testdata/huffman-shifts.sync.expect-noinput | Bin 32 -> 0 bytes .../flate/testdata/huffman-shifts.wb.expect | Bin 32 -> 0 bytes .../testdata/huffman-shifts.wb.expect-noinput | Bin 32 -> 0 bytes .../flate/testdata/huffman-text-shift.dyn.expect | Bin 231 -> 0 bytes .../testdata/huffman-text-shift.dyn.expect-noinput | Bin 231 -> 0 bytes .../flate/testdata/huffman-text-shift.golden | Bin 231 -> 0 bytes .../compress/flate/testdata/huffman-text-shift.in | 14 - .../flate/testdata/huffman-text-shift.sync.expect | Bin 231 -> 0 bytes .../huffman-text-shift.sync.expect-noinput | Bin 231 -> 0 bytes .../flate/testdata/huffman-text-shift.wb.expect | Bin 231 -> 0 bytes .../testdata/huffman-text-shift.wb.expect-noinput | Bin 231 -> 0 bytes .../flate/testdata/huffman-text.dyn.expect | 1 - .../flate/testdata/huffman-text.dyn.expect-noinput | 1 - .../compress/flate/testdata/huffman-text.golden | 3 - internal/compress/flate/testdata/huffman-text.in | 13 - .../flate/testdata/huffman-text.sync.expect | 1 - .../testdata/huffman-text.sync.expect-noinput | 1 - .../compress/flate/testdata/huffman-text.wb.expect | 1 - .../flate/testdata/huffman-text.wb.expect-noinput | 1 - .../flate/testdata/huffman-zero.dyn.expect | Bin 6 -> 0 bytes .../flate/testdata/huffman-zero.dyn.expect-noinput | Bin 6 -> 0 bytes .../compress/flate/testdata/huffman-zero.golden | Bin 51 -> 0 bytes internal/compress/flate/testdata/huffman-zero.in | 1 - .../flate/testdata/huffman-zero.sync.expect | Bin 6 -> 0 bytes .../testdata/huffman-zero.sync.expect-noinput | Bin 6 -> 0 bytes .../compress/flate/testdata/huffman-zero.wb.expect | Bin 6 -> 0 bytes .../flate/testdata/huffman-zero.wb.expect-noinput | Bin 6 -> 0 bytes .../testdata/null-long-match.dyn.expect-noinput | Bin 206 -> 0 bytes .../testdata/null-long-match.sync.expect-noinput | Bin 206 -> 0 bytes .../testdata/null-long-match.wb.expect-noinput | Bin 206 -> 0 bytes internal/compress/flate/testdata/partial-block | 1 - internal/compress/flate/testdata/regression.zip | Bin 483763 -> 0 bytes internal/compress/flate/testdata/tokens.bin | 63 - internal/compress/flate/token.go | 379 - internal/compress/flate/token_test.go | 54 - internal/compress/flate/writer_test.go | 544 -- internal/compress/internal/doc.go | 2 - internal/compress/internal/fuzz/helpers.go | 218 - internal/compress/internal/le/le.go | 6 - internal/compress/internal/le/unsafe_disabled.go | 42 - internal/compress/internal/le/unsafe_enabled.go | 52 - .../compress/testdata/Mark.Twain-Tom.Sawyer.txt | 8472 -------------------- internal/compress/testdata/case1.bin | Bin 55 -> 0 bytes internal/compress/testdata/case2.bin | 1 - internal/compress/testdata/case3.bin | 1 - internal/compress/testdata/crash1.bin | Bin 5 -> 0 bytes internal/compress/testdata/crash2.bin | 1 - internal/compress/testdata/crash3.bin | 1 - internal/compress/testdata/crash4.bin | 1 - internal/compress/testdata/crash5.bin | 1 - internal/compress/testdata/dec-crash6.bin | Bin 11 -> 0 bytes internal/compress/testdata/dec-hang1.bin | 1 - internal/compress/testdata/dec-hang2.bin | 1 - internal/compress/testdata/dec-hang3.bin | 1 - internal/compress/testdata/dec-symlen1.bin | Bin 49 -> 0 bytes internal/compress/testdata/e.txt | 1 - internal/compress/testdata/endnonzero.bin | Bin 7 -> 0 bytes internal/compress/testdata/endzerobits.bin | Bin 5 -> 0 bytes internal/compress/testdata/fse-artifact3.bin | Bin 4116 -> 0 bytes internal/compress/testdata/gettysburg.txt | 29 - internal/compress/testdata/html.txt | 1183 --- internal/compress/testdata/normcount2.bin | 1 - internal/compress/testdata/pi.txt | 1 - internal/compress/testdata/pngdata.bin | Bin 51200 -> 0 bytes internal/compress/testdata/sharnd.out | Bin 100004 -> 0 bytes internal/compress/zlib/reader.go | 182 - internal/compress/zlib/reader_reset.go | 112 - internal/compress/zlib/reader_test.go | 200 - internal/compress/zlib/writer.go | 205 - internal/compress/zlib/writer_header.go | 71 - internal/compress/zlib/writer_test.go | 248 - internal/cpu/LICENSE | 27 - internal/cpu/cpu.go | 9 - internal/cpu/cpu_amd64.go | 34 - internal/cpu/cpu_amd64.s | 22 - internal/cpu/cpu_other.go | 3 - internal/doc.go | 2 - internal/intconv/doc.go | 2 - internal/intconv/i64_i32.go | 15 - internal/intconv/i64_u64.go | 12 - internal/intconv/i_u32.go | 15 - internal/intconv/i_u64.go | 12 - internal/intconv/se_u8_u32.go | 10 - internal/intconv/u32_i.go | 15 - internal/intconv/u32_u8.go | 15 - internal/intconv/u64_i.go | 15 - internal/intconv/u64_i64.go | 15 - internal/intconv/uptr_int.go | 15 - internal/iolimit/capped_capture_writer.go | 52 - internal/iolimit/capped_capture_writer_test.go | 45 - internal/iolimit/doc.go | 5 - internal/iolimit/expect_length_reader.go | 79 - internal/iolimit/expect_length_reader_test.go | 78 - internal/lru/add.go | 35 - internal/lru/cache.go | 16 - internal/lru/clear.go | 10 - internal/lru/entries.go | 7 - internal/lru/evict.go | 17 - internal/lru/get.go | 17 - internal/lru/len.go | 6 - internal/lru/lru.go | 2 - internal/lru/lru_test.go | 245 - internal/lru/new.go | 23 - internal/lru/peek.go | 15 - internal/lru/remove.go | 33 - internal/lru/weight.go | 29 - internal/priorityqueue/doc.go | 2 - internal/priorityqueue/len.go | 6 - internal/priorityqueue/new.go | 6 - internal/priorityqueue/pop.go | 21 - internal/priorityqueue/push.go | 7 - internal/priorityqueue/queue.go | 9 - internal/priorityqueue/queue_test.go | 36 - internal/priorityqueue/sift_down.go | 24 - internal/priorityqueue/sift_up.go | 13 - internal/progress/constants.go | 11 - internal/progress/consume.go | 15 - internal/progress/counters.go | 23 - internal/progress/doc.go | 2 - internal/progress/humanize.go | 22 - internal/progress/meter.go | 30 - internal/progress/new.go | 21 - internal/progress/options.go | 22 - internal/progress/refresh.go | 25 - internal/progress/render.go | 38 - internal/progress/set.go | 39 - internal/progress/stop.go | 20 - internal/testgit/algorithms.go | 18 - internal/testgit/repo.go | 11 - internal/testgit/repo_cat_file.go | 14 - internal/testgit/repo_commit_graph_write.go | 13 - internal/testgit/repo_commit_tree.go | 29 - internal/testgit/repo_commit_tree_env.go | 51 - internal/testgit/repo_from_fixture.go | 36 - internal/testgit/repo_fs.go | 86 - internal/testgit/repo_hash_object.go | 20 - internal/testgit/repo_make_commit.go | 16 - internal/testgit/repo_make_many_objects_history.go | 83 - internal/testgit/repo_make_single_file_tree.go | 18 - internal/testgit/repo_mktree.go | 20 - internal/testgit/repo_new.go | 64 - internal/testgit/repo_open_commit_graph.go | 26 - internal/testgit/repo_open_object_store.go | 29 - internal/testgit/repo_open_repository.go | 25 - internal/testgit/repo_open_root.go | 87 - internal/testgit/repo_pack_objects_is_thin.go | 77 - internal/testgit/repo_pack_objects_reader.go | 94 - internal/testgit/repo_properties.go | 20 - internal/testgit/repo_refs.go | 48 - internal/testgit/repo_remove_loose_object.go | 22 - internal/testgit/repo_repack.go | 13 - internal/testgit/repo_rev_list.go | 37 - internal/testgit/repo_rev_parse.go | 20 - internal/testgit/repo_run.go | 95 - internal/testgit/repo_run_extra_files.go | 55 - internal/testgit/repo_tag_annotated.go | 15 - internal/utils/progress.go | 18 - network/doc.go | 5 - network/protocol/doc.go | 2 - network/protocol/pktline/append.go | 39 - .../append_data_preserves_dst_on_error_test.go | 25 - network/protocol/pktline/append_helpers_test.go | 24 - network/protocol/pktline/chunk_writer.go | 74 - .../chunk_writer_write_and_read_from_test.go | 60 - network/protocol/pktline/constants.go | 12 - network/protocol/pktline/decoder.go | 191 - .../pktline/decoder_data_control_and_0004_test.go | 60 - .../protocol/pktline/decoder_invalid_0003_test.go | 20 - network/protocol/pktline/decoder_peek_test.go | 32 - .../decoder_rejects_over_maximum_length_test.go | 22 - .../decoder_resync_after_over_max_data_test.go | 51 - .../decoder_resync_after_over_wire_max_test.go | 37 - .../pktline/decoder_unexpected_eof_test.go | 21 - network/protocol/pktline/doc.go | 2 - .../protocol/pktline/encode_length_header_test.go | 28 - network/protocol/pktline/encoder.go | 143 - .../encoder_buffered_flush_and_f_flush_test.go | 50 - .../encoder_buffered_flush_behavior_test.go | 86 - ...r_set_max_data_cannot_exceed_wire_limit_test.go | 26 - .../protocol/pktline/encoder_writes_frames_test.go | 51 - network/protocol/pktline/errors.go | 31 - network/protocol/pktline/frame.go | 10 - network/protocol/pktline/header.go | 57 - .../protocol/pktline/parse_length_header_test.go | 26 - network/protocol/pktline/type.go | 15 - network/protocol/sideband64k/append.go | 40 - .../protocol/sideband64k/append_helpers_test.go | 30 - .../append_preserves_dst_on_error_test.go | 34 - network/protocol/sideband64k/band.go | 13 - network/protocol/sideband64k/chunk_writer.go | 73 - .../chunk_writer_write_and_read_from_test.go | 60 - network/protocol/sideband64k/constants.go | 10 - network/protocol/sideband64k/decoder.go | 162 - .../decoder_data_control_and_keepalive_test.go | 78 - .../sideband64k/decoder_invalid_band_test.go | 20 - .../decoder_invalid_empty_payload_test.go | 20 - .../sideband64k/decoder_malformed_pktline_test.go | 32 - .../sideband64k/decoder_partial_read_test.go | 32 - network/protocol/sideband64k/decoder_peek_test.go | 34 - .../decoder_resync_after_over_max_data_test.go | 51 - .../decoder_resync_after_over_wire_max_test.go | 37 - .../sideband64k/decoder_unexpected_eof_test.go | 21 - network/protocol/sideband64k/doc.go | 2 - network/protocol/sideband64k/encoder.go | 103 - .../encoder_buffered_flush_behavior_test.go | 59 - .../sideband64k/encoder_partial_write_test.go | 46 - ...r_set_max_data_cannot_exceed_wire_limit_test.go | 23 - .../sideband64k/encoder_writes_frames_test.go | 58 - network/protocol/sideband64k/errors.go | 27 - network/protocol/sideband64k/frame.go | 12 - network/protocol/sideband64k/frame_type.go | 19 - network/protocol/sideband64k/helpers_test.go | 46 - network/protocol/v0v1/doc.go | 2 - network/protocol/v0v1/server/advertise.go | 53 - network/protocol/v0v1/server/advertise_test.go | 101 - network/protocol/v0v1/server/advertised_ref.go | 22 - network/protocol/v0v1/server/doc.go | 2 - network/protocol/v0v1/server/errors.go | 18 - network/protocol/v0v1/server/frame.go | 20 - network/protocol/v0v1/server/helpers.go | 29 - network/protocol/v0v1/server/helpers_test.go | 28 - .../v0v1/server/receivepack/capabilities.go | 192 - network/protocol/v0v1/server/receivepack/doc.go | 2 - network/protocol/v0v1/server/receivepack/errors.go | 11 - .../v0v1/server/receivepack/helpers_test.go | 28 - .../protocol/v0v1/server/receivepack/parse_test.go | 255 - .../v0v1/server/receivepack/report_status.go | 185 - .../v0v1/server/receivepack/report_status_test.go | 293 - .../protocol/v0v1/server/receivepack/session.go | 303 - network/protocol/v0v1/server/receivepack/types.go | 45 - network/protocol/v0v1/server/session.go | 142 - network/protocol/v0v1/server/version.go | 12 - network/receivepack/advertise.go | 57 - network/receivepack/capabilities_defaults.go | 17 - network/receivepack/commands.go | 19 - network/receivepack/doc.go | 3 - network/receivepack/errors.go | 15 - network/receivepack/hook.go | 97 - network/receivepack/hooks/chain.go | 51 - network/receivepack/hooks/doc.go | 2 - network/receivepack/hooks/reject_force_push.go | 69 - network/receivepack/int_test.go | 1095 --- network/receivepack/options.go | 71 - network/receivepack/receivepack.go | 139 - network/receivepack/results.go | 26 - network/receivepack/service/apply.go | 137 - network/receivepack/service/command.go | 26 - network/receivepack/service/command_result.go | 13 - network/receivepack/service/doc.go | 6 - network/receivepack/service/execute.go | 120 - network/receivepack/service/hook.go | 48 - network/receivepack/service/hook_apply.go | 31 - network/receivepack/service/ingest_quarantine.go | 81 - network/receivepack/service/options.go | 31 - network/receivepack/service/request.go | 15 - network/receivepack/service/result.go | 9 - network/receivepack/service/run_hook.go | 93 - network/receivepack/service/service.go | 13 - network/receivepack/service/service_test.go | 129 - network/receivepack/service/update.go | 11 - network/receivepack/version.go | 35 - object/blob/blob.go | 14 - object/blob/parse.go | 6 - object/blob/parse_test.go | 30 - object/blob/serialize.go | 32 - object/blob/serialize_test.go | 30 - object/blob/test.go | 10 - object/commit/commit.go | 25 - object/commit/extraheader.go | 7 - object/commit/parse.go | 94 - object/commit/parse_test.go | 91 - object/commit/serialize.go | 84 - object/commit/serialize_test.go | 34 - object/commit/type.go | 10 - object/doc.go | 7 - object/fetch/doc.go | 8 - object/fetch/exact_blob.go | 26 - object/fetch/exact_blob_reader.go | 16 - object/fetch/exact_commit.go | 26 - object/fetch/exact_object.go | 20 - object/fetch/exact_reader.go | 26 - object/fetch/exact_tag.go | 26 - object/fetch/exact_tree.go | 26 - object/fetch/fetcher.go | 20 - object/fetch/header.go | 18 - object/fetch/object_errors.go | 19 - object/fetch/object_parse.go | 27 - object/fetch/path.go | 105 - object/fetch/peel_to_blob.go | 31 - object/fetch/peel_to_blob_id.go | 38 - object/fetch/peel_to_blob_reader.go | 20 - object/fetch/peel_to_commit.go | 31 - object/fetch/peel_to_commit_id.go | 38 - object/fetch/peel_to_tree.go | 35 - object/fetch/peel_to_tree_id.go | 45 - object/fetch/size.go | 15 - object/fetch/treefs.go | 30 - object/fetch/treefs_entry.go | 85 - object/fetch/treefs_info.go | 75 - object/fetch/treefs_new.go | 19 - object/fetch/treefs_op.go | 28 - object/fetch/treefs_open.go | 122 - object/fetch/treefs_path.go | 11 - object/fetch/treefs_readdir.go | 20 - object/fetch/treefs_readfile.go | 40 - object/fetch/treefs_stat.go | 22 - object/fetch/treefs_sub.go | 22 - object/fetch/treefs_test.go | 111 - object/header/append.go | 29 - object/header/doc.go | 3 - object/header/encode.go | 8 - object/header/parse.go | 42 - object/id/algorithm.go | 12 - object/id/algorithm_details.go | 17 - object/id/algorithm_emptytree.go | 7 - object/id/algorithm_hexlen.go | 6 - object/id/algorithm_new.go | 13 - object/id/algorithm_packhashid.go | 8 - object/id/algorithm_parse.go | 8 - object/id/algorithm_signatureheadername.go | 6 - object/id/algorithm_size.go | 6 - object/id/algorithm_string.go | 11 - object/id/algorithm_sum.go | 6 - object/id/algorithm_supported.go | 7 - object/id/algorithm_tables.go | 72 - object/id/algorithm_zero.go | 11 - object/id/doc.go | 2 - object/id/errors.go | 10 - object/id/max_size.go | 6 - object/id/objectid.go | 11 - object/id/objectid_algorithm.go | 6 - object/id/objectid_byte.go | 19 - object/id/objectid_compare.go | 9 - object/id/objectid_frombytes.go | 20 - object/id/objectid_parse.go | 32 - object/id/objectid_string.go | 10 - object/id/objectid_test.go | 229 - object/id/signatureheadername_parse.go | 9 - object/object.go | 10 - object/parse_with_header.go | 25 - object/parse_without_header.go | 32 - object/signature/parse.go | 97 - object/signature/serialize.go | 33 - object/signature/signature.go | 10 - object/signature/when.go | 10 - object/signed/commit/commit.go | 15 - object/signed/commit/doc.go | 6 - object/signed/commit/integration_test.go | 138 - object/signed/commit/parse.go | 107 - object/signed/commit/payload_append.go | 11 - object/signed/commit/signature_algorithms.go | 16 - object/signed/commit/signature_append.go | 17 - object/signed/commit/unit_test.go | 170 - object/signed/doc.go | 7 - object/signed/tag/doc.go | 3 - object/signed/tag/integration_test.go | 139 - object/signed/tag/parse.go | 143 - object/signed/tag/payload_append.go | 11 - object/signed/tag/signature_algorithms.go | 16 - object/signed/tag/signature_append.go | 17 - object/signed/tag/tag.go | 15 - object/signed/tag/unit_test.go | 257 - object/store/base_quarantine.go | 17 - object/store/chain/bytes.go | 46 - object/store/chain/chain.go | 12 - object/store/chain/header.go | 28 - object/store/chain/new.go | 14 - object/store/chain/reader.go | 47 - object/store/chain/refresh.go | 17 - object/store/chain/size.go | 27 - object/store/cursor.go | 7 - object/store/doc.go | 19 - object/store/dual/doc.go | 8 - object/store/dual/dual.go | 33 - object/store/dual/dual_test.go | 266 - object/store/dual/new.go | 29 - object/store/dual/quarantine.go | 114 - object/store/dual/quarantine_begin.go | 22 - object/store/dual/quarantine_discard.go | 11 - object/store/dual/quarantine_promote.go | 13 - object/store/dual/reader.go | 57 - object/store/dual/writer_object.go | 32 - object/store/dual/writer_pack.go | 12 - object/store/errors.go | 8 - object/store/loose/helpers_test.go | 107 - object/store/loose/parse.go | 55 - object/store/loose/paths.go | 43 - object/store/loose/quarantine.go | 19 - object/store/loose/quarantine_begin.go | 63 - object/store/loose/quarantine_discard.go | 18 - object/store/loose/quarantine_promote.go | 116 - object/store/loose/quarantine_test.go | 119 - object/store/loose/read_bytes.go | 55 - object/store/loose/read_header.go | 37 - object/store/loose/read_reader.go | 114 - object/store/loose/read_size.go | 13 - object/store/loose/read_test.go | 212 - object/store/loose/refresh.go | 6 - object/store/loose/store.go | 43 - object/store/loose/write_bytes.go | 18 - object/store/loose/write_reader.go | 81 - object/store/loose/write_temp_object_file.go | 30 - object/store/loose/write_test.go | 137 - object/store/loose/write_writer.go | 94 - object/store/loose/write_writer_accept.go | 61 - object/store/loose/write_writer_finalize.go | 89 - object/store/memory/algorithm.go | 8 - object/store/memory/doc.go | 2 - object/store/memory/object.go | 9 - object/store/memory/read_bytes.go | 37 - object/store/memory/read_header.go | 17 - object/store/memory/read_reader.go | 29 - object/store/memory/read_size.go | 13 - object/store/memory/refresh.go | 6 - object/store/memory/store.go | 28 - object/store/memory/write_bytes.go | 35 - object/store/memory/write_reader.go | 55 - object/store/memory/write_test.go | 192 - object/store/mix/bytes.go | 51 - object/store/mix/header.go | 30 - object/store/mix/mix.go | 20 - object/store/mix/mru.go | 74 - object/store/mix/new.go | 40 - object/store/mix/reader.go | 53 - object/store/mix/refresh.go | 30 - object/store/mix/size.go | 29 - object/store/packed/doc.go | 3 - object/store/packed/internal/doc.go | 6 - object/store/packed/internal/ingest/TODO | 1 - .../packed/internal/ingest/byteslice_reader.go | 21 - object/store/packed/internal/ingest/cache.go | 53 - .../packed/internal/ingest/counting_writer.go | 17 - object/store/packed/internal/ingest/crc.go | 22 - .../store/packed/internal/ingest/delta_header.go | 11 - object/store/packed/internal/ingest/distance.go | 30 - object/store/packed/internal/ingest/doc.go | 3 - object/store/packed/internal/ingest/drain.go | 67 - object/store/packed/internal/ingest/entry.go | 91 - .../store/packed/internal/ingest/entry_header.go | 33 - .../store/packed/internal/ingest/entry_prefix.go | 95 - object/store/packed/internal/ingest/errors.go | 68 - .../packed/internal/ingest/file_section_writer.go | 22 - object/store/packed/internal/ingest/fill.go | 44 - object/store/packed/internal/ingest/finalize.go | 94 - object/store/packed/internal/ingest/flush.go | 37 - object/store/packed/internal/ingest/hash.go | 27 - object/store/packed/internal/ingest/header.go | 54 - object/store/packed/internal/ingest/idx_write.go | 262 - object/store/packed/internal/ingest/ingest.go | 68 - object/store/packed/internal/ingest/ingest_test.go | 411 - object/store/packed/internal/ingest/options.go | 26 - .../store/packed/internal/ingest/progress_write.go | 11 - .../store/packed/internal/ingest/record_content.go | 29 - .../store/packed/internal/ingest/record_delta.go | 60 - .../store/packed/internal/ingest/record_inflate.go | 46 - .../store/packed/internal/ingest/record_resolve.go | 116 - object/store/packed/internal/ingest/records.go | 46 - object/store/packed/internal/ingest/resolve_all.go | 70 - object/store/packed/internal/ingest/result.go | 23 - object/store/packed/internal/ingest/rev_write.go | 137 - .../internal/ingest/rewrite_header_trailer.go | 89 - object/store/packed/internal/ingest/scan.go | 105 - object/store/packed/internal/ingest/state.go | 70 - object/store/packed/internal/ingest/stream.go | 111 - object/store/packed/internal/ingest/temp.go | 103 - .../ingest/testdata/fixtures/sha1/METADATA.txt | 3 - .../ingest/testdata/fixtures/sha1/base.pack | Bin 81007 -> 0 bytes .../ingest/testdata/fixtures/sha1/nonthin.pack | Bin 117458 -> 0 bytes .../ingest/testdata/fixtures/sha1/thin.pack | Bin 38581 -> 0 bytes .../ingest/testdata/fixtures/sha256/METADATA.txt | 3 - .../ingest/testdata/fixtures/sha256/base.pack | Bin 105138 -> 0 bytes .../ingest/testdata/fixtures/sha256/nonthin.pack | Bin 152284 -> 0 bytes .../ingest/testdata/fixtures/sha256/thin.pack | Bin 49412 -> 0 bytes object/store/packed/internal/ingest/thin_append.go | 91 - object/store/packed/internal/ingest/thin_fix.go | 99 - .../packed/internal/ingest/thin_unresolved.go | 34 - object/store/packed/internal/ingest/trailer.go | 58 - object/store/packed/internal/ingest/use.go | 34 - object/store/packed/internal/ingest/write.go | 50 - object/store/packed/internal/ingest/write_empty.go | 58 - object/store/packed/internal/reading/TODO | 3 - object/store/packed/internal/reading/close.go | 35 - .../packed/internal/reading/delta_build_chain.go | 65 - .../store/packed/internal/reading/delta_cache.go | 61 - .../store/packed/internal/reading/delta_chain.go | 13 - object/store/packed/internal/reading/delta_node.go | 9 - .../packed/internal/reading/delta_resolve_chain.go | 61 - .../internal/reading/delta_resolve_chain_start.go | 58 - .../internal/reading/delta_resolve_content.go | 26 - object/store/packed/internal/reading/delta_size.go | 27 - object/store/packed/internal/reading/doc.go | 6 - .../store/packed/internal/reading/entry_inflate.go | 64 - object/store/packed/internal/reading/entry_meta.go | 16 - .../store/packed/internal/reading/entry_parse.go | 71 - .../store/packed/internal/reading/helpers_test.go | 102 - object/store/packed/internal/reading/idx.go | 36 - .../packed/internal/reading/idx_candidates_mru.go | 136 - object/store/packed/internal/reading/idx_close.go | 28 - object/store/packed/internal/reading/idx_lookup.go | 91 - .../internal/reading/idx_lookup_candidates.go | 126 - object/store/packed/internal/reading/idx_open.go | 98 - object/store/packed/internal/reading/idx_parse.go | 78 - object/store/packed/internal/reading/location.go | 7 - object/store/packed/internal/reading/new.go | 33 - object/store/packed/internal/reading/options.go | 16 - object/store/packed/internal/reading/pack.go | 82 - .../packed/internal/reading/pack_idx_checksum.go | 34 - object/store/packed/internal/reading/read_bytes.go | 46 - .../store/packed/internal/reading/read_closer.go | 19 - .../store/packed/internal/reading/read_header.go | 20 - .../packed/internal/reading/read_header_resolve.go | 65 - .../store/packed/internal/reading/read_reader.go | 92 - object/store/packed/internal/reading/read_size.go | 45 - object/store/packed/internal/reading/read_test.go | 301 - object/store/packed/internal/reading/store.go | 52 - .../store/packed/internal/reading/store_lookup.go | 106 - .../packed/internal/reading/store_open_pack.go | 57 - .../store/packed/internal/reading/trailer_match.go | 29 - object/store/packed/new.go | 25 - object/store/packed/options.go | 7 - object/store/packed/options_refresh.go | 11 - object/store/packed/quarantine.go | 19 - object/store/packed/quarantine_begin.go | 63 - object/store/packed/quarantine_discard.go | 18 - object/store/packed/quarantine_promote.go | 89 - object/store/packed/quarantine_test.go | 215 - object/store/packed/reader.go | 65 - object/store/packed/store.go | 23 - object/store/packed/writer.go | 22 - object/store/quarantine.go | 20 - object/store/reader.go | 55 - object/store/writer.go | 8 - object/store/writer_object.go | 37 - object/store/writer_pack.go | 58 - object/stored/doc.go | 7 - object/stored/id.go | 8 - object/stored/new.go | 11 - object/stored/object.go | 6 - object/stored/stored.go | 13 - object/tag/parse.go | 89 - object/tag/parse_test.go | 47 - object/tag/serialize.go | 68 - object/tag/serialize_test.go | 35 - object/tag/tag.go | 24 - object/tag/type.go | 10 - object/tree/entry.go | 57 - object/tree/helpers_test.go | 114 - object/tree/insert.go | 24 - object/tree/lookup.go | 18 - object/tree/mode.go | 12 - object/tree/mode_details.go | 10 - object/tree/mode_has_same_type.go | 12 - object/tree/mode_is_blob_like.go | 8 - object/tree/mode_is_regular_file.go | 6 - object/tree/mode_table.go | 24 - object/tree/name.go | 51 - object/tree/parse.go | 58 - object/tree/parse_test.go | 109 - object/tree/path_append.go | 14 - object/tree/path_clone.go | 16 - object/tree/path_prefix.go | 19 - object/tree/path_split.go | 19 - object/tree/remove.go | 22 - object/tree/serialize.go | 55 - object/tree/serialize_test.go | 73 - object/tree/tree.go | 12 - object/tree/type.go | 10 - object/type/details.go | 10 - object/type/is_base.go | 7 - object/type/name.go | 11 - object/type/parse.go | 8 - object/type/table.go | 21 - object/type/type.go | 16 - reachability/connected.go | 19 - reachability/doc.go | 5 - reachability/domain.go | 22 - reachability/integration_test.go | 373 - reachability/reachability.go | 22 - reachability/unit_test.go | 424 - reachability/walk.go | 43 - reachability/walk_expand.go | 9 - reachability/walk_expand_commits.go | 60 - reachability/walk_expand_commits_graph.go | 56 - reachability/walk_expand_objects.go | 69 - reachability/walk_item.go | 11 - reachability/walk_seq.go | 71 - reachability/walk_stack.go | 16 - reachability/walk_verify.go | 22 - ref/detached.go | 22 - ref/doc.go | 5 - ref/name/branch.go | 25 - ref/name/component.go | 88 - ref/name/current.go | 5 - ref/name/disposition.go | 20 - ref/name/doc.go | 7 - ref/name/errors.go | 14 - ref/name/flags.go | 6 - ref/name/length.go | 11 - ref/name/lock.go | 3 - ref/name/normalize.go | 53 - ref/name/options.go | 30 - ref/name/pseudo.go | 11 - ref/name/refname_test.go | 622 -- ref/name/root.go | 21 - ref/name/root_syntax.go | 13 - ref/name/safe.go | 31 - ref/name/sanitize.go | 19 - ref/name/slashes.go | 26 - ref/name/tag.go | 20 - ref/name/update.go | 56 - ref/name/utils.go | 22 - ref/name/validate.go | 65 - ref/name/worktree.go | 75 - ref/ref.go | 9 - ref/store/batch.go | 69 - ref/store/batch_store.go | 9 - ref/store/chain/chain.go | 12 - ref/store/chain/close.go | 6 - ref/store/chain/list.go | 40 - ref/store/chain/new.go | 14 - ref/store/chain/resolve.go | 64 - ref/store/doc.go | 14 - ref/store/errors.go | 7 - ref/store/files/batch.go | 11 - ref/store/files/batch_abort.go | 6 - ref/store/files/batch_apply.go | 129 - ref/store/files/batch_begin.go | 13 - ref/store/files/batch_queue.go | 12 - ref/store/files/batch_queue_ops.go | 43 - ref/store/files/batch_rejection.go | 28 - ref/store/files/batch_result_error.go | 21 - ref/store/files/batch_test.go | 129 - ref/store/files/broken_ref_error.go | 16 - ref/store/files/close.go | 8 - ref/store/files/helpers_test.go | 150 - ref/store/files/new.go | 31 - ref/store/files/packed_delete_test.go | 292 - ref/store/files/packed_parse.go | 113 - ref/store/files/packed_read.go | 35 - ref/store/files/packed_refs.go | 10 - ref/store/files/read_list.go | 76 - ref/store/files/read_list_collect.go | 78 - ref/store/files/read_loose.go | 48 - ref/store/files/read_resolve.go | 41 - ref/store/files/read_resolve_fully.go | 42 - ref/store/files/resolve_list_test.go | 269 - ref/store/files/root_for.go | 13 - ref/store/files/root_kind.go | 8 - ref/store/files/root_loose_path.go | 24 - ref/store/files/root_open_common.go | 31 - ref/store/files/store.go | 31 - ref/store/files/transaction.go | 13 - ref/store/files/transaction_abort.go | 4 - ref/store/files/transaction_begin.go | 13 - ref/store/files/transaction_commit.go | 13 - ref/store/files/transaction_dirs_test.go | 220 - ref/store/files/transaction_names_test.go | 188 - ref/store/files/transaction_pseudoref_test.go | 106 - ref/store/files/transaction_queue.go | 12 - ref/store/files/transaction_queue_ops.go | 43 - ref/store/files/transaction_symbolic_test.go | 154 - ref/store/files/transaction_update_test.go | 178 - ref/store/files/trim.go | 10 - ref/store/files/update_cleanup.go | 39 - ref/store/files/update_cleanup_parents.go | 30 - ref/store/files/update_commit.go | 25 - ref/store/files/update_commit_delete.go | 25 - ref/store/files/update_dir_tree.go | 59 - ref/store/files/update_direct_read.go | 76 - ref/store/files/update_direct_ref.go | 20 - ref/store/files/update_error.go | 28 - ref/store/files/update_executor.go | 5 - ref/store/files/update_kind.go | 14 - ref/store/files/update_lock.go | 25 - ref/store/files/update_lock_packed.go | 44 - ref/store/files/update_operation_prepared.go | 6 - ref/store/files/update_operation_queue.go | 12 - ref/store/files/update_path.go | 28 - ref/store/files/update_prepare.go | 48 - ref/store/files/update_prepare_lock.go | 29 - ref/store/files/update_prepare_resolve.go | 43 - ref/store/files/update_prepare_verify.go | 21 - ref/store/files/update_resolve_target.go | 21 - ref/store/files/update_resolve_target_ordinary.go | 48 - ref/store/files/update_target_resolved.go | 7 - ref/store/files/update_validate.go | 66 - ref/store/files/update_verify_current.go | 60 - ref/store/files/update_verify_refnames.go | 41 - ref/store/files/update_visible_names.go | 29 - ref/store/files/update_write_loose.go | 59 - ref/store/files/update_write_packed_refs.go | 98 - ref/store/files/worktree_test.go | 206 - ref/store/reading.go | 34 - ref/store/transaction.go | 52 - ref/store/transactional_store.go | 13 - ref/store/update_errors.go | 110 - ref/symbolic.go | 14 - repository/algorithm.go | 29 - repository/close.go | 54 - repository/commit_graph.go | 42 - repository/commit_queries.go | 13 - repository/config.go | 34 - repository/fetcher.go | 14 - repository/objects.go | 100 - repository/open.go | 78 - repository/reachability.go | 14 - repository/refs.go | 20 - repository/refs_test.go | 113 - repository/refs_timeout.go | 16 - repository/repository.go | 49 - repository/stored_test.go | 234 - repository/traversal_test.go | 210 - repository/write_loose_test.go | 113 - research/dynamic_packfiles.txt | 173 - research/packfile_bloom.txt | 142 - 1017 files changed, 8 insertions(+), 63245 deletions(-) delete mode 100644 BENCHMARKS.md delete mode 100644 ROADMAP.md delete mode 100644 TODO delete mode 100644 cmd/doc.go delete mode 100644 cmd/receivepack9418/conn.go delete mode 100644 cmd/receivepack9418/errpkt.go delete mode 100644 cmd/receivepack9418/gitproto.go delete mode 100644 cmd/receivepack9418/main.go delete mode 100644 cmd/receivepack9418/profile.go delete mode 100644 cmd/receivepack9418/request.go delete mode 100644 cmd/receivepack9418/run.go delete mode 100644 cmd/receivepack9418/server.go delete mode 100644 cmd/show-object/main.go delete mode 100644 cmd/show-object/print.go delete mode 100644 cmd/show-object/resolve.go delete mode 100644 cmd/show-object/run.go delete mode 100644 commitquery/commit_data.go delete mode 100644 commitquery/doc.go delete mode 100644 commitquery/errors.go delete mode 100644 commitquery/mark_bits.go delete mode 100644 commitquery/node.go delete mode 100644 commitquery/node_commit_time.go delete mode 100644 commitquery/node_compare.go delete mode 100644 commitquery/node_generation.go delete mode 100644 commitquery/node_id.go delete mode 100644 commitquery/node_index.go delete mode 100644 commitquery/node_new.go delete mode 100644 commitquery/node_parents.go delete mode 100644 commitquery/node_populate.go delete mode 100644 commitquery/parent_ref.go delete mode 100644 commitquery/queries.go delete mode 100644 commitquery/queries_acquire.go delete mode 100644 commitquery/queries_is_ancestor.go delete mode 100644 commitquery/queries_is_ancestor_integration_test.go delete mode 100644 commitquery/queries_is_ancestor_unit_test.go delete mode 100644 commitquery/queries_merge_base.go delete mode 100644 commitquery/queries_merge_bases.go delete mode 100644 commitquery/queries_merge_bases_integration_test.go delete mode 100644 commitquery/queries_merge_bases_unit_test.go delete mode 100644 commitquery/queries_new.go delete mode 100644 commitquery/queries_release.go delete mode 100644 commitquery/query.go delete mode 100644 commitquery/query_collect_marked_results.go delete mode 100644 commitquery/query_ensure_loaded.go delete mode 100644 commitquery/query_has_marks.go delete mode 100644 commitquery/query_is_ancestor.go delete mode 100644 commitquery/query_load_by_graph_pos.go delete mode 100644 commitquery/query_load_by_oid.go delete mode 100644 commitquery/query_load_commit_at_graph_pos.go delete mode 100644 commitquery/query_mark_phase.go delete mode 100644 commitquery/query_marks_get.go delete mode 100644 commitquery/query_merge_base.go delete mode 100644 commitquery/query_merge_bases.go delete mode 100644 commitquery/query_merge_bases_internal.go delete mode 100644 commitquery/query_new.go delete mode 100644 commitquery/query_paint_down_to_common.go delete mode 100644 commitquery/query_reduce.go delete mode 100644 commitquery/query_reset.go delete mode 100644 commitquery/query_resolve_commitish.go delete mode 100644 commitquery/query_resolve_graph_pos.go delete mode 100644 commitquery/query_resolve_oid.go delete mode 100644 commitquery/query_resolve_parent.go delete mode 100644 commitquery/query_set_clear_marks.go delete mode 100644 common/doc.go delete mode 100644 common/iowrap/doc.go delete mode 100644 common/iowrap/nop_flush.go delete mode 100644 common/iowrap/write_flusher.go delete mode 100644 config/bom.go delete mode 100644 config/char.go delete mode 100644 config/config.go delete mode 100644 config/config_test.go delete mode 100644 config/entry.go delete mode 100644 config/extended_section.go delete mode 100644 config/key_value.go delete mode 100644 config/lookup.go delete mode 100644 config/parser.go delete mode 100644 config/result.go delete mode 100644 config/section.go delete mode 100644 config/testdata/fuzz/FuzzConfig/86abac337c758b6b delete mode 100644 config/testdata/fuzz/FuzzConfig/a76c07b1ae70ed94 delete mode 100644 config/testdata/fuzz/FuzzConfig/c0718ca6bc57e0e2 delete mode 100644 config/testdata/fuzz/FuzzConfig/dc6f7dcd8aaa1cf7 delete mode 100644 config/typed.go delete mode 100644 config/value.go delete mode 100644 diff/diff.go delete mode 100644 diff/lines/chunk.go delete mode 100644 diff/lines/diff.go delete mode 100644 diff/lines/diff_test.go delete mode 100644 diff/trees/diff.go delete mode 100644 diff/trees/diff_recursive.go delete mode 100644 diff/trees/diff_test.go delete mode 100644 diff/trees/entry.go delete mode 100644 diff/trees/kind.go delete mode 100644 diff/trees/path.go delete mode 100644 errors/doc.go delete mode 100644 errors/missing.go delete mode 100644 errors/type.go delete mode 100644 format/commitgraph/TODO delete mode 100644 format/commitgraph/bloom/bloom.go delete mode 100644 format/commitgraph/bloom/constants.go delete mode 100644 format/commitgraph/bloom/contain.go delete mode 100644 format/commitgraph/bloom/errors.go delete mode 100644 format/commitgraph/bloom/filter.go delete mode 100644 format/commitgraph/bloom/key.go delete mode 100644 format/commitgraph/bloom/murmur.go delete mode 100644 format/commitgraph/bloom/settings.go delete mode 100644 format/commitgraph/constants.go delete mode 100644 format/commitgraph/doc.go delete mode 100644 format/commitgraph/read/bloom.go delete mode 100644 format/commitgraph/read/close.go delete mode 100644 format/commitgraph/read/commitat.go delete mode 100644 format/commitgraph/read/commits.go delete mode 100644 format/commitgraph/read/doc.go delete mode 100644 format/commitgraph/read/edges.go delete mode 100644 format/commitgraph/read/errors.go delete mode 100644 format/commitgraph/read/generation.go delete mode 100644 format/commitgraph/read/hash.go delete mode 100644 format/commitgraph/read/iterators.go delete mode 100644 format/commitgraph/read/layer.go delete mode 100644 format/commitgraph/read/layer_close.go delete mode 100644 format/commitgraph/read/layer_lookup.go delete mode 100644 format/commitgraph/read/layer_open.go delete mode 100644 format/commitgraph/read/layer_parse.go delete mode 100644 format/commitgraph/read/layer_pos.go delete mode 100644 format/commitgraph/read/layerinfo.go delete mode 100644 format/commitgraph/read/lookup.go delete mode 100644 format/commitgraph/read/mode.go delete mode 100644 format/commitgraph/read/oidat.go delete mode 100644 format/commitgraph/read/open.go delete mode 100644 format/commitgraph/read/open_chain.go delete mode 100644 format/commitgraph/read/open_single.go delete mode 100644 format/commitgraph/read/parents.go delete mode 100644 format/commitgraph/read/position.go delete mode 100644 format/commitgraph/read/read_test.go delete mode 100644 format/commitgraph/read/reader.go delete mode 100644 format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/HEAD delete mode 100644 format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/config delete mode 100644 format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/info/commit-graphs/commit-graph-chain delete mode 100644 format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/info/commit-graphs/graph-bf985c21612a52070d8b008e6ef51edf8b609401.graph delete mode 100644 format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/info/commit-graphs/graph-dd7578d5216ca76c25b19631ba90f7498aeabbe7.graph delete mode 100644 format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/info/packs delete mode 100644 format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/pack/pack-15b064d6a8ef8cff520565f6db8c006b2e6f7f2f.bitmap delete mode 100644 format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/pack/pack-15b064d6a8ef8cff520565f6db8c006b2e6f7f2f.idx delete mode 100644 format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/pack/pack-15b064d6a8ef8cff520565f6db8c006b2e6f7f2f.pack delete mode 100644 format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/pack/pack-15b064d6a8ef8cff520565f6db8c006b2e6f7f2f.rev delete mode 100644 format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/refs/heads/master delete mode 100644 format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/HEAD delete mode 100644 format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/config delete mode 100644 format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/objects/info/commit-graph delete mode 100644 format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/objects/info/packs delete mode 100644 format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/objects/pack/pack-34e9e132566989e2abfe8821731236c77f9bcbe9.bitmap delete mode 100644 format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/objects/pack/pack-34e9e132566989e2abfe8821731236c77f9bcbe9.idx delete mode 100644 format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/objects/pack/pack-34e9e132566989e2abfe8821731236c77f9bcbe9.pack delete mode 100644 format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/objects/pack/pack-34e9e132566989e2abfe8821731236c77f9bcbe9.rev delete mode 100644 format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/refs/heads/main delete mode 100644 format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/HEAD delete mode 100644 format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/config delete mode 100644 format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/objects/info/commit-graph delete mode 100644 format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/objects/info/packs delete mode 100644 format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/objects/pack/pack-a3da595034c94bb16b6829d757a66b7d259b9ffc.bitmap delete mode 100644 format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/objects/pack/pack-a3da595034c94bb16b6829d757a66b7d259b9ffc.idx delete mode 100644 format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/objects/pack/pack-a3da595034c94bb16b6829d757a66b7d259b9ffc.pack delete mode 100644 format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/objects/pack/pack-a3da595034c94bb16b6829d757a66b7d259b9ffc.rev delete mode 100644 format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/refs/heads/master delete mode 100644 format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/HEAD delete mode 100644 format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/config delete mode 100644 format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/info/commit-graphs/commit-graph-chain delete mode 100644 format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/info/commit-graphs/graph-505cab61f8ddfa614301e8f97943112739236c6bcd19ed4d1f7c6b830cab4f62.graph delete mode 100644 format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/info/commit-graphs/graph-77c47bd6ca2ce17208c9361717a5823c0cb4b5ee336a14959678e060d674ffb6.graph delete mode 100644 format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/info/packs delete mode 100644 format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/pack/pack-04168d0884c910f505cb9fbcf045957e44ccee06d812b5e531ae666014a26ed1.bitmap delete mode 100644 format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/pack/pack-04168d0884c910f505cb9fbcf045957e44ccee06d812b5e531ae666014a26ed1.idx delete mode 100644 format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/pack/pack-04168d0884c910f505cb9fbcf045957e44ccee06d812b5e531ae666014a26ed1.pack delete mode 100644 format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/pack/pack-04168d0884c910f505cb9fbcf045957e44ccee06d812b5e531ae666014a26ed1.rev delete mode 100644 format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/refs/heads/master delete mode 100644 format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/HEAD delete mode 100644 format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/config delete mode 100644 format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/objects/info/commit-graph delete mode 100644 format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/objects/info/packs delete mode 100644 format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/objects/pack/pack-316dbc67dac12d131591640da0c55b76387cbf1fd2a117ab3d7ca0d854a031c9.bitmap delete mode 100644 format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/objects/pack/pack-316dbc67dac12d131591640da0c55b76387cbf1fd2a117ab3d7ca0d854a031c9.idx delete mode 100644 format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/objects/pack/pack-316dbc67dac12d131591640da0c55b76387cbf1fd2a117ab3d7ca0d854a031c9.pack delete mode 100644 format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/objects/pack/pack-316dbc67dac12d131591640da0c55b76387cbf1fd2a117ab3d7ca0d854a031c9.rev delete mode 100644 format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/refs/heads/main delete mode 100644 format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/HEAD delete mode 100644 format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/config delete mode 100644 format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/objects/info/commit-graph delete mode 100644 format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/objects/info/packs delete mode 100644 format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/objects/pack/pack-d335453f760b064e36459d780ec9bf0e5dd596c0ee1ac6310136067c4f13438b.bitmap delete mode 100644 format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/objects/pack/pack-d335453f760b064e36459d780ec9bf0e5dd596c0ee1ac6310136067c4f13438b.idx delete mode 100644 format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/objects/pack/pack-d335453f760b064e36459d780ec9bf0e5dd596c0ee1ac6310136067c4f13438b.pack delete mode 100644 format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/objects/pack/pack-d335453f760b064e36459d780ec9bf0e5dd596c0ee1ac6310136067c4f13438b.rev delete mode 100644 format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/refs/heads/master delete mode 100644 format/doc.go delete mode 100644 format/packfile/delta/apply/apply.go delete mode 100644 format/packfile/delta/apply/header.go delete mode 100644 format/packfile/delta/doc.go delete mode 100644 format/packfile/doc.go delete mode 100644 format/packfile/entry.go delete mode 100644 format/packfile/entry_header.go delete mode 100644 format/packfile/header.go delete mode 100644 format/packfile/ofs.go delete mode 100644 furgit.go delete mode 100644 go.mod delete mode 100644 internal/adler32/LICENSE delete mode 100644 internal/adler32/LICENSE.ZLIB delete mode 100644 internal/adler32/README delete mode 100644 internal/adler32/adler32_amd64.go delete mode 100644 internal/adler32/adler32_avx2.go delete mode 100644 internal/adler32/adler32_avx2.s delete mode 100644 internal/adler32/adler32_fallback.go delete mode 100644 internal/adler32/adler32_generic.go delete mode 100644 internal/adler32/bench_test.go delete mode 100644 internal/adler32/doc.go delete mode 100644 internal/bufpool/append.go delete mode 100644 internal/bufpool/borrow.go delete mode 100644 internal/bufpool/buffer.go delete mode 100644 internal/bufpool/buffers_test.go delete mode 100644 internal/bufpool/bytes.go delete mode 100644 internal/bufpool/capacity.go delete mode 100644 internal/bufpool/class.go delete mode 100644 internal/bufpool/consts.go delete mode 100644 internal/bufpool/doc.go delete mode 100644 internal/bufpool/from_owned.go delete mode 100644 internal/bufpool/index.go delete mode 100644 internal/bufpool/pool.go delete mode 100644 internal/bufpool/release.go delete mode 100644 internal/bufpool/resize.go delete mode 100644 internal/bufpool/return.go delete mode 100644 internal/compress/LICENSE delete mode 100644 internal/compress/doc.go delete mode 100644 internal/compress/flate/_gen/gen_inflate.go delete mode 100644 internal/compress/flate/deflate.go delete mode 100644 internal/compress/flate/deflate_test.go delete mode 100644 internal/compress/flate/dict_decoder.go delete mode 100644 internal/compress/flate/dict_decoder_test.go delete mode 100644 internal/compress/flate/example_test.go delete mode 100644 internal/compress/flate/fast_encoder.go delete mode 100644 internal/compress/flate/flate_test.go delete mode 100644 internal/compress/flate/fuzz_test.go delete mode 100644 internal/compress/flate/huffman_bit_writer.go delete mode 100644 internal/compress/flate/huffman_bit_writer_test.go delete mode 100644 internal/compress/flate/huffman_code.go delete mode 100644 internal/compress/flate/huffman_sortByFreq.go delete mode 100644 internal/compress/flate/huffman_sortByLiteral.go delete mode 100644 internal/compress/flate/inflate.go delete mode 100644 internal/compress/flate/inflate_gen.go delete mode 100644 internal/compress/flate/inflate_test.go delete mode 100644 internal/compress/flate/level1.go delete mode 100644 internal/compress/flate/level2.go delete mode 100644 internal/compress/flate/level3.go delete mode 100644 internal/compress/flate/level4.go delete mode 100644 internal/compress/flate/level5.go delete mode 100644 internal/compress/flate/level6.go delete mode 100644 internal/compress/flate/matchlen_generic.go delete mode 100644 internal/compress/flate/reader_test.go delete mode 100644 internal/compress/flate/regmask_amd64.go delete mode 100644 internal/compress/flate/regmask_other.go delete mode 100644 internal/compress/flate/stateless.go delete mode 100644 internal/compress/flate/testdata/fuzz/FuzzEncoding.zip delete mode 100644 internal/compress/flate/testdata/fuzz/encode-raw-corpus.zip delete mode 100644 internal/compress/flate/testdata/huffman-null-max.dyn.expect delete mode 100644 internal/compress/flate/testdata/huffman-null-max.dyn.expect-noinput delete mode 100644 internal/compress/flate/testdata/huffman-null-max.golden delete mode 100644 internal/compress/flate/testdata/huffman-null-max.in delete mode 100644 internal/compress/flate/testdata/huffman-null-max.sync.expect delete mode 100644 internal/compress/flate/testdata/huffman-null-max.sync.expect-noinput delete mode 100644 internal/compress/flate/testdata/huffman-null-max.wb.expect delete mode 100644 internal/compress/flate/testdata/huffman-null-max.wb.expect-noinput delete mode 100644 internal/compress/flate/testdata/huffman-pi.dyn.expect delete mode 100644 internal/compress/flate/testdata/huffman-pi.dyn.expect-noinput delete mode 100644 internal/compress/flate/testdata/huffman-pi.golden delete mode 100644 internal/compress/flate/testdata/huffman-pi.in delete mode 100644 internal/compress/flate/testdata/huffman-pi.sync.expect delete mode 100644 internal/compress/flate/testdata/huffman-pi.sync.expect-noinput delete mode 100644 internal/compress/flate/testdata/huffman-pi.wb.expect delete mode 100644 internal/compress/flate/testdata/huffman-pi.wb.expect-noinput delete mode 100644 internal/compress/flate/testdata/huffman-rand-1k.dyn.expect delete mode 100644 internal/compress/flate/testdata/huffman-rand-1k.dyn.expect-noinput delete mode 100644 internal/compress/flate/testdata/huffman-rand-1k.golden delete mode 100644 internal/compress/flate/testdata/huffman-rand-1k.in delete mode 100644 internal/compress/flate/testdata/huffman-rand-1k.sync.expect delete mode 100644 internal/compress/flate/testdata/huffman-rand-1k.sync.expect-noinput delete mode 100644 internal/compress/flate/testdata/huffman-rand-1k.wb.expect delete mode 100644 internal/compress/flate/testdata/huffman-rand-1k.wb.expect-noinput delete mode 100644 internal/compress/flate/testdata/huffman-rand-limit.dyn.expect delete mode 100644 internal/compress/flate/testdata/huffman-rand-limit.dyn.expect-noinput delete mode 100644 internal/compress/flate/testdata/huffman-rand-limit.golden delete mode 100644 internal/compress/flate/testdata/huffman-rand-limit.in delete mode 100644 internal/compress/flate/testdata/huffman-rand-limit.sync.expect delete mode 100644 internal/compress/flate/testdata/huffman-rand-limit.sync.expect-noinput delete mode 100644 internal/compress/flate/testdata/huffman-rand-limit.wb.expect delete mode 100644 internal/compress/flate/testdata/huffman-rand-limit.wb.expect-noinput delete mode 100644 internal/compress/flate/testdata/huffman-rand-max.golden delete mode 100644 internal/compress/flate/testdata/huffman-rand-max.in delete mode 100644 internal/compress/flate/testdata/huffman-shifts.dyn.expect delete mode 100644 internal/compress/flate/testdata/huffman-shifts.dyn.expect-noinput delete mode 100644 internal/compress/flate/testdata/huffman-shifts.golden delete mode 100644 internal/compress/flate/testdata/huffman-shifts.in delete mode 100644 internal/compress/flate/testdata/huffman-shifts.sync.expect delete mode 100644 internal/compress/flate/testdata/huffman-shifts.sync.expect-noinput delete mode 100644 internal/compress/flate/testdata/huffman-shifts.wb.expect delete mode 100644 internal/compress/flate/testdata/huffman-shifts.wb.expect-noinput delete mode 100644 internal/compress/flate/testdata/huffman-text-shift.dyn.expect delete mode 100644 internal/compress/flate/testdata/huffman-text-shift.dyn.expect-noinput delete mode 100644 internal/compress/flate/testdata/huffman-text-shift.golden delete mode 100644 internal/compress/flate/testdata/huffman-text-shift.in delete mode 100644 internal/compress/flate/testdata/huffman-text-shift.sync.expect delete mode 100644 internal/compress/flate/testdata/huffman-text-shift.sync.expect-noinput delete mode 100644 internal/compress/flate/testdata/huffman-text-shift.wb.expect delete mode 100644 internal/compress/flate/testdata/huffman-text-shift.wb.expect-noinput delete mode 100644 internal/compress/flate/testdata/huffman-text.dyn.expect delete mode 100644 internal/compress/flate/testdata/huffman-text.dyn.expect-noinput delete mode 100644 internal/compress/flate/testdata/huffman-text.golden delete mode 100644 internal/compress/flate/testdata/huffman-text.in delete mode 100644 internal/compress/flate/testdata/huffman-text.sync.expect delete mode 100644 internal/compress/flate/testdata/huffman-text.sync.expect-noinput delete mode 100644 internal/compress/flate/testdata/huffman-text.wb.expect delete mode 100644 internal/compress/flate/testdata/huffman-text.wb.expect-noinput delete mode 100644 internal/compress/flate/testdata/huffman-zero.dyn.expect delete mode 100644 internal/compress/flate/testdata/huffman-zero.dyn.expect-noinput delete mode 100644 internal/compress/flate/testdata/huffman-zero.golden delete mode 100644 internal/compress/flate/testdata/huffman-zero.in delete mode 100644 internal/compress/flate/testdata/huffman-zero.sync.expect delete mode 100644 internal/compress/flate/testdata/huffman-zero.sync.expect-noinput delete mode 100644 internal/compress/flate/testdata/huffman-zero.wb.expect delete mode 100644 internal/compress/flate/testdata/huffman-zero.wb.expect-noinput delete mode 100644 internal/compress/flate/testdata/null-long-match.dyn.expect-noinput delete mode 100644 internal/compress/flate/testdata/null-long-match.sync.expect-noinput delete mode 100644 internal/compress/flate/testdata/null-long-match.wb.expect-noinput delete mode 100644 internal/compress/flate/testdata/partial-block delete mode 100644 internal/compress/flate/testdata/regression.zip delete mode 100644 internal/compress/flate/testdata/tokens.bin delete mode 100644 internal/compress/flate/token.go delete mode 100644 internal/compress/flate/token_test.go delete mode 100644 internal/compress/flate/writer_test.go delete mode 100644 internal/compress/internal/doc.go delete mode 100644 internal/compress/internal/fuzz/helpers.go delete mode 100644 internal/compress/internal/le/le.go delete mode 100644 internal/compress/internal/le/unsafe_disabled.go delete mode 100644 internal/compress/internal/le/unsafe_enabled.go delete mode 100644 internal/compress/testdata/Mark.Twain-Tom.Sawyer.txt delete mode 100644 internal/compress/testdata/case1.bin delete mode 100644 internal/compress/testdata/case2.bin delete mode 100644 internal/compress/testdata/case3.bin delete mode 100644 internal/compress/testdata/crash1.bin delete mode 100644 internal/compress/testdata/crash2.bin delete mode 100644 internal/compress/testdata/crash3.bin delete mode 100644 internal/compress/testdata/crash4.bin delete mode 100644 internal/compress/testdata/crash5.bin delete mode 100644 internal/compress/testdata/dec-crash6.bin delete mode 100644 internal/compress/testdata/dec-hang1.bin delete mode 100644 internal/compress/testdata/dec-hang2.bin delete mode 100644 internal/compress/testdata/dec-hang3.bin delete mode 100644 internal/compress/testdata/dec-symlen1.bin delete mode 100644 internal/compress/testdata/e.txt delete mode 100644 internal/compress/testdata/endnonzero.bin delete mode 100644 internal/compress/testdata/endzerobits.bin delete mode 100644 internal/compress/testdata/fse-artifact3.bin delete mode 100644 internal/compress/testdata/gettysburg.txt delete mode 100644 internal/compress/testdata/html.txt delete mode 100644 internal/compress/testdata/normcount2.bin delete mode 100644 internal/compress/testdata/pi.txt delete mode 100644 internal/compress/testdata/pngdata.bin delete mode 100644 internal/compress/testdata/sharnd.out delete mode 100644 internal/compress/zlib/reader.go delete mode 100644 internal/compress/zlib/reader_reset.go delete mode 100644 internal/compress/zlib/reader_test.go delete mode 100644 internal/compress/zlib/writer.go delete mode 100644 internal/compress/zlib/writer_header.go delete mode 100644 internal/compress/zlib/writer_test.go delete mode 100644 internal/cpu/LICENSE delete mode 100644 internal/cpu/cpu.go delete mode 100644 internal/cpu/cpu_amd64.go delete mode 100644 internal/cpu/cpu_amd64.s delete mode 100644 internal/cpu/cpu_other.go delete mode 100644 internal/doc.go delete mode 100644 internal/intconv/doc.go delete mode 100644 internal/intconv/i64_i32.go delete mode 100644 internal/intconv/i64_u64.go delete mode 100644 internal/intconv/i_u32.go delete mode 100644 internal/intconv/i_u64.go delete mode 100644 internal/intconv/se_u8_u32.go delete mode 100644 internal/intconv/u32_i.go delete mode 100644 internal/intconv/u32_u8.go delete mode 100644 internal/intconv/u64_i.go delete mode 100644 internal/intconv/u64_i64.go delete mode 100644 internal/intconv/uptr_int.go delete mode 100644 internal/iolimit/capped_capture_writer.go delete mode 100644 internal/iolimit/capped_capture_writer_test.go delete mode 100644 internal/iolimit/doc.go delete mode 100644 internal/iolimit/expect_length_reader.go delete mode 100644 internal/iolimit/expect_length_reader_test.go delete mode 100644 internal/lru/add.go delete mode 100644 internal/lru/cache.go delete mode 100644 internal/lru/clear.go delete mode 100644 internal/lru/entries.go delete mode 100644 internal/lru/evict.go delete mode 100644 internal/lru/get.go delete mode 100644 internal/lru/len.go delete mode 100644 internal/lru/lru.go delete mode 100644 internal/lru/lru_test.go delete mode 100644 internal/lru/new.go delete mode 100644 internal/lru/peek.go delete mode 100644 internal/lru/remove.go delete mode 100644 internal/lru/weight.go delete mode 100644 internal/priorityqueue/doc.go delete mode 100644 internal/priorityqueue/len.go delete mode 100644 internal/priorityqueue/new.go delete mode 100644 internal/priorityqueue/pop.go delete mode 100644 internal/priorityqueue/push.go delete mode 100644 internal/priorityqueue/queue.go delete mode 100644 internal/priorityqueue/queue_test.go delete mode 100644 internal/priorityqueue/sift_down.go delete mode 100644 internal/priorityqueue/sift_up.go delete mode 100644 internal/progress/constants.go delete mode 100644 internal/progress/consume.go delete mode 100644 internal/progress/counters.go delete mode 100644 internal/progress/doc.go delete mode 100644 internal/progress/humanize.go delete mode 100644 internal/progress/meter.go delete mode 100644 internal/progress/new.go delete mode 100644 internal/progress/options.go delete mode 100644 internal/progress/refresh.go delete mode 100644 internal/progress/render.go delete mode 100644 internal/progress/set.go delete mode 100644 internal/progress/stop.go delete mode 100644 internal/testgit/algorithms.go delete mode 100644 internal/testgit/repo.go delete mode 100644 internal/testgit/repo_cat_file.go delete mode 100644 internal/testgit/repo_commit_graph_write.go delete mode 100644 internal/testgit/repo_commit_tree.go delete mode 100644 internal/testgit/repo_commit_tree_env.go delete mode 100644 internal/testgit/repo_from_fixture.go delete mode 100644 internal/testgit/repo_fs.go delete mode 100644 internal/testgit/repo_hash_object.go delete mode 100644 internal/testgit/repo_make_commit.go delete mode 100644 internal/testgit/repo_make_many_objects_history.go delete mode 100644 internal/testgit/repo_make_single_file_tree.go delete mode 100644 internal/testgit/repo_mktree.go delete mode 100644 internal/testgit/repo_new.go delete mode 100644 internal/testgit/repo_open_commit_graph.go delete mode 100644 internal/testgit/repo_open_object_store.go delete mode 100644 internal/testgit/repo_open_repository.go delete mode 100644 internal/testgit/repo_open_root.go delete mode 100644 internal/testgit/repo_pack_objects_is_thin.go delete mode 100644 internal/testgit/repo_pack_objects_reader.go delete mode 100644 internal/testgit/repo_properties.go delete mode 100644 internal/testgit/repo_refs.go delete mode 100644 internal/testgit/repo_remove_loose_object.go delete mode 100644 internal/testgit/repo_repack.go delete mode 100644 internal/testgit/repo_rev_list.go delete mode 100644 internal/testgit/repo_rev_parse.go delete mode 100644 internal/testgit/repo_run.go delete mode 100644 internal/testgit/repo_run_extra_files.go delete mode 100644 internal/testgit/repo_tag_annotated.go delete mode 100644 internal/utils/progress.go delete mode 100644 network/doc.go delete mode 100644 network/protocol/doc.go delete mode 100644 network/protocol/pktline/append.go delete mode 100644 network/protocol/pktline/append_data_preserves_dst_on_error_test.go delete mode 100644 network/protocol/pktline/append_helpers_test.go delete mode 100644 network/protocol/pktline/chunk_writer.go delete mode 100644 network/protocol/pktline/chunk_writer_write_and_read_from_test.go delete mode 100644 network/protocol/pktline/constants.go delete mode 100644 network/protocol/pktline/decoder.go delete mode 100644 network/protocol/pktline/decoder_data_control_and_0004_test.go delete mode 100644 network/protocol/pktline/decoder_invalid_0003_test.go delete mode 100644 network/protocol/pktline/decoder_peek_test.go delete mode 100644 network/protocol/pktline/decoder_rejects_over_maximum_length_test.go delete mode 100644 network/protocol/pktline/decoder_resync_after_over_max_data_test.go delete mode 100644 network/protocol/pktline/decoder_resync_after_over_wire_max_test.go delete mode 100644 network/protocol/pktline/decoder_unexpected_eof_test.go delete mode 100644 network/protocol/pktline/doc.go delete mode 100644 network/protocol/pktline/encode_length_header_test.go delete mode 100644 network/protocol/pktline/encoder.go delete mode 100644 network/protocol/pktline/encoder_buffered_flush_and_f_flush_test.go delete mode 100644 network/protocol/pktline/encoder_buffered_flush_behavior_test.go delete mode 100644 network/protocol/pktline/encoder_set_max_data_cannot_exceed_wire_limit_test.go delete mode 100644 network/protocol/pktline/encoder_writes_frames_test.go delete mode 100644 network/protocol/pktline/errors.go delete mode 100644 network/protocol/pktline/frame.go delete mode 100644 network/protocol/pktline/header.go delete mode 100644 network/protocol/pktline/parse_length_header_test.go delete mode 100644 network/protocol/pktline/type.go delete mode 100644 network/protocol/sideband64k/append.go delete mode 100644 network/protocol/sideband64k/append_helpers_test.go delete mode 100644 network/protocol/sideband64k/append_preserves_dst_on_error_test.go delete mode 100644 network/protocol/sideband64k/band.go delete mode 100644 network/protocol/sideband64k/chunk_writer.go delete mode 100644 network/protocol/sideband64k/chunk_writer_write_and_read_from_test.go delete mode 100644 network/protocol/sideband64k/constants.go delete mode 100644 network/protocol/sideband64k/decoder.go delete mode 100644 network/protocol/sideband64k/decoder_data_control_and_keepalive_test.go delete mode 100644 network/protocol/sideband64k/decoder_invalid_band_test.go delete mode 100644 network/protocol/sideband64k/decoder_invalid_empty_payload_test.go delete mode 100644 network/protocol/sideband64k/decoder_malformed_pktline_test.go delete mode 100644 network/protocol/sideband64k/decoder_partial_read_test.go delete mode 100644 network/protocol/sideband64k/decoder_peek_test.go delete mode 100644 network/protocol/sideband64k/decoder_resync_after_over_max_data_test.go delete mode 100644 network/protocol/sideband64k/decoder_resync_after_over_wire_max_test.go delete mode 100644 network/protocol/sideband64k/decoder_unexpected_eof_test.go delete mode 100644 network/protocol/sideband64k/doc.go delete mode 100644 network/protocol/sideband64k/encoder.go delete mode 100644 network/protocol/sideband64k/encoder_buffered_flush_behavior_test.go delete mode 100644 network/protocol/sideband64k/encoder_partial_write_test.go delete mode 100644 network/protocol/sideband64k/encoder_set_max_data_cannot_exceed_wire_limit_test.go delete mode 100644 network/protocol/sideband64k/encoder_writes_frames_test.go delete mode 100644 network/protocol/sideband64k/errors.go delete mode 100644 network/protocol/sideband64k/frame.go delete mode 100644 network/protocol/sideband64k/frame_type.go delete mode 100644 network/protocol/sideband64k/helpers_test.go delete mode 100644 network/protocol/v0v1/doc.go delete mode 100644 network/protocol/v0v1/server/advertise.go delete mode 100644 network/protocol/v0v1/server/advertise_test.go delete mode 100644 network/protocol/v0v1/server/advertised_ref.go delete mode 100644 network/protocol/v0v1/server/doc.go delete mode 100644 network/protocol/v0v1/server/errors.go delete mode 100644 network/protocol/v0v1/server/frame.go delete mode 100644 network/protocol/v0v1/server/helpers.go delete mode 100644 network/protocol/v0v1/server/helpers_test.go delete mode 100644 network/protocol/v0v1/server/receivepack/capabilities.go delete mode 100644 network/protocol/v0v1/server/receivepack/doc.go delete mode 100644 network/protocol/v0v1/server/receivepack/errors.go delete mode 100644 network/protocol/v0v1/server/receivepack/helpers_test.go delete mode 100644 network/protocol/v0v1/server/receivepack/parse_test.go delete mode 100644 network/protocol/v0v1/server/receivepack/report_status.go delete mode 100644 network/protocol/v0v1/server/receivepack/report_status_test.go delete mode 100644 network/protocol/v0v1/server/receivepack/session.go delete mode 100644 network/protocol/v0v1/server/receivepack/types.go delete mode 100644 network/protocol/v0v1/server/session.go delete mode 100644 network/protocol/v0v1/server/version.go delete mode 100644 network/receivepack/advertise.go delete mode 100644 network/receivepack/capabilities_defaults.go delete mode 100644 network/receivepack/commands.go delete mode 100644 network/receivepack/doc.go delete mode 100644 network/receivepack/errors.go delete mode 100644 network/receivepack/hook.go delete mode 100644 network/receivepack/hooks/chain.go delete mode 100644 network/receivepack/hooks/doc.go delete mode 100644 network/receivepack/hooks/reject_force_push.go delete mode 100644 network/receivepack/int_test.go delete mode 100644 network/receivepack/options.go delete mode 100644 network/receivepack/receivepack.go delete mode 100644 network/receivepack/results.go delete mode 100644 network/receivepack/service/apply.go delete mode 100644 network/receivepack/service/command.go delete mode 100644 network/receivepack/service/command_result.go delete mode 100644 network/receivepack/service/doc.go delete mode 100644 network/receivepack/service/execute.go delete mode 100644 network/receivepack/service/hook.go delete mode 100644 network/receivepack/service/hook_apply.go delete mode 100644 network/receivepack/service/ingest_quarantine.go delete mode 100644 network/receivepack/service/options.go delete mode 100644 network/receivepack/service/request.go delete mode 100644 network/receivepack/service/result.go delete mode 100644 network/receivepack/service/run_hook.go delete mode 100644 network/receivepack/service/service.go delete mode 100644 network/receivepack/service/service_test.go delete mode 100644 network/receivepack/service/update.go delete mode 100644 network/receivepack/version.go delete mode 100644 object/blob/blob.go delete mode 100644 object/blob/parse.go delete mode 100644 object/blob/parse_test.go delete mode 100644 object/blob/serialize.go delete mode 100644 object/blob/serialize_test.go delete mode 100644 object/blob/test.go delete mode 100644 object/commit/commit.go delete mode 100644 object/commit/extraheader.go delete mode 100644 object/commit/parse.go delete mode 100644 object/commit/parse_test.go delete mode 100644 object/commit/serialize.go delete mode 100644 object/commit/serialize_test.go delete mode 100644 object/commit/type.go delete mode 100644 object/doc.go delete mode 100644 object/fetch/doc.go delete mode 100644 object/fetch/exact_blob.go delete mode 100644 object/fetch/exact_blob_reader.go delete mode 100644 object/fetch/exact_commit.go delete mode 100644 object/fetch/exact_object.go delete mode 100644 object/fetch/exact_reader.go delete mode 100644 object/fetch/exact_tag.go delete mode 100644 object/fetch/exact_tree.go delete mode 100644 object/fetch/fetcher.go delete mode 100644 object/fetch/header.go delete mode 100644 object/fetch/object_errors.go delete mode 100644 object/fetch/object_parse.go delete mode 100644 object/fetch/path.go delete mode 100644 object/fetch/peel_to_blob.go delete mode 100644 object/fetch/peel_to_blob_id.go delete mode 100644 object/fetch/peel_to_blob_reader.go delete mode 100644 object/fetch/peel_to_commit.go delete mode 100644 object/fetch/peel_to_commit_id.go delete mode 100644 object/fetch/peel_to_tree.go delete mode 100644 object/fetch/peel_to_tree_id.go delete mode 100644 object/fetch/size.go delete mode 100644 object/fetch/treefs.go delete mode 100644 object/fetch/treefs_entry.go delete mode 100644 object/fetch/treefs_info.go delete mode 100644 object/fetch/treefs_new.go delete mode 100644 object/fetch/treefs_op.go delete mode 100644 object/fetch/treefs_open.go delete mode 100644 object/fetch/treefs_path.go delete mode 100644 object/fetch/treefs_readdir.go delete mode 100644 object/fetch/treefs_readfile.go delete mode 100644 object/fetch/treefs_stat.go delete mode 100644 object/fetch/treefs_sub.go delete mode 100644 object/fetch/treefs_test.go delete mode 100644 object/header/append.go delete mode 100644 object/header/doc.go delete mode 100644 object/header/encode.go delete mode 100644 object/header/parse.go delete mode 100644 object/id/algorithm.go delete mode 100644 object/id/algorithm_details.go delete mode 100644 object/id/algorithm_emptytree.go delete mode 100644 object/id/algorithm_hexlen.go delete mode 100644 object/id/algorithm_new.go delete mode 100644 object/id/algorithm_packhashid.go delete mode 100644 object/id/algorithm_parse.go delete mode 100644 object/id/algorithm_signatureheadername.go delete mode 100644 object/id/algorithm_size.go delete mode 100644 object/id/algorithm_string.go delete mode 100644 object/id/algorithm_sum.go delete mode 100644 object/id/algorithm_supported.go delete mode 100644 object/id/algorithm_tables.go delete mode 100644 object/id/algorithm_zero.go delete mode 100644 object/id/doc.go delete mode 100644 object/id/errors.go delete mode 100644 object/id/max_size.go delete mode 100644 object/id/objectid.go delete mode 100644 object/id/objectid_algorithm.go delete mode 100644 object/id/objectid_byte.go delete mode 100644 object/id/objectid_compare.go delete mode 100644 object/id/objectid_frombytes.go delete mode 100644 object/id/objectid_parse.go delete mode 100644 object/id/objectid_string.go delete mode 100644 object/id/objectid_test.go delete mode 100644 object/id/signatureheadername_parse.go delete mode 100644 object/object.go delete mode 100644 object/parse_with_header.go delete mode 100644 object/parse_without_header.go delete mode 100644 object/signature/parse.go delete mode 100644 object/signature/serialize.go delete mode 100644 object/signature/signature.go delete mode 100644 object/signature/when.go delete mode 100644 object/signed/commit/commit.go delete mode 100644 object/signed/commit/doc.go delete mode 100644 object/signed/commit/integration_test.go delete mode 100644 object/signed/commit/parse.go delete mode 100644 object/signed/commit/payload_append.go delete mode 100644 object/signed/commit/signature_algorithms.go delete mode 100644 object/signed/commit/signature_append.go delete mode 100644 object/signed/commit/unit_test.go delete mode 100644 object/signed/doc.go delete mode 100644 object/signed/tag/doc.go delete mode 100644 object/signed/tag/integration_test.go delete mode 100644 object/signed/tag/parse.go delete mode 100644 object/signed/tag/payload_append.go delete mode 100644 object/signed/tag/signature_algorithms.go delete mode 100644 object/signed/tag/signature_append.go delete mode 100644 object/signed/tag/tag.go delete mode 100644 object/signed/tag/unit_test.go delete mode 100644 object/store/base_quarantine.go delete mode 100644 object/store/chain/bytes.go delete mode 100644 object/store/chain/chain.go delete mode 100644 object/store/chain/header.go delete mode 100644 object/store/chain/new.go delete mode 100644 object/store/chain/reader.go delete mode 100644 object/store/chain/refresh.go delete mode 100644 object/store/chain/size.go delete mode 100644 object/store/cursor.go delete mode 100644 object/store/doc.go delete mode 100644 object/store/dual/doc.go delete mode 100644 object/store/dual/dual.go delete mode 100644 object/store/dual/dual_test.go delete mode 100644 object/store/dual/new.go delete mode 100644 object/store/dual/quarantine.go delete mode 100644 object/store/dual/quarantine_begin.go delete mode 100644 object/store/dual/quarantine_discard.go delete mode 100644 object/store/dual/quarantine_promote.go delete mode 100644 object/store/dual/reader.go delete mode 100644 object/store/dual/writer_object.go delete mode 100644 object/store/dual/writer_pack.go delete mode 100644 object/store/errors.go delete mode 100644 object/store/loose/helpers_test.go delete mode 100644 object/store/loose/parse.go delete mode 100644 object/store/loose/paths.go delete mode 100644 object/store/loose/quarantine.go delete mode 100644 object/store/loose/quarantine_begin.go delete mode 100644 object/store/loose/quarantine_discard.go delete mode 100644 object/store/loose/quarantine_promote.go delete mode 100644 object/store/loose/quarantine_test.go delete mode 100644 object/store/loose/read_bytes.go delete mode 100644 object/store/loose/read_header.go delete mode 100644 object/store/loose/read_reader.go delete mode 100644 object/store/loose/read_size.go delete mode 100644 object/store/loose/read_test.go delete mode 100644 object/store/loose/refresh.go delete mode 100644 object/store/loose/store.go delete mode 100644 object/store/loose/write_bytes.go delete mode 100644 object/store/loose/write_reader.go delete mode 100644 object/store/loose/write_temp_object_file.go delete mode 100644 object/store/loose/write_test.go delete mode 100644 object/store/loose/write_writer.go delete mode 100644 object/store/loose/write_writer_accept.go delete mode 100644 object/store/loose/write_writer_finalize.go delete mode 100644 object/store/memory/algorithm.go delete mode 100644 object/store/memory/doc.go delete mode 100644 object/store/memory/object.go delete mode 100644 object/store/memory/read_bytes.go delete mode 100644 object/store/memory/read_header.go delete mode 100644 object/store/memory/read_reader.go delete mode 100644 object/store/memory/read_size.go delete mode 100644 object/store/memory/refresh.go delete mode 100644 object/store/memory/store.go delete mode 100644 object/store/memory/write_bytes.go delete mode 100644 object/store/memory/write_reader.go delete mode 100644 object/store/memory/write_test.go delete mode 100644 object/store/mix/bytes.go delete mode 100644 object/store/mix/header.go delete mode 100644 object/store/mix/mix.go delete mode 100644 object/store/mix/mru.go delete mode 100644 object/store/mix/new.go delete mode 100644 object/store/mix/reader.go delete mode 100644 object/store/mix/refresh.go delete mode 100644 object/store/mix/size.go delete mode 100644 object/store/packed/doc.go delete mode 100644 object/store/packed/internal/doc.go delete mode 100644 object/store/packed/internal/ingest/TODO delete mode 100644 object/store/packed/internal/ingest/byteslice_reader.go delete mode 100644 object/store/packed/internal/ingest/cache.go delete mode 100644 object/store/packed/internal/ingest/counting_writer.go delete mode 100644 object/store/packed/internal/ingest/crc.go delete mode 100644 object/store/packed/internal/ingest/delta_header.go delete mode 100644 object/store/packed/internal/ingest/distance.go delete mode 100644 object/store/packed/internal/ingest/doc.go delete mode 100644 object/store/packed/internal/ingest/drain.go delete mode 100644 object/store/packed/internal/ingest/entry.go delete mode 100644 object/store/packed/internal/ingest/entry_header.go delete mode 100644 object/store/packed/internal/ingest/entry_prefix.go delete mode 100644 object/store/packed/internal/ingest/errors.go delete mode 100644 object/store/packed/internal/ingest/file_section_writer.go delete mode 100644 object/store/packed/internal/ingest/fill.go delete mode 100644 object/store/packed/internal/ingest/finalize.go delete mode 100644 object/store/packed/internal/ingest/flush.go delete mode 100644 object/store/packed/internal/ingest/hash.go delete mode 100644 object/store/packed/internal/ingest/header.go delete mode 100644 object/store/packed/internal/ingest/idx_write.go delete mode 100644 object/store/packed/internal/ingest/ingest.go delete mode 100644 object/store/packed/internal/ingest/ingest_test.go delete mode 100644 object/store/packed/internal/ingest/options.go delete mode 100644 object/store/packed/internal/ingest/progress_write.go delete mode 100644 object/store/packed/internal/ingest/record_content.go delete mode 100644 object/store/packed/internal/ingest/record_delta.go delete mode 100644 object/store/packed/internal/ingest/record_inflate.go delete mode 100644 object/store/packed/internal/ingest/record_resolve.go delete mode 100644 object/store/packed/internal/ingest/records.go delete mode 100644 object/store/packed/internal/ingest/resolve_all.go delete mode 100644 object/store/packed/internal/ingest/result.go delete mode 100644 object/store/packed/internal/ingest/rev_write.go delete mode 100644 object/store/packed/internal/ingest/rewrite_header_trailer.go delete mode 100644 object/store/packed/internal/ingest/scan.go delete mode 100644 object/store/packed/internal/ingest/state.go delete mode 100644 object/store/packed/internal/ingest/stream.go delete mode 100644 object/store/packed/internal/ingest/temp.go delete mode 100644 object/store/packed/internal/ingest/testdata/fixtures/sha1/METADATA.txt delete mode 100644 object/store/packed/internal/ingest/testdata/fixtures/sha1/base.pack delete mode 100644 object/store/packed/internal/ingest/testdata/fixtures/sha1/nonthin.pack delete mode 100644 object/store/packed/internal/ingest/testdata/fixtures/sha1/thin.pack delete mode 100644 object/store/packed/internal/ingest/testdata/fixtures/sha256/METADATA.txt delete mode 100644 object/store/packed/internal/ingest/testdata/fixtures/sha256/base.pack delete mode 100644 object/store/packed/internal/ingest/testdata/fixtures/sha256/nonthin.pack delete mode 100644 object/store/packed/internal/ingest/testdata/fixtures/sha256/thin.pack delete mode 100644 object/store/packed/internal/ingest/thin_append.go delete mode 100644 object/store/packed/internal/ingest/thin_fix.go delete mode 100644 object/store/packed/internal/ingest/thin_unresolved.go delete mode 100644 object/store/packed/internal/ingest/trailer.go delete mode 100644 object/store/packed/internal/ingest/use.go delete mode 100644 object/store/packed/internal/ingest/write.go delete mode 100644 object/store/packed/internal/ingest/write_empty.go delete mode 100644 object/store/packed/internal/reading/TODO delete mode 100644 object/store/packed/internal/reading/close.go delete mode 100644 object/store/packed/internal/reading/delta_build_chain.go delete mode 100644 object/store/packed/internal/reading/delta_cache.go delete mode 100644 object/store/packed/internal/reading/delta_chain.go delete mode 100644 object/store/packed/internal/reading/delta_node.go delete mode 100644 object/store/packed/internal/reading/delta_resolve_chain.go delete mode 100644 object/store/packed/internal/reading/delta_resolve_chain_start.go delete mode 100644 object/store/packed/internal/reading/delta_resolve_content.go delete mode 100644 object/store/packed/internal/reading/delta_size.go delete mode 100644 object/store/packed/internal/reading/doc.go delete mode 100644 object/store/packed/internal/reading/entry_inflate.go delete mode 100644 object/store/packed/internal/reading/entry_meta.go delete mode 100644 object/store/packed/internal/reading/entry_parse.go delete mode 100644 object/store/packed/internal/reading/helpers_test.go delete mode 100644 object/store/packed/internal/reading/idx.go delete mode 100644 object/store/packed/internal/reading/idx_candidates_mru.go delete mode 100644 object/store/packed/internal/reading/idx_close.go delete mode 100644 object/store/packed/internal/reading/idx_lookup.go delete mode 100644 object/store/packed/internal/reading/idx_lookup_candidates.go delete mode 100644 object/store/packed/internal/reading/idx_open.go delete mode 100644 object/store/packed/internal/reading/idx_parse.go delete mode 100644 object/store/packed/internal/reading/location.go delete mode 100644 object/store/packed/internal/reading/new.go delete mode 100644 object/store/packed/internal/reading/options.go delete mode 100644 object/store/packed/internal/reading/pack.go delete mode 100644 object/store/packed/internal/reading/pack_idx_checksum.go delete mode 100644 object/store/packed/internal/reading/read_bytes.go delete mode 100644 object/store/packed/internal/reading/read_closer.go delete mode 100644 object/store/packed/internal/reading/read_header.go delete mode 100644 object/store/packed/internal/reading/read_header_resolve.go delete mode 100644 object/store/packed/internal/reading/read_reader.go delete mode 100644 object/store/packed/internal/reading/read_size.go delete mode 100644 object/store/packed/internal/reading/read_test.go delete mode 100644 object/store/packed/internal/reading/store.go delete mode 100644 object/store/packed/internal/reading/store_lookup.go delete mode 100644 object/store/packed/internal/reading/store_open_pack.go delete mode 100644 object/store/packed/internal/reading/trailer_match.go delete mode 100644 object/store/packed/new.go delete mode 100644 object/store/packed/options.go delete mode 100644 object/store/packed/options_refresh.go delete mode 100644 object/store/packed/quarantine.go delete mode 100644 object/store/packed/quarantine_begin.go delete mode 100644 object/store/packed/quarantine_discard.go delete mode 100644 object/store/packed/quarantine_promote.go delete mode 100644 object/store/packed/quarantine_test.go delete mode 100644 object/store/packed/reader.go delete mode 100644 object/store/packed/store.go delete mode 100644 object/store/packed/writer.go delete mode 100644 object/store/quarantine.go delete mode 100644 object/store/reader.go delete mode 100644 object/store/writer.go delete mode 100644 object/store/writer_object.go delete mode 100644 object/store/writer_pack.go delete mode 100644 object/stored/doc.go delete mode 100644 object/stored/id.go delete mode 100644 object/stored/new.go delete mode 100644 object/stored/object.go delete mode 100644 object/stored/stored.go delete mode 100644 object/tag/parse.go delete mode 100644 object/tag/parse_test.go delete mode 100644 object/tag/serialize.go delete mode 100644 object/tag/serialize_test.go delete mode 100644 object/tag/tag.go delete mode 100644 object/tag/type.go delete mode 100644 object/tree/entry.go delete mode 100644 object/tree/helpers_test.go delete mode 100644 object/tree/insert.go delete mode 100644 object/tree/lookup.go delete mode 100644 object/tree/mode.go delete mode 100644 object/tree/mode_details.go delete mode 100644 object/tree/mode_has_same_type.go delete mode 100644 object/tree/mode_is_blob_like.go delete mode 100644 object/tree/mode_is_regular_file.go delete mode 100644 object/tree/mode_table.go delete mode 100644 object/tree/name.go delete mode 100644 object/tree/parse.go delete mode 100644 object/tree/parse_test.go delete mode 100644 object/tree/path_append.go delete mode 100644 object/tree/path_clone.go delete mode 100644 object/tree/path_prefix.go delete mode 100644 object/tree/path_split.go delete mode 100644 object/tree/remove.go delete mode 100644 object/tree/serialize.go delete mode 100644 object/tree/serialize_test.go delete mode 100644 object/tree/tree.go delete mode 100644 object/tree/type.go delete mode 100644 object/type/details.go delete mode 100644 object/type/is_base.go delete mode 100644 object/type/name.go delete mode 100644 object/type/parse.go delete mode 100644 object/type/table.go delete mode 100644 object/type/type.go delete mode 100644 reachability/connected.go delete mode 100644 reachability/doc.go delete mode 100644 reachability/domain.go delete mode 100644 reachability/integration_test.go delete mode 100644 reachability/reachability.go delete mode 100644 reachability/unit_test.go delete mode 100644 reachability/walk.go delete mode 100644 reachability/walk_expand.go delete mode 100644 reachability/walk_expand_commits.go delete mode 100644 reachability/walk_expand_commits_graph.go delete mode 100644 reachability/walk_expand_objects.go delete mode 100644 reachability/walk_item.go delete mode 100644 reachability/walk_seq.go delete mode 100644 reachability/walk_stack.go delete mode 100644 reachability/walk_verify.go delete mode 100644 ref/detached.go delete mode 100644 ref/doc.go delete mode 100644 ref/name/branch.go delete mode 100644 ref/name/component.go delete mode 100644 ref/name/current.go delete mode 100644 ref/name/disposition.go delete mode 100644 ref/name/doc.go delete mode 100644 ref/name/errors.go delete mode 100644 ref/name/flags.go delete mode 100644 ref/name/length.go delete mode 100644 ref/name/lock.go delete mode 100644 ref/name/normalize.go delete mode 100644 ref/name/options.go delete mode 100644 ref/name/pseudo.go delete mode 100644 ref/name/refname_test.go delete mode 100644 ref/name/root.go delete mode 100644 ref/name/root_syntax.go delete mode 100644 ref/name/safe.go delete mode 100644 ref/name/sanitize.go delete mode 100644 ref/name/slashes.go delete mode 100644 ref/name/tag.go delete mode 100644 ref/name/update.go delete mode 100644 ref/name/utils.go delete mode 100644 ref/name/validate.go delete mode 100644 ref/name/worktree.go delete mode 100644 ref/ref.go delete mode 100644 ref/store/batch.go delete mode 100644 ref/store/batch_store.go delete mode 100644 ref/store/chain/chain.go delete mode 100644 ref/store/chain/close.go delete mode 100644 ref/store/chain/list.go delete mode 100644 ref/store/chain/new.go delete mode 100644 ref/store/chain/resolve.go delete mode 100644 ref/store/doc.go delete mode 100644 ref/store/errors.go delete mode 100644 ref/store/files/batch.go delete mode 100644 ref/store/files/batch_abort.go delete mode 100644 ref/store/files/batch_apply.go delete mode 100644 ref/store/files/batch_begin.go delete mode 100644 ref/store/files/batch_queue.go delete mode 100644 ref/store/files/batch_queue_ops.go delete mode 100644 ref/store/files/batch_rejection.go delete mode 100644 ref/store/files/batch_result_error.go delete mode 100644 ref/store/files/batch_test.go delete mode 100644 ref/store/files/broken_ref_error.go delete mode 100644 ref/store/files/close.go delete mode 100644 ref/store/files/helpers_test.go delete mode 100644 ref/store/files/new.go delete mode 100644 ref/store/files/packed_delete_test.go delete mode 100644 ref/store/files/packed_parse.go delete mode 100644 ref/store/files/packed_read.go delete mode 100644 ref/store/files/packed_refs.go delete mode 100644 ref/store/files/read_list.go delete mode 100644 ref/store/files/read_list_collect.go delete mode 100644 ref/store/files/read_loose.go delete mode 100644 ref/store/files/read_resolve.go delete mode 100644 ref/store/files/read_resolve_fully.go delete mode 100644 ref/store/files/resolve_list_test.go delete mode 100644 ref/store/files/root_for.go delete mode 100644 ref/store/files/root_kind.go delete mode 100644 ref/store/files/root_loose_path.go delete mode 100644 ref/store/files/root_open_common.go delete mode 100644 ref/store/files/store.go delete mode 100644 ref/store/files/transaction.go delete mode 100644 ref/store/files/transaction_abort.go delete mode 100644 ref/store/files/transaction_begin.go delete mode 100644 ref/store/files/transaction_commit.go delete mode 100644 ref/store/files/transaction_dirs_test.go delete mode 100644 ref/store/files/transaction_names_test.go delete mode 100644 ref/store/files/transaction_pseudoref_test.go delete mode 100644 ref/store/files/transaction_queue.go delete mode 100644 ref/store/files/transaction_queue_ops.go delete mode 100644 ref/store/files/transaction_symbolic_test.go delete mode 100644 ref/store/files/transaction_update_test.go delete mode 100644 ref/store/files/trim.go delete mode 100644 ref/store/files/update_cleanup.go delete mode 100644 ref/store/files/update_cleanup_parents.go delete mode 100644 ref/store/files/update_commit.go delete mode 100644 ref/store/files/update_commit_delete.go delete mode 100644 ref/store/files/update_dir_tree.go delete mode 100644 ref/store/files/update_direct_read.go delete mode 100644 ref/store/files/update_direct_ref.go delete mode 100644 ref/store/files/update_error.go delete mode 100644 ref/store/files/update_executor.go delete mode 100644 ref/store/files/update_kind.go delete mode 100644 ref/store/files/update_lock.go delete mode 100644 ref/store/files/update_lock_packed.go delete mode 100644 ref/store/files/update_operation_prepared.go delete mode 100644 ref/store/files/update_operation_queue.go delete mode 100644 ref/store/files/update_path.go delete mode 100644 ref/store/files/update_prepare.go delete mode 100644 ref/store/files/update_prepare_lock.go delete mode 100644 ref/store/files/update_prepare_resolve.go delete mode 100644 ref/store/files/update_prepare_verify.go delete mode 100644 ref/store/files/update_resolve_target.go delete mode 100644 ref/store/files/update_resolve_target_ordinary.go delete mode 100644 ref/store/files/update_target_resolved.go delete mode 100644 ref/store/files/update_validate.go delete mode 100644 ref/store/files/update_verify_current.go delete mode 100644 ref/store/files/update_verify_refnames.go delete mode 100644 ref/store/files/update_visible_names.go delete mode 100644 ref/store/files/update_write_loose.go delete mode 100644 ref/store/files/update_write_packed_refs.go delete mode 100644 ref/store/files/worktree_test.go delete mode 100644 ref/store/reading.go delete mode 100644 ref/store/transaction.go delete mode 100644 ref/store/transactional_store.go delete mode 100644 ref/store/update_errors.go delete mode 100644 ref/symbolic.go delete mode 100644 repository/algorithm.go delete mode 100644 repository/close.go delete mode 100644 repository/commit_graph.go delete mode 100644 repository/commit_queries.go delete mode 100644 repository/config.go delete mode 100644 repository/fetcher.go delete mode 100644 repository/objects.go delete mode 100644 repository/open.go delete mode 100644 repository/reachability.go delete mode 100644 repository/refs.go delete mode 100644 repository/refs_test.go delete mode 100644 repository/refs_timeout.go delete mode 100644 repository/repository.go delete mode 100644 repository/stored_test.go delete mode 100644 repository/traversal_test.go delete mode 100644 repository/write_loose_test.go delete mode 100644 research/dynamic_packfiles.txt delete mode 100644 research/packfile_bloom.txt diff --git a/BENCHMARKS.md b/BENCHMARKS.md deleted file mode 100644 index 36dfdb39..00000000 --- a/BENCHMARKS.md +++ /dev/null @@ -1,22 +0,0 @@ -# Benchmarks - -* See [gitbench](https://git.sr.ht/~runxiyu/gitbench). -* `legacy` branch furgit is slightly faster due to buffer reuse and custom - ZLIB. These will be re‐added. -* Alpine edge, i5‐10210U, `performance` governor, `linux.git`. -* go-git may become much faster when - [#1894](https://github.com/go-git/go-git/pull/1894) - and such are fully in use. -* These lone tests do not represent all workloads. Test your usage - pattern yourself (and contribute to gitbench). - -## Traversing all trees in `HEAD` and fetching each file size - -Mainly tests the packfile object reader. - -| Implementation | Total | User | System | -| - | - | - | - | -| Git | 337 ms | 226 ms | 108 ms | -| libgit2 | 391 ms | 269 ms | 120 ms | -| Furgit | 487 ms | 457 ms | 49 ms | -| go-git | 37 s | 35 s | 2 s | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1f5019b0..7b332cfe 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,7 @@ Refer to the README for community spaces. ## Repos and mirrors -* [Codeberg](https://codeberg.org/lindenii/furgit) +* [Main repository on Codeberg](https://codeberg.org/lindenii/furgit) * [SourceHut mirror](https://git.sr.ht/~runxiyu/furgit) * [tangled mirror](https://tangled.org/@runxiyu.tngl.sh/furgit) * [GitHub mirror](https://github.com/runxiyu/furgit) @@ -14,16 +14,7 @@ Refer to the README for community spaces. Bug reports ideally include a reproduction recipe: a Go program which starts out with an empty repository and calls Furgit and/or Git commands to trigger -undesirable behavior. - -Please ask for help with writing your regression test before asking for your -problem to be fixed. Time invested in writing a regression test saves time -wasted on back‐and‐forth discussion about how the problem can be reproduced. A -regression test will need to be written in any case to verify a fix and -prevent the problem from resurfacing. - -If writing an automated test really turns out to be impossible, please explain -in very clear terms how the problem can be reproduced. +undesirable behavior. Feel free to ask for help writing them. Choose any one of: @@ -37,7 +28,8 @@ When fixing a bug, please write a regression test in a separate commit before your fix. Demonstrate that the regression test fails and that your fix lets it succeed. Obviously, the regression test must be reasonable, demonstrate a real issue (however minor), and must not itself contain hacks solely designed for -the purposes of making the old code fail and the new one succeed. +the purposes of making the old code fail and the new one succeed. Feel free to +ask for help writing regression tests. Choose any one of: @@ -46,7 +38,7 @@ Choose any one of: [my public inbox](https://lists.sr.ht/~runxiyu/public-inbox) * Open a [pull request on GitHub](https://github.com/runxiyu/furgit/pulls) -## Licensing +## DCO sign-off For the purposes of the Developer Certificate of Origin, the "open source license" refers to the GNU Affero General Public License, Version 3.0, with diff --git a/README.md b/README.md index 990d46bd..eaaf735f 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ Furgit is a low‐level Git library in Go. +Please refer to the API reference. + ## Status * Years or decades away from stable @@ -27,7 +29,7 @@ Furgit is a low‐level Git library in Go. * [#lindenii](https://web.libera.chat/#lindenii) on [Libera.Chat](https://libera.chat) -See the CONTRIBUTING document for bug reports and patch submissions. +See `CONTRIBUTING.md` for bug reports and patch submissions. ## Acknowledgements @@ -50,14 +52,3 @@ under the GNU Affero General Public License, Version 3.0 only, a public acceptance by the Designated Proxy of any subsequent version of the GNU Affero General Public License shall permanently authorize the use of that accepted version for this Program. - -## Alternatives - -Not endorsements. - -* [github.com/go-git/go-git](https://github.com/go-git/go-git) (by far the most mature) -* [github.com/driusan/dgit](https://github.com/driusan/dgit) -* [github.com/Nivl/git-go](https://github.com/Nivl/git-go) -* [github.com/unkn0wn-root/git-go.git](https://github.com/unkn0wn-root/git-go.git) -* [github.com/speedata/gogit](https://github.com/speedata/gogit) - diff --git a/ROADMAP.md b/ROADMAP.md deleted file mode 100644 index c709bb7d..00000000 --- a/ROADMAP.md +++ /dev/null @@ -1,212 +0,0 @@ -# Roadmap - -* Configuration - * [X] Parsing - * [ ] Includes - * [ ] Writing -* [X] Object IDs - * [X] SHA‐256 - * [X] SHA‐1 -* [X] Object model (incl., parse, serialize) - * [X] Blobs - * [X] Trees - * [X] File mode definitions - * [X] Entry insertion ordering - * [X] Traversal - * [ ] Pathspec - * [X] Commits - * [X] Annotated tags - * [X] Stored objects -* Signed objects - * [X] Signed object payload/signature extraction - * [ ] Signature verification -* [X] Reading object stores - * [X] Pluggable interface - * [X] Chain lookup store - * [X] Mix lookup store - * [X] Reading loose objects - * [ ] Reading from bundles - * [ ] Promisor remotes - * [ ] Alternates - * [X] Reading packed objects - * [X] Pack index lookups - * [X] Delta caching - * [X] Delta application - * [ ] Pack‐wide bloom filters - * [ ] Multi pack indexes -* [ ] Writing objects - * [X] Loose object writing -* Misc bundle features - * [ ] Writing bundles -* Misc packfile features - * [X] Writing pack indexes - * [X] Writing reverse pack indexes - * [ ] Writing packfiles - * [ ] Writing thin packs - * [ ] Compressing deltas - * [ ] Delta islands - * [ ] Pack verification -* Compression - * [ ] Plugabble compression algorithms - * [X] ZLIB support - * [ ] DEFLATE optimizations - * [X] Adler‐32 SIMD optimizations -* [X] References - * [X] Detached references - * [X] Symbolic references - * [X] Name verification/resolution - * [X] Annotated tag ref peeling - * [ ] Describe - * [ ] Revision syntax - * [ ] Namespaces - * [ ] Replace refs, grafts -* [X] Reference stores - * [X] Chain lookup store - * [X] Files reference store - * [X] Reading loose refs - * [X] Reading packed refs - * [X] Atomic writes - * [X] Batched writes - * [ ] Packing refs - * [ ] Reflogs - * [ ] Reftable -* Reachability - * [X] Have/wants walks - * [X] Is ancestor - * [X] Merge bases - * [X] Commit graph - * [X] Changed path bloom filters - * [X] Chained graphs - * [ ] Writing - * [ ] Reachability bitmaps - * [ ] For a single packfile - * [ ] For Multi pack indexes -* Misc repository - * [X] Opening relevant stores - * [ ] Creating repositories - * [ ] Filter branch/repo - * [ ] Fast import/export - * [ ] Git notes - * [ ] Git attributes - * [ ] Full pseudoref support - * Integrity and maintenance - * [ ] Fsck - * [ ] Repacking - * [ ] Garbage collection - * [ ] Cruft packing - * [ ] Expiration - * [ ] Grep - * [ ] Submodules - * [ ] Worktrees - * [ ] Archive - * [ ] LFS - * [ ] Revision log walk - * [ ] Topological ordering - * [ ] Date ordering - * [ ] Path‐limited -* [ ] Diffing - * [ ] Blame - * [ ] Annotate - * [X] Tree diffing - * [ ] Similarity/rename/copy detection - * [ ] Multi‐way diffs - * [ ] Patch‐id - * [ ] Range‐diff - * Blob diffing - * [ ] Word diffs - * [X] Myers - * [ ] Patience - * [ ] Histogram - * [ ] Three‐way - * [ ] Format patch - * [ ] Apply/amend patch -* Branch integration/rewrite/etc methods - * [ ] Merge - * [ ] Recursive - * [ ] ORT - * [ ] Rebase - * [ ] Cherry pick - * [ ] Revert - * [ ] Rerere -* Network protocols and related features - * [X] pkt-line - * [X] side-band-64k - * [X] Ingesting packfiles - * [X] Quarantine areas - * [X] Un‐thinning thin packs - * Version 0, version 1 protocols - * [X] Server side - * [X] Reference advertisement - * [X] Capability negotiation - * [X] Receive - * [ ] "Upload" - * [ ] Client side - * [ ] Send - * [ ] Fetch - * Version 2 protocol - * [ ] Server side - * [ ] "Upload" - * [ ] Client side - * [ ] Fetch - * Protocol‐independent logic - * Common - * [X] Progress meters - * Client side - * [ ] Refspec - * [ ] Fetch - * [ ] Partial clones - * [ ] Object filtering - * [ ] Bundle URI - * [ ] Packfile URI - * [ ] Shallow clones - * [ ] Send - * Server side - * [ ] Upload - * [ ] Object filtering - * [X] Receive - * [ ] Signed push - * Hooks - * Slots - * [ ] After ref negotiation - * [X] After object unpacking - * Provided samples - * [X] Chain - * [X] Force push rejection -* [ ] Working trees - * [ ] Stashing - * [ ] Ignore rules - * [ ] Checkouts - * [ ] Sparse checkouts - * [ ] CR/LF conversions - * [ ] File mode conversions - * [ ] Indexes - * [ ] Conflict resolution - * [ ] Split index - * [ ] Sparse index - * [ ] Untracked cache - * [ ] Status listing - * [ ] Filesystem monitor - * [ ] Worktree - * [ ] Common directory - * [ ] Worktree‐specific references - * [X] Worktree‐specific reference name validation -* Research - * [ ] Dynamic packfiles - * [ ] Compaction; page‐sized hole punching - * [ ] Dynamic indexing - * [ ] Linear/extendible/spiral hashing - * [ ] Dynamic reachability bitmaps - -## Not planned - -* CLI tools -* Clone -* Anything reasonably considered "porcelain" -* Credential helper -* Transports -* Auth -* Remote management -* Bisect -* Any use of env vars -* Repository discovery walking - diff --git a/TODO b/TODO deleted file mode 100644 index 540b9055..00000000 --- a/TODO +++ /dev/null @@ -1,53 +0,0 @@ -Missing operations that git.sr.ht needs -======================================= - -* Repository init -* Clone/fetch (but that's handled by the CLI) -* Resolving revisions -* Better diffing; patch generation -* Log and revision walking, optionally filtered by path -* Config writing - -Missing operations that tangled needs -===================================== - -* Repository init -* Clone/fetch (but that's handled by the CLI) -* Resolving revisions -* Better diffing; patch generation -* Submodule config (but we should make the CLI handle these) -* Potentially, convenience APIs for branch/tag refs -* Log and revision walking, optionally filtered by path - -Unstructured ones -================= - -* Receive-pack hook shape redesign; separate post-ref and post-ingest hook - types. -* Completely redo error handling. - -* Better status reporting, filling in more things in report-status-v2. -* Maybe the Progress/Error writers should return error on creation - instead of automatically discarding content? -* Actually making signed-push work reasonably -* Investigate fsck issues with receive-pack -* Improve performance of delta resolution - -* Okay, I think this is still a real design issue, just at a different layer - now. receive-pack and the object stores are better than they were when pack - ingest still wanted raw roots, and we now have coordinated quarantines and a - dual store to represent the normal mix. However, that probably isn't the end - state either. In the usual repository layout, loose and packed objects are - really two parts of one files object storage, and dual may just be an - intermediate abstraction until files-backed storage gets a more integrated - API, including in particular, ingress/quarantine. We should preserve the - current separation for now, because it keeps the boundaries there, and is - much simpler than trying to prematurely fuse everything together, but if - receive-pack and hooks keep growing around dual then that is probably a sign - that the underlying files object storage wants a interface of its own. - -* Digital signature API -* Revision-ish entry points like if you get main or v1.0 we should - try to resolve that probably -* Needs much better diff API -* revwalk/log diff --git a/cmd/doc.go b/cmd/doc.go deleted file mode 100644 index cdc58288..00000000 --- a/cmd/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package cmd encapsulates various commands provided by this module. -package cmd diff --git a/cmd/receivepack9418/conn.go b/cmd/receivepack9418/conn.go deleted file mode 100644 index 755cf022..00000000 --- a/cmd/receivepack9418/conn.go +++ /dev/null @@ -1,122 +0,0 @@ -package main - -import ( - "bufio" - "context" - "fmt" - "log" - "net" - "os" - "strings" - - "codeberg.org/lindenii/furgit/network/receivepack" - objectdual "codeberg.org/lindenii/furgit/object/store/dual" - objectloose "codeberg.org/lindenii/furgit/object/store/loose" - objectpacked "codeberg.org/lindenii/furgit/object/store/packed" -) - -func (srv *server) handleConn(conn net.Conn) { - defer func() { _ = conn.Close() }() - - reader := bufio.NewReader(conn) - writer := bufio.NewWriter(conn) - - req, err := readGitProtoRequest(reader) - if err != nil { - writeErrPkt(writer, fmt.Sprintf("invalid initial request: %v", err)) - _ = writer.Flush() - - log.Printf("receivepack9418: %s: invalid initial request: %v", conn.RemoteAddr(), err) - - return - } - - if req.Command != "git-receive-pack" { - writeErrPkt(writer, fmt.Sprintf("unsupported command %q", req.Command)) - _ = writer.Flush() - - log.Printf("receivepack9418: %s: unsupported command %q", conn.RemoteAddr(), req.Command) - - return - } - - gitProtocol := strings.Join(req.ExtraParameters, ":") - - objectIngress, cleanupObjectIngress, err := srv.openObjectIngress() - if err != nil { - writeErrPkt(writer, fmt.Sprintf("object ingress unavailable: %v", err)) - _ = writer.Flush() - - log.Printf("receivepack9418: %s: object ingress unavailable: %v", conn.RemoteAddr(), err) - - return - } - - defer cleanupObjectIngress() - - opts := receivepack.Options{ - GitProtocol: gitProtocol, - Algorithm: srv.repo.Algorithm(), - Refs: srv.repo.Refs(), - ExistingObjects: srv.repo.Objects(), - ObjectIngress: objectIngress, - } - - err = receivepack.ReceivePack(context.Background(), writer, reader, opts) - if err != nil { - _ = writer.Flush() - - log.Printf( - "receivepack9418: %s: receive-pack failed (path=%q host=%q extras=%v): %v", - conn.RemoteAddr(), - req.Pathname, - req.Host, - req.ExtraParameters, - err, - ) - - return - } - - err = writer.Flush() - if err != nil { - log.Printf("receivepack9418: %s: flush failed: %v", conn.RemoteAddr(), err) - - return - } -} - -func (srv *server) openObjectIngress() (*objectdual.Dual, func(), error) { - err := srv.objectsRoot.Mkdir("pack", 0o755) - if err != nil && !os.IsExist(err) { - return nil, nil, err - } - - packRoot, err := srv.objectsRoot.OpenRoot("pack") - if err != nil { - return nil, nil, err - } - - looseStore, err := objectloose.New(srv.objectsRoot, srv.repo.Algorithm()) - if err != nil { - _ = packRoot.Close() - - return nil, nil, err - } - - packedStore, err := objectpacked.New(packRoot, srv.repo.Algorithm(), objectpacked.Options{WriteRev: true}) - if err != nil { - _ = looseStore.Close() - _ = packRoot.Close() - - return nil, nil, err - } - - cleanup := func() { - _ = packedStore.Close() - _ = looseStore.Close() - _ = packRoot.Close() - } - - return objectdual.New(looseStore, packedStore), cleanup, nil -} diff --git a/cmd/receivepack9418/errpkt.go b/cmd/receivepack9418/errpkt.go deleted file mode 100644 index 743811aa..00000000 --- a/cmd/receivepack9418/errpkt.go +++ /dev/null @@ -1,18 +0,0 @@ -package main - -import ( - "io" - - "codeberg.org/lindenii/furgit/network/protocol/pktline" -) - -func writeErrPkt(w io.Writer, message string) { - payload := []byte("ERR " + message + "\n") - - frame, err := pktline.AppendData(nil, payload) - if err != nil { - return - } - - _, _ = w.Write(frame) -} diff --git a/cmd/receivepack9418/gitproto.go b/cmd/receivepack9418/gitproto.go deleted file mode 100644 index 28c192d4..00000000 --- a/cmd/receivepack9418/gitproto.go +++ /dev/null @@ -1,23 +0,0 @@ -package main - -import ( - "fmt" - "io" - - "codeberg.org/lindenii/furgit/network/protocol/pktline" -) - -func readGitProtoRequest(r io.Reader) (gitProtoRequest, error) { - dec := pktline.NewDecoder(r, pktline.ReadOptions{}) - - frame, err := dec.ReadFrame() - if err != nil { - return gitProtoRequest{}, err - } - - if frame.Type != pktline.PacketData { - return gitProtoRequest{}, fmt.Errorf("expected initial pkt-line data, got %v", frame.Type) - } - - return parseGitProtoRequestPayload(frame.Payload) -} diff --git a/cmd/receivepack9418/main.go b/cmd/receivepack9418/main.go deleted file mode 100644 index 6884f326..00000000 --- a/cmd/receivepack9418/main.go +++ /dev/null @@ -1,61 +0,0 @@ -// Command receivepack9418 serves one fixed repository over git:// receive-pack on TCP 9418. -package main - -import ( - "flag" - "log" - "os" -) - -func main() { - os.Exit(runMain()) -} - -func runMain() int { - listenAddr := flag.String("listen", ":9418", "listen address") - repoPath := flag.String("repo", "", "path to git dir (.git or bare repo root)") - cpuProfilePath := flag.String("cpuprofile", "", "write CPU profile to file") - memProfilePath := flag.String("memprofile", "", "write heap profile to file at exit") - - flag.Parse() - - if *repoPath == "" { - log.Print("must provide -repo ") - - return 2 - } - - if *cpuProfilePath != "" { - stopCPUProfile, err := startCPUProfile(*cpuProfilePath) - if err != nil { - log.Printf("cpuprofile: %v", err) - - return 1 - } - - defer func() { - stopErr := stopCPUProfile() - if stopErr != nil { - log.Printf("cpuprofile: %v", stopErr) - } - }() - } - - if *memProfilePath != "" { - defer func() { - memErr := writeMemProfile(*memProfilePath) - if memErr != nil { - log.Printf("memprofile: %v", memErr) - } - }() - } - - err := run(*listenAddr, *repoPath) - if err != nil { - log.Printf("run: %v", err) - - return 1 - } - - return 0 -} diff --git a/cmd/receivepack9418/profile.go b/cmd/receivepack9418/profile.go deleted file mode 100644 index 40ba1d56..00000000 --- a/cmd/receivepack9418/profile.go +++ /dev/null @@ -1,58 +0,0 @@ -package main - -import ( - "fmt" - "os" - "runtime" - "runtime/pprof" -) - -func startCPUProfile(path string) (func() error, error) { - //#nosec G304 - file, err := os.Create(path) - if err != nil { - return nil, fmt.Errorf("create %q: %w", path, err) - } - - err = pprof.StartCPUProfile(file) - if err != nil { - _ = file.Close() - - return nil, fmt.Errorf("start cpu profile %q: %w", path, err) - } - - return func() error { - pprof.StopCPUProfile() - - err := file.Close() - if err != nil { - return fmt.Errorf("close cpu profile %q: %w", path, err) - } - - return nil - }, nil -} - -func writeMemProfile(path string) error { - //#nosec G304 - file, err := os.Create(path) - if err != nil { - return fmt.Errorf("create %q: %w", path, err) - } - - runtime.GC() - - err = pprof.WriteHeapProfile(file) - if err != nil { - _ = file.Close() - - return fmt.Errorf("write heap profile %q: %w", path, err) - } - - err = file.Close() - if err != nil { - return fmt.Errorf("close heap profile %q: %w", path, err) - } - - return nil -} diff --git a/cmd/receivepack9418/request.go b/cmd/receivepack9418/request.go deleted file mode 100644 index 57b55e30..00000000 --- a/cmd/receivepack9418/request.go +++ /dev/null @@ -1,61 +0,0 @@ -package main - -import ( - "bytes" - "errors" - "fmt" - "strings" -) - -type gitProtoRequest struct { - Command string - Pathname string - Host string - ExtraParameters []string -} - -func parseGitProtoRequestPayload(payload []byte) (gitProtoRequest, error) { - parts := bytes.Split(payload, []byte{0}) - if len(parts) == 0 || len(parts[0]) == 0 { - return gitProtoRequest{}, errors.New("missing command/path segment") - } - - commandPath := string(parts[0]) - - command, pathname, ok := strings.Cut(commandPath, " ") - if !ok || command == "" || pathname == "" { - return gitProtoRequest{}, fmt.Errorf("malformed command/path segment %q", commandPath) - } - - req := gitProtoRequest{ - Command: command, - Pathname: pathname, - } - - i := 1 - if i < len(parts) && strings.HasPrefix(string(parts[i]), "host=") { - req.Host = strings.TrimPrefix(string(parts[i]), "host=") - i++ - } - - // No tail left. - if i >= len(parts) { - return req, nil - } - - // If there is tail, grammar requires one empty field before extras. - if len(parts[i]) != 0 { - return gitProtoRequest{}, fmt.Errorf("unexpected token %q after host/path", string(parts[i])) - } - - i++ - for ; i < len(parts); i++ { - if len(parts[i]) == 0 { - continue - } - - req.ExtraParameters = append(req.ExtraParameters, string(parts[i])) - } - - return req, nil -} diff --git a/cmd/receivepack9418/run.go b/cmd/receivepack9418/run.go deleted file mode 100644 index 2932459d..00000000 --- a/cmd/receivepack9418/run.go +++ /dev/null @@ -1,70 +0,0 @@ -package main - -import ( - "context" - "errors" - "fmt" - "log" - "net" - "os" - - "codeberg.org/lindenii/furgit/repository" -) - -func run(listenAddr, repoPath string) error { - repoRoot, err := os.OpenRoot(repoPath) - if err != nil { - return fmt.Errorf("open repo root: %w", err) - } - - defer func() { _ = repoRoot.Close() }() - - repo, err := repository.Open(repoRoot) - if err != nil { - return fmt.Errorf("open repository: %w", err) - } - - defer func() { _ = repo.Close() }() - - objectsRoot, err := repoRoot.OpenRoot("objects") - if err != nil { - return fmt.Errorf("open objects root: %w", err) - } - - defer func() { _ = objectsRoot.Close() }() - - srv := &server{ - repo: repo, - objectsRoot: objectsRoot, - } - - ln, err := (&net.ListenConfig{}).Listen(context.Background(), "tcp", listenAddr) - if err != nil { - return fmt.Errorf("listen %q: %w", listenAddr, err) - } - - defer func() { _ = ln.Close() }() - - log.Printf("receivepack9418: listening on %s", listenAddr) - log.Printf("receivepack9418: repository=%s algorithm=%s", repoPath, repo.Algorithm()) - - for { - conn, err := ln.Accept() - if err != nil { - if errors.Is(err, net.ErrClosed) { - return nil - } - - nerr, ok := errors.AsType[net.Error](err) - if ok && nerr.Timeout() { - log.Printf("receivepack9418: timeout accept error: %v", err) - - continue - } - - return fmt.Errorf("accept: %w", err) - } - - go srv.handleConn(conn) - } -} diff --git a/cmd/receivepack9418/server.go b/cmd/receivepack9418/server.go deleted file mode 100644 index 74793712..00000000 --- a/cmd/receivepack9418/server.go +++ /dev/null @@ -1,12 +0,0 @@ -package main - -import ( - "os" - - "codeberg.org/lindenii/furgit/repository" -) - -type server struct { - repo *repository.Repository - objectsRoot *os.Root -} diff --git a/cmd/show-object/main.go b/cmd/show-object/main.go deleted file mode 100644 index 8fdffac8..00000000 --- a/cmd/show-object/main.go +++ /dev/null @@ -1,23 +0,0 @@ -// Command show-object provides a small command line utility to show the details of a specified Git object. -package main - -import ( - "flag" - "log" -) - -func main() { - repoPath := flag.String("r", "", "path to git dir (.git or bare repo root)") - name := flag.String("h", "", "reference name or object id") - - flag.Parse() - - if *repoPath == "" || *name == "" { - log.Fatal("must provide -r and -h ") - } - - err := run(repoPath, name) - if err != nil { - log.Fatalf("run: %v", err) - } -} diff --git a/cmd/show-object/print.go b/cmd/show-object/print.go deleted file mode 100644 index 75484f73..00000000 --- a/cmd/show-object/print.go +++ /dev/null @@ -1,74 +0,0 @@ -package main - -import ( - "fmt" - "os" - "strings" - - "codeberg.org/lindenii/furgit/object" - "codeberg.org/lindenii/furgit/object/blob" - "codeberg.org/lindenii/furgit/object/commit" - "codeberg.org/lindenii/furgit/object/stored" - "codeberg.org/lindenii/furgit/object/tag" - "codeberg.org/lindenii/furgit/object/tree" -) - -func printStored(s *stored.Stored[object.Object]) { - var b strings.Builder - - id := s.ID() - ty := s.Object().ObjectType() - - tyName, ok := ty.Name() - if !ok { - tyName = fmt.Sprintf("type %d", ty) - } - - fmt.Fprintf(&b, "id: %s\n", id) - fmt.Fprintf(&b, "type: %s\n", tyName) - - switch obj := s.Object().(type) { - case *blob.Blob: - blob := obj - fmt.Fprintf(&b, "size: %d\n", len(blob.Data)) - fmt.Fprintf(&b, "data: %q\n", string(blob.Data)) - case *tree.Tree: - tree := obj - fmt.Fprintf(&b, "entries: %d\n", len(tree.Entries)) - - for _, entry := range tree.Entries { - fmt.Fprintf(&b, "%06o %s\t%s\n", entry.Mode, entry.ID, entry.Name) - } - case *commit.Commit: - commit := obj - fmt.Fprintf(&b, "tree: %s\n", commit.Tree) - - for _, parent := range commit.Parents { - fmt.Fprintf(&b, "parent: %s\n", parent) - } - - fmt.Fprintf(&b, "author: %s <%s>\n", commit.Author.Name, commit.Author.Email) - fmt.Fprintf(&b, "committer: %s <%s>\n", commit.Committer.Name, commit.Committer.Email) - fmt.Fprintf(&b, "message:\n%s\n", string(commit.Message)) - case *tag.Tag: - tag := obj - - targetTy, ok := tag.TargetType.Name() - if !ok { - targetTy = fmt.Sprintf("type %d", tag.TargetType) - } - - fmt.Fprintf(&b, "target: %s (%s)\n", tag.Target, targetTy) - fmt.Fprintf(&b, "name: %s\n", tag.Name) - - if tag.Tagger != nil { - fmt.Fprintf(&b, "tagger: %s <%s>\n", tag.Tagger.Name, tag.Tagger.Email) - } - - fmt.Fprintf(&b, "message:\n%s\n", string(tag.Message)) - default: - fmt.Fprintf(&b, "%#v\n", obj) - } - - _, _ = os.Stdout.WriteString(b.String()) -} diff --git a/cmd/show-object/resolve.go b/cmd/show-object/resolve.go deleted file mode 100644 index eaf2c102..00000000 --- a/cmd/show-object/resolve.go +++ /dev/null @@ -1,22 +0,0 @@ -package main - -import ( - "strings" - - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/repository" -) - -func resolveInput(repo *repository.Repository, input string) (objectid.ObjectID, error) { - id, err := objectid.ParseHex(repo.Algorithm(), strings.TrimSpace(input)) - if err == nil { - return id, nil - } - - resolved, err := repo.Refs().ResolveToDetached(input) - if err != nil { - return objectid.ObjectID{}, err - } - - return resolved.ID, nil -} diff --git a/cmd/show-object/run.go b/cmd/show-object/run.go deleted file mode 100644 index f1a6fc6d..00000000 --- a/cmd/show-object/run.go +++ /dev/null @@ -1,45 +0,0 @@ -package main - -import ( - "fmt" - "os" - - "codeberg.org/lindenii/furgit/repository" -) - -func run(repoPath, name *string) error { - root, err := os.OpenRoot(*repoPath) - if err != nil { - return fmt.Errorf("open repo root: %w", err) - } - - defer func() { _ = root.Close() }() - - repo, err := repository.Open(root) - if err != nil { - return fmt.Errorf("open repository: %w", err) - } - - id, err := resolveInput(repo, *name) - if err != nil { - _ = repo.Close() - - return fmt.Errorf("resolve %q: %w", *name, err) - } - - s, err := repo.Fetcher().ExactObject(id) - if err != nil { - _ = repo.Close() - - return fmt.Errorf("read object %s: %w", id, err) - } - - printStored(s) - - err = repo.Close() - if err != nil { - return fmt.Errorf("close repository: %w", err) - } - - return nil -} diff --git a/commitquery/commit_data.go b/commitquery/commit_data.go deleted file mode 100644 index dff6a91c..00000000 --- a/commitquery/commit_data.go +++ /dev/null @@ -1,17 +0,0 @@ -package commitquery - -import ( - commitgraphread "codeberg.org/lindenii/furgit/format/commitgraph/read" - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// commitData stores the metadata needed by commit-domain queries. -type commitData struct { - ID objectid.ObjectID - Parents []parentRef - CommitTime int64 - Generation uint64 - HasGeneration bool - GraphPos commitgraphread.Position - HasGraphPos bool -} diff --git a/commitquery/doc.go b/commitquery/doc.go deleted file mode 100644 index 269512df..00000000 --- a/commitquery/doc.go +++ /dev/null @@ -1,6 +0,0 @@ -// Package commitquery provides commit ancestry and merge-base queries -// over object storage. -// -// It uses commit-ish object IDs, peeling annotated tags when needed, -// and can use an optional commit-graph reader for performance. -package commitquery diff --git a/commitquery/errors.go b/commitquery/errors.go deleted file mode 100644 index 0006c86b..00000000 --- a/commitquery/errors.go +++ /dev/null @@ -1,6 +0,0 @@ -package commitquery - -import "errors" - -// errBadGenerationOrder reports an invalid priority-queue ordering. -var errBadGenerationOrder = errors.New("commitquery: priority queue violated generation ordering") diff --git a/commitquery/mark_bits.go b/commitquery/mark_bits.go deleted file mode 100644 index b10c833b..00000000 --- a/commitquery/mark_bits.go +++ /dev/null @@ -1,17 +0,0 @@ -package commitquery - -// markBits stores one set of traversal marks on one node. -type markBits uint8 - -// markLeft, markRight, markStale, and markResult track traversal state. -const ( - markLeft markBits = 1 << iota - markRight - markStale - markResult -) - -// allMarks is the union of all defined mark bits. -const ( - allMarks = markLeft | markRight | markStale | markResult -) diff --git a/commitquery/node.go b/commitquery/node.go deleted file mode 100644 index 7432a719..00000000 --- a/commitquery/node.go +++ /dev/null @@ -1,25 +0,0 @@ -package commitquery - -import ( - commitgraphread "codeberg.org/lindenii/furgit/format/commitgraph/read" - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// node stores one mutable commit traversal node. -type node struct { - id objectid.ObjectID - - parents []nodeIndex - - commitTime int64 - generation uint64 - - hasGeneration bool - hasGraphPos bool - loaded bool - - graphPos commitgraphread.Position - marks markBits - - touchedPhase uint32 -} diff --git a/commitquery/node_commit_time.go b/commitquery/node_commit_time.go deleted file mode 100644 index 07c1f4e8..00000000 --- a/commitquery/node_commit_time.go +++ /dev/null @@ -1,6 +0,0 @@ -package commitquery - -// commitTime returns one node's commit time. -func (query *query) commitTime(idx nodeIndex) int64 { - return query.nodes[idx].commitTime -} diff --git a/commitquery/node_compare.go b/commitquery/node_compare.go deleted file mode 100644 index cf072af2..00000000 --- a/commitquery/node_compare.go +++ /dev/null @@ -1,25 +0,0 @@ -package commitquery - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// compare orders two internal nodes using merge-base queue ordering. -func (query *query) compare(left, right nodeIndex) int { - leftGeneration := query.effectiveGeneration(left) - rightGeneration := query.effectiveGeneration(right) - - switch { - case leftGeneration < rightGeneration: - return -1 - case leftGeneration > rightGeneration: - return 1 - } - - switch { - case query.nodes[left].commitTime < query.nodes[right].commitTime: - return -1 - case query.nodes[left].commitTime > query.nodes[right].commitTime: - return 1 - } - - return objectid.Compare(query.nodes[left].id, query.nodes[right].id) -} diff --git a/commitquery/node_generation.go b/commitquery/node_generation.go deleted file mode 100644 index 03283cf6..00000000 --- a/commitquery/node_generation.go +++ /dev/null @@ -1,45 +0,0 @@ -package commitquery - -import ( - "math" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// effectiveGeneration returns one node's generation value. -func (query *query) effectiveGeneration(idx nodeIndex) uint64 { - if !query.nodes[idx].hasGeneration { - return generationInfinity - } - - return query.nodes[idx].generation -} - -// generationInfinity sorts nodes without a known generation last. -const ( - generationInfinity = uint64(math.MaxUint64) -) - -// compareByGeneration builds one comparator ordered by generation first. -func (query *query) compareByGeneration() func(nodeIndex, nodeIndex) int { - return func(left, right nodeIndex) int { - leftGeneration := query.effectiveGeneration(left) - rightGeneration := query.effectiveGeneration(right) - - switch { - case leftGeneration < rightGeneration: - return -1 - case leftGeneration > rightGeneration: - return 1 - } - - switch { - case query.nodes[left].commitTime < query.nodes[right].commitTime: - return -1 - case query.nodes[left].commitTime > query.nodes[right].commitTime: - return 1 - } - - return objectid.Compare(query.nodes[left].id, query.nodes[right].id) - } -} diff --git a/commitquery/node_id.go b/commitquery/node_id.go deleted file mode 100644 index 8ec0b126..00000000 --- a/commitquery/node_id.go +++ /dev/null @@ -1,8 +0,0 @@ -package commitquery - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// id returns one node's object ID. -func (query *query) id(idx nodeIndex) objectid.ObjectID { - return query.nodes[idx].id -} diff --git a/commitquery/node_index.go b/commitquery/node_index.go deleted file mode 100644 index 06122d62..00000000 --- a/commitquery/node_index.go +++ /dev/null @@ -1,4 +0,0 @@ -package commitquery - -// nodeIndex identifies one internal query node. -type nodeIndex int diff --git a/commitquery/node_new.go b/commitquery/node_new.go deleted file mode 100644 index 14a35262..00000000 --- a/commitquery/node_new.go +++ /dev/null @@ -1,14 +0,0 @@ -package commitquery - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// newNode allocates one empty internal node. -func (query *query) newNode(id objectid.ObjectID) nodeIndex { - count := len(query.nodes) - - idx := nodeIndex(count) - - query.nodes = append(query.nodes, node{id: id}) - - return idx -} diff --git a/commitquery/node_parents.go b/commitquery/node_parents.go deleted file mode 100644 index a98a774f..00000000 --- a/commitquery/node_parents.go +++ /dev/null @@ -1,6 +0,0 @@ -package commitquery - -// parents returns resolved parent node indices for one internal node. -func (query *query) parents(idx nodeIndex) []nodeIndex { - return query.nodes[idx].parents -} diff --git a/commitquery/node_populate.go b/commitquery/node_populate.go deleted file mode 100644 index 26fb5629..00000000 --- a/commitquery/node_populate.go +++ /dev/null @@ -1,42 +0,0 @@ -package commitquery - -import "fmt" - -// populateNode fills one node's metadata and resolves its parents. -func (query *query) populateNode(idx nodeIndex, commit commitData) error { - if query.nodes[idx].loaded { - if query.nodes[idx].id != commit.ID { - return fmt.Errorf("commitquery: node identity mismatch: have %s, got %s", query.nodes[idx].id, commit.ID) - } - - return nil - } - - query.nodes[idx].id = commit.ID - query.nodes[idx].commitTime = commit.CommitTime - query.nodes[idx].generation = commit.Generation - query.nodes[idx].hasGeneration = commit.HasGeneration - - if commit.HasGraphPos { - query.nodes[idx].graphPos = commit.GraphPos - query.nodes[idx].hasGraphPos = true - query.byGraphPos[commit.GraphPos] = idx - } - - query.nodes[idx].loaded = true - query.nodes[idx].parents = query.nodes[idx].parents[:0] - - for _, parent := range commit.Parents { - parentIdx, err := query.resolveParent(parent) - if err != nil { - query.nodes[idx].loaded = false - query.nodes[idx].parents = nil - - return err - } - - query.nodes[idx].parents = append(query.nodes[idx].parents, parentIdx) - } - - return nil -} diff --git a/commitquery/parent_ref.go b/commitquery/parent_ref.go deleted file mode 100644 index 08d224df..00000000 --- a/commitquery/parent_ref.go +++ /dev/null @@ -1,13 +0,0 @@ -package commitquery - -import ( - commitgraphread "codeberg.org/lindenii/furgit/format/commitgraph/read" - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// parentRef references one commit parent. -type parentRef struct { - ID objectid.ObjectID - GraphPos commitgraphread.Position - HasGraphPos bool -} diff --git a/commitquery/queries.go b/commitquery/queries.go deleted file mode 100644 index 33709783..00000000 --- a/commitquery/queries.go +++ /dev/null @@ -1,26 +0,0 @@ -package commitquery - -import ( - "sync" - - commitgraphread "codeberg.org/lindenii/furgit/format/commitgraph/read" - objectfetch "codeberg.org/lindenii/furgit/object/fetch" -) - -// Queries provides commit-domain queries over one object fetcher -// and optional commit-graph reader. -// -// Queries reuses internal mutable query workers across operations. -// -// Labels: MT-Safe. -type Queries struct { - fetcher *objectfetch.Fetcher - graph *commitgraphread.Reader - - mu sync.Mutex - idle []*query - maxIdle int -} - -// TODO: Research a shared arena, or perhaps worker-reconciliation -// schemes if a complete shared arena proves to be too contentious. diff --git a/commitquery/queries_acquire.go b/commitquery/queries_acquire.go deleted file mode 100644 index a3aa0e58..00000000 --- a/commitquery/queries_acquire.go +++ /dev/null @@ -1,17 +0,0 @@ -package commitquery - -// acquire removes one worker from the idle pool or allocates one new worker. -func (queries *Queries) acquire() *query { - queries.mu.Lock() - defer queries.mu.Unlock() - - count := len(queries.idle) - if count == 0 { - return newQuery(queries.fetcher, queries.graph) - } - - q := queries.idle[count-1] - queries.idle = queries.idle[:count-1] - - return q -} diff --git a/commitquery/queries_is_ancestor.go b/commitquery/queries_is_ancestor.go deleted file mode 100644 index e2c955c6..00000000 --- a/commitquery/queries_is_ancestor.go +++ /dev/null @@ -1,14 +0,0 @@ -package commitquery - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// IsAncestor reports whether ancestor is reachable from descendant through -// commit parent edges. -// -// Both inputs are peeled through annotated tags before commit traversal. -func (queries *Queries) IsAncestor(ancestor, descendant objectid.ObjectID) (bool, error) { - query := queries.acquire() - defer queries.release(query) - - return query.IsAncestor(ancestor, descendant) -} diff --git a/commitquery/queries_is_ancestor_integration_test.go b/commitquery/queries_is_ancestor_integration_test.go deleted file mode 100644 index 7e8886a9..00000000 --- a/commitquery/queries_is_ancestor_integration_test.go +++ /dev/null @@ -1,133 +0,0 @@ -package commitquery_test - -import ( - "errors" - "testing" - - "codeberg.org/lindenii/furgit/commitquery" - giterrors "codeberg.org/lindenii/furgit/errors" - "codeberg.org/lindenii/furgit/internal/testgit" - "codeberg.org/lindenii/furgit/object/fetch" - objectid "codeberg.org/lindenii/furgit/object/id" -) - -func TestIsMatchesGitMergeBase(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ - ObjectFormat: algo, - Bare: true, - RefFormat: "files", - }) - - _, tree1 := testRepo.MakeSingleFileTree(t, "one.txt", []byte("one\n")) - c1 := testRepo.CommitTree(t, tree1, "c1") - - _, tree2 := testRepo.MakeSingleFileTree(t, "two.txt", []byte("two\n")) - c2 := testRepo.CommitTree(t, tree2, "c2", c1) - - _, tree3 := testRepo.MakeSingleFileTree(t, "three.txt", []byte("three\n")) - c3 := testRepo.CommitTree(t, tree3, "c3", c2) - - tag := testRepo.TagAnnotated(t, "tip", c2, "tip") - - store := testRepo.OpenObjectStore(t) - - got, err := commitquery.New(fetch.New(store), nil).IsAncestor(c1, tag) - if err != nil { - t.Fatalf("Is(c1, tag): %v", err) - } - - want := gitMergeBaseIsAncestor(t, testRepo, c1, c2) - if got != want { - t.Fatalf("Is(c1, tag)=%v, want %v", got, want) - } - - got, err = commitquery.New(fetch.New(store), nil).IsAncestor(c3, c2) - if err != nil { - t.Fatalf("Is(c3, c2): %v", err) - } - - want = gitMergeBaseIsAncestor(t, testRepo, c3, c2) - if got != want { - t.Fatalf("Is(c3, c2)=%v, want %v", got, want) - } - }) -} - -func TestIsMatchesGitMergeBaseWithCommitGraph(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ - ObjectFormat: algo, - Bare: true, - RefFormat: "files", - }) - - _, tree1 := testRepo.MakeSingleFileTree(t, "one.txt", []byte("one\n")) - c1 := testRepo.CommitTree(t, tree1, "c1") - - _, tree2 := testRepo.MakeSingleFileTree(t, "two.txt", []byte("two\n")) - c2 := testRepo.CommitTree(t, tree2, "c2", c1) - - testRepo.UpdateRef(t, "refs/heads/main", c2) - testRepo.SymbolicRef(t, "HEAD", "refs/heads/main") - testRepo.CommitGraphWrite(t, "--reachable") - - store := testRepo.OpenObjectStore(t) - graph := testRepo.OpenCommitGraph(t) - - got, err := commitquery.New(fetch.New(store), graph).IsAncestor(c1, c2) - if err != nil { - t.Fatalf("Is(c1, c2): %v", err) - } - - want := gitMergeBaseIsAncestor(t, testRepo, c1, c2) - if got != want { - t.Fatalf("Is(c1, c2)=%v, want %v", got, want) - } - }) -} - -func TestIsMissingObject(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ - ObjectFormat: algo, - Bare: true, - RefFormat: "files", - }) - - _, treeID, commitID := testRepo.MakeCommit(t, "missing") - - testRepo.RemoveLooseObject(t, treeID) - - store := testRepo.OpenObjectStore(t) - - _, err := commitquery.New(fetch.New(store), nil).IsAncestor(treeID, commitID) - if err == nil { - t.Fatal("expected error") - } - - missing, ok := errors.AsType[*giterrors.ObjectMissingError](err) - if !ok { - t.Fatalf("expected ObjectMissingError, got %T (%v)", err, err) - } - - if missing.OID != treeID { - t.Fatalf("missing oid = %s, want %s", missing.OID, treeID) - } - }) -} - -// gitMergeBaseIsAncestor reports Git's merge-base ancestry answer. -func gitMergeBaseIsAncestor(t *testing.T, testRepo *testgit.TestRepo, left, right objectid.ObjectID) bool { - t.Helper() - - out := testRepo.Run(t, "merge-base", left.String(), right.String()) - - return out == left.String() -} diff --git a/commitquery/queries_is_ancestor_unit_test.go b/commitquery/queries_is_ancestor_unit_test.go deleted file mode 100644 index 002c49ae..00000000 --- a/commitquery/queries_is_ancestor_unit_test.go +++ /dev/null @@ -1,166 +0,0 @@ -package commitquery_test - -import ( - "errors" - "fmt" - "testing" - - giterrors "codeberg.org/lindenii/furgit/errors" - "codeberg.org/lindenii/furgit/internal/testgit" - "codeberg.org/lindenii/furgit/object/fetch" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/store/memory" - objecttree "codeberg.org/lindenii/furgit/object/tree" - objecttype "codeberg.org/lindenii/furgit/object/type" - - "codeberg.org/lindenii/furgit/commitquery" -) - -// ancestorCommitBody serializes one minimal commit body. -func ancestorCommitBody(tree objectid.ObjectID, parents ...objectid.ObjectID) []byte { - buf := fmt.Appendf(nil, "tree %s\n", tree.String()) - for _, parent := range parents { - buf = append(buf, fmt.Appendf(nil, "parent %s\n", parent.String())...) - } - - buf = append(buf, []byte("\nmsg\n")...) - - return buf -} - -// ancestorTagBody serializes one minimal annotated tag body. -func ancestorTagBody(target objectid.ObjectID, targetType objecttype.Type) []byte { - targetName, ok := targetType.Name() - if !ok { - panic("invalid tag target type") - } - - return fmt.Appendf(nil, "object %s\ntype %s\ntag t\n\nmsg\n", target.String(), targetName) -} - -// mustSerializeAncestorTree serializes one tree or fails the test. -func mustSerializeAncestorTree(tb testing.TB, tree *objecttree.Tree) []byte { - tb.Helper() - - body, err := tree.SerializeWithoutHeader() - if err != nil { - tb.Fatalf("SerializeWithoutHeader: %v", err) - } - - return body -} - -func TestIs(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - store := memory.New(algo) - - blob, err := store.WriteBytesContent(objecttype.TypeBlob, []byte("blob\n")) - if err != nil { - t.Fatal(err) - } - - tree, err := store.WriteBytesContent(objecttype.TypeTree, mustSerializeAncestorTree(t, &objecttree.Tree{Entries: []objecttree.TreeEntry{{ - Mode: objecttree.FileModeRegular, - Name: []byte("f"), - ID: blob, - }}})) - if err != nil { - t.Fatal(err) - } - - c1, err := store.WriteBytesContent(objecttype.TypeCommit, ancestorCommitBody(tree)) - if err != nil { - t.Fatal(err) - } - - c2, err := store.WriteBytesContent(objecttype.TypeCommit, ancestorCommitBody(tree, c1)) - if err != nil { - t.Fatal(err) - } - - otherBlob, err := store.WriteBytesContent(objecttype.TypeBlob, []byte("other-blob\n")) - if err != nil { - t.Fatal(err) - } - - otherTree, err := store.WriteBytesContent(objecttype.TypeTree, mustSerializeAncestorTree(t, &objecttree.Tree{Entries: []objecttree.TreeEntry{{ - Mode: objecttree.FileModeRegular, - Name: []byte("g"), - ID: otherBlob, - }}})) - if err != nil { - t.Fatal(err) - } - - c3, err := store.WriteBytesContent(objecttype.TypeCommit, ancestorCommitBody(otherTree)) - if err != nil { - t.Fatal(err) - } - - tag, err := store.WriteBytesContent(objecttype.TypeTag, ancestorTagBody(c2, objecttype.TypeCommit)) - if err != nil { - t.Fatal(err) - } - - ok, err := commitquery.New(fetch.New(store), nil).IsAncestor(c1, tag) - if err != nil { - t.Fatalf("Is(c1, tag): %v", err) - } - - if !ok { - t.Fatal("expected c1 to be ancestor of tag->c2") - } - - ok, err = commitquery.New(fetch.New(store), nil).IsAncestor(c3, c2) - if err != nil { - t.Fatalf("Is(c3, c2): %v", err) - } - - if ok { - t.Fatal("did not expect c3 to be ancestor of c2") - } - }) -} - -func TestIsRejectsNonCommitAfterPeel(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - store := memory.New(algo) - - blob, err := store.WriteBytesContent(objecttype.TypeBlob, []byte("blob\n")) - if err != nil { - t.Fatal(err) - } - - tree, err := store.WriteBytesContent(objecttype.TypeTree, mustSerializeAncestorTree(t, &objecttree.Tree{Entries: []objecttree.TreeEntry{{ - Mode: objecttree.FileModeRegular, - Name: []byte("f"), - ID: blob, - }}})) - if err != nil { - t.Fatal(err) - } - - commit, err := store.WriteBytesContent(objecttype.TypeCommit, ancestorCommitBody(tree)) - if err != nil { - t.Fatal(err) - } - - tagToTree, err := store.WriteBytesContent(objecttype.TypeTag, ancestorTagBody(tree, objecttype.TypeTree)) - if err != nil { - t.Fatal(err) - } - - _, err = commitquery.New(fetch.New(store), nil).IsAncestor(commit, tagToTree) - if err == nil { - t.Fatal("expected error") - } - - if _, ok := errors.AsType[*giterrors.ObjectTypeError](err); !ok { - t.Fatalf("expected ObjectTypeError, got %T (%v)", err, err) - } - }) -} diff --git a/commitquery/queries_merge_base.go b/commitquery/queries_merge_base.go deleted file mode 100644 index 28de7fe2..00000000 --- a/commitquery/queries_merge_base.go +++ /dev/null @@ -1,11 +0,0 @@ -package commitquery - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// MergeBase reports one merge base between left and right, if any. -func (queries *Queries) MergeBase(left, right objectid.ObjectID) (objectid.ObjectID, bool, error) { - query := queries.acquire() - defer queries.release(query) - - return query.MergeBase(left, right) -} diff --git a/commitquery/queries_merge_bases.go b/commitquery/queries_merge_bases.go deleted file mode 100644 index 74c5054a..00000000 --- a/commitquery/queries_merge_bases.go +++ /dev/null @@ -1,13 +0,0 @@ -package commitquery - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// MergeBases reports all merge bases in Git's merge-base --all order. -// -// Both inputs are peeled through annotated tags before commit traversal. -func (queries *Queries) MergeBases(left, right objectid.ObjectID) ([]objectid.ObjectID, error) { - query := queries.acquire() - defer queries.release(query) - - return query.MergeBases(left, right) -} diff --git a/commitquery/queries_merge_bases_integration_test.go b/commitquery/queries_merge_bases_integration_test.go deleted file mode 100644 index 4fdfdf16..00000000 --- a/commitquery/queries_merge_bases_integration_test.go +++ /dev/null @@ -1,312 +0,0 @@ -package commitquery_test - -import ( - "maps" - "slices" - "strings" - "testing" - - "codeberg.org/lindenii/furgit/commitquery" - "codeberg.org/lindenii/furgit/internal/testgit" - "codeberg.org/lindenii/furgit/object/fetch" - objectid "codeberg.org/lindenii/furgit/object/id" -) - -func TestQueryMatchesGitMergeBaseAll(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ - ObjectFormat: algo, - Bare: true, - RefFormat: "files", - }) - - _, tree1 := testRepo.MakeSingleFileTree(t, "base.txt", []byte("base\n")) - base := testRepo.CommitTree(t, tree1, "base") - - _, tree2 := testRepo.MakeSingleFileTree(t, "left.txt", []byte("left\n")) - left := testRepo.CommitTree(t, tree2, "left", base) - - _, tree3 := testRepo.MakeSingleFileTree(t, "right.txt", []byte("right\n")) - right := testRepo.CommitTree(t, tree3, "right", base) - - tag := testRepo.TagAnnotated(t, "right-tag", right, "right-tag") - - store := testRepo.OpenObjectStore(t) - - query := commitquery.New(fetch.New(store), nil) - - all, err := query.MergeBases(left, tag) - if err != nil { - t.Fatalf("query.All(): %v", err) - } - - got := oidSetFromSlice(all) - - want := gitMergeBaseAllSet(t, testRepo, left, tag) - if !maps.Equal(got, want) { - t.Fatalf("Query(left, tag) mismatch:\n got=%v\nwant=%v", sortedOIDStrings(got), sortedOIDStrings(want)) - } - }) -} - -func TestQueryCrissCrossMatchesGitMergeBaseAll(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ - ObjectFormat: algo, - Bare: true, - RefFormat: "files", - }) - - _, tree1 := testRepo.MakeSingleFileTree(t, "root.txt", []byte("root\n")) - root := testRepo.CommitTree(t, tree1, "root") - - _, tree2 := testRepo.MakeSingleFileTree(t, "base1.txt", []byte("base1\n")) - base1 := testRepo.CommitTree(t, tree2, "base1", root) - - _, tree3 := testRepo.MakeSingleFileTree(t, "base2.txt", []byte("base2\n")) - base2 := testRepo.CommitTree(t, tree3, "base2", root) - - _, tree4 := testRepo.MakeSingleFileTree(t, "left.txt", []byte("left\n")) - left := testRepo.CommitTree(t, tree4, "left", base1, base2) - - _, tree5 := testRepo.MakeSingleFileTree(t, "right.txt", []byte("right\n")) - right := testRepo.CommitTree(t, tree5, "right", base2, base1) - - store := testRepo.OpenObjectStore(t) - - query := commitquery.New(fetch.New(store), nil) - - all, err := query.MergeBases(left, right) - if err != nil { - t.Fatalf("query.All(): %v", err) - } - - got := oidSetFromSlice(all) - - want := gitMergeBaseAllSet(t, testRepo, left, right) - if !maps.Equal(got, want) { - t.Fatalf("Query(left, right) mismatch:\n got=%v\nwant=%v", sortedOIDStrings(got), sortedOIDStrings(want)) - } - - first, ok, err := query.MergeBase(left, right) - if err != nil { - t.Fatalf("Base(left, right): %v", err) - } - - if !ok { - t.Fatal("Base(left, right) unexpectedly reported no base") - } - - if !containsID(want, first) { - t.Fatalf("Base(left, right)=%s, want one of %v", first, slices.Collect(maps.Keys(want))) - } - }) -} - -func TestQueryMatchesGitMergeBaseAllWithCommitGraph(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ - ObjectFormat: algo, - Bare: true, - RefFormat: "files", - }) - - _, tree1 := testRepo.MakeSingleFileTree(t, "root.txt", []byte("root\n")) - root := testRepo.CommitTree(t, tree1, "root") - - _, tree2 := testRepo.MakeSingleFileTree(t, "base1.txt", []byte("base1\n")) - base1 := testRepo.CommitTree(t, tree2, "base1", root) - - _, tree3 := testRepo.MakeSingleFileTree(t, "base2.txt", []byte("base2\n")) - base2 := testRepo.CommitTree(t, tree3, "base2", root) - - _, tree4 := testRepo.MakeSingleFileTree(t, "left.txt", []byte("left\n")) - left := testRepo.CommitTree(t, tree4, "left", base1, base2) - - _, tree5 := testRepo.MakeSingleFileTree(t, "right.txt", []byte("right\n")) - right := testRepo.CommitTree(t, tree5, "right", base2, base1) - - testRepo.UpdateRef(t, "refs/heads/main", right) - testRepo.SymbolicRef(t, "HEAD", "refs/heads/main") - testRepo.CommitGraphWrite(t, "--reachable") - - store := testRepo.OpenObjectStore(t) - graph := testRepo.OpenCommitGraph(t) - - query := commitquery.New(fetch.New(store), graph) - - all, err := query.MergeBases(left, right) - if err != nil { - t.Fatalf("query.All(): %v", err) - } - - got := oidSetFromSlice(all) - - want := gitMergeBaseAllSet(t, testRepo, left, right) - if !maps.Equal(got, want) { - t.Fatalf("Query(left, right) with commit-graph mismatch:\n got=%v\nwant=%v", sortedOIDStrings(got), sortedOIDStrings(want)) - } - - first, ok, err := query.MergeBase(left, right) - if err != nil { - t.Fatalf("Base(left, right): %v", err) - } - - if !ok { - t.Fatal("Base(left, right) unexpectedly reported no base") - } - - if !containsID(want, first) { - t.Fatalf("Base(left, right)=%s, want one of %v", first, slices.Collect(maps.Keys(want))) - } - }) -} - -func TestBaseMatchesGitMergeBaseWithoutAll(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ - ObjectFormat: algo, - Bare: true, - RefFormat: "files", - }) - - _, tree1 := testRepo.MakeSingleFileTree(t, "root.txt", []byte("root\n")) - root := testRepo.CommitTree(t, tree1, "root") - - _, tree2 := testRepo.MakeSingleFileTree(t, "base1.txt", []byte("base1\n")) - base1 := testRepo.CommitTreeWithEnv(t, []string{ - "GIT_AUTHOR_DATE=1234567890 +0000", - "GIT_COMMITTER_DATE=1234567890 +0000", - }, tree2, "base1", root) - - _, tree3 := testRepo.MakeSingleFileTree(t, "base2.txt", []byte("base2\n")) - base2 := testRepo.CommitTreeWithEnv(t, []string{ - "GIT_AUTHOR_DATE=1234567990 +0000", - "GIT_COMMITTER_DATE=1234567990 +0000", - }, tree3, "base2", root) - - _, tree4 := testRepo.MakeSingleFileTree(t, "left.txt", []byte("left\n")) - left := testRepo.CommitTree(t, tree4, "left", base1, base2) - - _, tree5 := testRepo.MakeSingleFileTree(t, "right.txt", []byte("right\n")) - right := testRepo.CommitTree(t, tree5, "right", base2, base1) - - store := testRepo.OpenObjectStore(t) - - query := commitquery.New(fetch.New(store), nil) - - got, ok, err := query.MergeBase(left, right) - if err != nil { - t.Fatalf("Base(left, right): %v", err) - } - - if !ok { - t.Fatal("Base(left, right) unexpectedly reported no base") - } - - want := gitMergeBaseOne(t, testRepo, left, right) - if got != want { - t.Fatalf("Base(left, right)=%s, want %s", got, want) - } - - testRepo.UpdateRef(t, "refs/heads/main", right) - testRepo.SymbolicRef(t, "HEAD", "refs/heads/main") - testRepo.CommitGraphWrite(t, "--reachable") - - graph := testRepo.OpenCommitGraph(t) - - got, ok, err = commitquery.New(fetch.New(store), graph).MergeBase(left, right) - if err != nil { - t.Fatalf("Base(left, right) with commit-graph: %v", err) - } - - if !ok { - t.Fatal("Base(left, right) with commit-graph unexpectedly reported no base") - } - - if got != want { - t.Fatalf("Base(left, right) with commit-graph=%s, want %s", got, want) - } - }) -} - -// oidSetFromSlice collects one object ID slice into a set. -func oidSetFromSlice(ids []objectid.ObjectID) map[objectid.ObjectID]struct{} { - out := make(map[objectid.ObjectID]struct{}) - - for _, id := range ids { - out[id] = struct{}{} - } - - return out -} - -// gitMergeBaseAllSet returns Git's merge-base --all output as a set. -func gitMergeBaseAllSet( - t *testing.T, - testRepo *testgit.TestRepo, - left objectid.ObjectID, - right objectid.ObjectID, -) map[objectid.ObjectID]struct{} { - t.Helper() - - out := testRepo.Run(t, "merge-base", "--all", left.String(), right.String()) - set := make(map[objectid.ObjectID]struct{}) - - for line := range strings.SplitSeq(strings.TrimSpace(out), "\n") { - line = strings.TrimSpace(line) - if line == "" { - continue - } - - id, err := objectid.ParseHex(testRepo.Algorithm(), line) - if err != nil { - t.Fatalf("parse merge-base oid %q: %v", line, err) - } - - set[id] = struct{}{} - } - - return set -} - -// gitMergeBaseOne returns Git's merge-base output without --all. -func gitMergeBaseOne( - t *testing.T, - testRepo *testgit.TestRepo, - left objectid.ObjectID, - right objectid.ObjectID, -) objectid.ObjectID { - t.Helper() - - out := strings.TrimSpace(testRepo.Run(t, "merge-base", left.String(), right.String())) - if out == "" { - t.Fatal("git merge-base returned no output") - } - - id, err := objectid.ParseHex(testRepo.Algorithm(), out) - if err != nil { - t.Fatalf("parse merge-base oid %q: %v", out, err) - } - - return id -} - -func sortedOIDStrings(set map[objectid.ObjectID]struct{}) []string { - out := make([]string, 0, len(set)) - for id := range set { - out = append(out, id.String()) - } - - slices.Sort(out) - - return out -} diff --git a/commitquery/queries_merge_bases_unit_test.go b/commitquery/queries_merge_bases_unit_test.go deleted file mode 100644 index 3e302536..00000000 --- a/commitquery/queries_merge_bases_unit_test.go +++ /dev/null @@ -1,485 +0,0 @@ -package commitquery_test - -import ( - "errors" - "fmt" - "maps" - "slices" - "testing" - - "codeberg.org/lindenii/furgit/commitquery" - giterrors "codeberg.org/lindenii/furgit/errors" - "codeberg.org/lindenii/furgit/internal/testgit" - "codeberg.org/lindenii/furgit/object/fetch" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/store/memory" - "codeberg.org/lindenii/furgit/object/tree" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// commitBody serializes one minimal commit body. -func commitBody(tree objectid.ObjectID, parents ...objectid.ObjectID) []byte { - buf := fmt.Appendf(nil, "tree %s\n", tree.String()) - for _, parent := range parents { - buf = append(buf, fmt.Appendf(nil, "parent %s\n", parent.String())...) - } - - buf = append(buf, []byte("\nmsg\n")...) - - return buf -} - -// tagBody serializes one minimal annotated tag body. -func tagBody(target objectid.ObjectID, targetType objecttype.Type) []byte { - targetName, ok := targetType.Name() - if !ok { - panic("invalid tag target type") - } - - return fmt.Appendf(nil, "object %s\ntype %s\ntag t\n\nmsg\n", target.String(), targetName) -} - -// toSet converts one slice of object IDs into a set. -func toSet(ids []objectid.ObjectID) map[objectid.ObjectID]struct{} { - set := make(map[objectid.ObjectID]struct{}, len(ids)) - for _, id := range ids { - set[id] = struct{}{} - } - - return set -} - -// containsID reports whether one set contains one object ID. -func containsID(set map[objectid.ObjectID]struct{}, id objectid.ObjectID) bool { - _, ok := set[id] - - return ok -} - -// mustSerializeTree serializes one tree or fails the test. -func mustSerializeTree(tb testing.TB, tree *tree.Tree) []byte { - tb.Helper() - - body, err := tree.SerializeWithoutHeader() - if err != nil { - tb.Fatalf("SerializeWithoutHeader: %v", err) - } - - return body -} - -// TestQueryLinearHistory reports one linear-history merge base. -func TestQueryLinearHistory(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - store := memory.New(algo) - - blob, err := store.WriteBytesContent(objecttype.TypeBlob, []byte("blob\n")) - if err != nil { - t.Fatal(err) - } - - tree, err := store.WriteBytesContent(objecttype.TypeTree, mustSerializeTree(t, &tree.Tree{Entries: []tree.TreeEntry{{ - Mode: tree.FileModeRegular, - Name: []byte("f"), - ID: blob, - }}})) - if err != nil { - t.Fatal(err) - } - - base, err := store.WriteBytesContent(objecttype.TypeCommit, commitBody(tree)) - if err != nil { - t.Fatal(err) - } - - left, err := store.WriteBytesContent(objecttype.TypeCommit, commitBody(tree, base)) - if err != nil { - t.Fatal(err) - } - - right, err := store.WriteBytesContent(objecttype.TypeCommit, commitBody(tree, left)) - if err != nil { - t.Fatal(err) - } - - query := commitquery.New(fetch.New(store), nil) - - got, err := query.MergeBases(left, right) - if err != nil { - t.Fatalf("query.All(): %v", err) - } - - if !slices.Equal(got, []objectid.ObjectID{left}) { - t.Fatalf("Query(left, right)=%v, want [%s]", got, left) - } - - first, ok, err := query.MergeBase(left, right) - if err != nil { - t.Fatalf("Base(left, right): %v", err) - } - - if !ok { - t.Fatal("Base(left, right) unexpectedly reported no base") - } - - if first != left { - t.Fatalf("Base(left, right)=%s, want %s", first, left) - } - }) -} - -func TestQueryPeelsAnnotatedTags(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - store := memory.New(algo) - - blob, err := store.WriteBytesContent(objecttype.TypeBlob, []byte("blob\n")) - if err != nil { - t.Fatal(err) - } - - leftTree, err := store.WriteBytesContent(objecttype.TypeTree, mustSerializeTree(t, &tree.Tree{Entries: []tree.TreeEntry{{ - Mode: tree.FileModeRegular, - Name: []byte("left"), - ID: blob, - }}})) - if err != nil { - t.Fatal(err) - } - - rightTree, err := store.WriteBytesContent(objecttype.TypeTree, mustSerializeTree(t, &tree.Tree{Entries: []tree.TreeEntry{{ - Mode: tree.FileModeRegular, - Name: []byte("right"), - ID: blob, - }}})) - if err != nil { - t.Fatal(err) - } - - base, err := store.WriteBytesContent(objecttype.TypeCommit, commitBody(leftTree)) - if err != nil { - t.Fatal(err) - } - - left, err := store.WriteBytesContent(objecttype.TypeCommit, commitBody(leftTree, base)) - if err != nil { - t.Fatal(err) - } - - right, err := store.WriteBytesContent(objecttype.TypeCommit, commitBody(rightTree, base)) - if err != nil { - t.Fatal(err) - } - - tag, err := store.WriteBytesContent(objecttype.TypeTag, tagBody(right, objecttype.TypeCommit)) - if err != nil { - t.Fatal(err) - } - - query := commitquery.New(fetch.New(store), nil) - - got, err := query.MergeBases(left, tag) - if err != nil { - t.Fatalf("query.All(): %v", err) - } - - if !slices.Equal(got, []objectid.ObjectID{base}) { - t.Fatalf("Query(left, tag)=%v, want [%s]", got, base) - } - }) -} - -func TestQueryCrissCrossReturnsAllBestCommonAncestors(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - store := memory.New(algo) - - blob, err := store.WriteBytesContent(objecttype.TypeBlob, []byte("blob\n")) - if err != nil { - t.Fatal(err) - } - - rootTree, err := store.WriteBytesContent(objecttype.TypeTree, mustSerializeTree(t, &tree.Tree{Entries: []tree.TreeEntry{{ - Mode: tree.FileModeRegular, - Name: []byte("root"), - ID: blob, - }}})) - if err != nil { - t.Fatal(err) - } - - base1Tree, err := store.WriteBytesContent(objecttype.TypeTree, mustSerializeTree(t, &tree.Tree{Entries: []tree.TreeEntry{{ - Mode: tree.FileModeRegular, - Name: []byte("base1"), - ID: blob, - }}})) - if err != nil { - t.Fatal(err) - } - - base2Tree, err := store.WriteBytesContent(objecttype.TypeTree, mustSerializeTree(t, &tree.Tree{Entries: []tree.TreeEntry{{ - Mode: tree.FileModeRegular, - Name: []byte("base2"), - ID: blob, - }}})) - if err != nil { - t.Fatal(err) - } - - leftTree, err := store.WriteBytesContent(objecttype.TypeTree, mustSerializeTree(t, &tree.Tree{Entries: []tree.TreeEntry{{ - Mode: tree.FileModeRegular, - Name: []byte("left"), - ID: blob, - }}})) - if err != nil { - t.Fatal(err) - } - - rightTree, err := store.WriteBytesContent(objecttype.TypeTree, mustSerializeTree(t, &tree.Tree{Entries: []tree.TreeEntry{{ - Mode: tree.FileModeRegular, - Name: []byte("right"), - ID: blob, - }}})) - if err != nil { - t.Fatal(err) - } - - root, err := store.WriteBytesContent(objecttype.TypeCommit, commitBody(rootTree)) - if err != nil { - t.Fatal(err) - } - - base1, err := store.WriteBytesContent(objecttype.TypeCommit, commitBody(base1Tree, root)) - if err != nil { - t.Fatal(err) - } - - base2, err := store.WriteBytesContent(objecttype.TypeCommit, commitBody(base2Tree, root)) - if err != nil { - t.Fatal(err) - } - - left, err := store.WriteBytesContent(objecttype.TypeCommit, commitBody(leftTree, base1, base2)) - if err != nil { - t.Fatal(err) - } - - right, err := store.WriteBytesContent(objecttype.TypeCommit, commitBody(rightTree, base2, base1)) - if err != nil { - t.Fatal(err) - } - - query := commitquery.New(fetch.New(store), nil) - - all, err := query.MergeBases(left, right) - if err != nil { - t.Fatalf("query.All(): %v", err) - } - - got := toSet(all) - - want := map[objectid.ObjectID]struct{}{base1: {}, base2: {}} - if !maps.Equal(got, want) { - t.Fatalf("Query(left, right)=%v, want %v", slices.Collect(maps.Keys(got)), slices.Collect(maps.Keys(want))) - } - - first, ok, err := query.MergeBase(left, right) - if err != nil { - t.Fatalf("Base(left, right): %v", err) - } - - if !ok { - t.Fatal("Base(left, right) unexpectedly reported no base") - } - - if !containsID(want, first) { - t.Fatalf("Base(left, right)=%s, want one of %v", first, slices.Collect(maps.Keys(want))) - } - }) -} - -func TestQueryReturnsNoResultWhenNoCommonAncestorExists(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - store := memory.New(algo) - - leftBlob, err := store.WriteBytesContent(objecttype.TypeBlob, []byte("left\n")) - if err != nil { - t.Fatal(err) - } - - leftTree, err := store.WriteBytesContent(objecttype.TypeTree, mustSerializeTree(t, &tree.Tree{Entries: []tree.TreeEntry{{ - Mode: tree.FileModeRegular, - Name: []byte("left"), - ID: leftBlob, - }}})) - if err != nil { - t.Fatal(err) - } - - rightBlob, err := store.WriteBytesContent(objecttype.TypeBlob, []byte("right\n")) - if err != nil { - t.Fatal(err) - } - - rightTree, err := store.WriteBytesContent(objecttype.TypeTree, mustSerializeTree(t, &tree.Tree{Entries: []tree.TreeEntry{{ - Mode: tree.FileModeRegular, - Name: []byte("right"), - ID: rightBlob, - }}})) - if err != nil { - t.Fatal(err) - } - - left, err := store.WriteBytesContent(objecttype.TypeCommit, commitBody(leftTree)) - if err != nil { - t.Fatal(err) - } - - right, err := store.WriteBytesContent(objecttype.TypeCommit, commitBody(rightTree)) - if err != nil { - t.Fatal(err) - } - - query := commitquery.New(fetch.New(store), nil) - - got, err := query.MergeBases(left, right) - if err != nil { - t.Fatalf("query.All(): %v", err) - } - - if len(got) != 0 { - t.Fatalf("Query(left, right)=%v, want no results", got) - } - - _, ok, err := query.MergeBase(left, right) - if err != nil { - t.Fatalf("Base(left, right): %v", err) - } - - if ok { - t.Fatal("Base(left, right) unexpectedly reported a base") - } - }) -} - -func TestQueryRejectsNonCommitAfterPeel(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - store := memory.New(algo) - - blob, err := store.WriteBytesContent(objecttype.TypeBlob, []byte("blob\n")) - if err != nil { - t.Fatal(err) - } - - tree, err := store.WriteBytesContent(objecttype.TypeTree, mustSerializeTree(t, &tree.Tree{Entries: []tree.TreeEntry{{ - Mode: tree.FileModeRegular, - Name: []byte("f"), - ID: blob, - }}})) - if err != nil { - t.Fatal(err) - } - - commit, err := store.WriteBytesContent(objecttype.TypeCommit, commitBody(tree)) - if err != nil { - t.Fatal(err) - } - - tagToTree, err := store.WriteBytesContent(objecttype.TypeTag, tagBody(tree, objecttype.TypeTree)) - if err != nil { - t.Fatal(err) - } - - query := commitquery.New(fetch.New(store), nil) - - _, err = query.MergeBases(commit, tagToTree) - if err == nil { - t.Fatal("expected error") - } - - typeErr, ok := errors.AsType[*giterrors.ObjectTypeError](err) - if !ok { - t.Fatalf("expected ObjectTypeError, got %T (%v)", err, err) - } - - if typeErr.Got != objecttype.TypeTree || typeErr.Want != objecttype.TypeCommit { - t.Fatalf("unexpected type error: %+v", typeErr) - } - }) -} - -func TestQueryAllIsRepeatable(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - store := memory.New(algo) - - blob, err := store.WriteBytesContent(objecttype.TypeBlob, []byte("blob\n")) - if err != nil { - t.Fatal(err) - } - - tree, err := store.WriteBytesContent(objecttype.TypeTree, mustSerializeTree(t, &tree.Tree{Entries: []tree.TreeEntry{{ - Mode: tree.FileModeRegular, - Name: []byte("f"), - ID: blob, - }}})) - if err != nil { - t.Fatal(err) - } - - base, err := store.WriteBytesContent(objecttype.TypeCommit, commitBody(tree)) - if err != nil { - t.Fatal(err) - } - - left, err := store.WriteBytesContent(objecttype.TypeCommit, commitBody(tree, base)) - if err != nil { - t.Fatal(err) - } - - right, err := store.WriteBytesContent(objecttype.TypeCommit, commitBody(tree, left)) - if err != nil { - t.Fatal(err) - } - - query := commitquery.New(fetch.New(store), nil) - - first, err := query.MergeBases(left, right) - if err != nil { - t.Fatalf("query.MergeBases() first call: %v", err) - } - - again, err := query.MergeBases(left, right) - if err != nil { - t.Fatalf("query.MergeBases() second call: %v", err) - } - - if !slices.Equal(again, first) { - t.Fatalf("second All()=%v, want %v", again, first) - } - - if len(first) == 0 { - t.Fatal("first MergeBases() unexpectedly returned no results") - } - - first[0] = objectid.ObjectID{} - - third, err := query.MergeBases(left, right) - if err != nil { - t.Fatalf("query.MergeBases() third call: %v", err) - } - - if third[0] == (objectid.ObjectID{}) { - t.Fatal("query.MergeBases() exposed internal slice state") - } - }) -} diff --git a/commitquery/queries_new.go b/commitquery/queries_new.go deleted file mode 100644 index 5eae7990..00000000 --- a/commitquery/queries_new.go +++ /dev/null @@ -1,22 +0,0 @@ -package commitquery - -import ( - "runtime" - - commitgraphread "codeberg.org/lindenii/furgit/format/commitgraph/read" - objectfetch "codeberg.org/lindenii/furgit/object/fetch" -) - -// New builds one concurrent-safe commit query service over one object fetcher -// and optional commit-graph reader. -// -// Labels: Deps-Borrowed, Life-Parent. -func New(fetcher *objectfetch.Fetcher, graph *commitgraphread.Reader) *Queries { - maxIdle := max(runtime.GOMAXPROCS(0), 1) - - return &Queries{ - fetcher: fetcher, - graph: graph, - maxIdle: maxIdle, - } -} diff --git a/commitquery/queries_release.go b/commitquery/queries_release.go deleted file mode 100644 index 5d0b2fde..00000000 --- a/commitquery/queries_release.go +++ /dev/null @@ -1,15 +0,0 @@ -package commitquery - -// release resets one worker and returns it to the idle pool if there is room. -func (queries *Queries) release(q *query) { - q.resetForReuse() - - queries.mu.Lock() - defer queries.mu.Unlock() - - if len(queries.idle) >= queries.maxIdle { - return - } - - queries.idle = append(queries.idle, q) -} diff --git a/commitquery/query.go b/commitquery/query.go deleted file mode 100644 index 65e90ec8..00000000 --- a/commitquery/query.go +++ /dev/null @@ -1,23 +0,0 @@ -package commitquery - -import ( - commitgraphread "codeberg.org/lindenii/furgit/format/commitgraph/read" - objectfetch "codeberg.org/lindenii/furgit/object/fetch" - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// query stores one mutable reusable worker and its cached node arena. -// -// Labels: MT-Unsafe. -type query struct { - fetcher *objectfetch.Fetcher - graph *commitgraphread.Reader - - nodes []node - - byOID map[objectid.ObjectID]nodeIndex - byGraphPos map[commitgraphread.Position]nodeIndex - - markPhase uint32 - touched []nodeIndex -} diff --git a/commitquery/query_collect_marked_results.go b/commitquery/query_collect_marked_results.go deleted file mode 100644 index 7139fb9b..00000000 --- a/commitquery/query_collect_marked_results.go +++ /dev/null @@ -1,20 +0,0 @@ -package commitquery - -// collectMarkedResults returns touched nodes marked as non-stale results. -func (query *query) collectMarkedResults() []nodeIndex { - out := make([]nodeIndex, 0, 4) - - for _, idx := range query.touched { - if !query.hasAnyMarks(idx, markResult) { - continue - } - - if query.hasAnyMarks(idx, markStale) { - continue - } - - out = append(out, idx) - } - - return out -} diff --git a/commitquery/query_ensure_loaded.go b/commitquery/query_ensure_loaded.go deleted file mode 100644 index 830e9b19..00000000 --- a/commitquery/query_ensure_loaded.go +++ /dev/null @@ -1,14 +0,0 @@ -package commitquery - -// ensureLoaded completes one node's metadata load if it is not loaded yet. -func (query *query) ensureLoaded(idx nodeIndex) error { - if query.nodes[idx].loaded { - return nil - } - - if query.nodes[idx].hasGraphPos { - return query.loadByGraphPos(idx) - } - - return query.loadByOID(idx) -} diff --git a/commitquery/query_has_marks.go b/commitquery/query_has_marks.go deleted file mode 100644 index 22f44bd4..00000000 --- a/commitquery/query_has_marks.go +++ /dev/null @@ -1,11 +0,0 @@ -package commitquery - -// hasAnyMarks reports whether one internal node has any requested bit. -func (query *query) hasAnyMarks(idx nodeIndex, bits markBits) bool { - return query.nodes[idx].marks&bits != 0 -} - -// hasAllMarks reports whether one internal node already has all requested bits. -func (query *query) hasAllMarks(idx nodeIndex, bits markBits) bool { - return query.nodes[idx].marks&bits == bits -} diff --git a/commitquery/query_is_ancestor.go b/commitquery/query_is_ancestor.go deleted file mode 100644 index c21892c8..00000000 --- a/commitquery/query_is_ancestor.go +++ /dev/null @@ -1,49 +0,0 @@ -package commitquery - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// IsAncestor reports whether ancestor is reachable from descendant through -// commit parent edges. -// -// Both inputs are peeled through annotated tags before commit traversal. -func (query *query) IsAncestor(ancestor, descendant objectid.ObjectID) (bool, error) { - ancestorIdx, err := query.resolveCommitish(ancestor) - if err != nil { - return false, err - } - - descendantIdx, err := query.resolveCommitish(descendant) - if err != nil { - return false, err - } - - return query.isAncestor(ancestorIdx, descendantIdx) -} - -// isAncestor answers one ancestry query between two resolved internal nodes. -func (query *query) isAncestor(ancestor, descendant nodeIndex) (bool, error) { - if ancestor == descendant { - return true, nil - } - - ancestorGeneration := query.effectiveGeneration(ancestor) - descendantGeneration := query.effectiveGeneration(descendant) - - if ancestorGeneration != generationInfinity && - descendantGeneration != generationInfinity && - ancestorGeneration > descendantGeneration { - return false, nil - } - - minGeneration := uint64(0) - if ancestorGeneration != generationInfinity { - minGeneration = ancestorGeneration - } - - err := query.paintDownToCommon(ancestor, []nodeIndex{descendant}, minGeneration) - if err != nil { - return false, err - } - - return query.hasAnyMarks(ancestor, markRight), nil -} diff --git a/commitquery/query_load_by_graph_pos.go b/commitquery/query_load_by_graph_pos.go deleted file mode 100644 index 718d99b5..00000000 --- a/commitquery/query_load_by_graph_pos.go +++ /dev/null @@ -1,8 +0,0 @@ -package commitquery - -// loadByGraphPos populates one node from a commit-graph position. -func (query *query) loadByGraphPos(idx nodeIndex) error { - pos := query.nodes[idx].graphPos - - return query.loadCommitAtGraphPos(idx, pos) -} diff --git a/commitquery/query_load_by_oid.go b/commitquery/query_load_by_oid.go deleted file mode 100644 index f9c956ee..00000000 --- a/commitquery/query_load_by_oid.go +++ /dev/null @@ -1,41 +0,0 @@ -package commitquery - -import ( - stderrors "errors" - - commitgraphread "codeberg.org/lindenii/furgit/format/commitgraph/read" -) - -// loadByOID populates one node from an object ID. -func (query *query) loadByOID(idx nodeIndex) error { - id := query.nodes[idx].id - - if query.graph != nil { - pos, err := query.graph.Lookup(id) - if err != nil { - if _, ok := stderrors.AsType[*commitgraphread.NotFoundError](err); !ok { - return err - } - } else { - return query.loadCommitAtGraphPos(idx, pos) - } - } - - commit, err := query.fetcher.ExactCommit(id) - if err != nil { - return err - } - - parents := make([]parentRef, 0, len(commit.Object().Parents)) - for _, parentID := range commit.Object().Parents { - parents = append(parents, parentRef{ID: parentID}) - } - - commitData := commitData{ - ID: id, - Parents: parents, - CommitTime: commit.Object().Committer.WhenUnix, - } - - return query.populateNode(idx, commitData) -} diff --git a/commitquery/query_load_commit_at_graph_pos.go b/commitquery/query_load_commit_at_graph_pos.go deleted file mode 100644 index f63b6385..00000000 --- a/commitquery/query_load_commit_at_graph_pos.go +++ /dev/null @@ -1,64 +0,0 @@ -package commitquery - -import commitgraphread "codeberg.org/lindenii/furgit/format/commitgraph/read" - -// loadCommitAtGraphPos populates one node from one commit-graph record. -func (query *query) loadCommitAtGraphPos(idx nodeIndex, pos commitgraphread.Position) error { - commit, err := query.graph.CommitAt(pos) - if err != nil { - return err - } - - parents := make([]parentRef, 0, 2+len(commit.ExtraParents)) - - if commit.Parent1.Valid { - parentOID, err := query.graph.OIDAt(commit.Parent1.Pos) - if err != nil { - return err - } - - parents = append(parents, parentRef{ - ID: parentOID, - GraphPos: commit.Parent1.Pos, - HasGraphPos: true, - }) - } - - if commit.Parent2.Valid { - parentOID, err := query.graph.OIDAt(commit.Parent2.Pos) - if err != nil { - return err - } - - parents = append(parents, parentRef{ - ID: parentOID, - GraphPos: commit.Parent2.Pos, - HasGraphPos: true, - }) - } - - for _, parentPos := range commit.ExtraParents { - parentOID, err := query.graph.OIDAt(parentPos) - if err != nil { - return err - } - - parents = append(parents, parentRef{ - ID: parentOID, - GraphPos: parentPos, - HasGraphPos: true, - }) - } - - data := commitData{ - ID: commit.OID, - Parents: parents, - CommitTime: commit.CommitTimeUnix, - Generation: commit.GenerationV2, - HasGeneration: commit.GenerationV2 != 0, - GraphPos: pos, - HasGraphPos: true, - } - - return query.populateNode(idx, data) -} diff --git a/commitquery/query_mark_phase.go b/commitquery/query_mark_phase.go deleted file mode 100644 index 0814df38..00000000 --- a/commitquery/query_mark_phase.go +++ /dev/null @@ -1,36 +0,0 @@ -package commitquery - -// beginMarkPhase starts one tracked mark-mutation phase. -func (query *query) beginMarkPhase() { - for _, idx := range query.touched { - query.nodes[idx].marks = 0 - } - - query.markPhase++ - if query.markPhase == 0 { - query.markPhase++ - for i := range query.nodes { - query.nodes[i].touchedPhase = 0 - } - } - - query.touched = query.touched[:0] -} - -// clearTouchedMarks clears the provided bits from all nodes touched in the -// current mark phase. -func (query *query) clearTouchedMarks(bits markBits) { - for _, idx := range query.touched { - query.nodes[idx].marks &^= bits - } -} - -// trackTouched records one node in the current mark phase. -func (query *query) trackTouched(idx nodeIndex) { - if query.nodes[idx].touchedPhase == query.markPhase { - return - } - - query.nodes[idx].touchedPhase = query.markPhase - query.touched = append(query.touched, idx) -} diff --git a/commitquery/query_marks_get.go b/commitquery/query_marks_get.go deleted file mode 100644 index 28136d84..00000000 --- a/commitquery/query_marks_get.go +++ /dev/null @@ -1,6 +0,0 @@ -package commitquery - -// marks returns the mark bits of one internal node. -func (query *query) marks(idx nodeIndex) markBits { - return query.nodes[idx].marks -} diff --git a/commitquery/query_merge_base.go b/commitquery/query_merge_base.go deleted file mode 100644 index e1ba3126..00000000 --- a/commitquery/query_merge_base.go +++ /dev/null @@ -1,17 +0,0 @@ -package commitquery - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// MergeBase reports one merge base between left and right, if any. -func (query *query) MergeBase(left, right objectid.ObjectID) (objectid.ObjectID, bool, error) { - bases, err := query.MergeBases(left, right) - if err != nil { - return objectid.ObjectID{}, false, err - } - - if len(bases) == 0 { - return objectid.ObjectID{}, false, nil - } - - return bases[0], true, nil -} diff --git a/commitquery/query_merge_bases.go b/commitquery/query_merge_bases.go deleted file mode 100644 index 384ee019..00000000 --- a/commitquery/query_merge_bases.go +++ /dev/null @@ -1,45 +0,0 @@ -package commitquery - -import ( - "slices" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// MergeBases reports all merge bases in Git's merge-base --all order. -// -// Both inputs are peeled through annotated tags before commit traversal. -func (query *query) MergeBases(left, right objectid.ObjectID) ([]objectid.ObjectID, error) { - leftIdx, err := query.resolveCommitish(left) - if err != nil { - return nil, err - } - - rightIdx, err := query.resolveCommitish(right) - if err != nil { - return nil, err - } - - candidates, err := query.mergeBases(leftIdx, rightIdx) - if err != nil { - return nil, err - } - - slices.SortFunc(candidates, func(left, right nodeIndex) int { - switch { - case query.commitTime(left) > query.commitTime(right): - return -1 - case query.commitTime(left) < query.commitTime(right): - return 1 - default: - return objectid.Compare(query.id(left), query.id(right)) - } - }) - - out := make([]objectid.ObjectID, 0, len(candidates)) - for _, idx := range candidates { - out = append(out, query.id(idx)) - } - - return out, nil -} diff --git a/commitquery/query_merge_bases_internal.go b/commitquery/query_merge_bases_internal.go deleted file mode 100644 index 2d133435..00000000 --- a/commitquery/query_merge_bases_internal.go +++ /dev/null @@ -1,34 +0,0 @@ -package commitquery - -import "slices" - -// mergeBases returns internal merge-base candidates for two resolved nodes. -func (query *query) mergeBases(left, right nodeIndex) ([]nodeIndex, error) { - if left == right { - return []nodeIndex{left}, nil - } - - err := query.paintDownToCommon(left, []nodeIndex{right}, 0) - if err != nil { - return nil, err - } - - candidates := query.collectMarkedResults() - - if len(candidates) <= 1 { - slices.SortFunc(candidates, query.compare) - - return candidates, nil - } - - query.clearTouchedMarks(allMarks) - - reduced, err := removeRedundant(query, candidates) - if err != nil { - return nil, err - } - - slices.SortFunc(reduced, query.compare) - - return reduced, nil -} diff --git a/commitquery/query_new.go b/commitquery/query_new.go deleted file mode 100644 index 0f23a321..00000000 --- a/commitquery/query_new.go +++ /dev/null @@ -1,19 +0,0 @@ -package commitquery - -import ( - commitgraphread "codeberg.org/lindenii/furgit/format/commitgraph/read" - objectfetch "codeberg.org/lindenii/furgit/object/fetch" - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// newQuery builds one empty mutable worker over one object fetcher and graph. -// -// Labels: Deps-Borrowed, Life-Parent. -func newQuery(fetcher *objectfetch.Fetcher, graph *commitgraphread.Reader) *query { - return &query{ - fetcher: fetcher, - graph: graph, - byOID: make(map[objectid.ObjectID]nodeIndex), - byGraphPos: make(map[commitgraphread.Position]nodeIndex), - } -} diff --git a/commitquery/query_paint_down_to_common.go b/commitquery/query_paint_down_to_common.go deleted file mode 100644 index e152e159..00000000 --- a/commitquery/query_paint_down_to_common.go +++ /dev/null @@ -1,67 +0,0 @@ -package commitquery - -import "codeberg.org/lindenii/furgit/internal/priorityqueue" - -// paintDownToCommon propagates left and right marks downward until common nodes. -func (query *query) paintDownToCommon(left nodeIndex, rights []nodeIndex, minGeneration uint64) error { - query.beginMarkPhase() - - query.setMarks(left, markLeft) - - if len(rights) == 0 { - query.setMarks(left, markResult) - - return nil - } - - queue := priorityqueue.New(func(left, right nodeIndex) bool { - return query.compare(left, right) > 0 - }) - queue.Push(left) - - for _, right := range rights { - query.setMarks(right, markRight) - queue.Push(right) - } - - lastGeneration := generationInfinity - - for queue.Len() > 0 { - idx, ok := queue.Pop() - if !ok { - break - } - - if query.hasAnyMarks(idx, markStale) { - continue - } - - generation := query.effectiveGeneration(idx) - if generation > lastGeneration { - return errBadGenerationOrder - } - - lastGeneration = generation - if generation < minGeneration { - break - } - - flags := query.marks(idx) & (markLeft | markRight | markStale) - if flags == (markLeft | markRight) { - query.setMarks(idx, markResult) - - flags |= markStale - } - - for _, parent := range query.parents(idx) { - if query.hasAllMarks(parent, flags) { - continue - } - - query.setMarks(parent, flags) - queue.Push(parent) - } - } - - return nil -} diff --git a/commitquery/query_reduce.go b/commitquery/query_reduce.go deleted file mode 100644 index b7ea5df1..00000000 --- a/commitquery/query_reduce.go +++ /dev/null @@ -1,166 +0,0 @@ -package commitquery - -import "slices" - -// removeRedundant removes redundant merge-base candidates. -func removeRedundant(query *query, candidates []nodeIndex) ([]nodeIndex, error) { - for _, idx := range candidates { - if query.effectiveGeneration(idx) != generationInfinity { - return removeRedundantWithGen(query, candidates), nil - } - } - - return removeRedundantNoGen(query, candidates) -} - -// removeRedundantNoGen removes redundant candidates without generation data. -func removeRedundantNoGen(query *query, candidates []nodeIndex) ([]nodeIndex, error) { - redundant := make([]bool, len(candidates)) - work := make([]nodeIndex, 0, len(candidates)-1) - filledIndex := make([]int, 0, len(candidates)-1) - - for i, candidate := range candidates { - if redundant[i] { - continue - } - - work = work[:0] - filledIndex = filledIndex[:0] - - minGeneration := query.effectiveGeneration(candidate) - - for j, other := range candidates { - if i == j || redundant[j] { - continue - } - - work = append(work, other) - filledIndex = append(filledIndex, j) - - otherGeneration := query.effectiveGeneration(other) - if otherGeneration < minGeneration { - minGeneration = otherGeneration - } - } - - err := query.paintDownToCommon(candidate, work, minGeneration) - if err != nil { - return nil, err - } - - if query.hasAnyMarks(candidate, markRight) { - redundant[i] = true - } - - for j, other := range work { - if query.hasAnyMarks(other, markLeft) { - redundant[filledIndex[j]] = true - } - } - - query.clearTouchedMarks(allMarks) - } - - out := make([]nodeIndex, 0, len(candidates)) - for i, idx := range candidates { - if !redundant[i] { - out = append(out, idx) - } - } - - return out, nil -} - -// removeRedundantWithGen removes redundant candidates using generation data. -func removeRedundantWithGen(query *query, candidates []nodeIndex) []nodeIndex { - sorted := append([]nodeIndex(nil), candidates...) - slices.SortFunc(sorted, query.compareByGeneration()) - - minGeneration := query.effectiveGeneration(sorted[0]) - minGenPos := 0 - countStillIndependent := len(candidates) - - query.beginMarkPhase() - - walkStart := make([]nodeIndex, 0, len(candidates)*2) - - for _, idx := range candidates { - query.setMarks(idx, markResult) - - for _, parent := range query.parents(idx) { - if query.hasAnyMarks(parent, markStale) { - continue - } - - query.setMarks(parent, markStale) - walkStart = append(walkStart, parent) - } - } - - slices.SortFunc(walkStart, query.compareByGeneration()) - - for _, idx := range walkStart { - query.clearMarks(idx, markStale) - } - - for i := len(walkStart) - 1; i >= 0 && countStillIndependent > 1; i-- { - stack := []nodeIndex{walkStart[i]} - query.setMarks(walkStart[i], markStale) - - for len(stack) > 0 { - top := stack[len(stack)-1] - - if query.hasAnyMarks(top, markResult) { - query.clearMarks(top, markResult) - - countStillIndependent-- - if countStillIndependent <= 1 { - break - } - - if top == sorted[minGenPos] { - for minGenPos < len(sorted)-1 && query.hasAnyMarks(sorted[minGenPos], markStale) { - minGenPos++ - } - - minGeneration = query.effectiveGeneration(sorted[minGenPos]) - } - } - - if query.effectiveGeneration(top) < minGeneration { - stack = stack[:len(stack)-1] - - continue - } - - pushed := false - - for _, parent := range query.parents(top) { - if query.hasAnyMarks(parent, markStale) { - continue - } - - query.setMarks(parent, markStale) - stack = append(stack, parent) - pushed = true - - break - } - - if !pushed { - stack = stack[:len(stack)-1] - } - } - } - - out := make([]nodeIndex, 0, len(candidates)) - for _, idx := range candidates { - if !query.hasAnyMarks(idx, markStale) { - out = append(out, idx) - } - } - - query.clearTouchedMarks(markStale | markResult) - - return out -} diff --git a/commitquery/query_reset.go b/commitquery/query_reset.go deleted file mode 100644 index 11f7cb3e..00000000 --- a/commitquery/query_reset.go +++ /dev/null @@ -1,10 +0,0 @@ -package commitquery - -// resetForReuse clears transient state before one worker returns to the pool. -func (query *query) resetForReuse() { - for _, idx := range query.touched { - query.nodes[idx].marks = 0 - } - - query.touched = query.touched[:0] -} diff --git a/commitquery/query_resolve_commitish.go b/commitquery/query_resolve_commitish.go deleted file mode 100644 index 1e14a1c0..00000000 --- a/commitquery/query_resolve_commitish.go +++ /dev/null @@ -1,13 +0,0 @@ -package commitquery - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// resolveCommitish peels one commit-ish object ID and resolves the commit. -func (query *query) resolveCommitish(id objectid.ObjectID) (nodeIndex, error) { - id, err := query.fetcher.PeelToCommitID(id) - if err != nil { - return 0, err - } - - return query.resolveOID(id) -} diff --git a/commitquery/query_resolve_graph_pos.go b/commitquery/query_resolve_graph_pos.go deleted file mode 100644 index dce8fc22..00000000 --- a/commitquery/query_resolve_graph_pos.go +++ /dev/null @@ -1,40 +0,0 @@ -package commitquery - -import commitgraphread "codeberg.org/lindenii/furgit/format/commitgraph/read" - -// resolveGraphPos resolves one commit-graph position to one internal query node. -func (query *query) resolveGraphPos(pos commitgraphread.Position) (nodeIndex, error) { - idx, ok := query.byGraphPos[pos] - if ok { - err := query.ensureLoaded(idx) - if err != nil { - return 0, err - } - - return idx, nil - } - - commit, err := query.graph.CommitAt(pos) - if err != nil { - return 0, err - } - - idx, ok = query.byOID[commit.OID] - if !ok { - idx = query.newNode(commit.OID) - query.byOID[commit.OID] = idx - } - - query.byGraphPos[pos] = idx - query.nodes[idx].graphPos = pos - query.nodes[idx].hasGraphPos = true - - err = query.loadCommitAtGraphPos(idx, pos) - if err != nil { - delete(query.byGraphPos, pos) - - return 0, err - } - - return idx, nil -} diff --git a/commitquery/query_resolve_oid.go b/commitquery/query_resolve_oid.go deleted file mode 100644 index ad47829c..00000000 --- a/commitquery/query_resolve_oid.go +++ /dev/null @@ -1,28 +0,0 @@ -package commitquery - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// resolveOID resolves one commit object ID to one internal query node. -func (query *query) resolveOID(id objectid.ObjectID) (nodeIndex, error) { - idx, ok := query.byOID[id] - if ok { - err := query.ensureLoaded(idx) - if err != nil { - return 0, err - } - - return idx, nil - } - - idx = query.newNode(id) - query.byOID[id] = idx - - err := query.loadByOID(idx) - if err != nil { - delete(query.byOID, id) - - return 0, err - } - - return idx, nil -} diff --git a/commitquery/query_resolve_parent.go b/commitquery/query_resolve_parent.go deleted file mode 100644 index 6cd75898..00000000 --- a/commitquery/query_resolve_parent.go +++ /dev/null @@ -1,10 +0,0 @@ -package commitquery - -// resolveParent resolves one parent descriptor to one internal node. -func (query *query) resolveParent(parent parentRef) (nodeIndex, error) { - if parent.HasGraphPos { - return query.resolveGraphPos(parent.GraphPos) - } - - return query.resolveOID(parent.ID) -} diff --git a/commitquery/query_set_clear_marks.go b/commitquery/query_set_clear_marks.go deleted file mode 100644 index b9619338..00000000 --- a/commitquery/query_set_clear_marks.go +++ /dev/null @@ -1,22 +0,0 @@ -package commitquery - -// setMarks ORs one set of mark bits into one internal node. -func (query *query) setMarks(idx nodeIndex, bits markBits) { - newBits := bits &^ query.nodes[idx].marks - if newBits == 0 { - return - } - - query.trackTouched(idx) - query.nodes[idx].marks |= bits -} - -// clearMarks removes one set of mark bits from one internal node. -func (query *query) clearMarks(idx nodeIndex, bits markBits) { - if query.nodes[idx].marks&bits == 0 { - return - } - - query.trackTouched(idx) - query.nodes[idx].marks &^= bits -} diff --git a/common/doc.go b/common/doc.go deleted file mode 100644 index 1f685411..00000000 --- a/common/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package common encapsulates various helper packages not directly related to Git. -package common diff --git a/common/iowrap/doc.go b/common/iowrap/doc.go deleted file mode 100644 index 8e25db16..00000000 --- a/common/iowrap/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package iowrap provides small public I/O wrapper interfaces and adapters. -package iowrap diff --git a/common/iowrap/nop_flush.go b/common/iowrap/nop_flush.go deleted file mode 100644 index 38fc5f55..00000000 --- a/common/iowrap/nop_flush.go +++ /dev/null @@ -1,20 +0,0 @@ -package iowrap - -import "io" - -type nopFlusher struct { - io.Writer -} - -// NopFlush adapts writer into a WriteFlusher with a no-op Flush. -func NopFlush(writer io.Writer) WriteFlusher { - if writer == nil { - return nil - } - - return nopFlusher{Writer: writer} -} - -func (nopFlusher) Flush() error { - return nil -} diff --git a/common/iowrap/write_flusher.go b/common/iowrap/write_flusher.go deleted file mode 100644 index aaac8724..00000000 --- a/common/iowrap/write_flusher.go +++ /dev/null @@ -1,9 +0,0 @@ -package iowrap - -import "io" - -// WriteFlusher writes bytes and flushes buffered output state. -type WriteFlusher interface { - io.Writer - Flush() error -} diff --git a/config/bom.go b/config/bom.go deleted file mode 100644 index 48963e7f..00000000 --- a/config/bom.go +++ /dev/null @@ -1,56 +0,0 @@ -package config - -import ( - "errors" - "io" -) - -func (p *configParser) skipBOM() error { - first, err := p.reader.ReadByte() - if errors.Is(err, io.EOF) { - return nil - } - - if err != nil { - return err - } - - if first != 0xef { - _ = p.reader.UnreadByte() - - return nil - } - - second, err := p.reader.ReadByte() - if err != nil { - if errors.Is(err, io.EOF) { - _ = p.reader.UnreadByte() - - return nil - } - - return err - } - - third, err := p.reader.ReadByte() - if err != nil { - if errors.Is(err, io.EOF) { - _ = p.reader.UnreadByte() - _ = p.reader.UnreadByte() - - return nil - } - - return err - } - - if second == 0xbb && third == 0xbf { - return nil - } - - _ = p.reader.UnreadByte() - _ = p.reader.UnreadByte() - _ = p.reader.UnreadByte() - - return nil -} diff --git a/config/char.go b/config/char.go deleted file mode 100644 index da52013c..00000000 --- a/config/char.go +++ /dev/null @@ -1,52 +0,0 @@ -package config - -func (p *configParser) nextChar() (byte, error) { - if p.hasPeeked { - p.hasPeeked = false - - return p.peeked, nil - } - - ch, err := p.reader.ReadByte() - if err != nil { - return 0, err - } - - if ch == '\r' { - next, err := p.reader.ReadByte() - if err == nil && next == '\n' { - ch = '\n' - } else if err == nil { - // Weird but ok - _ = p.reader.UnreadByte() - } - } - - if ch == '\n' { - p.lineNum++ - } - - return ch, nil -} - -func (p *configParser) unreadChar(ch byte) { - p.peeked = ch - - p.hasPeeked = true - if ch == '\n' && p.lineNum > 1 { - p.lineNum-- - } -} - -func (p *configParser) skipToEOL() error { - for { - ch, err := p.nextChar() - if err != nil { - return err - } - - if ch == '\n' { - return nil - } - } -} diff --git a/config/config.go b/config/config.go deleted file mode 100644 index 5564d940..00000000 --- a/config/config.go +++ /dev/null @@ -1,14 +0,0 @@ -// Package config provides configuration parsing. -package config - -// Config holds all parsed configuration entries from a Git config file. -// -// A Config preserves the ordering of entries as they appeared in the source. -// -// Lookups are matched case-insensitively for section and key names, and -// subsections must match exactly. -// -// Includes aren't supported yet; they will be supported in a later revision. -type Config struct { - entries []ConfigEntry -} diff --git a/config/config_test.go b/config/config_test.go deleted file mode 100644 index 86b8be50..00000000 --- a/config/config_test.go +++ /dev/null @@ -1,606 +0,0 @@ -package config_test - -import ( - "bytes" - "os" - "strings" - "testing" - - "codeberg.org/lindenii/furgit/config" - "codeberg.org/lindenii/furgit/internal/testgit" - objectid "codeberg.org/lindenii/furgit/object/id" -) - -func openConfig(t *testing.T, testRepo *testgit.TestRepo) *os.File { - t.Helper() - - root := testRepo.OpenGitRoot(t) - - cfgFile, err := root.Open("config") - if err != nil { - t.Fatalf("failed to open config: %v", err) - } - - return cfgFile -} - -func gitConfigGet(t *testing.T, testRepo *testgit.TestRepo, key string) string { - t.Helper() - - return testRepo.Run(t, "config", "--get", key) -} - -func gitConfigGetE(t *testing.T, testRepo *testgit.TestRepo, key string) (string, error) { - t.Helper() - - return testRepo.RunE(t, "config", "--get", key) -} - -func lookupValue(cfg *config.Config, section, subsection, key string) string { - result := cfg.Lookup(section, subsection, key) - if result.Kind == config.ValueMissing { - return "" - } - - return result.Value -} - -func lookupAllValues(cfg *config.Config, section, subsection, key string) []string { - results := cfg.LookupAll(section, subsection, key) - - values := make([]string, 0, len(results)) - for _, result := range results { - values = append(values, result.Value) - } - - return values -} - -func TestConfigAgainstGit(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - testRepo.Run(t, "config", "core.bare", "true") - testRepo.Run(t, "config", "core.filemode", "false") - testRepo.Run(t, "config", "user.name", "Jane Doe") - testRepo.Run(t, "config", "user.email", "jane@example.org") - - cfgFile := openConfig(t, testRepo) - - defer func() { _ = cfgFile.Close() }() - - cfg, err := config.ParseConfig(cfgFile) - if err != nil { - t.Fatalf("ParseConfig failed: %v", err) - } - - if got := lookupValue(cfg, "core", "", "bare"); got != "true" { - t.Errorf("core.bare: got %q, want %q", got, "true") - } - - if got := lookupValue(cfg, "core", "", "filemode"); got != "false" { - t.Errorf("core.filemode: got %q, want %q", got, "false") - } - - if got := lookupValue(cfg, "user", "", "name"); got != "Jane Doe" { - t.Errorf("user.name: got %q, want %q", got, "Jane Doe") - } - - if got := lookupValue(cfg, "user", "", "email"); got != "jane@example.org" { - t.Errorf("user.email: got %q, want %q", got, "jane@example.org") - } - }) -} - -func TestConfigSubsectionAgainstGit(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - testRepo.Run(t, "config", "remote.origin.url", "https://example.org/repo.git") - testRepo.Run(t, "config", "remote.origin.fetch", "+refs/heads/*:refs/remotes/origin/*") - - cfgFile := openConfig(t, testRepo) - - defer func() { _ = cfgFile.Close() }() - - cfg, err := config.ParseConfig(cfgFile) - if err != nil { - t.Fatalf("ParseConfig failed: %v", err) - } - - if got := lookupValue(cfg, "remote", "origin", "url"); got != "https://example.org/repo.git" { - t.Errorf("remote.origin.url: got %q, want %q", got, "https://example.org/repo.git") - } - - if got := lookupValue(cfg, "remote", "origin", "fetch"); got != "+refs/heads/*:refs/remotes/origin/*" { - t.Errorf("remote.origin.fetch: got %q, want %q", got, "+refs/heads/*:refs/remotes/origin/*") - } - }) -} - -func TestConfigMultiValueAgainstGit(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - testRepo.Run(t, "config", "--add", "remote.origin.fetch", "+refs/heads/main:refs/remotes/origin/main") - testRepo.Run(t, "config", "--add", "remote.origin.fetch", "+refs/heads/dev:refs/remotes/origin/dev") - testRepo.Run(t, "config", "--add", "remote.origin.fetch", "+refs/tags/*:refs/tags/*") - - cfgFile := openConfig(t, testRepo) - - defer func() { _ = cfgFile.Close() }() - - cfg, err := config.ParseConfig(cfgFile) - if err != nil { - t.Fatalf("ParseConfig failed: %v", err) - } - - fetches := lookupAllValues(cfg, "remote", "origin", "fetch") - if len(fetches) != 3 { - t.Fatalf("expected 3 fetch values, got %d", len(fetches)) - } - - expected := []string{ - "+refs/heads/main:refs/remotes/origin/main", - "+refs/heads/dev:refs/remotes/origin/dev", - "+refs/tags/*:refs/tags/*", - } - for i, want := range expected { - if fetches[i] != want { - t.Errorf("fetch[%d]: got %q, want %q", i, fetches[i], want) - } - } - }) -} - -func TestConfigCaseInsensitiveAgainstGit(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - testRepo.Run(t, "config", "Core.Bare", "true") - testRepo.Run(t, "config", "CORE.FileMode", "false") - - gitVerifyBare := gitConfigGet(t, testRepo, "core.bare") - gitVerifyFilemode := gitConfigGet(t, testRepo, "core.filemode") - - cfgFile := openConfig(t, testRepo) - - defer func() { _ = cfgFile.Close() }() - - cfg, err := config.ParseConfig(cfgFile) - if err != nil { - t.Fatalf("ParseConfig failed: %v", err) - } - - if got := lookupValue(cfg, "core", "", "bare"); got != gitVerifyBare { - t.Errorf("core.bare: got %q, want %q (from git)", got, gitVerifyBare) - } - - if got := lookupValue(cfg, "CORE", "", "BARE"); got != gitVerifyBare { - t.Errorf("CORE.BARE: got %q, want %q (from git)", got, gitVerifyBare) - } - - if got := lookupValue(cfg, "core", "", "filemode"); got != gitVerifyFilemode { - t.Errorf("core.filemode: got %q, want %q (from git)", got, gitVerifyFilemode) - } - }) -} - -func TestConfigBooleanAgainstGit(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - testRepo.Run(t, "config", "test.flag1", "true") - testRepo.Run(t, "config", "test.flag2", "false") - testRepo.Run(t, "config", "test.flag3", "yes") - testRepo.Run(t, "config", "test.flag4", "no") - - cfgFile := openConfig(t, testRepo) - - defer func() { _ = cfgFile.Close() }() - - cfg, err := config.ParseConfig(cfgFile) - if err != nil { - t.Fatalf("ParseConfig failed: %v", err) - } - - tests := []struct { - key string - want string - }{ - {"flag1", gitConfigGet(t, testRepo, "test.flag1")}, - {"flag2", gitConfigGet(t, testRepo, "test.flag2")}, - {"flag3", gitConfigGet(t, testRepo, "test.flag3")}, - {"flag4", gitConfigGet(t, testRepo, "test.flag4")}, - } - - for _, tt := range tests { - if got := lookupValue(cfg, "test", "", tt.key); got != tt.want { - t.Errorf("test.%s: got %q, want %q (from git)", tt.key, got, tt.want) - } - } - }) -} - -func TestConfigLookupKindsAndBool(t *testing.T) { - t.Parallel() - - cfgText := "[test]\nnovalue\nempty =\ntruthy = yes\nnumeric = -2\nleadspace = \" 1\"\nleadtab = \"\t-2\"\nksuffix = 1k\nhex = 0x10\nmaxi32 = 2147483647\ntoobig = 2147483648\ntoosmall = -2147483649\nbadnum = \" 2x\"\n" - - cfg, err := config.ParseConfig(strings.NewReader(cfgText)) - if err != nil { - t.Fatalf("ParseConfig failed: %v", err) - } - - novalue := cfg.Lookup("test", "", "novalue") - if novalue.Kind != config.ValueValueless { - t.Fatalf("novalue kind: got %v, want %v", novalue.Kind, config.ValueValueless) - } - - novalueBool, err := novalue.Bool() - if err != nil || !novalueBool { - t.Fatalf("novalue bool: got (%v, %v), want (true, nil)", novalueBool, err) - } - - empty := cfg.Lookup("test", "", "empty") - if empty.Kind != config.ValueString || empty.Value != "" { - t.Fatalf("empty: got (%v, %q), want (%v, %q)", empty.Kind, empty.Value, config.ValueString, "") - } - - emptyBool, err := empty.Bool() - if err != nil || emptyBool { - t.Fatalf("empty bool: got (%v, %v), want (false, nil)", emptyBool, err) - } - - truthyBool, err := cfg.Lookup("test", "", "truthy").Bool() - if err != nil || !truthyBool { - t.Fatalf("truthy bool: got (%v, %v), want (true, nil)", truthyBool, err) - } - - numericBool, err := cfg.Lookup("test", "", "numeric").Bool() - if err != nil || !numericBool { - t.Fatalf("numeric bool: got (%v, %v), want (true, nil)", numericBool, err) - } - - leadspaceBool, err := cfg.Lookup("test", "", "leadspace").Bool() - if err != nil || !leadspaceBool { - t.Fatalf("leadspace bool: got (%v, %v), want (true, nil)", leadspaceBool, err) - } - - leadtabBool, err := cfg.Lookup("test", "", "leadtab").Bool() - if err != nil || !leadtabBool { - t.Fatalf("leadtab bool: got (%v, %v), want (true, nil)", leadtabBool, err) - } - - ksuffixBool, err := cfg.Lookup("test", "", "ksuffix").Bool() - if err != nil || !ksuffixBool { - t.Fatalf("ksuffix bool: got (%v, %v), want (true, nil)", ksuffixBool, err) - } - - maxi32Bool, err := cfg.Lookup("test", "", "maxi32").Bool() - if err != nil || !maxi32Bool { - t.Fatalf("maxi32 bool: got (%v, %v), want (true, nil)", maxi32Bool, err) - } - - _, err = cfg.Lookup("test", "", "toobig").Bool() - if err == nil { - t.Fatal("toobig bool: expected error") - } - - _, err = cfg.Lookup("test", "", "toosmall").Bool() - if err == nil { - t.Fatal("toosmall bool: expected error") - } - - _, err = cfg.Lookup("test", "", "badnum").Bool() - if err == nil { - t.Fatal("badnum bool: expected error") - } - - _, err = novalue.String() - if err == nil { - t.Fatal("novalue string: expected error") - } - - emptyString, err := empty.String() - if err != nil || emptyString != "" { - t.Fatalf("empty string: got (%q, %v), want (%q, nil)", emptyString, err, "") - } - - numericInt, err := cfg.Lookup("test", "", "numeric").Int() - if err != nil || numericInt != -2 { - t.Fatalf("numeric int: got (%v, %v), want (-2, nil)", numericInt, err) - } - - ksuffixInt, err := cfg.Lookup("test", "", "ksuffix").Int() - if err != nil || ksuffixInt != 1024 { - t.Fatalf("ksuffix int: got (%v, %v), want (1024, nil)", ksuffixInt, err) - } - - hexInt64, err := cfg.Lookup("test", "", "hex").Int64() - if err != nil || hexInt64 != 16 { - t.Fatalf("hex int64: got (%v, %v), want (16, nil)", hexInt64, err) - } - - _, err = cfg.Lookup("test", "", "badnum").Int() - if err == nil { - t.Fatal("badnum int: expected error") - } - - missing := cfg.Lookup("test", "", "missing") - if missing.Kind != config.ValueMissing { - t.Fatalf("missing kind: got %v, want %v", missing.Kind, config.ValueMissing) - } - - _, err = missing.Bool() - if err == nil { - t.Fatal("missing bool: expected error") - } - - _, err = missing.Int() - if err == nil { - t.Fatal("missing int: expected error") - } - - _, err = missing.String() - if err == nil { - t.Fatal("missing string: expected error") - } -} - -func TestConfigComplexValuesAgainstGit(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - testRepo.Run(t, "config", "test.spaced", "value with spaces") - testRepo.Run(t, "config", "test.special", "value=with=equals") - testRepo.Run(t, "config", "test.path", "/path/to/something") - testRepo.Run(t, "config", "test.number", "12345") - - cfgFile := openConfig(t, testRepo) - - defer func() { _ = cfgFile.Close() }() - - cfg, err := config.ParseConfig(cfgFile) - if err != nil { - t.Fatalf("ParseConfig failed: %v", err) - } - - tests := []string{"spaced", "special", "path", "number"} - for _, key := range tests { - want := gitConfigGet(t, testRepo, "test."+key) - if got := lookupValue(cfg, "test", "", key); got != want { - t.Errorf("test.%s: got %q, want %q (from git)", key, got, want) - } - } - }) -} - -func TestConfigEntriesAgainstGit(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - testRepo.Run(t, "config", "core.bare", "true") - testRepo.Run(t, "config", "core.filemode", "false") - testRepo.Run(t, "config", "user.name", "Test User") - - cfgFile := openConfig(t, testRepo) - - defer func() { _ = cfgFile.Close() }() - - cfg, err := config.ParseConfig(cfgFile) - if err != nil { - t.Fatalf("ParseConfig failed: %v", err) - } - - entries := cfg.Entries() - if len(entries) < 3 { - t.Errorf("expected at least 3 entries, got %d", len(entries)) - } - - found := make(map[string]bool) - - for _, entry := range entries { - key := entry.Section + "." + entry.Key - if entry.Subsection != "" { - key = entry.Section + "." + entry.Subsection + "." + entry.Key - } - - found[key] = true - - gitValue := gitConfigGet(t, testRepo, key) - if entry.Value != gitValue { - t.Errorf("entry %s: got value %q, git has %q", key, entry.Value, gitValue) - } - } - }) -} - -func TestConfigErrorCases(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - config string - }{ - { - name: "key before section", - config: "bare = true", - }, - { - name: "invalid section character", - config: "[core/invalid]", - }, - { - name: "unterminated section", - config: "[core", - }, - { - name: "unterminated quote", - config: "[core]\n\tbare = \"true", - }, - { - name: "invalid escape", - config: "[core]\n\tvalue = \"test\\x\"", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - r := strings.NewReader(tt.config) - - _, err := config.ParseConfig(r) - if err == nil { - t.Errorf("expected error for %s", tt.name) - } - }) - } -} - -func TestConfigEOFAfterKeyAgainstGit(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - cfgData := []byte("[Core]BAre") - - testRepo.WriteFile(t, "config", cfgData, 0o600) - - gitValue, gitErr := gitConfigGetE(t, testRepo, "Core.BAre") - furConfig, furErr := config.ParseConfig(bytes.NewReader(cfgData)) - - if (gitErr == nil) != (furErr == nil) { - t.Fatalf("git: %v\nfur: %v", gitErr, furErr) - } - - if furErr != nil { - return - } - - if got := lookupValue(furConfig, "Core", "", "BAre"); got != gitValue { - t.Fatalf("git: %q\nfur: %q", gitValue, got) - } - }) -} - -func TestConfigNULValueAgainstGit(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - cfgData := []byte("[Core]BAre=\x00") - - testRepo.WriteFile(t, "config", cfgData, 0o600) - - gitValue, gitErr := gitConfigGetE(t, testRepo, "Core.BAre") - furConfig, furErr := config.ParseConfig(bytes.NewReader(cfgData)) - - if (gitErr == nil) != (furErr == nil) { - t.Fatalf("git: %v\nfur: %v", gitErr, furErr) - } - - if furErr != nil { - return - } - - if got := lookupValue(furConfig, "Core", "", "BAre"); got != gitValue { - t.Fatalf("git: %q\nfur: %q", gitValue, got) - } - }) -} - -func TestConfigCarriageReturnSeparatorAgainstGit(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - cfgData := []byte("[Core \"sub\"]\rBAre") - - testRepo.WriteFile(t, "config", cfgData, 0o600) - - gitValue, gitErr := gitConfigGetE(t, testRepo, "Core.sub.BAre") - furConfig, furErr := config.ParseConfig(bytes.NewReader(cfgData)) - - if (gitErr == nil) != (furErr == nil) { - t.Fatalf("git: %v\nfur: %v", gitErr, furErr) - } - - if furErr != nil { - return - } - - if got := lookupValue(furConfig, "Core", "sub", "BAre"); got != gitValue { - t.Fatalf("git: %q\nfur: %q", gitValue, got) - } - }) -} - -func FuzzConfig(f *testing.F) { - f.Add([]byte("[core]\nbare = true"), "core.bare") - f.Add([]byte("[core]\nbare = true\n[core/invalid]"), "core.bare") - f.Add([]byte("[core \"sub\"]\nbare = true"), "core.sub.bare") - - type fuzzRepoState struct { - repo *testgit.TestRepo - } - - repos := make(map[objectid.Algorithm]fuzzRepoState, len(objectid.SupportedAlgorithms())) - for _, algo := range objectid.SupportedAlgorithms() { - testRepo := testgit.NewRepo(f, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - repos[algo] = fuzzRepoState{ - repo: testRepo, - } - } - - f.Fuzz(func(t *testing.T, cfgData []byte, gitKey string) { - for _, algo := range objectid.SupportedAlgorithms() { - state, ok := repos[algo] - if !ok { - t.Fatalf("missing fuzz repo state for %v", algo) - } - - state.repo.WriteFile(t, "config", cfgData, 0o600) - - gitValue, gitErr := gitConfigGetE(t, state.repo, gitKey) - - furConfig, furErr := config.ParseConfig(bytes.NewReader(cfgData)) - if furErr == nil && furConfig == nil { - t.Fatalf("ParseConfig returned nil config with nil error") - } - - sameErr := (gitErr == nil) == (furErr == nil) - if !sameErr { - if furErr == nil { - return - } - - t.Fatalf("git: %v\nfur: %v", gitErr, furErr) - } - - if furErr == nil { - parts := strings.SplitN(gitKey, ".", 3) - furSection := parts[0] - - var furSubsection, furKey string - - switch len(parts) { - case 1: - case 2: - furKey = parts[1] - case 3: - furSubsection = parts[1] - furKey = parts[2] - default: - t.Fatalf("unexpected split(%q): %v", gitKey, parts) - } - - furValue := lookupValue(furConfig, furSection, furSubsection, furKey) - if gitValue != furValue { - t.Fatalf( - "key: %v (%v.%v.%v)\ngit: %q\nfur: %q", - gitKey, furSection, furSubsection, furKey, gitValue, furValue, - ) - } - } - } - }) -} diff --git a/config/entry.go b/config/entry.go deleted file mode 100644 index a2a39965..00000000 --- a/config/entry.go +++ /dev/null @@ -1,25 +0,0 @@ -package config - -// ConfigEntry represents a single parsed configuration directive. -type ConfigEntry struct { - // The section name in canonical lowercase form. - Section string - // The subsection name, retaining the exact form parsed from the input. - Subsection string - // The key name in canonical lowercase form. - Key string - // Kind records whether this entry has no value or an explicit value. - Kind ValueKind - // The interpreted value of the configuration entry, including unescaped - // characters where appropriate. - Value string -} - -// Entries returns a copy of all parsed configuration entries in the order they -// appeared. Modifying the returned slice does not affect the Config. -func (c *Config) Entries() []ConfigEntry { - result := make([]ConfigEntry, len(c.entries)) - copy(result, c.entries) - - return result -} diff --git a/config/extended_section.go b/config/extended_section.go deleted file mode 100644 index 410009e7..00000000 --- a/config/extended_section.go +++ /dev/null @@ -1,76 +0,0 @@ -package config - -import ( - "bytes" - "errors" - "fmt" - "strings" -) - -func (p *configParser) parseExtendedSection(sectionName *bytes.Buffer) error { - for { - ch, err := p.nextChar() - if err != nil { - return errors.New("unexpected EOF in section header") - } - - if !isWhitespace(ch) { - if ch != '"' { - return errors.New("expected quote after section name") - } - - break - } - } - - var subsec bytes.Buffer - - for { - ch, err := p.nextChar() - if err != nil { - return errors.New("unexpected EOF in subsection") - } - - if ch == '\n' { - return errors.New("newline in subsection") - } - - if ch == '"' { - break - } - - if ch == '\\' { - next, err := p.nextChar() - if err != nil { - return errors.New("unexpected EOF after backslash in subsection") - } - - if next == '\n' { - return errors.New("newline after backslash in subsection") - } - - subsec.WriteByte(next) - } else { - subsec.WriteByte(ch) - } - } - - ch, err := p.nextChar() - if err != nil { - return errors.New("unexpected EOF after subsection") - } - - if ch != ']' { - return fmt.Errorf("expected ']' after subsection, got %q", ch) - } - - section := sectionName.String() - if !isValidSection(section) { - return fmt.Errorf("invalid section name: %q", section) - } - - p.currentSection = strings.ToLower(section) - p.currentSubsec = subsec.String() - - return nil -} diff --git a/config/key_value.go b/config/key_value.go deleted file mode 100644 index 482bfcc7..00000000 --- a/config/key_value.go +++ /dev/null @@ -1,119 +0,0 @@ -package config - -import ( - "bytes" - "errors" - "fmt" - "io" -) - -func (p *configParser) parseKeyValue(cfg *Config) error { - if p.currentSection == "" { - return errors.New("key-value pair before any section header") - } - - var key bytes.Buffer - - for { - ch, err := p.nextChar() - if errors.Is(err, io.EOF) { - break - } - - if err != nil { - return err - } - - if ch == '=' || ch == '\n' || isSpace(ch) { - p.unreadChar(ch) - - break - } - - if !isKeyChar(ch) { - return fmt.Errorf("invalid character in key: %q", ch) - } - - key.WriteByte(toLower(ch)) - } - - keyStr := key.String() - if len(keyStr) == 0 { - return errors.New("empty key name") - } - - if !isLetter(keyStr[0]) { - return errors.New("key must start with a letter") - } - - for { - ch, err := p.nextChar() - if errors.Is(err, io.EOF) { - cfg.entries = append(cfg.entries, ConfigEntry{ - Section: p.currentSection, - Subsection: p.currentSubsec, - Key: keyStr, - Kind: ValueValueless, - Value: "", - }) - - return nil - } - - if err != nil { - return err - } - - if ch == '\n' { - cfg.entries = append(cfg.entries, ConfigEntry{ - Section: p.currentSection, - Subsection: p.currentSubsec, - Key: keyStr, - Kind: ValueValueless, - Value: "", - }) - - return nil - } - - if ch == '#' || ch == ';' { - err := p.skipToEOL() - if err != nil && !errors.Is(err, io.EOF) { - return err - } - - cfg.entries = append(cfg.entries, ConfigEntry{ - Section: p.currentSection, - Subsection: p.currentSubsec, - Key: keyStr, - Kind: ValueValueless, - Value: "", - }) - - return nil - } - - if ch == '=' { - break - } - - if !isSpace(ch) { - return fmt.Errorf("unexpected character after key: %q", ch) - } - } - - value, err := p.parseValue() - if err != nil { - return err - } - - cfg.entries = append(cfg.entries, ConfigEntry{ - Section: p.currentSection, - Subsection: p.currentSubsec, - Key: keyStr, - Kind: ValueString, - Value: value, - }) - - return nil -} diff --git a/config/lookup.go b/config/lookup.go deleted file mode 100644 index 1f3c03fe..00000000 --- a/config/lookup.go +++ /dev/null @@ -1,45 +0,0 @@ -package config - -import "strings" - -// Lookup retrieves the first value for a given section, optional subsection, -// and key. -func (c *Config) Lookup(section, subsection, key string) LookupResult { - section = strings.ToLower(section) - - key = strings.ToLower(key) - for _, entry := range c.entries { - if strings.EqualFold(entry.Section, section) && - entry.Subsection == subsection && - strings.EqualFold(entry.Key, key) { - return LookupResult{ - Kind: entry.Kind, - Value: entry.Value, - } - } - } - - return LookupResult{Kind: ValueMissing} -} - -// LookupAll retrieves all values for a given section, optional subsection, -// and key. -func (c *Config) LookupAll(section, subsection, key string) []LookupResult { - section = strings.ToLower(section) - key = strings.ToLower(key) - - var values []LookupResult - - for _, entry := range c.entries { - if strings.EqualFold(entry.Section, section) && - entry.Subsection == subsection && - strings.EqualFold(entry.Key, key) { - values = append(values, LookupResult{ - Kind: entry.Kind, - Value: entry.Value, - }) - } - } - - return values -} diff --git a/config/parser.go b/config/parser.go deleted file mode 100644 index c56a68d5..00000000 --- a/config/parser.go +++ /dev/null @@ -1,88 +0,0 @@ -package config - -import ( - "bufio" - "errors" - "fmt" - "io" -) - -// ParseConfig reads and parses Git configuration entries from r. -func ParseConfig(r io.Reader) (*Config, error) { - parser := &configParser{ - reader: bufio.NewReader(r), - lineNum: 1, - } - - return parser.parse() -} - -type configParser struct { - reader *bufio.Reader - lineNum int - currentSection string - currentSubsec string - peeked byte - hasPeeked bool -} - -func (p *configParser) parse() (*Config, error) { - cfg := &Config{} - - err := p.skipBOM() - if err != nil { - return nil, err - } - - for { - ch, err := p.nextChar() - if errors.Is(err, io.EOF) { - break - } - - if err != nil { - return nil, err - } - - // Skip leading whitespace between entries. - if isWhitespace(ch) { - continue - } - - // Comments - if ch == '#' || ch == ';' { - err := p.skipToEOL() - if err != nil && !errors.Is(err, io.EOF) { - return nil, err - } - - continue - } - - // Section header - if ch == '[' { - err := p.parseSection() - if err != nil { - return nil, fmt.Errorf("furgit: config: line %d: %w", p.lineNum, err) - } - - continue - } - - // Key-value pair - if isLetter(ch) { - p.unreadChar(ch) - - err := p.parseKeyValue(cfg) - if err != nil { - return nil, fmt.Errorf("furgit: config: line %d: %w", p.lineNum, err) - } - - continue - } - - return nil, fmt.Errorf("furgit: config: line %d: unexpected character %q", p.lineNum, ch) - } - - return cfg, nil -} diff --git a/config/result.go b/config/result.go deleted file mode 100644 index 1f53edc5..00000000 --- a/config/result.go +++ /dev/null @@ -1,68 +0,0 @@ -package config - -import ( - "errors" - "fmt" -) - -// LookupResult is a value returned by Lookup/LookupAll. -type LookupResult struct { - Kind ValueKind - Value string -} - -// String returns the explicit string value. -func (r LookupResult) String() (string, error) { - switch r.Kind { - case ValueMissing: - return "", errors.New("missing config value") - case ValueValueless: - return "", errors.New("valueless config key") - case ValueString: - return r.Value, nil - default: - return "", fmt.Errorf("unknown value kind %d", r.Kind) - } -} - -// Bool interprets this lookup result using Git config boolean rules. -func (r LookupResult) Bool() (bool, error) { - switch r.Kind { - case ValueMissing: - return false, errors.New("missing config value") - case ValueValueless: - return true, nil - case ValueString: - return parseBool(r.Value) - default: - return false, fmt.Errorf("unknown value kind %d", r.Kind) - } -} - -// Int interprets this lookup result as a Git integer value. -func (r LookupResult) Int() (int, error) { - switch r.Kind { - case ValueMissing: - return 0, errors.New("missing config value") - case ValueValueless: - return 0, errors.New("valueless config key") - case ValueString: - return parseInt(r.Value) - default: - return 0, fmt.Errorf("unknown value kind %d", r.Kind) - } -} - -// Int64 interprets this lookup result as a Git int64 value. -func (r LookupResult) Int64() (int64, error) { - switch r.Kind { - case ValueMissing: - return 0, errors.New("missing config value") - case ValueValueless: - return 0, errors.New("valueless config key") - case ValueString: - return parseInt64(r.Value) - default: - return 0, fmt.Errorf("unknown value kind %d", r.Kind) - } -} diff --git a/config/section.go b/config/section.go deleted file mode 100644 index 66adf011..00000000 --- a/config/section.go +++ /dev/null @@ -1,41 +0,0 @@ -package config - -import ( - "bytes" - "errors" - "fmt" - "strings" -) - -func (p *configParser) parseSection() error { - var name bytes.Buffer - - for { - ch, err := p.nextChar() - if err != nil { - return errors.New("unexpected EOF in section header") - } - - if ch == ']' { - section := name.String() - if !isValidSection(section) { - return fmt.Errorf("invalid section name: %q", section) - } - - p.currentSection = strings.ToLower(section) - p.currentSubsec = "" - - return nil - } - - if isWhitespace(ch) { - return p.parseExtendedSection(&name) - } - - if !isKeyChar(ch) && ch != '.' { - return fmt.Errorf("invalid character in section name: %q", ch) - } - - name.WriteByte(toLower(ch)) - } -} diff --git a/config/testdata/fuzz/FuzzConfig/86abac337c758b6b b/config/testdata/fuzz/FuzzConfig/86abac337c758b6b deleted file mode 100644 index c4099bbf..00000000 --- a/config/testdata/fuzz/FuzzConfig/86abac337c758b6b +++ /dev/null @@ -1,3 +0,0 @@ -go test fuzz v1 -[]byte("[Core \"sub\"]BAre=\xfe") -string("Core.sub.BAre") diff --git a/config/testdata/fuzz/FuzzConfig/a76c07b1ae70ed94 b/config/testdata/fuzz/FuzzConfig/a76c07b1ae70ed94 deleted file mode 100644 index 1e8eeb79..00000000 --- a/config/testdata/fuzz/FuzzConfig/a76c07b1ae70ed94 +++ /dev/null @@ -1,3 +0,0 @@ -go test fuzz v1 -[]byte("[Core \"sub\"]\rBAre") -string("Core.sub.BAre") diff --git a/config/testdata/fuzz/FuzzConfig/c0718ca6bc57e0e2 b/config/testdata/fuzz/FuzzConfig/c0718ca6bc57e0e2 deleted file mode 100644 index 61580109..00000000 --- a/config/testdata/fuzz/FuzzConfig/c0718ca6bc57e0e2 +++ /dev/null @@ -1,3 +0,0 @@ -go test fuzz v1 -[]byte("[Core]BAre=\x00") -string("Core.BAre") diff --git a/config/testdata/fuzz/FuzzConfig/dc6f7dcd8aaa1cf7 b/config/testdata/fuzz/FuzzConfig/dc6f7dcd8aaa1cf7 deleted file mode 100644 index 6276303a..00000000 --- a/config/testdata/fuzz/FuzzConfig/dc6f7dcd8aaa1cf7 +++ /dev/null @@ -1,3 +0,0 @@ -go test fuzz v1 -[]byte("[Core]BAre") -string("Core.BAre") diff --git a/config/typed.go b/config/typed.go deleted file mode 100644 index 39eeb767..00000000 --- a/config/typed.go +++ /dev/null @@ -1,170 +0,0 @@ -package config - -import ( - "errors" - "fmt" - "math" - "strconv" - "strings" - - "codeberg.org/lindenii/furgit/internal/intconv" -) - -// ValueKind describes the presence and form of a config value. -type ValueKind uint8 - -const ( - // ValueMissing means the queried key does not exist. - ValueMissing ValueKind = iota - // ValueValueless means the key exists but has no "= " part. - ValueValueless - // ValueString means the key exists and has an explicit value (possibly ""). - ValueString -) - -func isValidSection(s string) bool { - if len(s) == 0 { - return false - } - - for i := range len(s) { - ch := s[i] - if !isLetter(ch) && !isDigit(ch) && ch != '-' && ch != '.' { - return false - } - } - - return true -} - -func isKeyChar(ch byte) bool { - return isLetter(ch) || isDigit(ch) || ch == '-' -} - -func parseBool(value string) (bool, error) { - switch { - case strings.EqualFold(value, "true"), - strings.EqualFold(value, "yes"), - strings.EqualFold(value, "on"): - return true, nil - case strings.EqualFold(value, "false"), - strings.EqualFold(value, "no"), - strings.EqualFold(value, "off"), - value == "": - return false, nil - } - - n, err := parseInt32(value) - if err != nil { - return false, fmt.Errorf("invalid boolean value %q", value) - } - - return n != 0, nil -} - -func parseInt32(value string) (int32, error) { - n64, err := parseInt64WithMax(value, math.MaxInt32) - if err != nil { - return 0, err - } - - return intconv.Int64ToInt32(n64) -} - -func parseInt(value string) (int, error) { - n64, err := parseInt64WithMax(value, int64(int(^uint(0)>>1))) - if err != nil { - return 0, err - } - - return int(n64), nil -} - -func parseInt64(value string) (int64, error) { - return parseInt64WithMax(value, int64(^uint64(0)>>1)) -} - -func parseInt64WithMax(value string, maxValue int64) (int64, error) { - if value == "" { - return 0, errors.New("empty value") - } - - trimmed := strings.TrimLeft(value, " \t\n\r\f\v") - if trimmed == "" { - return 0, errors.New("empty value") - } - - numPart := trimmed - factor := int64(1) - - if last := trimmed[len(trimmed)-1]; last == 'k' || last == 'K' || last == 'm' || last == 'M' || last == 'g' || last == 'G' { - switch toLower(last) { - case 'k': - factor = 1024 - case 'm': - factor = 1024 * 1024 - case 'g': - factor = 1024 * 1024 * 1024 - } - - numPart = trimmed[:len(trimmed)-1] - } - - if numPart == "" { - return 0, errors.New("missing integer value") - } - - n, err := strconv.ParseInt(numPart, 0, 64) - if err != nil { - return 0, err - } - - intMax := maxValue - intMin := -maxValue - 1 - - if n > 0 && n > intMax/factor { - return 0, errors.New("integer overflow") - } - - if n < 0 && n < intMin/factor { - return 0, errors.New("integer overflow") - } - - n *= factor - - return n, nil -} - -func truncateAtNUL(value string) string { - for i := range len(value) { - if value[i] == 0 { - return value[:i] - } - } - - return value -} - -func isSpace(ch byte) bool { - return ch == ' ' || ch == '\t' -} - -func isWhitespace(ch byte) bool { - return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' || ch == '\v' || ch == '\f' -} - -func isLetter(ch byte) bool { - return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') -} - -func isDigit(ch byte) bool { - return ch >= '0' && ch <= '9' -} - -func toLower(ch byte) byte { - if ch >= 'A' && ch <= 'Z' { - return ch + ('a' - 'A') - } - - return ch -} diff --git a/config/value.go b/config/value.go deleted file mode 100644 index 3ade9c16..00000000 --- a/config/value.go +++ /dev/null @@ -1,111 +0,0 @@ -package config - -import ( - "bytes" - "errors" - "fmt" - "io" -) - -func (p *configParser) parseValue() (string, error) { - var ( - value bytes.Buffer - inQuote bool - inComment bool - ) - - trimLen := 0 - - for { - ch, err := p.nextChar() - if errors.Is(err, io.EOF) { - if inQuote { - return "", errors.New("unexpected EOF in quoted value") - } - - if trimLen > 0 { - return truncateAtNUL(value.String()[:trimLen]), nil - } - - return truncateAtNUL(value.String()), nil - } - - if err != nil { - return "", err - } - - if ch == '\n' { - if inQuote { - return "", errors.New("newline in quoted value") - } - - if trimLen > 0 { - return truncateAtNUL(value.String()[:trimLen]), nil - } - - return truncateAtNUL(value.String()), nil - } - - if inComment { - continue - } - - if isWhitespace(ch) && !inQuote { - if trimLen == 0 && value.Len() > 0 { - trimLen = value.Len() - } - - if value.Len() > 0 { - value.WriteByte(ch) - } - - continue - } - - if !inQuote && (ch == '#' || ch == ';') { - inComment = true - - continue - } - - if trimLen > 0 { - trimLen = 0 - } - - if ch == '\\' { - next, err := p.nextChar() - if errors.Is(err, io.EOF) { - return "", errors.New("unexpected EOF after backslash") - } - - if err != nil { - return "", err - } - - switch next { - case '\n': - continue - case 'n': - value.WriteByte('\n') - case 't': - value.WriteByte('\t') - case 'b': - value.WriteByte('\b') - case '\\', '"': - value.WriteByte(next) - default: - return "", fmt.Errorf("invalid escape sequence: \\%c", next) - } - - continue - } - - if ch == '"' { - inQuote = !inQuote - - continue - } - - value.WriteByte(ch) - } -} diff --git a/diff/diff.go b/diff/diff.go deleted file mode 100644 index 74b62119..00000000 --- a/diff/diff.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package diff encapsulates diff-providing subpackages for direct use. -package diff diff --git a/diff/lines/chunk.go b/diff/lines/chunk.go deleted file mode 100644 index b5856d29..00000000 --- a/diff/lines/chunk.go +++ /dev/null @@ -1,20 +0,0 @@ -package lines - -// Chunk represents a contiguous region of lines categorized -// as unchanged, deleted, or added. -type Chunk struct { - Kind ChunkKind - Data []byte -} - -// ChunkKind enumerates the type of diff chunk. -type ChunkKind int - -const ( - // ChunkKindUnchanged represents an unchanged diff chunk. - ChunkKindUnchanged ChunkKind = iota - // ChunkKindDeleted represents a deleted diff chunk. - ChunkKindDeleted - // ChunkKindAdded represents an added diff chunk. - ChunkKindAdded -) diff --git a/diff/lines/diff.go b/diff/lines/diff.go deleted file mode 100644 index 6f958923..00000000 --- a/diff/lines/diff.go +++ /dev/null @@ -1,231 +0,0 @@ -// Package lines provides routines to perform line-based diffs. -package lines - -import "bytes" - -// Diff performs a line-based diff. -// Lines are bytes up to and including '\n' (final line may lack '\n'). -func Diff(oldB, newB []byte) ([]Chunk, error) { //nolint:maintidx - type lineRef struct { - base []byte - start int - end int - } - - split := func(b []byte) []lineRef { - if len(b) == 0 { - return nil - } - - var res []lineRef - - start := 0 - - for i := range b { - if b[i] == '\n' { - res = append(res, lineRef{base: b, start: start, end: i + 1}) - start = i + 1 - } - } - - if start < len(b) { - res = append(res, lineRef{base: b, start: start, end: len(b)}) - } - - return res - } - - oldLines := split(oldB) - newLines := split(newB) - - n := len(oldLines) - - m := len(newLines) - if n == 0 && m == 0 { - return nil, nil - } - - idOf := make(map[string]int) - nextID := 0 - oldIDs := make([]int, n) - - for i, ln := range oldLines { - key := string(ln.base[ln.start:ln.end]) - - id, ok := idOf[key] - if !ok { - id = nextID - idOf[key] = id - nextID++ - } - - oldIDs[i] = id - } - - newIDs := make([]int, m) - - for i, ln := range newLines { - key := string(ln.base[ln.start:ln.end]) - - id, ok := idOf[key] - if !ok { - id = nextID - idOf[key] = id - nextID++ - } - - newIDs[i] = id - } - - maxDist := n + m - offset := maxDist - trace := make([][]int, 0, maxDist+1) - - Vprev := make([]int, 2*maxDist+1) - for i := range Vprev { - Vprev[i] = -1 - } - - x0 := 0 - - y0 := 0 - for x0 < n && y0 < m && oldIDs[x0] == newIDs[y0] { - x0++ - y0++ - } - - Vprev[offset+0] = x0 - trace = append(trace, append([]int(nil), Vprev...)) - - found := x0 >= n && y0 >= m - - for D := 1; D <= maxDist && !found; D++ { - V := make([]int, 2*maxDist+1) - for i := range V { - V[i] = -1 - } - - for k := -D; k <= D; k += 2 { - var x int - if k == -D || (k != D && Vprev[offset+(k-1)] < Vprev[offset+(k+1)]) { - x = Vprev[offset+(k+1)] - } else { - x = Vprev[offset+(k-1)] + 1 - } - - y := x - k - - for x < n && y < m && oldIDs[x] == newIDs[y] { - x++ - y++ - } - - V[offset+k] = x - - if x >= n && y >= m { - trace = append(trace, V) - found = true - - break - } - } - - if !found { - trace = append(trace, V) - Vprev = V - } - } - - type edit struct { - kind ChunkKind - lineref lineRef - } - - revEdits := make([]edit, 0, n+m) - - x := n - - y := m - for D := len(trace) - 1; D >= 0; D-- { - k := x - y - - var ( - prevK int - prevX int - prevY int - ) - - if D > 0 { - prevV := trace[D-1] - if k == -D || (k != D && prevV[offset+(k-1)] < prevV[offset+(k+1)]) { - prevK = k + 1 - } else { - prevK = k - 1 - } - - prevX = prevV[offset+prevK] - prevY = prevX - prevK - } - - for x > prevX && y > prevY { - x-- - y-- - - revEdits = append(revEdits, edit{kind: ChunkKindUnchanged, lineref: oldLines[x]}) - } - - if D == 0 { - break - } - - if x == prevX { - y-- - revEdits = append(revEdits, edit{kind: ChunkKindAdded, lineref: newLines[y]}) - } else { - x-- - revEdits = append(revEdits, edit{kind: ChunkKindDeleted, lineref: oldLines[x]}) - } - } - - for i, j := 0, len(revEdits)-1; i < j; i, j = i+1, j-1 { - revEdits[i], revEdits[j] = revEdits[j], revEdits[i] - } - - var out []Chunk - - type meta struct { - base []byte - start int - end int - } - - var metas []meta - - for _, e := range revEdits { - curBase := e.lineref.base - curStart := e.lineref.start - curEnd := e.lineref.end - - if len(out) == 0 || out[len(out)-1].Kind != e.kind { - out = append(out, Chunk{Kind: e.kind, Data: curBase[curStart:curEnd]}) - metas = append(metas, meta{base: curBase, start: curStart, end: curEnd}) - - continue - } - - lastIdx := len(out) - 1 - lastMeta := metas[lastIdx] - - if bytes.Equal(lastMeta.base, curBase) && lastMeta.end == curStart { - metas[lastIdx].end = curEnd - out[lastIdx].Data = curBase[metas[lastIdx].start:metas[lastIdx].end] - - continue - } - - out[lastIdx].Data = append(out[lastIdx].Data, curBase[curStart:curEnd]...) - metas[lastIdx] = meta{base: nil, start: 0, end: 0} - } - - return out, nil -} diff --git a/diff/lines/diff_test.go b/diff/lines/diff_test.go deleted file mode 100644 index c5d5be9f..00000000 --- a/diff/lines/diff_test.go +++ /dev/null @@ -1,333 +0,0 @@ -package lines_test - -import ( - "bytes" - "strconv" - "strings" - "testing" - - "codeberg.org/lindenii/furgit/diff/lines" -) - -func TestDiff(t *testing.T) { //nolint:maintidx - t.Parallel() - - tests := []struct { - name string - oldInput string - newInput string - expected []lines.Chunk - }{ - { - name: "empty inputs produce no chunks", - oldInput: "", - newInput: "", - expected: []lines.Chunk{}, - }, - { - name: "only additions", - oldInput: "", - newInput: "alpha\nbeta\n", - expected: []lines.Chunk{ - {Kind: lines.ChunkKindAdded, Data: []byte("alpha\nbeta\n")}, - }, - }, - { - name: "only deletions", - oldInput: "alpha\nbeta\n", - newInput: "", - expected: []lines.Chunk{ - {Kind: lines.ChunkKindDeleted, Data: []byte("alpha\nbeta\n")}, - }, - }, - { - name: "unchanged content is grouped", - oldInput: "same\nlines\n", - newInput: "same\nlines\n", - expected: []lines.Chunk{ - {Kind: lines.ChunkKindUnchanged, Data: []byte("same\nlines\n")}, - }, - }, - { - name: "insertion in the middle", - oldInput: "a\nb\nc\n", - newInput: "a\nb\nX\nc\n", - expected: []lines.Chunk{ - {Kind: lines.ChunkKindUnchanged, Data: []byte("a\nb\n")}, - {Kind: lines.ChunkKindAdded, Data: []byte("X\n")}, - {Kind: lines.ChunkKindUnchanged, Data: []byte("c\n")}, - }, - }, - { - name: "replacement without trailing newline", - oldInput: "first\nsecond", - newInput: "first\nsecond\n", - expected: []lines.Chunk{ - {Kind: lines.ChunkKindUnchanged, Data: []byte("first\n")}, - {Kind: lines.ChunkKindDeleted, Data: []byte("second")}, - {Kind: lines.ChunkKindAdded, Data: []byte("second\n")}, - }, - }, - { - name: "line replacement", - oldInput: "a\nb\nc\n", - newInput: "a\nB\nc\n", - expected: []lines.Chunk{ - {Kind: lines.ChunkKindUnchanged, Data: []byte("a\n")}, - {Kind: lines.ChunkKindDeleted, Data: []byte("b\n")}, - {Kind: lines.ChunkKindAdded, Data: []byte("B\n")}, - {Kind: lines.ChunkKindUnchanged, Data: []byte("c\n")}, - }, - }, - { - name: "swap adjacent lines", - oldInput: "A\nB\n", - newInput: "B\nA\n", - expected: []lines.Chunk{ - {Kind: lines.ChunkKindDeleted, Data: []byte("A\n")}, - {Kind: lines.ChunkKindUnchanged, Data: []byte("B\n")}, - {Kind: lines.ChunkKindAdded, Data: []byte("A\n")}, - }, - }, - { - name: "indentation change is a full line replacement", - oldInput: "func main() {\n\treturn\n}\n", - newInput: "func main() {\n return\n}\n", - expected: []lines.Chunk{ - {Kind: lines.ChunkKindUnchanged, Data: []byte("func main() {\n")}, - {Kind: lines.ChunkKindDeleted, Data: []byte("\treturn\n")}, - {Kind: lines.ChunkKindAdded, Data: []byte(" return\n")}, - {Kind: lines.ChunkKindUnchanged, Data: []byte("}\n")}, - }, - }, - { - name: "commenting out lines", - oldInput: "code\n", - newInput: "// code\n", - expected: []lines.Chunk{ - {Kind: lines.ChunkKindDeleted, Data: []byte("code\n")}, - {Kind: lines.ChunkKindAdded, Data: []byte("// code\n")}, - }, - }, - { - name: "reducing repeating lines", - oldInput: "log\nlog\nlog\n", - newInput: "log\n", - expected: []lines.Chunk{ - {Kind: lines.ChunkKindUnchanged, Data: []byte("log\n")}, - {Kind: lines.ChunkKindDeleted, Data: []byte("log\nlog\n")}, - }, - }, - { - name: "expanding repeating lines", - oldInput: "tick\n", - newInput: "tick\ntick\ntick\n", - expected: []lines.Chunk{ - {Kind: lines.ChunkKindUnchanged, Data: []byte("tick\n")}, - {Kind: lines.ChunkKindAdded, Data: []byte("tick\ntick\n")}, - }, - }, - { - name: "interleaved modifications", - oldInput: "keep\nchange\nkeep\nchange\n", - newInput: "keep\nfixed\nkeep\nfixed\n", - expected: []lines.Chunk{ - {Kind: lines.ChunkKindUnchanged, Data: []byte("keep\n")}, - {Kind: lines.ChunkKindDeleted, Data: []byte("change\n")}, - {Kind: lines.ChunkKindAdded, Data: []byte("fixed\n")}, - {Kind: lines.ChunkKindUnchanged, Data: []byte("keep\n")}, - {Kind: lines.ChunkKindDeleted, Data: []byte("change\n")}, - {Kind: lines.ChunkKindAdded, Data: []byte("fixed\n")}, - }, - }, - { - name: "large common header and footer", - oldInput: "header\nheader\nheader\nOLD\nfooter\nfooter\n", - newInput: "header\nheader\nheader\nNEW\nfooter\nfooter\n", - expected: []lines.Chunk{ - {Kind: lines.ChunkKindUnchanged, Data: []byte("header\nheader\nheader\n")}, - {Kind: lines.ChunkKindDeleted, Data: []byte("OLD\n")}, - {Kind: lines.ChunkKindAdded, Data: []byte("NEW\n")}, - {Kind: lines.ChunkKindUnchanged, Data: []byte("footer\nfooter\n")}, - }, - }, - { - name: "completely different content", - oldInput: "apple\nbanana\n", - newInput: "cherry\ndate\n", - expected: []lines.Chunk{ - {Kind: lines.ChunkKindDeleted, Data: []byte("apple\nbanana\n")}, - {Kind: lines.ChunkKindAdded, Data: []byte("cherry\ndate\n")}, - }, - }, - { - name: "unicode and emoji changes", - oldInput: "Hello 🌍\nYay\n", - newInput: "Hello 🌎\nYay\n", - expected: []lines.Chunk{ - {Kind: lines.ChunkKindDeleted, Data: []byte("Hello 🌍\n")}, - {Kind: lines.ChunkKindAdded, Data: []byte("Hello 🌎\n")}, - {Kind: lines.ChunkKindUnchanged, Data: []byte("Yay\n")}, - }, - }, - { - name: "binary data with embedded newlines", - oldInput: "\x00\x01\n\x02\x03\n", - newInput: "\x00\x01\n\x02\xFF\n", - expected: []lines.Chunk{ - {Kind: lines.ChunkKindUnchanged, Data: []byte("\x00\x01\n")}, - {Kind: lines.ChunkKindDeleted, Data: []byte("\x02\x03\n")}, - {Kind: lines.ChunkKindAdded, Data: []byte("\x02\xFF\n")}, - }, - }, - { - name: "adding trailing newline to last line", - oldInput: "Line 1\nLine 2", - newInput: "Line 1\nLine 2\n", - expected: []lines.Chunk{ - {Kind: lines.ChunkKindUnchanged, Data: []byte("Line 1\n")}, - {Kind: lines.ChunkKindDeleted, Data: []byte("Line 2")}, - {Kind: lines.ChunkKindAdded, Data: []byte("Line 2\n")}, - }, - }, - { - name: "removing trailing newline", - oldInput: "A\nB\n", - newInput: "A\nB", - expected: []lines.Chunk{ - {Kind: lines.ChunkKindUnchanged, Data: []byte("A\n")}, - {Kind: lines.ChunkKindDeleted, Data: []byte("B\n")}, - {Kind: lines.ChunkKindAdded, Data: []byte("B")}, - }, - }, - { - name: "inserting blank lines", - oldInput: "A\nB\n", - newInput: "A\n\n\nB\n", - expected: []lines.Chunk{ - {Kind: lines.ChunkKindUnchanged, Data: []byte("A\n")}, - {Kind: lines.ChunkKindAdded, Data: []byte("\n\n")}, - {Kind: lines.ChunkKindUnchanged, Data: []byte("B\n")}, - }, - }, - { - name: "collapsing blank lines", - oldInput: "A\n\n\n\nB\n", - newInput: "A\nB\n", - expected: []lines.Chunk{ - {Kind: lines.ChunkKindUnchanged, Data: []byte("A\n")}, - {Kind: lines.ChunkKindDeleted, Data: []byte("\n\n\n")}, - {Kind: lines.ChunkKindUnchanged, Data: []byte("B\n")}, - }, - }, - { - name: "case sensitivity check", - oldInput: "FOO\nbar\n", - newInput: "foo\nbar\n", - expected: []lines.Chunk{ - {Kind: lines.ChunkKindDeleted, Data: []byte("FOO\n")}, - {Kind: lines.ChunkKindAdded, Data: []byte("foo\n")}, - {Kind: lines.ChunkKindUnchanged, Data: []byte("bar\n")}, - }, - }, - { - name: "partial line match is full mismatch", - oldInput: "The quick brown fox\n", - newInput: "The quick brown fox jumps\n", - expected: []lines.Chunk{ - {Kind: lines.ChunkKindDeleted, Data: []byte("The quick brown fox\n")}, - {Kind: lines.ChunkKindAdded, Data: []byte("The quick brown fox jumps\n")}, - }, - }, - { - name: "inserting middle content", - oldInput: "Top\nBottom\n", - newInput: "Top\nMiddle\nBottom\n", - expected: []lines.Chunk{ - {Kind: lines.ChunkKindUnchanged, Data: []byte("Top\n")}, - {Kind: lines.ChunkKindAdded, Data: []byte("Middle\n")}, - {Kind: lines.ChunkKindUnchanged, Data: []byte("Bottom\n")}, - }, - }, - { - name: "block move simulated", - oldInput: "BlockA\nBlockB\nBlockC\n", - newInput: "BlockA\nBlockC\nBlockB\n", - expected: []lines.Chunk{ - {Kind: lines.ChunkKindUnchanged, Data: []byte("BlockA\n")}, - {Kind: lines.ChunkKindDeleted, Data: []byte("BlockB\n")}, - {Kind: lines.ChunkKindUnchanged, Data: []byte("BlockC\n")}, - {Kind: lines.ChunkKindAdded, Data: []byte("BlockB\n")}, - }, - }, - { - name: "alternating additions", - oldInput: "A\nB\nC\n", - newInput: "A\n1\nB\n2\nC\n", - expected: []lines.Chunk{ - {Kind: lines.ChunkKindUnchanged, Data: []byte("A\n")}, - {Kind: lines.ChunkKindAdded, Data: []byte("1\n")}, - {Kind: lines.ChunkKindUnchanged, Data: []byte("B\n")}, - {Kind: lines.ChunkKindAdded, Data: []byte("2\n")}, - {Kind: lines.ChunkKindUnchanged, Data: []byte("C\n")}, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - chunks, err := lines.Diff([]byte(tt.oldInput), []byte(tt.newInput)) - if err != nil { - t.Fatalf("Diff returned error: %v", err) - } - - if len(chunks) != len(tt.expected) { - t.Fatalf("expected %d chunks, got %d: %s", len(tt.expected), len(chunks), formatChunks(chunks)) - } - - for i := range tt.expected { - if chunks[i].Kind != tt.expected[i].Kind { - t.Fatalf("chunk %d kind mismatch: got %v, want %v; chunks: %s", i, chunks[i].Kind, tt.expected[i].Kind, formatChunks(chunks)) - } - - if !bytes.Equal(chunks[i].Data, tt.expected[i].Data) { - t.Fatalf("chunk %d data mismatch: got %q, want %q; chunks: %s", i, string(chunks[i].Data), string(tt.expected[i].Data), formatChunks(chunks)) - } - } - }) - } -} - -func formatChunks(chunks []lines.Chunk) string { - var b strings.Builder - b.WriteByte('[') - - for i, chunk := range chunks { - if i > 0 { - b.WriteString(", ") - } - - b.WriteString(chunkKindName(chunk.Kind)) - b.WriteByte(':') - b.WriteString(strconv.Quote(string(chunk.Data))) - } - - b.WriteByte(']') - - return b.String() -} - -func chunkKindName(kind lines.ChunkKind) string { - switch kind { - case lines.ChunkKindUnchanged: - return "U" - case lines.ChunkKindDeleted: - return "D" - case lines.ChunkKindAdded: - return "A" - default: - return "?" - } -} diff --git a/diff/trees/diff.go b/diff/trees/diff.go deleted file mode 100644 index 0f3cf1f2..00000000 --- a/diff/trees/diff.go +++ /dev/null @@ -1,22 +0,0 @@ -// Package trees provides recursive diffs between Git tree objects. -package trees - -import ( - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/tree" -) - -// Diff compares two trees and returns recursive differences. -// -// readTree is used to lazily load child trees by object ID when recursion -// reaches directory entries. -func Diff(a, b *tree.Tree, readTree func(objectid.ObjectID) (*tree.Tree, error)) ([]Entry, error) { - var out []Entry - - err := diffRecursive(a, b, nil, readTree, &out) - if err != nil { - return nil, err - } - - return out, nil -} diff --git a/diff/trees/diff_recursive.go b/diff/trees/diff_recursive.go deleted file mode 100644 index 98848b24..00000000 --- a/diff/trees/diff_recursive.go +++ /dev/null @@ -1,176 +0,0 @@ -package trees - -import ( - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/tree" -) - -func diffRecursive(a, b *tree.Tree, prefix []byte, readTree func(objectid.ObjectID) (*tree.Tree, error), out *[]Entry) error { - if a == nil && b == nil { - return nil - } - - if a == nil { - for i := range b.Entries { - entry := &b.Entries[i] - full := joinPath(prefix, entry.Name) - - *out = append(*out, Entry{Path: full, Kind: EntryKindAdded, Old: nil, New: entry}) - if entry.Mode != tree.FileModeDir { - continue - } - - sub, err := readTree(entry.ID) - if err != nil { - return err - } - - err = diffRecursive(nil, sub, full, readTree, out) - if err != nil { - return err - } - } - - return nil - } - - if b == nil { - for i := range a.Entries { - entry := &a.Entries[i] - full := joinPath(prefix, entry.Name) - - *out = append(*out, Entry{Path: full, Kind: EntryKindDeleted, Old: entry, New: nil}) - if entry.Mode != tree.FileModeDir { - continue - } - - sub, err := readTree(entry.ID) - if err != nil { - return err - } - - err = diffRecursive(sub, nil, full, readTree, out) - if err != nil { - return err - } - } - - return nil - } - - i := 0 - - j := 0 - for i < len(a.Entries) && j < len(b.Entries) { - left := &a.Entries[i] - right := &b.Entries[j] - - cmp := tree.TreeEntryNameCompare( - left.Name, - left.Mode, - right.Name, - right.Mode == tree.FileModeDir, - ) - switch { - case cmp < 0: - full := joinPath(prefix, left.Name) - - *out = append(*out, Entry{Path: full, Kind: EntryKindDeleted, Old: left, New: nil}) - if left.Mode == tree.FileModeDir { - sub, err := readTree(left.ID) - if err != nil { - return err - } - - err = diffRecursive(sub, nil, full, readTree, out) - if err != nil { - return err - } - } - - i++ - case cmp > 0: - full := joinPath(prefix, right.Name) - - *out = append(*out, Entry{Path: full, Kind: EntryKindAdded, Old: nil, New: right}) - if right.Mode == tree.FileModeDir { - sub, err := readTree(right.ID) - if err != nil { - return err - } - - err = diffRecursive(nil, sub, full, readTree, out) - if err != nil { - return err - } - } - - j++ - default: - full := joinPath(prefix, left.Name) - - modified := left.Mode != right.Mode || left.ID != right.ID - if modified { - *out = append(*out, Entry{Path: full, Kind: EntryKindModified, Old: left, New: right}) - } - - if left.Mode == tree.FileModeDir && right.Mode == tree.FileModeDir && left.ID != right.ID { - leftSub, err := readTree(left.ID) - if err != nil { - return err - } - - rightSub, err := readTree(right.ID) - if err != nil { - return err - } - - err = diffRecursive(leftSub, rightSub, full, readTree, out) - if err != nil { - return err - } - } - - i++ - j++ - } - } - - for ; i < len(a.Entries); i++ { - left := &a.Entries[i] - full := joinPath(prefix, left.Name) - - *out = append(*out, Entry{Path: full, Kind: EntryKindDeleted, Old: left, New: nil}) - if left.Mode == tree.FileModeDir { - sub, err := readTree(left.ID) - if err != nil { - return err - } - - err = diffRecursive(sub, nil, full, readTree, out) - if err != nil { - return err - } - } - } - - for ; j < len(b.Entries); j++ { - right := &b.Entries[j] - full := joinPath(prefix, right.Name) - - *out = append(*out, Entry{Path: full, Kind: EntryKindAdded, Old: nil, New: right}) - if right.Mode == tree.FileModeDir { - sub, err := readTree(right.ID) - if err != nil { - return err - } - - err = diffRecursive(nil, sub, full, readTree, out) - if err != nil { - return err - } - } - } - - return nil -} diff --git a/diff/trees/diff_test.go b/diff/trees/diff_test.go deleted file mode 100644 index 50989a4c..00000000 --- a/diff/trees/diff_test.go +++ /dev/null @@ -1,255 +0,0 @@ -package trees_test - -import ( - "errors" - "testing" - - "codeberg.org/lindenii/furgit/diff/trees" - "codeberg.org/lindenii/furgit/internal/testgit" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/store/loose" - "codeberg.org/lindenii/furgit/object/tree" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -func TestDiffComplexNestedChanges(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - repo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: false}) - - writeTestFile(t, repo, "README.md", "initial readme\n") - writeTestFile(t, repo, "unchanged.txt", "leave me as-is\n") - writeTestFile(t, repo, "dir/file_a.txt", "alpha v1\n") - writeTestFile(t, repo, "dir/nested/file_b.txt", "beta v1\n") - writeTestFile(t, repo, "dir/nested/deeper/file_c.txt", "gamma v1\n") - writeTestFile(t, repo, "dir/nested/deeper/old.txt", "old branch\n") - writeTestFile(t, repo, "treeB/legacy.txt", "legacy root\n") - writeTestFile(t, repo, "treeB/sub/retired.txt", "retired\n") - - repo.Run(t, "add", ".") - baseTreeID := parseID(t, algo, repo.Run(t, "write-tree")) - - writeTestFile(t, repo, "README.md", "updated readme\n") - repo.Run(t, "rm", "-f", "dir/file_a.txt") - writeTestFile(t, repo, "dir/nested/file_b.txt", "beta v2\n") - repo.Run(t, "rm", "-f", "dir/nested/deeper/old.txt") - writeTestFile(t, repo, "dir/nested/deeper/new.txt", "new branch entry\n") - writeTestFile(t, repo, "dir/nested/deeper/branch/info.md", "branch info\n") - writeTestFile(t, repo, "dir/nested/deeper/branch/subbranch/leaf.txt", "leaf data\n") - writeTestFile(t, repo, "dir/nested/deeper/branch/subbranch/deep/final.txt", "final artifact\n") - writeTestFile(t, repo, "dir/newchild.txt", "brand new sibling\n") - repo.Run(t, "rm", "-r", "-f", "treeB") - writeTestFile(t, repo, "features/alpha/README.md", "alpha docs\n") - writeTestFile(t, repo, "features/alpha/beta/gamma.txt", "gamma payload\n") - writeTestFile(t, repo, "modules/v2/core/main.go", "package core\n") - writeTestFile(t, repo, "root_addition.txt", "root level file\n") - - repo.Run(t, "add", ".") - updatedTreeID := parseID(t, algo, repo.Run(t, "write-tree")) - - store := openLooseStore(t, repo, algo) - readTree := makeReadTree(t, store, algo) - baseTree := mustReadTree(t, readTree, baseTreeID) - updatedTree := mustReadTree(t, readTree, updatedTreeID) - - diffs, err := trees.Diff(baseTree, updatedTree, readTree) - if err != nil { - t.Fatalf("trees.Diff: %v", err) - } - - expected := map[string]diffExpectation{ - "README.md": {kind: trees.EntryKindModified}, - "dir": {kind: trees.EntryKindModified}, - "dir/file_a.txt": {kind: trees.EntryKindDeleted, newNil: true}, - "dir/newchild.txt": {kind: trees.EntryKindAdded, oldNil: true}, - "dir/nested": {kind: trees.EntryKindModified}, - "dir/nested/file_b.txt": {kind: trees.EntryKindModified}, - "dir/nested/deeper": {kind: trees.EntryKindModified}, - "dir/nested/deeper/old.txt": {kind: trees.EntryKindDeleted, newNil: true}, - "dir/nested/deeper/new.txt": {kind: trees.EntryKindAdded, oldNil: true}, - "dir/nested/deeper/branch": {kind: trees.EntryKindAdded, oldNil: true}, - "dir/nested/deeper/branch/info.md": {kind: trees.EntryKindAdded, oldNil: true}, - "dir/nested/deeper/branch/subbranch": {kind: trees.EntryKindAdded, oldNil: true}, - "dir/nested/deeper/branch/subbranch/leaf.txt": {kind: trees.EntryKindAdded, oldNil: true}, - "dir/nested/deeper/branch/subbranch/deep": {kind: trees.EntryKindAdded, oldNil: true}, - "dir/nested/deeper/branch/subbranch/deep/final.txt": { - kind: trees.EntryKindAdded, - oldNil: true, - }, - "features": {kind: trees.EntryKindAdded, oldNil: true}, - "features/alpha": {kind: trees.EntryKindAdded, oldNil: true}, - "features/alpha/README.md": {kind: trees.EntryKindAdded, oldNil: true}, - "features/alpha/beta": {kind: trees.EntryKindAdded, oldNil: true}, - "features/alpha/beta/gamma.txt": {kind: trees.EntryKindAdded, oldNil: true}, - "modules": {kind: trees.EntryKindAdded, oldNil: true}, - "modules/v2": {kind: trees.EntryKindAdded, oldNil: true}, - "modules/v2/core": {kind: trees.EntryKindAdded, oldNil: true}, - "modules/v2/core/main.go": {kind: trees.EntryKindAdded, oldNil: true}, - "root_addition.txt": {kind: trees.EntryKindAdded, oldNil: true}, - "treeB": {kind: trees.EntryKindDeleted, newNil: true}, - "treeB/legacy.txt": {kind: trees.EntryKindDeleted, newNil: true}, - "treeB/sub": {kind: trees.EntryKindDeleted, newNil: true}, - "treeB/sub/retired.txt": {kind: trees.EntryKindDeleted, newNil: true}, - } - - checkDiffs(t, diffs, expected) - }) -} - -func TestDiffDirectoryAddDeleteDeep(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - repo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: false}) - - writeTestFile(t, repo, "old_dir/old.txt", "stale directory\n") - writeTestFile(t, repo, "old_dir/sub1/legacy.txt", "legacy path\n") - writeTestFile(t, repo, "old_dir/sub1/nested/end.txt", "legacy end\n") - - repo.Run(t, "add", ".") - originalTreeID := parseID(t, algo, repo.Run(t, "write-tree")) - - repo.Run(t, "rm", "-r", "-f", "old_dir") - writeTestFile(t, repo, "fresh/alpha/beta/new.txt", "brand new directory\n") - writeTestFile(t, repo, "fresh/alpha/docs/note.md", "docs note\n") - writeTestFile(t, repo, "fresh/alpha/beta/gamma/delta.txt", "delta payload\n") - - repo.Run(t, "add", ".") - nextTreeID := parseID(t, algo, repo.Run(t, "write-tree")) - - store := openLooseStore(t, repo, algo) - readTree := makeReadTree(t, store, algo) - originalTree := mustReadTree(t, readTree, originalTreeID) - nextTree := mustReadTree(t, readTree, nextTreeID) - - diffs, err := trees.Diff(originalTree, nextTree, readTree) - if err != nil { - t.Fatalf("trees.Diff: %v", err) - } - - expected := map[string]diffExpectation{ - "fresh": {kind: trees.EntryKindAdded, oldNil: true}, - "fresh/alpha": {kind: trees.EntryKindAdded, oldNil: true}, - "fresh/alpha/beta": {kind: trees.EntryKindAdded, oldNil: true}, - "fresh/alpha/beta/new.txt": {kind: trees.EntryKindAdded, oldNil: true}, - "fresh/alpha/beta/gamma": {kind: trees.EntryKindAdded, oldNil: true}, - "fresh/alpha/beta/gamma/delta.txt": {kind: trees.EntryKindAdded, oldNil: true}, - "fresh/alpha/docs": {kind: trees.EntryKindAdded, oldNil: true}, - "fresh/alpha/docs/note.md": {kind: trees.EntryKindAdded, oldNil: true}, - "old_dir": {kind: trees.EntryKindDeleted, newNil: true}, - "old_dir/old.txt": {kind: trees.EntryKindDeleted, newNil: true}, - "old_dir/sub1": {kind: trees.EntryKindDeleted, newNil: true}, - "old_dir/sub1/legacy.txt": {kind: trees.EntryKindDeleted, newNil: true}, - "old_dir/sub1/nested": {kind: trees.EntryKindDeleted, newNil: true}, - "old_dir/sub1/nested/end.txt": {kind: trees.EntryKindDeleted, newNil: true}, - } - - checkDiffs(t, diffs, expected) - }) -} - -type diffExpectation struct { - kind trees.EntryKind - oldNil bool - newNil bool -} - -func writeTestFile(t *testing.T, repo *testgit.TestRepo, path, data string) { - t.Helper() - - repo.WriteFileAll(t, path, []byte(data), 0o755, 0o644) -} - -func openLooseStore(t *testing.T, repo *testgit.TestRepo, algo objectid.Algorithm) *loose.Store { - t.Helper() - - root := repo.OpenObjectsRoot(t) - - store, err := loose.New(root, algo) - if err != nil { - t.Fatalf("loose.New: %v", err) - } - - t.Cleanup(func() { _ = store.Close() }) - - return store -} - -func makeReadTree(t *testing.T, store *loose.Store, algo objectid.Algorithm) func(objectid.ObjectID) (*tree.Tree, error) { - t.Helper() - - return func(id objectid.ObjectID) (*tree.Tree, error) { - ty, content, err := store.ReadBytesContent(id) - if err != nil { - return nil, err - } - - if ty != objecttype.TypeTree { - return nil, errors.New("diff/trees test: object is not a tree") - } - - return tree.Parse(content, algo) - } -} - -func mustReadTree(t *testing.T, readTree func(objectid.ObjectID) (*tree.Tree, error), id objectid.ObjectID) *tree.Tree { - t.Helper() - - tree, err := readTree(id) - if err != nil { - t.Fatalf("read tree %s: %v", id, err) - } - - return tree -} - -func parseID(t *testing.T, algo objectid.Algorithm, hex string) objectid.ObjectID { - t.Helper() - - id, err := objectid.ParseHex(algo, hex) - if err != nil { - t.Fatalf("parse object id %q: %v", hex, err) - } - - return id -} - -func checkDiffs(t *testing.T, diffs []trees.Entry, expected map[string]diffExpectation) { - t.Helper() - - got := make(map[string]trees.Entry, len(diffs)) - for _, diff := range diffs { - path := string(diff.Path) - if _, exists := got[path]; exists { - t.Fatalf("duplicate diff path %q", path) - } - - got[path] = diff - } - - if len(got) != len(expected) { - t.Fatalf("diff count = %d, want %d", len(got), len(expected)) - } - - for path, want := range expected { - diff, ok := got[path] - if !ok { - t.Fatalf("missing diff for %q", path) - } - - if diff.Kind != want.kind { - t.Errorf("%s kind = %v, want %v", path, diff.Kind, want.kind) - } - - if (diff.Old == nil) != want.oldNil { - t.Errorf("%s old nil = %v, want %v", path, diff.Old == nil, want.oldNil) - } - - if (diff.New == nil) != want.newNil { - t.Errorf("%s new nil = %v, want %v", path, diff.New == nil, want.newNil) - } - - if diff.Kind == trees.EntryKindModified && diff.Old != nil && diff.New != nil && diff.Old.ID == diff.New.ID { - t.Errorf("%s modified entry should change IDs", path) - } - } -} diff --git a/diff/trees/entry.go b/diff/trees/entry.go deleted file mode 100644 index 84813a79..00000000 --- a/diff/trees/entry.go +++ /dev/null @@ -1,15 +0,0 @@ -package trees - -import "codeberg.org/lindenii/furgit/object/tree" - -// Entry is one recursive tree difference at a path. -type Entry struct { - // Path is the slash-separated path relative to the diff root. - Path []byte - // Kind is the difference kind for this path. - Kind EntryKind - // Old is the old tree entry (nil when Kind is EntryKindAdded). - Old *tree.TreeEntry - // New is the new tree entry (nil when Kind is EntryKindDeleted). - New *tree.TreeEntry -} diff --git a/diff/trees/kind.go b/diff/trees/kind.go deleted file mode 100644 index 6fdc6e0d..00000000 --- a/diff/trees/kind.go +++ /dev/null @@ -1,15 +0,0 @@ -package trees - -// EntryKind identifies a tree-diff entry kind. -type EntryKind int - -const ( - // EntryKindInvalid indicates an invalid diff entry kind. - EntryKindInvalid EntryKind = iota - // EntryKindDeleted indicates a deleted path. - EntryKindDeleted - // EntryKindAdded indicates an added path. - EntryKindAdded - // EntryKindModified indicates a modified path. - EntryKindModified -) diff --git a/diff/trees/path.go b/diff/trees/path.go deleted file mode 100644 index e40f3de5..00000000 --- a/diff/trees/path.go +++ /dev/null @@ -1,17 +0,0 @@ -package trees - -func joinPath(prefix, name []byte) []byte { - if len(prefix) == 0 { - out := make([]byte, len(name)) - copy(out, name) - - return out - } - - out := make([]byte, len(prefix)+1+len(name)) - copy(out, prefix) - out[len(prefix)] = '/' - copy(out[len(prefix)+1:], name) - - return out -} diff --git a/errors/doc.go b/errors/doc.go deleted file mode 100644 index 32afcd10..00000000 --- a/errors/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package errors defines error types shared across furgit. -package errors diff --git a/errors/missing.go b/errors/missing.go deleted file mode 100644 index 9efc029a..00000000 --- a/errors/missing.go +++ /dev/null @@ -1,18 +0,0 @@ -package errors - -import ( - "fmt" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// ObjectMissingError indicates that a referenced object is absent from the -// repository object store. -type ObjectMissingError struct { - OID objectid.ObjectID -} - -// Error implements error. -func (e *ObjectMissingError) Error() string { - return fmt.Sprintf("missing object %s", e.OID) -} diff --git a/errors/type.go b/errors/type.go deleted file mode 100644 index bf3ba110..00000000 --- a/errors/type.go +++ /dev/null @@ -1,31 +0,0 @@ -package errors - -import ( - "fmt" - - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// ObjectTypeError indicates that a referenced object has a different type than -// what the operation expected. -type ObjectTypeError struct { - OID objectid.ObjectID - Got objecttype.Type - Want objecttype.Type -} - -// Error implements error. -func (e *ObjectTypeError) Error() string { - gotName, gotOK := e.Got.Name() - if !gotOK { - gotName = fmt.Sprintf("type(%d)", e.Got) - } - - wantName, wantOK := e.Want.Name() - if !wantOK { - wantName = fmt.Sprintf("type(%d)", e.Want) - } - - return fmt.Sprintf("object %s has type %s, want %s", e.OID, gotName, wantName) -} diff --git a/format/commitgraph/TODO b/format/commitgraph/TODO deleted file mode 100644 index 87e0888d..00000000 --- a/format/commitgraph/TODO +++ /dev/null @@ -1,6 +0,0 @@ -Paranoia mode -Split commit-graph chain with mixed generation and bloom setting -Separate chunk parsing layer -Config stuff - -Writing diff --git a/format/commitgraph/bloom/bloom.go b/format/commitgraph/bloom/bloom.go deleted file mode 100644 index 9653d595..00000000 --- a/format/commitgraph/bloom/bloom.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package bloom provides a bloom filter implementation used for changed-path -// filters in Git commit graphs. -package bloom diff --git a/format/commitgraph/bloom/constants.go b/format/commitgraph/bloom/constants.go deleted file mode 100644 index 958e551e..00000000 --- a/format/commitgraph/bloom/constants.go +++ /dev/null @@ -1,8 +0,0 @@ -package bloom - -const ( - // DataHeaderSize is the size of the BDAT header in commit-graph files. - DataHeaderSize = 3 * 4 - // DefaultMaxChange matches Git's default max-changed-paths behavior. - DefaultMaxChange = 512 -) diff --git a/format/commitgraph/bloom/contain.go b/format/commitgraph/bloom/contain.go deleted file mode 100644 index 331b7687..00000000 --- a/format/commitgraph/bloom/contain.go +++ /dev/null @@ -1,25 +0,0 @@ -package bloom - -// MightContain reports whether the Bloom filter may contain the given path. -// -// Evaluated against the full path and each of its directory prefixes. A true -// result indicates a possible match; false means the path definitely did not -// change. -func (f *Filter) MightContain(path []byte) (bool, error) { - if len(f.Data) == 0 { - return false, nil - } - - keys, err := keyvec(path, f) - if err != nil { - return false, err - } - - for i := range keys { - if filterContainsKey(f, keys[i]) { - return true, nil - } - } - - return false, nil -} diff --git a/format/commitgraph/bloom/errors.go b/format/commitgraph/bloom/errors.go deleted file mode 100644 index fe38d1bc..00000000 --- a/format/commitgraph/bloom/errors.go +++ /dev/null @@ -1,5 +0,0 @@ -package bloom - -import "errors" - -var ErrInvalid = errors.New("bloom: invalid data") diff --git a/format/commitgraph/bloom/filter.go b/format/commitgraph/bloom/filter.go deleted file mode 100644 index 395dd5ce..00000000 --- a/format/commitgraph/bloom/filter.go +++ /dev/null @@ -1,26 +0,0 @@ -package bloom - -// Filter represents a changed-paths Bloom filter associated with a commit. -// -// The filter encodes which paths changed between a commit and its first -// parent. Paths are expected to be in Git's slash-separated form and -// are queried using a path and its prefixes (e.g. "a/b/c", "a/b", "a"). -type Filter struct { - Data []byte - - HashVersion uint32 - NumHashes uint32 - BitsPerEntry uint32 - MaxChangePaths uint32 -} - -// NewFilter constructs one query-ready bloom filter from raw data/settings. -func NewFilter(data []byte, settings Settings) Filter { - return Filter{ - Data: data, - HashVersion: settings.HashVersion, - NumHashes: settings.NumHashes, - BitsPerEntry: settings.BitsPerEntry, - MaxChangePaths: settings.MaxChangePaths, - } -} diff --git a/format/commitgraph/bloom/key.go b/format/commitgraph/bloom/key.go deleted file mode 100644 index a15df904..00000000 --- a/format/commitgraph/bloom/key.go +++ /dev/null @@ -1,117 +0,0 @@ -package bloom - -import "codeberg.org/lindenii/furgit/internal/intconv" - -type key struct { - hashes []uint32 -} - -func keyvec(path []byte, filter *Filter) ([]key, error) { - if len(path) == 0 { - return nil, nil - } - - count := 1 - - for _, b := range path { - if b == '/' { - count++ - } - } - - keys := make([]key, 0, count) - - full, err := keyFill(path, filter) - if err != nil { - return nil, err - } - - keys = append(keys, full) - - for i := len(path) - 1; i >= 0; i-- { - if path[i] == '/' { - k, err := keyFill(path[:i], filter) - if err != nil { - return nil, err - } - - keys = append(keys, k) - } - } - - return keys, nil -} - -func keyFill(path []byte, filter *Filter) (key, error) { - const ( - seed0 = 0x293ae76f - seed1 = 0x7e646e2c - ) - - var ( - h0 uint32 - h1 uint32 - err error - ) - - switch filter.HashVersion { - case 2: - h0, err = murmur3SeededV2(seed0, path) - if err != nil { - return key{}, err - } - - h1, err = murmur3SeededV2(seed1, path) - if err != nil { - return key{}, err - } - case 1: - h0, err = murmur3SeededV1(seed0, path) - if err != nil { - return key{}, err - } - - h1, err = murmur3SeededV1(seed1, path) - if err != nil { - return key{}, err - } - default: - return key{}, ErrInvalid - } - - hashCount, err := intconv.Uint32ToInt(filter.NumHashes) - if err != nil { - return key{}, ErrInvalid - } - - hashes := make([]uint32, hashCount) - for i := range hashCount { - iU32, err := intconv.IntToUint32(i) - if err != nil { - return key{}, ErrInvalid - } - - hashes[i] = h0 + iU32*h1 - } - - return key{hashes: hashes}, nil -} - -func filterContainsKey(filter *Filter, key key) bool { - if len(filter.Data) == 0 { - return false - } - - mod := uint64(len(filter.Data)) * 8 - for _, h := range key.hashes { - idx := uint64(h) % mod - bytePos := idx / 8 - - bit := byte(1 << (idx & 7)) - if filter.Data[bytePos]&bit == 0 { - return false - } - } - - return true -} diff --git a/format/commitgraph/bloom/murmur.go b/format/commitgraph/bloom/murmur.go deleted file mode 100644 index 363b63ae..00000000 --- a/format/commitgraph/bloom/murmur.go +++ /dev/null @@ -1,127 +0,0 @@ -package bloom - -import "codeberg.org/lindenii/furgit/internal/intconv" - -func murmur3SeededV2(seed uint32, data []byte) (uint32, error) { - const ( - c1 = 0xcc9e2d51 - c2 = 0x1b873593 - r1 = 15 - r2 = 13 - m = 5 - n = 0xe6546b64 - ) - - h := seed - - nblocks := len(data) / 4 - for i := range nblocks { - k := uint32(data[4*i]) | - (uint32(data[4*i+1]) << 8) | - (uint32(data[4*i+2]) << 16) | - (uint32(data[4*i+3]) << 24) - k *= c1 - k = (k << r1) | (k >> (32 - r1)) - k *= c2 - - h ^= k - h = (h << r2) | (h >> (32 - r2)) - h = h*m + n - } - - var k1 uint32 - - tail := data[nblocks*4:] - switch len(tail) & 3 { - case 3: - k1 ^= uint32(tail[2]) << 16 - - fallthrough - case 2: - k1 ^= uint32(tail[1]) << 8 - - fallthrough - case 1: - k1 ^= uint32(tail[0]) - k1 *= c1 - k1 = (k1 << r1) | (k1 >> (32 - r1)) - k1 *= c2 - h ^= k1 - } - - dataLen, err := intconv.IntToUint32(len(data)) - if err != nil { - return 0, err - } - - h ^= dataLen - h ^= h >> 16 - h *= 0x85ebca6b - h ^= h >> 13 - h *= 0xc2b2ae35 - h ^= h >> 16 - - return h, nil -} - -func murmur3SeededV1(seed uint32, data []byte) (uint32, error) { - const ( - c1 = 0xcc9e2d51 - c2 = 0x1b873593 - r1 = 15 - r2 = 13 - m = 5 - n = 0xe6546b64 - ) - - h := seed - - nblocks := len(data) / 4 - for i := range nblocks { - k := intconv.SignExtendByteToUint32(data[4*i]) | - (intconv.SignExtendByteToUint32(data[4*i+1]) << 8) | - (intconv.SignExtendByteToUint32(data[4*i+2]) << 16) | - (intconv.SignExtendByteToUint32(data[4*i+3]) << 24) - k *= c1 - k = (k << r1) | (k >> (32 - r1)) - k *= c2 - - h ^= k - h = (h << r2) | (h >> (32 - r2)) - h = h*m + n - } - - var k1 uint32 - - tail := data[nblocks*4:] - switch len(tail) & 3 { - case 3: - k1 ^= intconv.SignExtendByteToUint32(tail[2]) << 16 - - fallthrough - case 2: - k1 ^= intconv.SignExtendByteToUint32(tail[1]) << 8 - - fallthrough - case 1: - k1 ^= intconv.SignExtendByteToUint32(tail[0]) - k1 *= c1 - k1 = (k1 << r1) | (k1 >> (32 - r1)) - k1 *= c2 - h ^= k1 - } - - dataLen, err := intconv.IntToUint32(len(data)) - if err != nil { - return 0, err - } - - h ^= dataLen - h ^= h >> 16 - h *= 0x85ebca6b - h ^= h >> 13 - h *= 0xc2b2ae35 - h ^= h >> 16 - - return h, nil -} diff --git a/format/commitgraph/bloom/settings.go b/format/commitgraph/bloom/settings.go deleted file mode 100644 index 764653bd..00000000 --- a/format/commitgraph/bloom/settings.go +++ /dev/null @@ -1,50 +0,0 @@ -package bloom - -import ( - "encoding/binary" - - "codeberg.org/lindenii/furgit/internal/intconv" -) - -// Settings describe the changed-paths Bloom filter parameters stored in -// commit-graph BDAT chunks. -// -// Obviously, they must match the repository's commit-graph settings to -// interpret filters correctly. -type Settings struct { - HashVersion uint32 - NumHashes uint32 - BitsPerEntry uint32 - MaxChangePaths uint32 -} - -// ParseSettings reads Bloom filter settings from a BDAT chunk header. -func ParseSettings(bdat []byte) (*Settings, error) { - if len(bdat) < DataHeaderSize { - return nil, ErrInvalid - } - - settings := &Settings{ - HashVersion: binary.BigEndian.Uint32(bdat[0:4]), - NumHashes: binary.BigEndian.Uint32(bdat[4:8]), - BitsPerEntry: binary.BigEndian.Uint32(bdat[8:12]), - MaxChangePaths: DefaultMaxChange, - } - - switch settings.HashVersion { - case 1, 2: - default: - return nil, ErrInvalid - } - - if settings.NumHashes == 0 { - return nil, ErrInvalid - } - - _, err := intconv.Uint32ToInt(settings.NumHashes) - if err != nil { - return nil, ErrInvalid - } - - return settings, nil -} diff --git a/format/commitgraph/constants.go b/format/commitgraph/constants.go deleted file mode 100644 index 3a06a290..00000000 --- a/format/commitgraph/constants.go +++ /dev/null @@ -1,32 +0,0 @@ -package commitgraph - -const ( - FileSignature = 0x43475048 // "CGPH" - FileVersion = 1 -) - -const ( - ChunkOIDF = 0x4f494446 // "OIDF" - ChunkOIDL = 0x4f49444c // "OIDL" - ChunkCDAT = 0x43444154 // "CDAT" - ChunkGDA2 = 0x47444132 // "GDA2" - ChunkGDO2 = 0x47444f32 // "GDO2" - ChunkEDGE = 0x45444745 // "EDGE" - ChunkBIDX = 0x42494458 // "BIDX" - ChunkBDAT = 0x42444154 // "BDAT" - ChunkBASE = 0x42415345 // "BASE" -) - -const ( - HeaderSize = 8 - ChunkEntrySize = 12 - FanoutSize = 256 * 4 -) - -const ( - ParentNone = 0x70000000 - ParentExtraMask = 0x80000000 - ParentLastMask = 0x7fffffff - - GenerationOverflow = 0x80000000 -) diff --git a/format/commitgraph/doc.go b/format/commitgraph/doc.go deleted file mode 100644 index abf5f3d3..00000000 --- a/format/commitgraph/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package commitgraph provides constants and common utilities for handling commit graphs. -package commitgraph diff --git a/format/commitgraph/read/bloom.go b/format/commitgraph/read/bloom.go deleted file mode 100644 index 53d724f9..00000000 --- a/format/commitgraph/read/bloom.go +++ /dev/null @@ -1,119 +0,0 @@ -package read - -import ( - "encoding/binary" - - "codeberg.org/lindenii/furgit/format/commitgraph/bloom" - "codeberg.org/lindenii/furgit/internal/intconv" -) - -// HasBloom reports whether any layer has changed-path Bloom data. -func (reader *Reader) HasBloom() bool { - for i := range reader.layers { - layer := &reader.layers[i] - if layer.chunkBloomIndex != nil && layer.chunkBloomData != nil && layer.bloomSettings != nil { - return true - } - } - - return false -} - -// BloomVersion returns the changed-path Bloom hash version, or 0 if absent. -func (reader *Reader) BloomVersion() uint8 { - for i := len(reader.layers) - 1; i >= 0; i-- { - layer := &reader.layers[i] - if layer.bloomSettings != nil { - version, err := intconv.Uint32ToUint8(layer.bloomSettings.HashVersion) - if err != nil { - return 0 - } - - return version - } - } - - return 0 -} - -// BloomFilterAt returns one commit's changed-path Bloom filter. -// -// The returned filter borrows reader-owned mapped commit-graph data and is -// only valid until the reader is closed. -// -// Returns BloomUnavailableError when this commit graph has no Bloom data. -// -// Labels: Life-Parent. -func (reader *Reader) BloomFilterAt(pos Position) (bloom.Filter, error) { - layer, err := reader.layerByPosition(pos) - if err != nil { - return bloom.Filter{}, err - } - - if layer.chunkBloomIndex == nil || layer.chunkBloomData == nil || layer.bloomSettings == nil { - return bloom.Filter{}, &BloomUnavailableError{Pos: pos} - } - - start, end, err := bloomRange(layer, pos.Index) - if err != nil { - return bloom.Filter{}, err - } - - filter := bloom.NewFilter( - layer.chunkBloomData[bloom.DataHeaderSize+start:bloom.DataHeaderSize+end], - *layer.bloomSettings, - ) - - return filter, nil -} - -func bloomRange(layer *layer, commitIndex uint32) (int, int, error) { - off64 := uint64(commitIndex) * 4 - - off, err := intconv.Uint64ToInt(off64) - if err != nil { - return 0, 0, err - } - - end := binary.BigEndian.Uint32(layer.chunkBloomIndex[off : off+4]) - - var start uint32 - - if commitIndex > 0 { - prevOff64 := uint64(commitIndex-1) * 4 - - prevOff, err := intconv.Uint64ToInt(prevOff64) - if err != nil { - return 0, 0, err - } - - start = binary.BigEndian.Uint32(layer.chunkBloomIndex[prevOff : prevOff+4]) - } - - if end < start { - return 0, 0, &MalformedError{Path: layer.path, Reason: "invalid BIDX range"} - } - - bdatLen := len(layer.chunkBloomData) - bloom.DataHeaderSize - - bdatLenU32, err := intconv.IntToUint32(bdatLen) - if err != nil { - return 0, 0, err - } - - if end > bdatLenU32 { - return 0, 0, &MalformedError{Path: layer.path, Reason: "BIDX range out of BDAT bounds"} - } - - startInt, err := intconv.Uint64ToInt(uint64(start)) - if err != nil { - return 0, 0, err - } - - endInt, err := intconv.Uint64ToInt(uint64(end)) - if err != nil { - return 0, 0, err - } - - return startInt, endInt, nil -} diff --git a/format/commitgraph/read/close.go b/format/commitgraph/read/close.go deleted file mode 100644 index c13b5f55..00000000 --- a/format/commitgraph/read/close.go +++ /dev/null @@ -1,20 +0,0 @@ -package read - -// Close releases all mapped commit-graph files. -// -// Labels: MT-Unsafe. -func (reader *Reader) Close() error { - var closeErr error - - for i := len(reader.layers) - 1; i >= 0; i-- { - err := reader.layers[i].close() - if err != nil && closeErr == nil { - closeErr = err - } - } - - reader.layers = nil - reader.total = 0 - - return closeErr -} diff --git a/format/commitgraph/read/commitat.go b/format/commitgraph/read/commitat.go deleted file mode 100644 index 827c72ce..00000000 --- a/format/commitgraph/read/commitat.go +++ /dev/null @@ -1,87 +0,0 @@ -package read - -import ( - "encoding/binary" - - "codeberg.org/lindenii/furgit/internal/intconv" - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// CommitAt returns decoded commit-graph metadata at one position. -// -// Labels: Life-Independent. -func (reader *Reader) CommitAt(pos Position) (Commit, error) { - layer, err := reader.layerByPosition(pos) - if err != nil { - return Commit{}, err - } - - hashSize := reader.algo.Size() - stride := hashSize + 16 - - strideU64, err := intconv.IntToUint64(stride) - if err != nil { - return Commit{}, err - } - - start64 := uint64(pos.Index) * strideU64 - end64 := start64 + strideU64 - - start, err := intconv.Uint64ToInt(start64) - if err != nil { - return Commit{}, err - } - - end, err := intconv.Uint64ToInt(end64) - if err != nil { - return Commit{}, err - } - - record := layer.chunkCommit[start:end] - - treeOID, err := objectid.FromBytes(reader.algo, record[:hashSize]) - if err != nil { - return Commit{}, err - } - - oid, err := reader.OIDAt(pos) - if err != nil { - return Commit{}, err - } - - p1 := binary.BigEndian.Uint32(record[hashSize : hashSize+4]) - p2 := binary.BigEndian.Uint32(record[hashSize+4 : hashSize+8]) - genAndTimeHi := binary.BigEndian.Uint32(record[hashSize+8 : hashSize+12]) - timeLow := binary.BigEndian.Uint32(record[hashSize+12 : hashSize+16]) - - timeHigh := uint64(genAndTimeHi & 0x3) - commitTimeU64 := (timeHigh << 32) | uint64(timeLow) - - commitTime, err := intconv.Uint64ToInt64(commitTimeU64) - if err != nil { - return Commit{}, err - } - - generationV1 := genAndTimeHi >> 2 - - generationV2, err := reader.readGenerationV2(layer, pos.Index, commitTimeU64) - if err != nil { - return Commit{}, err - } - - parent1, parent2, extra, err := reader.decodeParents(layer, p1, p2) - if err != nil { - return Commit{}, err - } - - return Commit{ - OID: oid, - TreeOID: treeOID, - Parent1: parent1, - Parent2: parent2, - ExtraParents: extra, - CommitTimeUnix: commitTime, - GenerationV1: generationV1, - GenerationV2: generationV2, - }, nil -} diff --git a/format/commitgraph/read/commits.go b/format/commitgraph/read/commits.go deleted file mode 100644 index 48984ecb..00000000 --- a/format/commitgraph/read/commits.go +++ /dev/null @@ -1,20 +0,0 @@ -package read - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// Commit stores decoded commit-graph record data. -type Commit struct { - OID objectid.ObjectID - TreeOID objectid.ObjectID - Parent1 ParentRef - Parent2 ParentRef - ExtraParents []Position - CommitTimeUnix int64 - GenerationV1 uint32 - GenerationV2 uint64 -} - -// NumCommits returns total commits across loaded layers. -func (reader *Reader) NumCommits() uint32 { - return reader.total -} diff --git a/format/commitgraph/read/doc.go b/format/commitgraph/read/doc.go deleted file mode 100644 index 573ddc19..00000000 --- a/format/commitgraph/read/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package read provides routines for reading commit graphs. -package read diff --git a/format/commitgraph/read/edges.go b/format/commitgraph/read/edges.go deleted file mode 100644 index 96ffeb6d..00000000 --- a/format/commitgraph/read/edges.go +++ /dev/null @@ -1,48 +0,0 @@ -package read - -import ( - "encoding/binary" - - "codeberg.org/lindenii/furgit/format/commitgraph" - "codeberg.org/lindenii/furgit/internal/intconv" -) - -func (reader *Reader) decodeExtraEdgeList(layer *layer, edgeStart uint32) ([]Position, error) { - if len(layer.chunkExtraEdges) == 0 { - return nil, &MalformedError{Path: layer.path, Reason: "missing EDGE chunk"} - } - - out := make([]Position, 0) - - cur := edgeStart - for { - off64 := uint64(cur) * 4 - - off, err := intconv.Uint64ToInt(off64) - if err != nil { - return nil, err - } - - if off+4 > len(layer.chunkExtraEdges) { - return nil, &MalformedError{Path: layer.path, Reason: "EDGE index out of range"} - } - - word := binary.BigEndian.Uint32(layer.chunkExtraEdges[off : off+4]) - parentGlobal := word & commitgraph.ParentLastMask - - parentPos, err := reader.globalToPosition(parentGlobal) - if err != nil { - return nil, err - } - - out = append(out, parentPos) - - if word&commitgraph.ParentExtraMask != 0 { - break - } - - cur++ - } - - return out, nil -} diff --git a/format/commitgraph/read/errors.go b/format/commitgraph/read/errors.go deleted file mode 100644 index 0a32a368..00000000 --- a/format/commitgraph/read/errors.go +++ /dev/null @@ -1,58 +0,0 @@ -package read - -import ( - "fmt" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// NotFoundError reports a missing commit graph entry by object ID. -type NotFoundError struct { - OID objectid.ObjectID -} - -// Error implements error. -func (err *NotFoundError) Error() string { - return fmt.Sprintf("commitgraph: object not found: %s", err.OID) -} - -// PositionOutOfRangeError reports an invalid graph position. -type PositionOutOfRangeError struct { - Pos Position -} - -// Error implements error. -func (err *PositionOutOfRangeError) Error() string { - return fmt.Sprintf("commitgraph: position out of range: graph=%d index=%d", err.Pos.Graph, err.Pos.Index) -} - -// MalformedError reports malformed commit-graph data. -type MalformedError struct { - Path string - Reason string -} - -// Error implements error. -func (err *MalformedError) Error() string { - return fmt.Sprintf("commitgraph: malformed %q: %s", err.Path, err.Reason) -} - -// UnsupportedVersionError reports unsupported commit-graph version. -type UnsupportedVersionError struct { - Version uint8 -} - -// Error implements error. -func (err *UnsupportedVersionError) Error() string { - return fmt.Sprintf("commitgraph: unsupported version %d", err.Version) -} - -// BloomUnavailableError reports missing changed-path bloom data at one position. -type BloomUnavailableError struct { - Pos Position -} - -// Error implements error. -func (err *BloomUnavailableError) Error() string { - return fmt.Sprintf("commitgraph: bloom unavailable at position graph=%d index=%d", err.Pos.Graph, err.Pos.Index) -} diff --git a/format/commitgraph/read/generation.go b/format/commitgraph/read/generation.go deleted file mode 100644 index 62e47996..00000000 --- a/format/commitgraph/read/generation.go +++ /dev/null @@ -1,43 +0,0 @@ -package read - -import ( - "encoding/binary" - - "codeberg.org/lindenii/furgit/format/commitgraph" - "codeberg.org/lindenii/furgit/internal/intconv" -) - -func (reader *Reader) readGenerationV2(layer *layer, index uint32, commitTime uint64) (uint64, error) { - if len(layer.chunkGeneration) == 0 { - return 0, nil - } - - off64 := uint64(index) * 4 - - off, err := intconv.Uint64ToInt(off64) - if err != nil { - return 0, err - } - - value := binary.BigEndian.Uint32(layer.chunkGeneration[off : off+4]) - - if value&commitgraph.GenerationOverflow == 0 { - return commitTime + uint64(value), nil - } - - gdo2Index := value ^ commitgraph.GenerationOverflow - gdo2Off64 := uint64(gdo2Index) * 8 - - gdo2Off, err := intconv.Uint64ToInt(gdo2Off64) - if err != nil { - return 0, err - } - - if gdo2Off+8 > len(layer.chunkGenerationOv) { - return 0, &MalformedError{Path: layer.path, Reason: "GDO2 index out of range"} - } - - overflow := binary.BigEndian.Uint64(layer.chunkGenerationOv[gdo2Off : gdo2Off+8]) - - return commitTime + overflow, nil -} diff --git a/format/commitgraph/read/hash.go b/format/commitgraph/read/hash.go deleted file mode 100644 index 3a525afe..00000000 --- a/format/commitgraph/read/hash.go +++ /dev/null @@ -1,79 +0,0 @@ -package read - -import ( - "bytes" - "fmt" - "io" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// HashVersion returns the commit-graph hash version. -func (reader *Reader) HashVersion() uint8 { - return reader.hashVersion -} - -func validateChainBaseHashes(algo objectid.Algorithm, chain []string, idx int, graph *layer) error { - if idx == 0 { - if len(graph.chunkBaseGraphs) != 0 { - return &MalformedError{Path: graph.path, Reason: "unexpected BASE chunk in first graph"} - } - - return nil - } - - hashSize := algo.Size() - - expectedLen := idx * hashSize - if len(graph.chunkBaseGraphs) != expectedLen { - return &MalformedError{ - Path: graph.path, - Reason: fmt.Sprintf("BASE chunk length %d does not match expected %d", len(graph.chunkBaseGraphs), expectedLen), - } - } - - for i := range idx { - start := i * hashSize - end := start + hashSize - - baseHash, err := objectid.FromBytes(algo, graph.chunkBaseGraphs[start:end]) - if err != nil { - return err - } - - if baseHash.String() != chain[i] { - return &MalformedError{ - Path: graph.path, - Reason: fmt.Sprintf("BASE chunk mismatch at index %d", i), - } - } - } - - return nil -} - -func verifyTrailerHash(data []byte, algo objectid.Algorithm, path string) error { - hashSize := algo.Size() - if len(data) < hashSize { - return &MalformedError{Path: path, Reason: "file too short for trailer"} - } - - hashImpl, err := algo.New() - if err != nil { - return err - } - - _, err = io.Copy(hashImpl, bytes.NewReader(data[:len(data)-hashSize])) - if err != nil { - return err - } - - got := hashImpl.Sum(nil) - - want := data[len(data)-hashSize:] - if !bytes.Equal(got, want) { - return &MalformedError{Path: path, Reason: "trailer hash mismatch"} - } - - return nil -} diff --git a/format/commitgraph/read/iterators.go b/format/commitgraph/read/iterators.go deleted file mode 100644 index 0e31f7e5..00000000 --- a/format/commitgraph/read/iterators.go +++ /dev/null @@ -1,49 +0,0 @@ -package read - -import ( - "iter" - - "codeberg.org/lindenii/furgit/internal/intconv" - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// AllPositions iterates all commit positions in native layer order. -// -// Labels: Life-Parent. -func (reader *Reader) AllPositions() iter.Seq[Position] { - return func(yield func(Position) bool) { - for layerIdx := range reader.layers { - layer := &reader.layers[layerIdx] - - graph, err := intconv.IntToUint32(layerIdx) - if err != nil { - return - } - - for idx := range layer.numCommits { - if !yield(Position{Graph: graph, Index: idx}) { - return - } - } - } - } -} - -// AllOIDs iterates all commit object IDs in native layer order. -// -// Labels: Life-Parent. -func (reader *Reader) AllOIDs() iter.Seq[objectid.ObjectID] { - return func(yield func(objectid.ObjectID) bool) { - positions := reader.AllPositions() - for pos := range positions { - oid, err := reader.OIDAt(pos) - if err != nil { - return - } - - if !yield(oid) { - return - } - } - } -} diff --git a/format/commitgraph/read/layer.go b/format/commitgraph/read/layer.go deleted file mode 100644 index 53ab1663..00000000 --- a/format/commitgraph/read/layer.go +++ /dev/null @@ -1,28 +0,0 @@ -package read - -import ( - "os" - - "codeberg.org/lindenii/furgit/format/commitgraph/bloom" -) - -type layer struct { - path string - file *os.File - data []byte - numCommits uint32 - baseCount uint32 - globalFrom uint32 - - chunkOIDFanout []byte - chunkOIDLookup []byte - chunkCommit []byte - chunkGeneration []byte - chunkGenerationOv []byte - chunkExtraEdges []byte - chunkBloomIndex []byte - chunkBloomData []byte - chunkBaseGraphs []byte - - bloomSettings *bloom.Settings -} diff --git a/format/commitgraph/read/layer_close.go b/format/commitgraph/read/layer_close.go deleted file mode 100644 index 03dc91d5..00000000 --- a/format/commitgraph/read/layer_close.go +++ /dev/null @@ -1,33 +0,0 @@ -package read - -import "syscall" - -func closeLayers(layers []layer) { - for i := len(layers) - 1; i >= 0; i-- { - _ = layers[i].close() - } -} - -func (layer *layer) close() error { - var closeErr error - - if layer.data != nil { - err := syscall.Munmap(layer.data) - if err != nil { - closeErr = err - } - - layer.data = nil - } - - if layer.file != nil { - err := layer.file.Close() - if err != nil && closeErr == nil { - closeErr = err - } - - layer.file = nil - } - - return closeErr -} diff --git a/format/commitgraph/read/layer_lookup.go b/format/commitgraph/read/layer_lookup.go deleted file mode 100644 index fafc594b..00000000 --- a/format/commitgraph/read/layer_lookup.go +++ /dev/null @@ -1,53 +0,0 @@ -package read - -import ( - "bytes" - "encoding/binary" - - "codeberg.org/lindenii/furgit/internal/intconv" - objectid "codeberg.org/lindenii/furgit/object/id" -) - -func layerLookup(layer *layer, oid objectid.ObjectID) (uint32, bool) { - hashSize := oid.Algorithm().Size() - first := int(oid.RawBytes()[0]) - - var lo uint32 - if first > 0 { - lo = binary.BigEndian.Uint32(layer.chunkOIDFanout[(first-1)*4 : first*4]) - } - - hi := binary.BigEndian.Uint32(layer.chunkOIDFanout[first*4 : (first+1)*4]) - if hi == 0 || lo >= hi { - return 0, false - } - - target := oid.RawBytes() - left := int(lo) - - right := int(hi) - 1 - for left <= right { - mid := left + (right-left)/2 - start := mid * hashSize - end := start + hashSize - - current := layer.chunkOIDLookup[start:end] - - cmp := bytes.Compare(current, target) - switch { - case cmp == 0: - pos, err := intconv.IntToUint32(mid) - if err != nil { - return 0, false - } - - return pos, true - case cmp < 0: - left = mid + 1 - default: - right = mid - 1 - } - } - - return 0, false -} diff --git a/format/commitgraph/read/layer_open.go b/format/commitgraph/read/layer_open.go deleted file mode 100644 index 21a97644..00000000 --- a/format/commitgraph/read/layer_open.go +++ /dev/null @@ -1,81 +0,0 @@ -package read - -import ( - "os" - "syscall" - - "codeberg.org/lindenii/furgit/format/commitgraph" - "codeberg.org/lindenii/furgit/internal/intconv" - objectid "codeberg.org/lindenii/furgit/object/id" -) - -func openLayer(root *os.Root, relPath string, algo objectid.Algorithm) (*layer, error) { - file, err := root.Open(relPath) - if err != nil { - return nil, err - } - - info, err := file.Stat() - if err != nil { - _ = file.Close() - - return nil, err - } - - size := info.Size() - if size < int64(commitgraph.HeaderSize+commitgraph.FanoutSize+algo.Size()) { - _ = file.Close() - - return nil, &MalformedError{Path: relPath, Reason: "file too short"} - } - - mapLen, err := intconv.Int64ToUint64(size) - if err != nil { - _ = file.Close() - - return nil, err - } - - mapLenInt, err := intconv.Uint64ToInt(mapLen) - if err != nil { - _ = file.Close() - - return nil, err - } - - fd, err := intconv.UintptrToInt(file.Fd()) - if err != nil { - _ = file.Close() - - return nil, err - } - - data, err := syscall.Mmap(fd, 0, mapLenInt, syscall.PROT_READ, syscall.MAP_PRIVATE) - if err != nil { - _ = file.Close() - - return nil, err - } - - out := &layer{ - path: relPath, - file: file, - data: data, - } - - parseErr := parseLayer(out, algo) - if parseErr != nil { - _ = out.close() - - return nil, parseErr - } - - verifyErr := verifyTrailerHash(out.data, algo, relPath) - if verifyErr != nil { - _ = out.close() - - return nil, verifyErr - } - - return out, nil -} diff --git a/format/commitgraph/read/layer_parse.go b/format/commitgraph/read/layer_parse.go deleted file mode 100644 index 13e36c0a..00000000 --- a/format/commitgraph/read/layer_parse.go +++ /dev/null @@ -1,276 +0,0 @@ -package read - -import ( - "encoding/binary" - - "codeberg.org/lindenii/furgit/format/commitgraph" - "codeberg.org/lindenii/furgit/format/commitgraph/bloom" - "codeberg.org/lindenii/furgit/internal/intconv" - objectid "codeberg.org/lindenii/furgit/object/id" -) - -func parseLayer(layer *layer, algo objectid.Algorithm) error { //nolint:maintidx - if len(layer.data) < commitgraph.HeaderSize { - return &MalformedError{Path: layer.path, Reason: "file too short"} - } - - header := layer.data[:commitgraph.HeaderSize] - - signature := binary.BigEndian.Uint32(header[:4]) - if signature != commitgraph.FileSignature { - return &MalformedError{Path: layer.path, Reason: "invalid signature"} - } - - version := header[4] - if version != commitgraph.FileVersion { - return &UnsupportedVersionError{Version: version} - } - - expectedHashVersion, err := intconv.Uint32ToUint8(algo.PackHashID()) - if err != nil { - return err - } - - hashVersion := header[5] - if hashVersion != expectedHashVersion { - return &MalformedError{Path: layer.path, Reason: "hash version does not match object format"} - } - - numChunks := int(header[6]) - baseCount := uint32(header[7]) - - tocLen := (numChunks + 1) * commitgraph.ChunkEntrySize - tocStart := commitgraph.HeaderSize - - tocEnd := tocStart + tocLen - if tocEnd > len(layer.data) { - return &MalformedError{Path: layer.path, Reason: "truncated chunk table"} - } - - type tocEntry struct { - id uint32 - offset uint64 - } - - entries := make([]tocEntry, 0, numChunks+1) - for i := range numChunks + 1 { - entryOff := tocStart + i*commitgraph.ChunkEntrySize - entryData := layer.data[entryOff : entryOff+commitgraph.ChunkEntrySize] - - entry := tocEntry{ - id: binary.BigEndian.Uint32(entryData[:4]), - offset: binary.BigEndian.Uint64(entryData[4:]), - } - entries = append(entries, entry) - } - - if entries[len(entries)-1].id != 0 { - return &MalformedError{Path: layer.path, Reason: "missing chunk table terminator"} - } - - trailerStart := len(layer.data) - algo.Size() - - chunks := make(map[uint32][]byte, numChunks) - for i := range numChunks { - entry := entries[i] - if entry.id == 0 { - return &MalformedError{Path: layer.path, Reason: "early chunk table terminator"} - } - - next := entries[i+1] - - start, err := intconv.Uint64ToInt(entry.offset) - if err != nil { - return err - } - - end, err := intconv.Uint64ToInt(next.offset) - if err != nil { - return err - } - - if start < tocEnd || end < start || end > trailerStart { - return &MalformedError{Path: layer.path, Reason: "invalid chunk offsets"} - } - - if _, exists := chunks[entry.id]; exists { - return &MalformedError{Path: layer.path, Reason: "duplicate chunk id"} - } - - chunks[entry.id] = layer.data[start:end] - } - - oidf := chunks[commitgraph.ChunkOIDF] - if len(oidf) != commitgraph.FanoutSize { - return &MalformedError{Path: layer.path, Reason: "invalid OIDF length"} - } - - layer.chunkOIDFanout = oidf - layer.numCommits = binary.BigEndian.Uint32(oidf[commitgraph.FanoutSize-4:]) - - for i := range 255 { - cur := binary.BigEndian.Uint32(oidf[i*4 : (i+1)*4]) - - next := binary.BigEndian.Uint32(oidf[(i+1)*4 : (i+2)*4]) - if cur > next { - return &MalformedError{Path: layer.path, Reason: "non-monotonic OIDF fanout"} - } - } - - hashSize := algo.Size() - - hashSizeU64, err := intconv.IntToUint64(hashSize) - if err != nil { - return err - } - - oidl := chunks[commitgraph.ChunkOIDL] - oidlWantLen64 := uint64(layer.numCommits) * hashSizeU64 - - oidlWantLen, err := intconv.Uint64ToInt(oidlWantLen64) - if err != nil { - return err - } - - if len(oidl) != oidlWantLen { - return &MalformedError{Path: layer.path, Reason: "invalid OIDL length"} - } - - layer.chunkOIDLookup = oidl - - stride := hashSize + 16 - - strideU64, err := intconv.IntToUint64(stride) - if err != nil { - return err - } - - cdat := chunks[commitgraph.ChunkCDAT] - cdatWantLen64 := uint64(layer.numCommits) * strideU64 - - cdatWantLen, err := intconv.Uint64ToInt(cdatWantLen64) - if err != nil { - return err - } - - if len(cdat) != cdatWantLen { - return &MalformedError{Path: layer.path, Reason: "invalid CDAT length"} - } - - layer.chunkCommit = cdat - - gda2 := chunks[commitgraph.ChunkGDA2] - if len(gda2) != 0 { - wantLen64 := uint64(layer.numCommits) * 4 - - wantLen, err := intconv.Uint64ToInt(wantLen64) - if err != nil { - return err - } - - if len(gda2) != wantLen { - return &MalformedError{Path: layer.path, Reason: "invalid GDA2 length"} - } - - layer.chunkGeneration = gda2 - } - - gdo2 := chunks[commitgraph.ChunkGDO2] - if len(gdo2) != 0 { - if len(gdo2)%8 != 0 { - return &MalformedError{Path: layer.path, Reason: "invalid GDO2 length"} - } - - layer.chunkGenerationOv = gdo2 - } - - edge := chunks[commitgraph.ChunkEDGE] - if len(edge) != 0 { - if len(edge)%4 != 0 { - return &MalformedError{Path: layer.path, Reason: "invalid EDGE length"} - } - - layer.chunkExtraEdges = edge - } - - base := chunks[commitgraph.ChunkBASE] - if baseCount == 0 { - if len(base) != 0 { - return &MalformedError{Path: layer.path, Reason: "unexpected BASE chunk"} - } - } else { - wantLen64 := uint64(baseCount) * hashSizeU64 - - wantLen, err := intconv.Uint64ToInt(wantLen64) - if err != nil { - return err - } - - if len(base) != wantLen { - return &MalformedError{Path: layer.path, Reason: "invalid BASE length"} - } - - layer.chunkBaseGraphs = base - } - - layer.baseCount = baseCount - - bidx := chunks[commitgraph.ChunkBIDX] - - bdat := chunks[commitgraph.ChunkBDAT] - if len(bidx) != 0 || len(bdat) != 0 { //nolint:nestif - if len(bidx) == 0 || len(bdat) == 0 { - return &MalformedError{Path: layer.path, Reason: "BIDX/BDAT must both be present"} - } - - bidxWantLen64 := uint64(layer.numCommits) * 4 - - bidxWantLen, err := intconv.Uint64ToInt(bidxWantLen64) - if err != nil { - return err - } - - if len(bidx) != bidxWantLen { - return &MalformedError{Path: layer.path, Reason: "invalid BIDX length"} - } - - if len(bdat) < bloom.DataHeaderSize { - return &MalformedError{Path: layer.path, Reason: "invalid BDAT length"} - } - - settings, err := bloom.ParseSettings(bdat) - if err != nil { - return err - } - - prev := uint32(0) - - for i := range layer.numCommits { - off := int(i) * 4 - - cur := binary.BigEndian.Uint32(bidx[off : off+4]) - if i > 0 && cur < prev { - return &MalformedError{Path: layer.path, Reason: "non-monotonic BIDX"} - } - - bdatDataLen := len(bdat) - bloom.DataHeaderSize - - bdatDataLenU32, err := intconv.IntToUint32(bdatDataLen) - if err != nil { - return err - } - - if cur > bdatDataLenU32 { - return &MalformedError{Path: layer.path, Reason: "BIDX offset out of range"} - } - - prev = cur - } - - layer.chunkBloomIndex = bidx - layer.chunkBloomData = bdat - layer.bloomSettings = settings - } - - return nil -} diff --git a/format/commitgraph/read/layer_pos.go b/format/commitgraph/read/layer_pos.go deleted file mode 100644 index 7b87b381..00000000 --- a/format/commitgraph/read/layer_pos.go +++ /dev/null @@ -1,21 +0,0 @@ -package read - -import "codeberg.org/lindenii/furgit/internal/intconv" - -func (reader *Reader) layerByPosition(pos Position) (*layer, error) { - graphIdx, err := intconv.Uint64ToInt(uint64(pos.Graph)) - if err != nil { - return nil, err - } - - if graphIdx < 0 || graphIdx >= len(reader.layers) { - return nil, &PositionOutOfRangeError{Pos: pos} - } - - layer := &reader.layers[graphIdx] - if pos.Index >= layer.numCommits { - return nil, &PositionOutOfRangeError{Pos: pos} - } - - return layer, nil -} diff --git a/format/commitgraph/read/layerinfo.go b/format/commitgraph/read/layerinfo.go deleted file mode 100644 index d4dbfad3..00000000 --- a/format/commitgraph/read/layerinfo.go +++ /dev/null @@ -1,25 +0,0 @@ -package read - -// LayerInfo describes one loaded commit-graph layer. -type LayerInfo struct { - Path string - BaseCount uint32 - Commits uint32 -} - -// Layers returns loaded layer metadata in native chain order. -// -// Labels: Life-Independent. -func (reader *Reader) Layers() []LayerInfo { - out := make([]LayerInfo, 0, len(reader.layers)) - for i := range reader.layers { - layer := reader.layers[i] - out = append(out, LayerInfo{ - Path: layer.path, - BaseCount: layer.baseCount, - Commits: layer.numCommits, - }) - } - - return out -} diff --git a/format/commitgraph/read/lookup.go b/format/commitgraph/read/lookup.go deleted file mode 100644 index 5f1b08f6..00000000 --- a/format/commitgraph/read/lookup.go +++ /dev/null @@ -1,29 +0,0 @@ -package read - -import ( - "codeberg.org/lindenii/furgit/internal/intconv" - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// Lookup resolves one object ID to one graph position. -func (reader *Reader) Lookup(oid objectid.ObjectID) (Position, error) { - if oid.Algorithm() != reader.algo { - return Position{}, &NotFoundError{OID: oid} - } - - for layerIdx := len(reader.layers) - 1; layerIdx >= 0; layerIdx-- { - layer := &reader.layers[layerIdx] - - found, ok := layerLookup(layer, oid) - if ok { - idxU32, err := intconv.IntToUint32(layerIdx) - if err != nil { - return Position{}, err - } - - return Position{Graph: idxU32, Index: found}, nil - } - } - - return Position{}, &NotFoundError{OID: oid} -} diff --git a/format/commitgraph/read/mode.go b/format/commitgraph/read/mode.go deleted file mode 100644 index 76afa21f..00000000 --- a/format/commitgraph/read/mode.go +++ /dev/null @@ -1,11 +0,0 @@ -package read - -// OpenMode controls which commit-graph layout Open loads. -type OpenMode uint8 - -const ( - // OpenSingle opens one commit-graph file at info/commit-graph. - OpenSingle OpenMode = iota - // OpenChain opens chained commit-graphs from info/commit-graphs. - OpenChain -) diff --git a/format/commitgraph/read/oidat.go b/format/commitgraph/read/oidat.go deleted file mode 100644 index 99259995..00000000 --- a/format/commitgraph/read/oidat.go +++ /dev/null @@ -1,38 +0,0 @@ -package read - -import ( - "codeberg.org/lindenii/furgit/internal/intconv" - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// OIDAt returns object ID at one position. -// -// Labels: Life-Independent. -func (reader *Reader) OIDAt(pos Position) (objectid.ObjectID, error) { - layer, err := reader.layerByPosition(pos) - if err != nil { - return objectid.ObjectID{}, err - } - - hashSize := reader.algo.Size() - - hashSizeU64, err := intconv.IntToUint64(hashSize) - if err != nil { - return objectid.ObjectID{}, err - } - - start64 := uint64(pos.Index) * hashSizeU64 - end64 := start64 + hashSizeU64 - - start, err := intconv.Uint64ToInt(start64) - if err != nil { - return objectid.ObjectID{}, err - } - - end, err := intconv.Uint64ToInt(end64) - if err != nil { - return objectid.ObjectID{}, err - } - - return objectid.FromBytes(reader.algo, layer.chunkOIDLookup[start:end]) -} diff --git a/format/commitgraph/read/open.go b/format/commitgraph/read/open.go deleted file mode 100644 index d03c8572..00000000 --- a/format/commitgraph/read/open.go +++ /dev/null @@ -1,26 +0,0 @@ -package read - -import ( - "fmt" - "os" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// Open opens commit-graph data from one objects root. -// -// Labels: Deps-Borrowed. -func Open(root *os.Root, algo objectid.Algorithm, mode OpenMode) (*Reader, error) { - if algo.Size() == 0 { - return nil, objectid.ErrInvalidAlgorithm - } - - switch mode { - case OpenSingle: - return openSingle(root, algo) - case OpenChain: - return openChain(root, algo) - default: - return nil, fmt.Errorf("commitgraph: invalid open mode %d", mode) - } -} diff --git a/format/commitgraph/read/open_chain.go b/format/commitgraph/read/open_chain.go deleted file mode 100644 index b55f3e57..00000000 --- a/format/commitgraph/read/open_chain.go +++ /dev/null @@ -1,133 +0,0 @@ -package read - -import ( - "bufio" - "errors" - "fmt" - "os" - "strings" - - "codeberg.org/lindenii/furgit/internal/intconv" - objectid "codeberg.org/lindenii/furgit/object/id" -) - -func openChain(root *os.Root, algo objectid.Algorithm) (*Reader, error) { - chainPath := "info/commit-graphs/commit-graph-chain" - - file, err := root.Open(chainPath) - if err != nil { - if errors.Is(err, os.ErrNotExist) { - return nil, &MalformedError{Path: chainPath, Reason: "missing commit-graph-chain"} - } - - return nil, err - } - - scanner := bufio.NewScanner(file) - hashes := make([]string, 0) - - for scanner.Scan() { - line := strings.TrimSpace(scanner.Text()) - if line == "" { - continue - } - - hashes = append(hashes, line) - } - - scanErr := scanner.Err() - closeErr := file.Close() - - if scanErr != nil { - return nil, scanErr - } - - if closeErr != nil { - return nil, closeErr - } - - if len(hashes) == 0 { - return nil, &MalformedError{Path: chainPath, Reason: "empty chain"} - } - - layers := make([]layer, 0, len(hashes)) - - var total uint32 - - hashVersion, err := intconv.Uint32ToUint8(algo.PackHashID()) - if err != nil { - return nil, err - } - - for i, hashHex := range hashes { - expectedBaseCount, err := intconv.IntToUint32(i) - if err != nil { - closeLayers(layers) - - return nil, err - } - - if len(hashHex) != algo.HexLen() { - closeLayers(layers) - - return nil, &MalformedError{ - Path: chainPath, - Reason: fmt.Sprintf("invalid graph hash length at line %d", i+1), - } - } - - relPath := fmt.Sprintf("info/commit-graphs/graph-%s.graph", hashHex) - - loaded, loadErr := openLayer(root, relPath, algo) - if loadErr != nil { - closeLayers(layers) - - return nil, loadErr - } - - if loaded.baseCount != expectedBaseCount { - _ = loaded.close() - - closeLayers(layers) - - return nil, &MalformedError{ - Path: relPath, - Reason: fmt.Sprintf("BASE count %d does not match chain depth %d", loaded.baseCount, i), - } - } - - validateErr := validateChainBaseHashes(algo, hashes, i, loaded) - if validateErr != nil { - _ = loaded.close() - - closeLayers(layers) - - return nil, validateErr - } - - loaded.globalFrom = total - loaded.baseCount = expectedBaseCount - - totalNext := total + loaded.numCommits - if totalNext < total { - _ = loaded.close() - - closeLayers(layers) - - return nil, &MalformedError{Path: relPath, Reason: "total commit count overflow"} - } - - total = totalNext - - layers = append(layers, *loaded) - } - - out := &Reader{ - algo: algo, - hashVersion: hashVersion, - layers: layers, - total: total, - } - - return out, nil -} diff --git a/format/commitgraph/read/open_single.go b/format/commitgraph/read/open_single.go deleted file mode 100644 index 9ad6607f..00000000 --- a/format/commitgraph/read/open_single.go +++ /dev/null @@ -1,32 +0,0 @@ -package read - -import ( - "os" - - "codeberg.org/lindenii/furgit/internal/intconv" - objectid "codeberg.org/lindenii/furgit/object/id" -) - -func openSingle(root *os.Root, algo objectid.Algorithm) (*Reader, error) { - graph, err := openLayer(root, "info/commit-graph", algo) - if err != nil { - return nil, err - } - - graph.baseCount = 0 - graph.globalFrom = 0 - - hashVersion, err := intconv.Uint32ToUint8(algo.PackHashID()) - if err != nil { - return nil, err - } - - out := &Reader{ - algo: algo, - hashVersion: hashVersion, - layers: []layer{*graph}, - total: graph.numCommits, - } - - return out, nil -} diff --git a/format/commitgraph/read/parents.go b/format/commitgraph/read/parents.go deleted file mode 100644 index fcaad8b6..00000000 --- a/format/commitgraph/read/parents.go +++ /dev/null @@ -1,67 +0,0 @@ -package read - -import "codeberg.org/lindenii/furgit/format/commitgraph" - -// ParentRef references one parent position. -type ParentRef struct { - Valid bool - Pos Position -} - -func (reader *Reader) decodeParents(layer *layer, p1, p2 uint32) (ParentRef, ParentRef, []Position, error) { - parent1, err := reader.decodeSingleParent(p1) - if err != nil { - return ParentRef{}, ParentRef{}, nil, err - } - - if p2 == commitgraph.ParentNone { - return parent1, ParentRef{}, nil, nil - } - - if p2&commitgraph.ParentExtraMask == 0 { - parent2, err := reader.decodeSingleParent(p2) - if err != nil { - return ParentRef{}, ParentRef{}, nil, err - } - - return parent1, parent2, nil, nil - } - - edgeStart := p2 & commitgraph.ParentLastMask - - parents, err := reader.decodeExtraEdgeList(layer, edgeStart) - if err != nil { - return ParentRef{}, ParentRef{}, nil, err - } - - if len(parents) == 0 { - return ParentRef{}, ParentRef{}, nil, &MalformedError{Path: layer.path, Reason: "empty EDGE list"} - } - - parent2 := ParentRef{Valid: true, Pos: parents[0]} - if len(parents) == 1 { - return parent1, parent2, nil, nil - } - - return parent1, parent2, parents[1:], nil -} - -func (reader *Reader) decodeSingleParent(raw uint32) (ParentRef, error) { - if raw == commitgraph.ParentNone { - return ParentRef{}, nil - } - - if raw&commitgraph.ParentExtraMask != 0 { - return ParentRef{}, &MalformedError{ - Path: "commit-graph", - Reason: "unexpected EDGE marker in single-parent slot", - } - } - - pos, err := reader.globalToPosition(raw) - if err != nil { - return ParentRef{}, err - } - - return ParentRef{Valid: true, Pos: pos}, nil -} diff --git a/format/commitgraph/read/position.go b/format/commitgraph/read/position.go deleted file mode 100644 index b2e1138b..00000000 --- a/format/commitgraph/read/position.go +++ /dev/null @@ -1,38 +0,0 @@ -package read - -import ( - "fmt" - - "codeberg.org/lindenii/furgit/internal/intconv" -) - -// Position identifies one commit record by layer and row index. -type Position struct { - Graph uint32 - Index uint32 -} - -func (reader *Reader) globalToPosition(global uint32) (Position, error) { - for i := range reader.layers { - layer := &reader.layers[i] - from := layer.globalFrom - - to := from + layer.numCommits - if global >= from && global < to { - graph, err := intconv.IntToUint32(i) - if err != nil { - return Position{}, err - } - - return Position{ - Graph: graph, - Index: global - from, - }, nil - } - } - - return Position{}, &MalformedError{ - Path: "commit-graph", - Reason: fmt.Sprintf("parent global position out of range: %d", global), - } -} diff --git a/format/commitgraph/read/read_test.go b/format/commitgraph/read/read_test.go deleted file mode 100644 index c65b183e..00000000 --- a/format/commitgraph/read/read_test.go +++ /dev/null @@ -1,322 +0,0 @@ -package read_test - -import ( - "errors" - "path/filepath" - "strconv" - "strings" - "testing" - - "codeberg.org/lindenii/furgit/format/commitgraph/bloom" - "codeberg.org/lindenii/furgit/format/commitgraph/read" - "codeberg.org/lindenii/furgit/internal/intconv" - "codeberg.org/lindenii/furgit/internal/testgit" - objectid "codeberg.org/lindenii/furgit/object/id" -) - -func fixtureRepoPath(t *testing.T, algo objectid.Algorithm, name string) string { - t.Helper() - - return filepath.Join("testdata", "fixtures", algo.String(), name, "repo.git") -} - -func fixtureRepo(t *testing.T, algo objectid.Algorithm, name string) *testgit.TestRepo { - t.Helper() - - return testgit.NewRepoFromFixture(t, algo, fixtureRepoPath(t, algo, name)) -} - -func TestReadSingleMatchesGit(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := fixtureRepo(t, algo, "single_changed") - - reader := openReader(t, testRepo, read.OpenSingle) - - defer func() { _ = reader.Close() }() - - allIDs := testRepo.RevList(t, "--all") - if len(allIDs) == 0 { - t.Fatal("git rev-list --all returned no commits") - } - - wantCommitCount, err := intconv.IntToUint32(len(allIDs)) - if err != nil { - t.Fatalf("len(allIDs) convert: %v", err) - } - - if got := reader.NumCommits(); got != wantCommitCount { - t.Fatalf("NumCommits() = %d, want %d", got, len(allIDs)) - } - - if !reader.HasBloom() { - t.Fatal("HasBloom() = false, want true") - } - - bloomVersion := reader.BloomVersion() - if bloomVersion == 0 { - t.Fatal("BloomVersion() = 0, want non-zero when HasBloom() is true") - } - - for _, id := range allIDs { - pos, err := reader.Lookup(id) - if err != nil { - t.Fatalf("Lookup(%s): %v", id, err) - } - - gotID, err := reader.OIDAt(pos) - if err != nil { - t.Fatalf("OIDAt(%+v): %v", pos, err) - } - - if gotID != id { - t.Fatalf("OIDAt(Lookup(%s)) = %s, want %s", id, gotID, id) - } - } - - step := max(len(allIDs)/24, 1) - - for i, id := range allIDs { - if i%step != 0 && i != len(allIDs)-1 { - continue - } - - verifyCommitAgainstGit(t, testRepo, reader, id) - } - }) -} - -func TestReadChainMatchesGit(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := fixtureRepo(t, algo, "chain_changed") - - reader := openReader(t, testRepo, read.OpenChain) - - defer func() { _ = reader.Close() }() - - layers := reader.Layers() - if len(layers) < 2 { - t.Fatalf("Layers len = %d, want >= 2", len(layers)) - } - - allIDs := testRepo.RevList(t, "--all") - - wantCommitCount, err := intconv.IntToUint32(len(allIDs)) - if err != nil { - t.Fatalf("len(allIDs) convert: %v", err) - } - - if got := reader.NumCommits(); got != wantCommitCount { - t.Fatalf("NumCommits() = %d, want %d", got, len(allIDs)) - } - - step := max(len(allIDs)/20, 1) - - for i, id := range allIDs { - pos, err := reader.Lookup(id) - if err != nil { - t.Fatalf("Lookup(%s): %v", id, err) - } - - if i%step != 0 && i != len(allIDs)-1 { - continue - } - - gotID, err := reader.OIDAt(pos) - if err != nil { - t.Fatalf("OIDAt(%+v): %v", pos, err) - } - - if gotID != id { - t.Fatalf("OIDAt(Lookup(%s)) = %s, want %s", id, gotID, id) - } - } - }) -} - -func TestBloomUnavailableWithoutChangedPaths(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := fixtureRepo(t, algo, "single_nochanged") - - reader := openReader(t, testRepo, read.OpenSingle) - - defer func() { _ = reader.Close() }() - - head := testRepo.RevParse(t, "HEAD") - - pos, err := reader.Lookup(head) - if err != nil { - t.Fatalf("Lookup(%s): %v", head, err) - } - - _, err = reader.BloomFilterAt(pos) - if err == nil { - t.Fatal("BloomFilterAt() error = nil, want BloomUnavailableError") - } - - unavailable, ok := errors.AsType[*read.BloomUnavailableError](err) - if !ok { - t.Fatalf("BloomFilterAt() error type = %T, want *BloomUnavailableError", err) - } - - if unavailable.Pos != pos { - t.Fatalf("BloomUnavailableError.Pos = %+v, want %+v", unavailable.Pos, pos) - } - }) -} - -func openReader(tb testing.TB, testRepo *testgit.TestRepo, mode read.OpenMode) *read.Reader { - tb.Helper() - - root := testRepo.OpenObjectsRoot(tb) - - reader, err := read.Open(root, testRepo.Algorithm(), mode) - if err != nil { - tb.Fatalf("read.Open(objects): %v", err) - } - - return reader -} - -func verifyCommitAgainstGit(tb testing.TB, testRepo *testgit.TestRepo, reader *read.Reader, id objectid.ObjectID) { - tb.Helper() - - pos, err := reader.Lookup(id) - if err != nil { - tb.Fatalf("Lookup(%s): %v", id, err) - } - - commit, err := reader.CommitAt(pos) - if err != nil { - tb.Fatalf("CommitAt(%+v): %v", pos, err) - } - - if commit.OID != id { - tb.Fatalf("CommitAt(%+v).OID = %s, want %s", pos, commit.OID, id) - } - - treeHex := testRepo.Run(tb, "show", "-s", "--format=%T", id.String()) - - wantTree, err := objectid.ParseHex(testRepo.Algorithm(), treeHex) - if err != nil { - tb.Fatalf("parse tree id %q: %v", treeHex, err) - } - - if commit.TreeOID != wantTree { - tb.Fatalf("CommitAt(%+v).TreeOID = %s, want %s", pos, commit.TreeOID, wantTree) - } - - wantParents := parseOIDLine(tb, testRepo.Algorithm(), testRepo.Run(tb, "show", "-s", "--format=%P", id.String())) - - gotParents := commitParents(tb, reader, commit) - if len(gotParents) != len(wantParents) { - tb.Fatalf("parent count for %s = %d, want %d", id, len(gotParents), len(wantParents)) - } - - for i := range gotParents { - if gotParents[i] != wantParents[i] { - tb.Fatalf("parent %d for %s = %s, want %s", i, id, gotParents[i], wantParents[i]) - } - } - - commitTimeRaw := testRepo.Run(tb, "show", "-s", "--format=%ct", id.String()) - - wantCommitTime, err := strconv.ParseInt(strings.TrimSpace(commitTimeRaw), 10, 64) - if err != nil { - tb.Fatalf("parse commit time %q: %v", commitTimeRaw, err) - } - - if commit.CommitTimeUnix != wantCommitTime { - tb.Fatalf("CommitAt(%+v).CommitTimeUnix = %d, want %d", pos, commit.CommitTimeUnix, wantCommitTime) - } - - filter, err := reader.BloomFilterAt(pos) - if err != nil { - tb.Fatalf("BloomFilterAt(%+v): %v", pos, err) - } - - if filter.HashVersion != uint32(reader.BloomVersion()) { - tb.Fatalf("filter.HashVersion = %d, want %d", filter.HashVersion, reader.BloomVersion()) - } - - assertChangedPathsBloomPositive(tb, testRepo, filter, id) -} - -func commitParents(tb testing.TB, reader *read.Reader, commit read.Commit) []objectid.ObjectID { - tb.Helper() - - out := make([]objectid.ObjectID, 0, 2+len(commit.ExtraParents)) - - if commit.Parent1.Valid { - id, err := reader.OIDAt(commit.Parent1.Pos) - if err != nil { - tb.Fatalf("OIDAt(parent1 %+v): %v", commit.Parent1.Pos, err) - } - - out = append(out, id) - } - - if commit.Parent2.Valid { - id, err := reader.OIDAt(commit.Parent2.Pos) - if err != nil { - tb.Fatalf("OIDAt(parent2 %+v): %v", commit.Parent2.Pos, err) - } - - out = append(out, id) - } - - for _, parentPos := range commit.ExtraParents { - id, err := reader.OIDAt(parentPos) - if err != nil { - tb.Fatalf("OIDAt(extra parent %+v): %v", parentPos, err) - } - - out = append(out, id) - } - - return out -} - -func assertChangedPathsBloomPositive(tb testing.TB, testRepo *testgit.TestRepo, filter bloom.Filter, commitID objectid.ObjectID) { - tb.Helper() - - changedPaths := testRepo.Run(tb, "diff-tree", "--no-commit-id", "--name-only", "-r", "--root", commitID.String()) - for line := range strings.SplitSeq(strings.TrimSpace(changedPaths), "\n") { - path := strings.TrimSpace(line) - if path == "" { - continue - } - - mightContain, err := filter.MightContain([]byte(path)) - if err != nil { - tb.Fatalf("MightContain(%q): %v", path, err) - } - - if !mightContain { - tb.Fatalf("Bloom filter false negative for commit %s path %q", commitID, path) - } - } -} - -func parseOIDLine(tb testing.TB, algo objectid.Algorithm, line string) []objectid.ObjectID { - tb.Helper() - - toks := strings.Fields(line) - - out := make([]objectid.ObjectID, 0, len(toks)) - for _, tok := range toks { - id, err := objectid.ParseHex(algo, tok) - if err != nil { - tb.Fatalf("parse object id %q: %v", tok, err) - } - - out = append(out, id) - } - - return out -} diff --git a/format/commitgraph/read/reader.go b/format/commitgraph/read/reader.go deleted file mode 100644 index 03698eb1..00000000 --- a/format/commitgraph/read/reader.go +++ /dev/null @@ -1,14 +0,0 @@ -package read - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// Reader provides read-only access to one mmap-backed commit-graph snapshot. -// -// Labels: MT-Safe, Close-Caller. -type Reader struct { - algo objectid.Algorithm - hashVersion uint8 - - layers []layer - total uint32 -} diff --git a/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/HEAD b/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/HEAD deleted file mode 100644 index cb089cd8..00000000 --- a/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/HEAD +++ /dev/null @@ -1 +0,0 @@ -ref: refs/heads/master diff --git a/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/config b/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/config deleted file mode 100644 index 07d359d0..00000000 --- a/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/config +++ /dev/null @@ -1,4 +0,0 @@ -[core] - repositoryformatversion = 0 - filemode = true - bare = true diff --git a/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/info/commit-graphs/commit-graph-chain b/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/info/commit-graphs/commit-graph-chain deleted file mode 100644 index 74c46b64..00000000 --- a/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/info/commit-graphs/commit-graph-chain +++ /dev/null @@ -1,2 +0,0 @@ -dd7578d5216ca76c25b19631ba90f7498aeabbe7 -bf985c21612a52070d8b008e6ef51edf8b609401 diff --git a/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/info/commit-graphs/graph-bf985c21612a52070d8b008e6ef51edf8b609401.graph b/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/info/commit-graphs/graph-bf985c21612a52070d8b008e6ef51edf8b609401.graph deleted file mode 100644 index c31869c1..00000000 Binary files a/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/info/commit-graphs/graph-bf985c21612a52070d8b008e6ef51edf8b609401.graph and /dev/null differ diff --git a/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/info/commit-graphs/graph-dd7578d5216ca76c25b19631ba90f7498aeabbe7.graph b/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/info/commit-graphs/graph-dd7578d5216ca76c25b19631ba90f7498aeabbe7.graph deleted file mode 100644 index 241eb3cc..00000000 Binary files a/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/info/commit-graphs/graph-dd7578d5216ca76c25b19631ba90f7498aeabbe7.graph and /dev/null differ diff --git a/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/info/packs b/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/info/packs deleted file mode 100644 index 61decf9b..00000000 --- a/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/info/packs +++ /dev/null @@ -1,2 +0,0 @@ -P pack-15b064d6a8ef8cff520565f6db8c006b2e6f7f2f.pack - diff --git a/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/pack/pack-15b064d6a8ef8cff520565f6db8c006b2e6f7f2f.bitmap b/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/pack/pack-15b064d6a8ef8cff520565f6db8c006b2e6f7f2f.bitmap deleted file mode 100644 index 1508cf18..00000000 Binary files a/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/pack/pack-15b064d6a8ef8cff520565f6db8c006b2e6f7f2f.bitmap and /dev/null differ diff --git a/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/pack/pack-15b064d6a8ef8cff520565f6db8c006b2e6f7f2f.idx b/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/pack/pack-15b064d6a8ef8cff520565f6db8c006b2e6f7f2f.idx deleted file mode 100644 index 00ee2646..00000000 Binary files a/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/pack/pack-15b064d6a8ef8cff520565f6db8c006b2e6f7f2f.idx and /dev/null differ diff --git a/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/pack/pack-15b064d6a8ef8cff520565f6db8c006b2e6f7f2f.pack b/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/pack/pack-15b064d6a8ef8cff520565f6db8c006b2e6f7f2f.pack deleted file mode 100644 index c65ae27f..00000000 Binary files a/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/pack/pack-15b064d6a8ef8cff520565f6db8c006b2e6f7f2f.pack and /dev/null differ diff --git a/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/pack/pack-15b064d6a8ef8cff520565f6db8c006b2e6f7f2f.rev b/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/pack/pack-15b064d6a8ef8cff520565f6db8c006b2e6f7f2f.rev deleted file mode 100644 index d0689f72..00000000 Binary files a/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/pack/pack-15b064d6a8ef8cff520565f6db8c006b2e6f7f2f.rev and /dev/null differ diff --git a/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/refs/heads/master b/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/refs/heads/master deleted file mode 100644 index 8942d437..00000000 --- a/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/refs/heads/master +++ /dev/null @@ -1 +0,0 @@ -46ca641fd65e566b8ecfa567a1f01766289192f8 diff --git a/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/HEAD b/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/HEAD deleted file mode 100644 index b870d826..00000000 --- a/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/HEAD +++ /dev/null @@ -1 +0,0 @@ -ref: refs/heads/main diff --git a/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/config b/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/config deleted file mode 100644 index 07d359d0..00000000 --- a/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/config +++ /dev/null @@ -1,4 +0,0 @@ -[core] - repositoryformatversion = 0 - filemode = true - bare = true diff --git a/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/objects/info/commit-graph b/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/objects/info/commit-graph deleted file mode 100644 index 56b59a54..00000000 Binary files a/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/objects/info/commit-graph and /dev/null differ diff --git a/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/objects/info/packs b/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/objects/info/packs deleted file mode 100644 index ecf5d272..00000000 --- a/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/objects/info/packs +++ /dev/null @@ -1,2 +0,0 @@ -P pack-34e9e132566989e2abfe8821731236c77f9bcbe9.pack - diff --git a/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/objects/pack/pack-34e9e132566989e2abfe8821731236c77f9bcbe9.bitmap b/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/objects/pack/pack-34e9e132566989e2abfe8821731236c77f9bcbe9.bitmap deleted file mode 100644 index 9fec7b16..00000000 Binary files a/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/objects/pack/pack-34e9e132566989e2abfe8821731236c77f9bcbe9.bitmap and /dev/null differ diff --git a/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/objects/pack/pack-34e9e132566989e2abfe8821731236c77f9bcbe9.idx b/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/objects/pack/pack-34e9e132566989e2abfe8821731236c77f9bcbe9.idx deleted file mode 100644 index e30cbb5a..00000000 Binary files a/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/objects/pack/pack-34e9e132566989e2abfe8821731236c77f9bcbe9.idx and /dev/null differ diff --git a/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/objects/pack/pack-34e9e132566989e2abfe8821731236c77f9bcbe9.pack b/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/objects/pack/pack-34e9e132566989e2abfe8821731236c77f9bcbe9.pack deleted file mode 100644 index 8da45eab..00000000 Binary files a/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/objects/pack/pack-34e9e132566989e2abfe8821731236c77f9bcbe9.pack and /dev/null differ diff --git a/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/objects/pack/pack-34e9e132566989e2abfe8821731236c77f9bcbe9.rev b/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/objects/pack/pack-34e9e132566989e2abfe8821731236c77f9bcbe9.rev deleted file mode 100644 index 3bcd2e2c..00000000 Binary files a/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/objects/pack/pack-34e9e132566989e2abfe8821731236c77f9bcbe9.rev and /dev/null differ diff --git a/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/refs/heads/main b/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/refs/heads/main deleted file mode 100644 index 090ca933..00000000 --- a/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/refs/heads/main +++ /dev/null @@ -1 +0,0 @@ -d02a8dbd1a8fbaac8ab7f7f1533cc312ab2c9eec diff --git a/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/HEAD b/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/HEAD deleted file mode 100644 index cb089cd8..00000000 --- a/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/HEAD +++ /dev/null @@ -1 +0,0 @@ -ref: refs/heads/master diff --git a/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/config b/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/config deleted file mode 100644 index 07d359d0..00000000 --- a/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/config +++ /dev/null @@ -1,4 +0,0 @@ -[core] - repositoryformatversion = 0 - filemode = true - bare = true diff --git a/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/objects/info/commit-graph b/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/objects/info/commit-graph deleted file mode 100644 index 28f7d06a..00000000 Binary files a/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/objects/info/commit-graph and /dev/null differ diff --git a/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/objects/info/packs b/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/objects/info/packs deleted file mode 100644 index 8434a002..00000000 --- a/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/objects/info/packs +++ /dev/null @@ -1,2 +0,0 @@ -P pack-a3da595034c94bb16b6829d757a66b7d259b9ffc.pack - diff --git a/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/objects/pack/pack-a3da595034c94bb16b6829d757a66b7d259b9ffc.bitmap b/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/objects/pack/pack-a3da595034c94bb16b6829d757a66b7d259b9ffc.bitmap deleted file mode 100644 index 64a36c71..00000000 Binary files a/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/objects/pack/pack-a3da595034c94bb16b6829d757a66b7d259b9ffc.bitmap and /dev/null differ diff --git a/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/objects/pack/pack-a3da595034c94bb16b6829d757a66b7d259b9ffc.idx b/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/objects/pack/pack-a3da595034c94bb16b6829d757a66b7d259b9ffc.idx deleted file mode 100644 index f5e16674..00000000 Binary files a/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/objects/pack/pack-a3da595034c94bb16b6829d757a66b7d259b9ffc.idx and /dev/null differ diff --git a/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/objects/pack/pack-a3da595034c94bb16b6829d757a66b7d259b9ffc.pack b/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/objects/pack/pack-a3da595034c94bb16b6829d757a66b7d259b9ffc.pack deleted file mode 100644 index 8f82b451..00000000 Binary files a/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/objects/pack/pack-a3da595034c94bb16b6829d757a66b7d259b9ffc.pack and /dev/null differ diff --git a/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/objects/pack/pack-a3da595034c94bb16b6829d757a66b7d259b9ffc.rev b/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/objects/pack/pack-a3da595034c94bb16b6829d757a66b7d259b9ffc.rev deleted file mode 100644 index 64771f70..00000000 Binary files a/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/objects/pack/pack-a3da595034c94bb16b6829d757a66b7d259b9ffc.rev and /dev/null differ diff --git a/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/refs/heads/master b/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/refs/heads/master deleted file mode 100644 index 475cb2c1..00000000 --- a/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/refs/heads/master +++ /dev/null @@ -1 +0,0 @@ -dda8217252bdf3e01fdf31309d0e5c3051b00945 diff --git a/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/HEAD b/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/HEAD deleted file mode 100644 index cb089cd8..00000000 --- a/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/HEAD +++ /dev/null @@ -1 +0,0 @@ -ref: refs/heads/master diff --git a/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/config b/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/config deleted file mode 100644 index 7d1c0006..00000000 --- a/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/config +++ /dev/null @@ -1,6 +0,0 @@ -[extensions] - objectformat = sha256 -[core] - repositoryformatversion = 1 - filemode = true - bare = true diff --git a/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/info/commit-graphs/commit-graph-chain b/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/info/commit-graphs/commit-graph-chain deleted file mode 100644 index 4e7d76fe..00000000 --- a/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/info/commit-graphs/commit-graph-chain +++ /dev/null @@ -1,2 +0,0 @@ -505cab61f8ddfa614301e8f97943112739236c6bcd19ed4d1f7c6b830cab4f62 -77c47bd6ca2ce17208c9361717a5823c0cb4b5ee336a14959678e060d674ffb6 diff --git a/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/info/commit-graphs/graph-505cab61f8ddfa614301e8f97943112739236c6bcd19ed4d1f7c6b830cab4f62.graph b/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/info/commit-graphs/graph-505cab61f8ddfa614301e8f97943112739236c6bcd19ed4d1f7c6b830cab4f62.graph deleted file mode 100644 index 4a93de94..00000000 Binary files a/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/info/commit-graphs/graph-505cab61f8ddfa614301e8f97943112739236c6bcd19ed4d1f7c6b830cab4f62.graph and /dev/null differ diff --git a/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/info/commit-graphs/graph-77c47bd6ca2ce17208c9361717a5823c0cb4b5ee336a14959678e060d674ffb6.graph b/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/info/commit-graphs/graph-77c47bd6ca2ce17208c9361717a5823c0cb4b5ee336a14959678e060d674ffb6.graph deleted file mode 100644 index 7807351d..00000000 Binary files a/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/info/commit-graphs/graph-77c47bd6ca2ce17208c9361717a5823c0cb4b5ee336a14959678e060d674ffb6.graph and /dev/null differ diff --git a/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/info/packs b/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/info/packs deleted file mode 100644 index 3b1241c4..00000000 --- a/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/info/packs +++ /dev/null @@ -1,2 +0,0 @@ -P pack-04168d0884c910f505cb9fbcf045957e44ccee06d812b5e531ae666014a26ed1.pack - diff --git a/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/pack/pack-04168d0884c910f505cb9fbcf045957e44ccee06d812b5e531ae666014a26ed1.bitmap b/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/pack/pack-04168d0884c910f505cb9fbcf045957e44ccee06d812b5e531ae666014a26ed1.bitmap deleted file mode 100644 index 007fcd0e..00000000 Binary files a/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/pack/pack-04168d0884c910f505cb9fbcf045957e44ccee06d812b5e531ae666014a26ed1.bitmap and /dev/null differ diff --git a/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/pack/pack-04168d0884c910f505cb9fbcf045957e44ccee06d812b5e531ae666014a26ed1.idx b/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/pack/pack-04168d0884c910f505cb9fbcf045957e44ccee06d812b5e531ae666014a26ed1.idx deleted file mode 100644 index 248cf8fc..00000000 Binary files a/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/pack/pack-04168d0884c910f505cb9fbcf045957e44ccee06d812b5e531ae666014a26ed1.idx and /dev/null differ diff --git a/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/pack/pack-04168d0884c910f505cb9fbcf045957e44ccee06d812b5e531ae666014a26ed1.pack b/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/pack/pack-04168d0884c910f505cb9fbcf045957e44ccee06d812b5e531ae666014a26ed1.pack deleted file mode 100644 index 92cea7fb..00000000 Binary files a/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/pack/pack-04168d0884c910f505cb9fbcf045957e44ccee06d812b5e531ae666014a26ed1.pack and /dev/null differ diff --git a/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/pack/pack-04168d0884c910f505cb9fbcf045957e44ccee06d812b5e531ae666014a26ed1.rev b/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/pack/pack-04168d0884c910f505cb9fbcf045957e44ccee06d812b5e531ae666014a26ed1.rev deleted file mode 100644 index 569862ce..00000000 Binary files a/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/pack/pack-04168d0884c910f505cb9fbcf045957e44ccee06d812b5e531ae666014a26ed1.rev and /dev/null differ diff --git a/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/refs/heads/master b/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/refs/heads/master deleted file mode 100644 index 29d83be8..00000000 --- a/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/refs/heads/master +++ /dev/null @@ -1 +0,0 @@ -10d2943dc7ad88011cae3b776d9565d6451a350ce1d16949bc8546a5fe6c0a53 diff --git a/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/HEAD b/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/HEAD deleted file mode 100644 index b870d826..00000000 --- a/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/HEAD +++ /dev/null @@ -1 +0,0 @@ -ref: refs/heads/main diff --git a/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/config b/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/config deleted file mode 100644 index 7d1c0006..00000000 --- a/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/config +++ /dev/null @@ -1,6 +0,0 @@ -[extensions] - objectformat = sha256 -[core] - repositoryformatversion = 1 - filemode = true - bare = true diff --git a/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/objects/info/commit-graph b/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/objects/info/commit-graph deleted file mode 100644 index f4dd0e0c..00000000 Binary files a/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/objects/info/commit-graph and /dev/null differ diff --git a/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/objects/info/packs b/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/objects/info/packs deleted file mode 100644 index 0f39ed89..00000000 --- a/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/objects/info/packs +++ /dev/null @@ -1,2 +0,0 @@ -P pack-316dbc67dac12d131591640da0c55b76387cbf1fd2a117ab3d7ca0d854a031c9.pack - diff --git a/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/objects/pack/pack-316dbc67dac12d131591640da0c55b76387cbf1fd2a117ab3d7ca0d854a031c9.bitmap b/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/objects/pack/pack-316dbc67dac12d131591640da0c55b76387cbf1fd2a117ab3d7ca0d854a031c9.bitmap deleted file mode 100644 index b5c5055c..00000000 Binary files a/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/objects/pack/pack-316dbc67dac12d131591640da0c55b76387cbf1fd2a117ab3d7ca0d854a031c9.bitmap and /dev/null differ diff --git a/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/objects/pack/pack-316dbc67dac12d131591640da0c55b76387cbf1fd2a117ab3d7ca0d854a031c9.idx b/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/objects/pack/pack-316dbc67dac12d131591640da0c55b76387cbf1fd2a117ab3d7ca0d854a031c9.idx deleted file mode 100644 index 144778cd..00000000 Binary files a/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/objects/pack/pack-316dbc67dac12d131591640da0c55b76387cbf1fd2a117ab3d7ca0d854a031c9.idx and /dev/null differ diff --git a/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/objects/pack/pack-316dbc67dac12d131591640da0c55b76387cbf1fd2a117ab3d7ca0d854a031c9.pack b/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/objects/pack/pack-316dbc67dac12d131591640da0c55b76387cbf1fd2a117ab3d7ca0d854a031c9.pack deleted file mode 100644 index 599ccae0..00000000 Binary files a/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/objects/pack/pack-316dbc67dac12d131591640da0c55b76387cbf1fd2a117ab3d7ca0d854a031c9.pack and /dev/null differ diff --git a/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/objects/pack/pack-316dbc67dac12d131591640da0c55b76387cbf1fd2a117ab3d7ca0d854a031c9.rev b/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/objects/pack/pack-316dbc67dac12d131591640da0c55b76387cbf1fd2a117ab3d7ca0d854a031c9.rev deleted file mode 100644 index 3c093f31..00000000 Binary files a/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/objects/pack/pack-316dbc67dac12d131591640da0c55b76387cbf1fd2a117ab3d7ca0d854a031c9.rev and /dev/null differ diff --git a/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/refs/heads/main b/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/refs/heads/main deleted file mode 100644 index 4ba32358..00000000 --- a/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/refs/heads/main +++ /dev/null @@ -1 +0,0 @@ -a9ff114900e6be139ec66a2a61c930973d8c4bc6fd3b899405ee7ab8740bdbd3 diff --git a/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/HEAD b/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/HEAD deleted file mode 100644 index cb089cd8..00000000 --- a/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/HEAD +++ /dev/null @@ -1 +0,0 @@ -ref: refs/heads/master diff --git a/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/config b/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/config deleted file mode 100644 index 7d1c0006..00000000 --- a/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/config +++ /dev/null @@ -1,6 +0,0 @@ -[extensions] - objectformat = sha256 -[core] - repositoryformatversion = 1 - filemode = true - bare = true diff --git a/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/objects/info/commit-graph b/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/objects/info/commit-graph deleted file mode 100644 index f98ca4a1..00000000 Binary files a/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/objects/info/commit-graph and /dev/null differ diff --git a/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/objects/info/packs b/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/objects/info/packs deleted file mode 100644 index 65184c9a..00000000 --- a/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/objects/info/packs +++ /dev/null @@ -1,2 +0,0 @@ -P pack-d335453f760b064e36459d780ec9bf0e5dd596c0ee1ac6310136067c4f13438b.pack - diff --git a/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/objects/pack/pack-d335453f760b064e36459d780ec9bf0e5dd596c0ee1ac6310136067c4f13438b.bitmap b/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/objects/pack/pack-d335453f760b064e36459d780ec9bf0e5dd596c0ee1ac6310136067c4f13438b.bitmap deleted file mode 100644 index 53530f4c..00000000 Binary files a/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/objects/pack/pack-d335453f760b064e36459d780ec9bf0e5dd596c0ee1ac6310136067c4f13438b.bitmap and /dev/null differ diff --git a/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/objects/pack/pack-d335453f760b064e36459d780ec9bf0e5dd596c0ee1ac6310136067c4f13438b.idx b/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/objects/pack/pack-d335453f760b064e36459d780ec9bf0e5dd596c0ee1ac6310136067c4f13438b.idx deleted file mode 100644 index b3a417a8..00000000 Binary files a/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/objects/pack/pack-d335453f760b064e36459d780ec9bf0e5dd596c0ee1ac6310136067c4f13438b.idx and /dev/null differ diff --git a/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/objects/pack/pack-d335453f760b064e36459d780ec9bf0e5dd596c0ee1ac6310136067c4f13438b.pack b/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/objects/pack/pack-d335453f760b064e36459d780ec9bf0e5dd596c0ee1ac6310136067c4f13438b.pack deleted file mode 100644 index d8dcedbf..00000000 Binary files a/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/objects/pack/pack-d335453f760b064e36459d780ec9bf0e5dd596c0ee1ac6310136067c4f13438b.pack and /dev/null differ diff --git a/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/objects/pack/pack-d335453f760b064e36459d780ec9bf0e5dd596c0ee1ac6310136067c4f13438b.rev b/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/objects/pack/pack-d335453f760b064e36459d780ec9bf0e5dd596c0ee1ac6310136067c4f13438b.rev deleted file mode 100644 index e50d1a81..00000000 Binary files a/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/objects/pack/pack-d335453f760b064e36459d780ec9bf0e5dd596c0ee1ac6310136067c4f13438b.rev and /dev/null differ diff --git a/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/refs/heads/master b/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/refs/heads/master deleted file mode 100644 index a4e184b4..00000000 --- a/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/refs/heads/master +++ /dev/null @@ -1 +0,0 @@ -7e396bf648e3b045c293d9fbdc533d4377d4e801d5d1fb57b84d22dd054a5860 diff --git a/format/doc.go b/format/doc.go deleted file mode 100644 index 0d2ec813..00000000 --- a/format/doc.go +++ /dev/null @@ -1,5 +0,0 @@ -// Package format encapsulates various git-related file formats. -// -// These are particularly the ones that aren't necessarily associated with -// a very clear domain that they obviously belong to. -package format diff --git a/format/packfile/delta/apply/apply.go b/format/packfile/delta/apply/apply.go deleted file mode 100644 index f5006e3c..00000000 --- a/format/packfile/delta/apply/apply.go +++ /dev/null @@ -1,160 +0,0 @@ -// Package apply applies Git delta instruction streams. -package apply - -import "fmt" - -// Apply applies one Git delta instruction stream to base. -func Apply(base, delta []byte) ([]byte, error) { - pos := 0 - - srcSize, err := readVarint(delta, &pos) - if err != nil { - return nil, err - } - - dstSize, err := readVarint(delta, &pos) - if err != nil { - return nil, err - } - - if srcSize != len(base) { - return nil, fmt.Errorf("delta/apply: delta source size mismatch: got %d want %d", srcSize, len(base)) - } - - out := make([]byte, dstSize) - outPos := 0 - - for pos < len(delta) { - op := delta[pos] - pos++ - - //nolint:nestif - if op&0x80 != 0 { - off := 0 - - if op&0x01 != 0 { - if pos >= len(delta) { - return nil, fmt.Errorf("delta/apply: malformed delta copy offset") - } - - off |= int(delta[pos]) - pos++ - } - - if op&0x02 != 0 { - if pos >= len(delta) { - return nil, fmt.Errorf("delta/apply: malformed delta copy offset") - } - - off |= int(delta[pos]) << 8 - pos++ - } - - if op&0x04 != 0 { - if pos >= len(delta) { - return nil, fmt.Errorf("delta/apply: malformed delta copy offset") - } - - off |= int(delta[pos]) << 16 - pos++ - } - - if op&0x08 != 0 { - if pos >= len(delta) { - return nil, fmt.Errorf("delta/apply: malformed delta copy offset") - } - - off |= int(delta[pos]) << 24 - pos++ - } - - n := 0 - - if op&0x10 != 0 { - if pos >= len(delta) { - return nil, fmt.Errorf("delta/apply: malformed delta copy size") - } - - n |= int(delta[pos]) - pos++ - } - - if op&0x20 != 0 { - if pos >= len(delta) { - return nil, fmt.Errorf("delta/apply: malformed delta copy size") - } - - n |= int(delta[pos]) << 8 - pos++ - } - - if op&0x40 != 0 { - if pos >= len(delta) { - return nil, fmt.Errorf("delta/apply: malformed delta copy size") - } - - n |= int(delta[pos]) << 16 - pos++ - } - - if n == 0 { - n = 0x10000 - } - - if off < 0 || n < 0 || off+n > len(base) || outPos+n > len(out) { - return nil, fmt.Errorf("delta/apply: delta copy out of bounds") - } - - copy(out[outPos:outPos+n], base[off:off+n]) - outPos += n - - continue - } - - if op == 0 { - return nil, fmt.Errorf("delta/apply: invalid delta opcode 0") - } - - n := int(op) - if pos+n > len(delta) || outPos+n > len(out) { - return nil, fmt.Errorf("delta/apply: delta insert out of bounds") - } - - copy(out[outPos:outPos+n], delta[pos:pos+n]) - outPos += n - pos += n - } - - if outPos != len(out) { - return nil, fmt.Errorf("delta/apply: delta output size mismatch: got %d want %d", outPos, len(out)) - } - - return out, nil -} - -// readVarint parses one Git delta varint and advances pos. -func readVarint(buf []byte, pos *int) (int, error) { - value := 0 - shift := uint(0) - - for { - if *pos >= len(buf) { - return 0, fmt.Errorf("delta/apply: malformed delta varint") - } - - b := buf[*pos] - *pos++ - - value |= int(b&0x7f) << shift - if b&0x80 == 0 { - break - } - - shift += 7 - if shift > 63 { - return 0, fmt.Errorf("delta/apply: delta varint overflow") - } - } - - return value, nil -} diff --git a/format/packfile/delta/apply/header.go b/format/packfile/delta/apply/header.go deleted file mode 100644 index 69c9659a..00000000 --- a/format/packfile/delta/apply/header.go +++ /dev/null @@ -1,47 +0,0 @@ -package apply - -import ( - "fmt" - "io" -) - -// ReadHeaderSizes reads the first two varints in one inflated delta stream. -// -// Callers that continue reading the same stream should pass their own buffered -// byte reader and keep using that same reader afterwards. -func ReadHeaderSizes(reader io.ByteReader) (int, int, error) { - srcSize, err := readVarintFromByteReader(reader) - if err != nil { - return 0, 0, err - } - - dstSize, err := readVarintFromByteReader(reader) - if err != nil { - return 0, 0, err - } - - return srcSize, dstSize, nil -} - -// readVarintFromByteReader parses one Git delta varint from reader. -func readVarintFromByteReader(reader io.ByteReader) (int, error) { - value := 0 - shift := uint(0) - - for { - b, err := reader.ReadByte() - if err != nil { - return 0, fmt.Errorf("delta/apply: malformed delta varint: %w", err) - } - - value |= int(b&0x7f) << shift - if b&0x80 == 0 { - return value, nil - } - - shift += 7 - if shift > 63 { - return 0, fmt.Errorf("delta/apply: delta varint overflow") - } - } -} diff --git a/format/packfile/delta/doc.go b/format/packfile/delta/doc.go deleted file mode 100644 index f63c96a8..00000000 --- a/format/packfile/delta/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package delta provides various routines to handle Git delta compression. -package delta diff --git a/format/packfile/doc.go b/format/packfile/doc.go deleted file mode 100644 index cd4aacfc..00000000 --- a/format/packfile/doc.go +++ /dev/null @@ -1,5 +0,0 @@ -// Package packfile provides Git packfile format parsing primitives. -package packfile - -// TODO: This could probably be moved into object/store/packed when we get the pack ingestion semantics right? -// Oh, wait, the other stores might still want pack constants like we provide here. diff --git a/format/packfile/entry.go b/format/packfile/entry.go deleted file mode 100644 index 0f9c7c8d..00000000 --- a/format/packfile/entry.go +++ /dev/null @@ -1,76 +0,0 @@ -package packfile - -import ( - "fmt" - - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// Entry is one parsed pack entry prefix, including any delta base reference -// data that appears before the compressed payload. -type Entry struct { - // Type is the pack entry type. - Type objecttype.Type - // Size is the declared resulting object size. - Size int64 - // DataOffset is the byte offset from the start of the entry to the zlib - // payload bytes. - DataOffset int - // RefBaseID is the referenced base object ID bytes for ref-delta entries. - RefBaseID []byte - // OfsBaseDistance is the backward distance for ofs-delta entries. - OfsBaseDistance uint64 -} - -// ParseEntry parses one full pack entry prefix from data. -// -// hashSize must match the hash algorithm size used by the pack/index. -func ParseEntry(data []byte, hashSize int) (Entry, error) { - var zero Entry - - header, err := ParseEntryHeader(data) - if err != nil { - return zero, err - } - - entry := Entry{ - Type: header.Type, - Size: header.Size, - DataOffset: header.HeaderSize, - } - - switch entry.Type { - case objecttype.TypeCommit, objecttype.TypeTree, objecttype.TypeBlob, objecttype.TypeTag: - // Base object entries have no extra prefix fields. - case objecttype.TypeRefDelta: - if hashSize <= 0 { - return zero, fmt.Errorf("packfile: invalid hash size %d", hashSize) - } - - end := entry.DataOffset + hashSize - if end > len(data) { - return zero, fmt.Errorf("packfile: truncated ref-delta base id") - } - - entry.RefBaseID = data[entry.DataOffset:end] - entry.DataOffset = end - case objecttype.TypeOfsDelta: - dist, consumed, err := ParseOfsDeltaDistance(data[entry.DataOffset:]) - if err != nil { - return zero, err - } - - entry.OfsBaseDistance = dist - entry.DataOffset += consumed - case objecttype.TypeInvalid, objecttype.TypeFuture: - return zero, fmt.Errorf("packfile: unsupported object type %d", entry.Type) - default: - return zero, fmt.Errorf("packfile: unsupported object type %d", entry.Type) - } - - if entry.DataOffset > len(data) { - return zero, fmt.Errorf("packfile: entry data offset out of bounds") - } - - return entry, nil -} diff --git a/format/packfile/entry_header.go b/format/packfile/entry_header.go deleted file mode 100644 index 05664268..00000000 --- a/format/packfile/entry_header.go +++ /dev/null @@ -1,52 +0,0 @@ -package packfile - -import ( - "fmt" - - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// EntryHeader is one parsed pack entry header prefix. -type EntryHeader struct { - // Type is the entry type tag from the first header byte. - Type objecttype.Type - // Size is the declared resulting object size. - Size int64 - // HeaderSize is the number of bytes consumed by the type/size header. - HeaderSize int -} - -// ParseEntryHeader parses one pack entry type/size header from data. -func ParseEntryHeader(data []byte) (EntryHeader, error) { - var zero EntryHeader - if len(data) == 0 { - return zero, fmt.Errorf("packfile: truncated entry header") - } - - first := data[0] - header := EntryHeader{ - Type: objecttype.Type((first >> 4) & 0x07), - Size: int64(first & 0x0f), - HeaderSize: 1, - } - - shift := uint(4) - - b := first - for b&0x80 != 0 { - if header.HeaderSize >= len(data) { - return zero, fmt.Errorf("packfile: truncated entry header") - } - - b = data[header.HeaderSize] - header.HeaderSize++ - header.Size |= int64(b&0x7f) << shift - shift += 7 - } - - if header.Size < 0 { - return zero, fmt.Errorf("packfile: negative entry size") - } - - return header, nil -} diff --git a/format/packfile/header.go b/format/packfile/header.go deleted file mode 100644 index 5f4e4508..00000000 --- a/format/packfile/header.go +++ /dev/null @@ -1,9 +0,0 @@ -package packfile - -// Signature is the 4-byte "PACK" magic at the start of pack files. -const Signature = 0x5041434b - -// SupportedVersion reports whether one pack version is supported. -func SupportedVersion(version uint32) bool { - return version == 2 || version == 3 -} diff --git a/format/packfile/ofs.go b/format/packfile/ofs.go deleted file mode 100644 index 4992a506..00000000 --- a/format/packfile/ofs.go +++ /dev/null @@ -1,26 +0,0 @@ -package packfile - -import "fmt" - -// ParseOfsDeltaDistance parses one ofs-delta backward distance. -func ParseOfsDeltaDistance(buf []byte) (uint64, int, error) { - if len(buf) == 0 { - return 0, 0, fmt.Errorf("packfile: malformed ofs-delta distance") - } - - b := buf[0] - dist := uint64(b & 0x7f) - - consumed := 1 - for b&0x80 != 0 { - if consumed >= len(buf) { - return 0, 0, fmt.Errorf("packfile: malformed ofs-delta distance") - } - - b = buf[consumed] - consumed++ - dist = ((dist + 1) << 7) + uint64(b&0x7f) - } - - return dist, consumed, nil -} diff --git a/furgit.go b/furgit.go deleted file mode 100644 index 60966584..00000000 --- a/furgit.go +++ /dev/null @@ -1,71 +0,0 @@ -// Package furgit provides low-level Git operations. -// -// Furgit provides absolutely no guarantees on correctness, performance, -// API stability. In particular, before version 1.0.0, no attempt at -// API stability is made at all, and breaking changes may be introduced -// in patch-level releases. See also the warranty and liability disclaimers -// in the license. -// -// Git libraries often center on a repository type that owns objects, refs, -// worktree state, and configuration behind a single facade. Furgit inverts -// that: objects are plain values, stored objects are separate types that -// associate objects with their object IDs, object storage and ref storage -// are sets of narrow interfaces consisting only of things that are truly -// reasonable for all implementations to satisfy, and every higher-level -// operation, such as commit traversal, reachability analysis, and -// recursive peeling, is built over those interfaces. -// -// While the [codeberg.org/lindenii/furgit/repository] package is where -// most users should begin, it only exists as one convenient composition of -// those pieces for the standard on-disk repository layout. Nothing inside -// furgit should depend on it; extensions to furgit such as alterntaive -// object stores must not depend on it either. -// -// # Contract labels -// -// Many furgit APIs document concurrency, dependency ownership, value lifetime, -// and close behavior using short labels. -// These labels summarize the API contract, but they do not replace the full -// doc comment on a package, type, function, method, constant, or variable. -// -// When both a type and one of its methods specify labels, the method-level -// labels take precedence for that operation. -// -// Concurrency labels: -// -// - MT-Safe: safe for concurrent use. -// - MT-Unsafe: not safe for concurrent use without external synchronization. -// -// Dependency labels: -// -// - Deps-Owned: the receiver takes ownership of all supplied dependencies -// where ownership is a reasonable concept. -// - Deps-Borrowed: the value borrows supplied dependencies. Also Life-Parent -// in most cases, unless those dependencies are not retained past -// construction. -// - Deps-Mixed: some supplied dependencies are owned and others are borrowed. -// -// Lifetime labels: -// -// - Life-Independent: returned values remain valid independently of the -// parent or provider. -// - Life-Parent: returned values are only valid while the parent or provider -// remains valid. -// - Life-Call: returned values are only valid for the duration of the -// current call, callback, or hook invocation. -// -// Close labels: -// -// - Close-Caller: the caller must close the returned value. -// - Close-No: the caller must not close the returned value directly. -// - Close-Idem: repeated Close calls are safe. -// -// Mutation labels: -// -// - Mut-Never: returned values must not be mutated. -// -// Unless Close-Idem is specified, repeated Close calls are undefined behavior. -// -// Unless a doc comment explicitly states otherwise, these labels describe the -// API contract only. They do not imply any specific implementation strategy. -package furgit diff --git a/go.mod b/go.mod deleted file mode 100644 index 390ca978..00000000 --- a/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module codeberg.org/lindenii/furgit - -go 1.26.0 diff --git a/internal/adler32/LICENSE b/internal/adler32/LICENSE deleted file mode 100644 index 5cec357a..00000000 --- a/internal/adler32/LICENSE +++ /dev/null @@ -1,30 +0,0 @@ -Copyright (c) 2024, Michal Hruby -Copyright (c) 2017 The Chromium Authors. All rights reserved. -Copyright (c) 1995-2024 Mark Adler -Copyright (c) 1995-2024 Jean-loup Gailly -Copyright (c) 2022 Adam Stylinski - -BSD 2-Clause License - - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - diff --git a/internal/adler32/LICENSE.ZLIB b/internal/adler32/LICENSE.ZLIB deleted file mode 100644 index c75c1568..00000000 --- a/internal/adler32/LICENSE.ZLIB +++ /dev/null @@ -1,17 +0,0 @@ -Copyright (C) 1995-2024 Jean-loup Gailly and Mark Adler - -This software is provided 'as-is', without any express or implied -warranty. In no event will the authors be held liable for any damages -arising from the use of this software. - -Permission is granted to anyone to use this software for any purpose, -including commercial applications, and to alter it and redistribute it -freely, subject to the following restrictions: - -1. The origin of this software must not be misrepresented; you must not - claim that you wrote the original software. If you use this software - in a product, an acknowledgment in the product documentation would be - appreciated but is not required. -2. Altered source versions must be plainly marked as such, and must not be - misrepresented as being the original software. -3. This notice may not be removed or altered from any source distribution. diff --git a/internal/adler32/README b/internal/adler32/README deleted file mode 100644 index b80acd00..00000000 --- a/internal/adler32/README +++ /dev/null @@ -1 +0,0 @@ -This package was mostly copied from github.com/mhr3/adler32-simd. diff --git a/internal/adler32/adler32_amd64.go b/internal/adler32/adler32_amd64.go deleted file mode 100644 index 49ed8b6e..00000000 --- a/internal/adler32/adler32_amd64.go +++ /dev/null @@ -1,89 +0,0 @@ -//go:build amd64 && !purego - -package adler32 - -import ( - "encoding/binary" - "errors" - "hash" - "hash/adler32" - - "codeberg.org/lindenii/furgit/internal/cpu" -) - -// Size of an Adler-32 checksum in bytes. -const Size = 4 - -//nolint:gochecknoglobals -var hasAVX2 = cpu.X86.HasAVX2 - -// digest represents the partial evaluation of a checksum. -// The low 16 bits are s1, the high 16 bits are s2. -type digest uint32 - -func (d *digest) Reset() { *d = 1 } - -// New returns a new hash.Hash32 computing the Adler-32 checksum. -func New() hash.Hash32 { - if !hasAVX2 { - return adler32.New() - } - - d := new(digest) - d.Reset() - - return d -} - -func (d *digest) MarshalBinary() ([]byte, error) { - b := make([]byte, 0, marshaledSize) - b = append(b, magic...) - b = binary.BigEndian.AppendUint32(b, uint32(*d)) - - return b, nil -} - -func (d *digest) UnmarshalBinary(b []byte) error { - if len(b) < len(magic) || string(b[:len(magic)]) != magic { - return errors.New("hash/adler32: invalid hash state identifier") - } - - if len(b) != marshaledSize { - return errors.New("hash/adler32: invalid hash state size") - } - - *d = digest(binary.BigEndian.Uint32(b[len(magic):])) - - return nil -} - -func (d *digest) Size() int { return Size } - -func (d *digest) BlockSize() int { return 4 } - -func (d *digest) Write(data []byte) (nn int, err error) { - if hasAVX2 && len(data) >= 64 { - h := adler32_avx2(uint32(*d), data) - *d = digest(h) - } else { - h := update(uint32(*d), data) - *d = digest(h) - } - - return len(data), nil -} - -func (d *digest) Sum32() uint32 { return uint32(*d) } - -func (d *digest) Sum(in []byte) []byte { - return binary.BigEndian.AppendUint32(in, uint32(*d)) -} - -// Checksum returns the Adler-32 checksum of data. -func Checksum(data []byte) uint32 { - if hasAVX2 && len(data) >= 64 { - return adler32_avx2(1, data) - } - - return adler32.Checksum(data) -} diff --git a/internal/adler32/adler32_avx2.go b/internal/adler32/adler32_avx2.go deleted file mode 100644 index 042812b8..00000000 --- a/internal/adler32/adler32_avx2.go +++ /dev/null @@ -1,6 +0,0 @@ -//go:build !purego && amd64 - -package adler32 - -//go:noescape -func adler32_avx2(in uint32, buf []byte) uint32 diff --git a/internal/adler32/adler32_avx2.s b/internal/adler32/adler32_avx2.s deleted file mode 100644 index a883e357..00000000 --- a/internal/adler32/adler32_avx2.s +++ /dev/null @@ -1,251 +0,0 @@ -//go:build !purego && amd64 - -#include "textflag.h" - -DATA adler32AVX2ByteWeights<>+0x00(SB)/8, $0x191a1b1c1d1e1f20 -DATA adler32AVX2ByteWeights<>+0x08(SB)/8, $0x1112131415161718 -DATA adler32AVX2ByteWeights<>+0x10(SB)/8, $0x090a0b0c0d0e0f10 -DATA adler32AVX2ByteWeights<>+0x18(SB)/8, $0x0102030405060708 -GLOBL adler32AVX2ByteWeights<>(SB), (RODATA|NOPTR), $32 - -DATA adler32AVX2WordOne<>+0x00(SB)/2, $0x0001 -GLOBL adler32AVX2WordOne<>(SB), (RODATA|NOPTR), $2 - -TEXT ·adler32_avx2(SB), NOSPLIT, $0-36 - MOVLQZX in+0(FP), DI - MOVQ buf_base+8(FP), SI - MOVQ buf_len+16(FP), DX - MOVQ buf_cap+24(FP), CX - TESTQ SI, SI - JE return_one - MOVL DI, AX - TESTQ DX, DX - JE return_current - MOVL AX, CX - SHRL $0x10, CX - MOVWLZX AX, AX - CMPQ DX, $0x20 - JB scalar_unrolled16 - MOVL $2147975281, DI - VPXOR X0, X0, X0 - VMOVDQA adler32AVX2ByteWeights<>(SB), Y1 - VPBROADCASTW adler32AVX2WordOne<>(SB), Y2 - JMP vector_outer - -vector_tail_init: - VMOVDQA Y4, Y6 - VPXOR X5, X5, X5 - -vector_reduce_finalize_chunk: - SUBQ AX, DX - VPSLLD $0x05, Y5, Y4 - VPADDD Y3, Y4, Y3 - VEXTRACTI128 $0x1, Y6, X4 - VSHUFPS $0x88, X4, X6, X5 - VPSHUFD $0x88, X4, X4 - VPADDD X4, X5, X4 - VPSHUFD $0x55, X4, X5 - VPADDD X4, X5, X4 - VMOVD X4, AX - MOVQ AX, CX - IMULQ DI, CX - SHRQ $0x2f, CX - IMULL $0xfff1, CX - SUBL CX, AX - VEXTRACTI128 $0x1, Y3, X4 - VPADDD X3, X4, X3 - VPSHUFD $0xee, X3, X4 - VPADDD X4, X3, X3 - VPSHUFD $0x55, X3, X4 - VPADDD X3, X4, X3 - VMOVD X3, CX - MOVQ CX, R8 - IMULQ DI, R8 - SHRQ $0x2f, R8 - IMULL $0xfff1, R8 - SUBL R8, CX - CMPQ DX, $0x1f - JBE scalar_entry - -vector_outer: - VMOVD AX, X4 - VMOVD CX, X3 - CMPQ DX, $0x15b0 - MOVL $0x15b0, R8 - CMOVQCS DX, R8 - MOVL R8, AX - ANDL $0x1fe0, AX - JE vector_tail_init - ADDQ $-0x20, R8 - VPXOR X5, X5, X5 - TESTL $0x20, R8 - JNE vector_block32_check - VMOVDQU 0(SI), Y5 - ADDQ $0x20, SI - LEAQ -0x20(AX), CX - VPSADBW Y0, Y5, Y6 - VPADDD Y4, Y6, Y6 - VPMADDUBSW Y1, Y5, Y5 - VPMADDWD Y2, Y5, Y5 - VPADDD Y3, Y5, Y3 - VMOVDQA Y4, Y5 - VMOVDQA Y6, Y4 - CMPQ R8, $0x20 - JAE vector_block64_loop - JMP vector_reduce_finalize_chunk - -vector_block32_check: - MOVQ AX, CX - CMPQ R8, $0x20 - JB vector_reduce_finalize_chunk - -vector_block64_loop: - VMOVDQU 0(SI), Y6 - VMOVDQU 0x20(SI), Y7 - VPSADBW Y0, Y6, Y8 - VPADDD Y4, Y8, Y8 - VPADDD Y4, Y5, Y5 - VPMADDUBSW Y1, Y6, Y4 - VPMADDWD Y2, Y4, Y4 - VPADDD Y3, Y4, Y3 - ADDQ $0x40, SI - VPSADBW Y0, Y7, Y4 - VPADDD Y4, Y8, Y4 - VPADDD Y5, Y8, Y5 - VPMADDUBSW Y1, Y7, Y6 - VPMADDWD Y2, Y6, Y6 - VPADDD Y3, Y6, Y3 - ADDQ $-0x40, CX - JNE vector_block64_loop - VMOVDQA Y4, Y6 - JMP vector_reduce_finalize_chunk - -return_one: - MOVL $0x1, AX - -return_current: - MOVL AX, ret+32(FP) - RET - -scalar_entry: - TESTQ DX, DX - JE return_final - -scalar_unrolled16: - CMPQ DX, $0x10 - JB scalar_byte_prelude - MOVBLZX 0(SI), DI - ADDL DI, AX - ADDL AX, CX - MOVBLZX 0x1(SI), DI - ADDL AX, DI - ADDL DI, CX - MOVBLZX 0x2(SI), AX - ADDL DI, AX - ADDL AX, CX - MOVBLZX 0x3(SI), DI - ADDL AX, DI - ADDL DI, CX - MOVBLZX 0x4(SI), AX - ADDL DI, AX - ADDL AX, CX - MOVBLZX 0x5(SI), DI - ADDL AX, DI - ADDL DI, CX - MOVBLZX 0x6(SI), AX - ADDL DI, AX - ADDL AX, CX - MOVBLZX 0x7(SI), DI - ADDL AX, DI - ADDL DI, CX - MOVBLZX 0x8(SI), AX - ADDL DI, AX - ADDL AX, CX - MOVBLZX 0x9(SI), DI - ADDL AX, DI - ADDL DI, CX - MOVBLZX 0xa(SI), AX - ADDL DI, AX - ADDL AX, CX - MOVBLZX 0xb(SI), DI - ADDL AX, DI - ADDL DI, CX - MOVBLZX 0xc(SI), AX - ADDL DI, AX - ADDL AX, CX - MOVBLZX 0xd(SI), DI - ADDL AX, DI - ADDL DI, CX - MOVBLZX 0xe(SI), R8 - ADDL DI, R8 - ADDL R8, CX - MOVBLZX 0xf(SI), AX - ADDL R8, AX - ADDL AX, CX - ADDQ $-0x10, DX - JE scalar_finalize - ADDQ $0x10, SI - -scalar_byte_prelude: - LEAQ -0x1(DX), DI - MOVQ DX, R9 - ANDQ $0x3, R9 - JE scalar_dword_prelude - XORL R8, R8 - -scalar_byte_prelude_loop: - MOVBLZX 0(SI)(R8*1), R10 - ADDL R10, AX - ADDL AX, CX - INCQ R8 - CMPQ R9, R8 - JNE scalar_byte_prelude_loop - ADDQ R8, SI - SUBQ R8, DX - -scalar_dword_prelude: - CMPQ DI, $0x3 - JB scalar_finalize - XORL DI, DI - -scalar_dword_loop: - MOVBLZX 0(SI)(DI*1), R8 - ADDL AX, R8 - ADDL R8, CX - MOVBLZX 0x1(SI)(DI*1), AX - ADDL R8, AX - ADDL AX, CX - MOVBLZX 0x2(SI)(DI*1), R8 - ADDL AX, R8 - ADDL R8, CX - MOVBLZX 0x3(SI)(DI*1), AX - ADDL R8, AX - ADDL AX, CX - ADDQ $0x4, DI - CMPQ DX, DI - JNE scalar_dword_loop - -scalar_finalize: - LEAL -0xfff1(AX), DX - CMPL AX, $0xfff1 - CMOVLCS AX, DX - MOVL CX, AX - MOVL $2147975281, SI - IMULQ AX, SI - SHRQ $0x2f, SI - MOVL SI, AX - IMULL $0xfff1, AX - SUBL AX, CX - SHLL $0x10, CX - ORL DX, CX - MOVL CX, AX - VZEROUPPER - MOVL AX, ret+32(FP) - RET - -return_final: - SHLL $0x10, CX - ORL CX, AX - VZEROUPPER - MOVL AX, ret+32(FP) - RET diff --git a/internal/adler32/adler32_fallback.go b/internal/adler32/adler32_fallback.go deleted file mode 100644 index 717d860d..00000000 --- a/internal/adler32/adler32_fallback.go +++ /dev/null @@ -1,19 +0,0 @@ -//go:build !amd64 || purego - -package adler32 - -import ( - "hash" - "hash/adler32" -) - -// The size of an Adler-32 checksum in bytes. -const Size = 4 - -// New returns a new hash.Hash32 computing the Adler-32 checksum. -func New() hash.Hash32 { - return adler32.New() -} - -// Checksum returns the Adler-32 checksum of data. -func Checksum(data []byte) uint32 { return adler32.Checksum(data) } diff --git a/internal/adler32/adler32_generic.go b/internal/adler32/adler32_generic.go deleted file mode 100644 index 56e3ff8b..00000000 --- a/internal/adler32/adler32_generic.go +++ /dev/null @@ -1,49 +0,0 @@ -package adler32 - -const ( - // mod is the largest prime that is less than 65536. - mod = 65521 - // nmax is the largest n such that - // 255 * n * (n+1) / 2 + (n+1) * (mod-1) <= 2^32-1. - // It is mentioned in RFC 1950 (search for "5552"). - nmax = 5552 - - // binary representation compatible with standard library. - magic = "adl\x01" - marshaledSize = len(magic) + 4 -) - -// Add p to the running checksum d. -func update(d uint32, p []byte) uint32 { - s1, s2 := d&0xffff, d>>16 - - for len(p) > 0 { - var q []byte - if len(p) > nmax { - p, q = p[:nmax], p[nmax:] - } - - for len(p) >= 4 { - s1 += uint32(p[0]) - s2 += s1 - s1 += uint32(p[1]) - s2 += s1 - s1 += uint32(p[2]) - s2 += s1 - s1 += uint32(p[3]) - s2 += s1 - p = p[4:] - } - - for _, x := range p { - s1 += uint32(x) - s2 += s1 - } - - s1 %= mod - s2 %= mod - p = q - } - - return s2<<16 | s1 -} diff --git a/internal/adler32/bench_test.go b/internal/adler32/bench_test.go deleted file mode 100644 index 1161221a..00000000 --- a/internal/adler32/bench_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package adler32_test - -import ( - "testing" - - "codeberg.org/lindenii/furgit/internal/adler32" -) - -const benchmarkSize = 64 * 1024 - -//nolint:gochecknoglobals -var data = make([]byte, benchmarkSize) - -func init() { //nolint:gochecknoinits - for i := range benchmarkSize { - data[i] = byte(i % 256) - } -} - -func BenchmarkChecksum(b *testing.B) { - b.ReportAllocs() - - for b.Loop() { - adler32.Checksum(data) - } -} diff --git a/internal/adler32/doc.go b/internal/adler32/doc.go deleted file mode 100644 index add30867..00000000 --- a/internal/adler32/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package adler32 implements an SIMD-optimized Adler-32 checksum. -package adler32 diff --git a/internal/bufpool/append.go b/internal/bufpool/append.go deleted file mode 100644 index f19dbc78..00000000 --- a/internal/bufpool/append.go +++ /dev/null @@ -1,16 +0,0 @@ -package bufpool - -// Append copies the provided bytes onto the end of the buffer, growing its -// capacity if required. If src is empty, the method does nothing. -// -// The receiver retains ownership of the data; the caller may reuse src freely. -func (buf *Buffer) Append(src []byte) { - if len(src) == 0 { - return - } - - start := len(buf.buf) - buf.ensureCapacity(start + len(src)) - buf.buf = buf.buf[:start+len(src)] - copy(buf.buf[start:], src) -} diff --git a/internal/bufpool/borrow.go b/internal/bufpool/borrow.go deleted file mode 100644 index ff212a9b..00000000 --- a/internal/bufpool/borrow.go +++ /dev/null @@ -1,31 +0,0 @@ -package bufpool - -// Borrow retrieves a Buffer suitable for storing up to capHint bytes. -// The returned Buffer may come from an internal sync.Pool. -// -// If capHint is smaller than DefaultBufferCap, it is automatically raised -// to DefaultBufferCap. If no pooled buffer has sufficient capacity, a new -// unpooled buffer is allocated. -// -// The caller must call Release() when finished using the returned Buffer. -func Borrow(capHint int) Buffer { - if capHint < DefaultBufferCap { - capHint = DefaultBufferCap - } - - classIdx, classCap, pooled := classFor(capHint) - if !pooled { - newBuf := make([]byte, 0, capHint) - - return Buffer{buf: newBuf, pool: unpooled} - } - //nolint:forcetypeassert - buf := bufferPools[classIdx].Get().(*[]byte) - if cap(*buf) < classCap { - *buf = make([]byte, 0, classCap) - } - - slice := (*buf)[:0] - - return Buffer{buf: slice, pool: poolIndex(classIdx)} //#nosec G115 -} diff --git a/internal/bufpool/buffer.go b/internal/bufpool/buffer.go deleted file mode 100644 index b2d648a1..00000000 --- a/internal/bufpool/buffer.go +++ /dev/null @@ -1,24 +0,0 @@ -package bufpool - -// Buffer is a growable byte container that optionally participates in a -// memory pool. A Buffer may be obtained through Borrow() or constructed -// directly from owned data via FromOwned(). -// -// A Buffer's underlying slice may grow as needed. When finished with a -// pooled buffer, the caller should invoke Release() to return it to the pool. -// -// Buffers must not be copied after first use; doing so can cause double-returns -// to the pool and data races. -// -// In general, pass Buffer around when used internally, and directly .Bytes() when -// returning output across our API boundary. It is neither necessary nor efficient -// to copy/append the .Bytes() to a newly-allocated slice; in cases where we do -// want the raw byte slice out of our API boundary, it is perfectly acceptable to -// simply not call Release(). -// -//go:nocopy -type Buffer struct { - _ struct{} // for nocopy - buf []byte - pool poolIndex -} diff --git a/internal/bufpool/buffers_test.go b/internal/bufpool/buffers_test.go deleted file mode 100644 index 224fa98c..00000000 --- a/internal/bufpool/buffers_test.go +++ /dev/null @@ -1,97 +0,0 @@ -//nolint:testpackage -package bufpool - -import "testing" - -func TestBorrowBufferResizeAndAppend(t *testing.T) { - t.Parallel() - - b := Borrow(1) - defer b.Release() - - if cap(b.buf) < DefaultBufferCap { - t.Fatalf("expected capacity >= %d, got %d", DefaultBufferCap, cap(b.buf)) - } - - b.Append([]byte("alpha")) - b.Append([]byte("beta")) - - if got := string(b.Bytes()); got != "alphabeta" { - t.Fatalf("unexpected contents: %q", got) - } - - b.Resize(3) - - if got := string(b.Bytes()); got != "alp" { - t.Fatalf("resize shrink mismatch: %q", got) - } - - b.Resize(8) - - if len(b.Bytes()) != 8 { - t.Fatalf("expected len 8 after grow, got %d", len(b.Bytes())) - } - - if prefix := string(b.Bytes()[:3]); prefix != "alp" { - t.Fatalf("prefix lost after grow: %q", prefix) - } -} - -func TestBorrowBufferRelease(t *testing.T) { - t.Parallel() - - b := Borrow(DefaultBufferCap / 2) - b.Append([]byte("data")) - b.Release() - - if b.buf != nil { - t.Fatal("expected buffer cleared after release") - } -} - -func TestBorrowUsesLargerPools(t *testing.T) { - t.Parallel() - - const request = DefaultBufferCap * 4 - - classIdx, classCap, pooled := classFor(request) - if !pooled { - t.Fatalf("expected %d to map to a pooled class", request) - } - - b := Borrow(request) - //#nosec G115 - if b.pool != poolIndex(classIdx) { - t.Fatalf("expected pooled buffer in class %d, got %d", classIdx, b.pool) - } - - if cap(b.buf) != classCap { - t.Fatalf("expected capacity %d, got %d", classCap, cap(b.buf)) - } - - b.Release() - - b2 := Borrow(request) - defer b2.Release() - //#nosec G115 - if b2.pool != poolIndex(classIdx) { - t.Fatalf("expected pooled buffer in class %d on reuse, got %d", classIdx, b2.pool) - } - - if cap(b2.buf) != classCap { - t.Fatalf("expected capacity %d on reuse, got %d", classCap, cap(b2.buf)) - } -} - -func TestGrowingBufferStaysPooled(t *testing.T) { - t.Parallel() - - b := Borrow(DefaultBufferCap) - defer b.Release() - - b.Append(make([]byte, DefaultBufferCap*3)) - - if b.pool == unpooled { - t.Fatal("buffer should stay pooled after growth within limit") - } -} diff --git a/internal/bufpool/bytes.go b/internal/bufpool/bytes.go deleted file mode 100644 index bcefbdfd..00000000 --- a/internal/bufpool/bytes.go +++ /dev/null @@ -1,7 +0,0 @@ -package bufpool - -// Bytes returns the underlying byte slice that represents the current contents -// of the buffer. Modifying the returned slice modifies the Buffer itself. -func (buf *Buffer) Bytes() []byte { - return buf.buf -} diff --git a/internal/bufpool/capacity.go b/internal/bufpool/capacity.go deleted file mode 100644 index ecbd7d76..00000000 --- a/internal/bufpool/capacity.go +++ /dev/null @@ -1,37 +0,0 @@ -package bufpool - -// ensureCapacity grows the underlying buffer to accommodate the requested -// number of bytes. Growth doubles the capacity by default unless a larger -// expansion is needed. If the previous storage was pooled and not oversized, -// it is returned to the pool. -func (buf *Buffer) ensureCapacity(needed int) { - if cap(buf.buf) >= needed { - return - } - - classIdx, classCap, pooled := classFor(needed) - - var newBuf []byte - - if pooled { - //nolint:forcetypeassert - raw := bufferPools[classIdx].Get().(*[]byte) - if cap(*raw) < classCap { - *raw = make([]byte, 0, classCap) - } - - newBuf = (*raw)[:len(buf.buf)] - } else { - newBuf = make([]byte, len(buf.buf), classCap) - } - - copy(newBuf, buf.buf) - buf.returnToPool() - - buf.buf = newBuf - if pooled { - buf.pool = poolIndex(classIdx) //#nosec G115 - } else { - buf.pool = unpooled - } -} diff --git a/internal/bufpool/class.go b/internal/bufpool/class.go deleted file mode 100644 index 92b9742a..00000000 --- a/internal/bufpool/class.go +++ /dev/null @@ -1,24 +0,0 @@ -package bufpool - -//nolint:gochecknoglobals -var sizeClasses = [...]int{ - DefaultBufferCap, - 64 << 10, - 128 << 10, - 256 << 10, - 512 << 10, - 1 << 20, - 2 << 20, - 4 << 20, - maxPooledBuffer, -} - -func classFor(size int) (idx, classCap int, ok bool) { - for i, class := range sizeClasses { - if size <= class { - return i, class, true - } - } - - return -1, size, false -} diff --git a/internal/bufpool/consts.go b/internal/bufpool/consts.go deleted file mode 100644 index 4c205879..00000000 --- a/internal/bufpool/consts.go +++ /dev/null @@ -1,12 +0,0 @@ -package bufpool - -const ( - // DefaultBufferCap is the minimum capacity a borrowed buffer will have. - // Borrow() will allocate or retrieve a buffer with at least this capacity. - DefaultBufferCap = 32 * 1024 - - // maxPooledBuffer defines the maximum capacity of a buffer that may be - // returned to the pool. Buffers larger than this will not be pooled to - // avoid unbounded memory usage. - maxPooledBuffer = 8 << 20 -) diff --git a/internal/bufpool/doc.go b/internal/bufpool/doc.go deleted file mode 100644 index cadfe26e..00000000 --- a/internal/bufpool/doc.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package bufpool provides a lightweight byte-buffer type with optional -// pooling. -package bufpool diff --git a/internal/bufpool/from_owned.go b/internal/bufpool/from_owned.go deleted file mode 100644 index 65c5f471..00000000 --- a/internal/bufpool/from_owned.go +++ /dev/null @@ -1,8 +0,0 @@ -package bufpool - -// FromOwned constructs a Buffer from a caller-owned byte slice. The resulting -// Buffer does not participate in pooling and will never be returned to the -// internal pool when released. -func FromOwned(buf []byte) Buffer { - return Buffer{buf: buf, pool: unpooled} -} diff --git a/internal/bufpool/index.go b/internal/bufpool/index.go deleted file mode 100644 index 5f59b0ed..00000000 --- a/internal/bufpool/index.go +++ /dev/null @@ -1,7 +0,0 @@ -package bufpool - -type poolIndex int8 - -const ( - unpooled poolIndex = -1 -) diff --git a/internal/bufpool/pool.go b/internal/bufpool/pool.go deleted file mode 100644 index d776eaa8..00000000 --- a/internal/bufpool/pool.go +++ /dev/null @@ -1,18 +0,0 @@ -package bufpool - -import "sync" - -//nolint:gochecknoglobals -var bufferPools = func() []sync.Pool { - pools := make([]sync.Pool, len(sizeClasses)) - for i, classCap := range sizeClasses { - capCopy := classCap - pools[i].New = func() any { - buf := make([]byte, 0, capCopy) - - return &buf - } - } - - return pools -}() diff --git a/internal/bufpool/release.go b/internal/bufpool/release.go deleted file mode 100644 index d8a52061..00000000 --- a/internal/bufpool/release.go +++ /dev/null @@ -1,17 +0,0 @@ -package bufpool - -// Release returns the buffer to the global pool if it originated from the -// pool and its capacity is no larger than maxPooledBuffer. After release, the -// Buffer becomes invalid and should not be used further. -// -// Releasing a non-pooled buffer has no effect beyond clearing its internal -// storage. -func (buf *Buffer) Release() { - if buf.buf == nil { - return - } - - buf.returnToPool() - buf.buf = nil - buf.pool = unpooled -} diff --git a/internal/bufpool/resize.go b/internal/bufpool/resize.go deleted file mode 100644 index 78dc1dd7..00000000 --- a/internal/bufpool/resize.go +++ /dev/null @@ -1,15 +0,0 @@ -package bufpool - -// Resize adjusts the length of the buffer to n bytes. If n exceeds the current -// capacity, the underlying storage is grown. If n is negative, it is treated -// as zero. -// -// The buffer's new contents beyond the previous length are undefined. -func (buf *Buffer) Resize(n int) { - if n < 0 { - n = 0 - } - - buf.ensureCapacity(n) - buf.buf = buf.buf[:n] -} diff --git a/internal/bufpool/return.go b/internal/bufpool/return.go deleted file mode 100644 index fd08c121..00000000 --- a/internal/bufpool/return.go +++ /dev/null @@ -1,10 +0,0 @@ -package bufpool - -func (buf *Buffer) returnToPool() { - if buf.pool == unpooled { - return - } - - tmp := buf.buf[:0] - bufferPools[int(buf.pool)].Put(&tmp) -} diff --git a/internal/compress/LICENSE b/internal/compress/LICENSE deleted file mode 100644 index a013710f..00000000 --- a/internal/compress/LICENSE +++ /dev/null @@ -1,29 +0,0 @@ -Copyright (c) 2012 The Go Authors. All rights reserved. -Copyright (c) 2019 Klaus Post. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - diff --git a/internal/compress/doc.go b/internal/compress/doc.go deleted file mode 100644 index 5fcda97f..00000000 --- a/internal/compress/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package compress encapsulates custom compression algorithms. -package compress diff --git a/internal/compress/flate/_gen/gen_inflate.go b/internal/compress/flate/_gen/gen_inflate.go deleted file mode 100644 index 33f14005..00000000 --- a/internal/compress/flate/_gen/gen_inflate.go +++ /dev/null @@ -1,303 +0,0 @@ -//go:build generate -// +build generate - -//go:generate go run $GOFILE -//go:generate go fmt ../inflate_gen.go - -package main - -import ( - "os" - "strings" -) - -func main() { - f, err := os.Create("../inflate_gen.go") - if err != nil { - panic(err) - } - defer f.Close() - types := []string{"*bytes.Buffer", "*bytes.Reader", "*bufio.Reader", "*strings.Reader", "Reader"} - names := []string{"BytesBuffer", "BytesReader", "BufioReader", "StringsReader", "GenericReader"} - imports := []string{"bytes", "bufio", "fmt", "strings", "math/bits"} - f.WriteString(`// Code generated by go generate gen_inflate.go. DO NOT EDIT. - -package flate - -import ( -`) - - for _, imp := range imports { - f.WriteString("\t\"" + imp + "\"\n") - } - f.WriteString(")\n\n") - - template := ` - -// Decode a single Huffman block from f. -// hl and hd are the Huffman states for the lit/length values -// and the distance values, respectively. If hd == nil, using the -// fixed distance encoding associated with fixed Huffman blocks. -func (f *decompressor) $FUNCNAME$() { - const ( - stateInit = iota // Zero value must be stateInit - stateDict - ) - fr := f.r.($TYPE$) - - // Optimization. Compiler isn't smart enough to keep f.b,f.nb in registers, - // but is smart enough to keep local variables in registers, so use nb and b, - // inline call to moreBits and reassign b,nb back to f on return. - fnb, fb, dict := f.nb, f.b, &f.dict - - switch f.stepState { - case stateInit: - goto readLiteral - case stateDict: - goto copyHistory - } - -readLiteral: - // Read literal and/or (length, distance) according to RFC section 3.2.3. - { - var v int - { - // Inlined v, err := f.huffSym(f.hl) - // Since a huffmanDecoder can be empty or be composed of a degenerate tree - // with single element, huffSym must error on these two edge cases. In both - // cases, the chunks slice will be 0 for the invalid sequence, leading it - // satisfy the n == 0 check below. - n := uint(f.hl.maxRead) - for { - for fnb < n { - c, err := fr.ReadByte() - if err != nil { - f.b, f.nb = fb, fnb - f.err = noEOF(err) - return - } - f.roffset++ - fb |= uint32(c) << (fnb & regSizeMaskUint32) - fnb += 8 - } - chunk := f.hl.chunks[fb&(huffmanNumChunks-1)] - n = uint(chunk & huffmanCountMask) - if n > huffmanChunkBits { - chunk = f.hl.links[chunk>>huffmanValueShift][(fb>>huffmanChunkBits)&f.hl.linkMask] - n = uint(chunk & huffmanCountMask) - } - if n <= fnb { - if n == 0 { - f.b, f.nb = fb, fnb - if debugDecode { - fmt.Println("huffsym: n==0") - } - f.err = CorruptInputError(f.roffset) - return - } - fb = fb >> (n & regSizeMaskUint32) - fnb = fnb - n - v = int(chunk >> huffmanValueShift) - break - } - } - } - - var length int - switch { - case v < 256: - dict.writeByte(byte(v)) - if dict.availWrite() == 0 { - f.toRead = dict.readFlush() - f.step = $FUNCNAME$ - f.stepState = stateInit - f.b, f.nb = fb, fnb - return - } - goto readLiteral - case v == 256: - f.b, f.nb = fb, fnb - f.finishBlock() - return - // otherwise, reference to older data - case v < 265: - length = v - (257 - 3) - case v < maxNumLit: - val := decCodeToLen[(v - 257)] - length = int(val.length) + 3 - n := uint(val.extra) - for fnb < n { - c, err := fr.ReadByte() - if err != nil { - f.b, f.nb = fb, fnb - if debugDecode { - fmt.Println("morebits n>0:", err) - } - f.err = err - return - } - f.roffset++ - fb |= uint32(c) << (fnb®SizeMaskUint32) - fnb += 8 - } - length += int(fb & bitMask32[n]) - fb >>= n & regSizeMaskUint32 - fnb -= n - default: - if debugDecode { - fmt.Println(v, ">= maxNumLit") - } - f.err = CorruptInputError(f.roffset) - f.b, f.nb = fb, fnb - return - } - - var dist uint32 - if f.hd == nil { - for fnb < 5 { - c, err := fr.ReadByte() - if err != nil { - f.b, f.nb = fb, fnb - if debugDecode { - fmt.Println("morebits f.nb<5:", err) - } - f.err = err - return - } - f.roffset++ - fb |= uint32(c) << (fnb®SizeMaskUint32) - fnb += 8 - } - dist = uint32(bits.Reverse8(uint8(fb & 0x1F << 3))) - fb >>= 5 - fnb -= 5 - } else { - // Since a huffmanDecoder can be empty or be composed of a degenerate tree - // with single element, huffSym must error on these two edge cases. In both - // cases, the chunks slice will be 0 for the invalid sequence, leading it - // satisfy the n == 0 check below. - n := uint(f.hd.maxRead) - // Optimization. Compiler isn't smart enough to keep f.b,f.nb in registers, - // but is smart enough to keep local variables in registers, so use nb and b, - // inline call to moreBits and reassign b,nb back to f on return. - for { - for fnb < n { - c, err := fr.ReadByte() - if err != nil { - f.b, f.nb = fb, fnb - f.err = noEOF(err) - return - } - f.roffset++ - fb |= uint32(c) << (fnb & regSizeMaskUint32) - fnb += 8 - } - chunk := f.hd.chunks[fb&(huffmanNumChunks-1)] - n = uint(chunk & huffmanCountMask) - if n > huffmanChunkBits { - chunk = f.hd.links[chunk>>huffmanValueShift][(fb>>huffmanChunkBits)&f.hd.linkMask] - n = uint(chunk & huffmanCountMask) - } - if n <= fnb { - if n == 0 { - f.b, f.nb = fb, fnb - if debugDecode { - fmt.Println("huffsym: n==0") - } - f.err = CorruptInputError(f.roffset) - return - } - fb = fb >> (n & regSizeMaskUint32) - fnb = fnb - n - dist = uint32(chunk >> huffmanValueShift) - break - } - } - } - - switch { - case dist < 4: - dist++ - case dist < maxNumDist: - nb := uint(dist-2) >> 1 - // have 1 bit in bottom of dist, need nb more. - extra := (dist & 1) << (nb & regSizeMaskUint32) - for fnb < nb { - c, err := fr.ReadByte() - if err != nil { - f.b, f.nb = fb, fnb - if debugDecode { - fmt.Println("morebits f.nb>= nb & regSizeMaskUint32 - fnb -= nb - dist = 1<<((nb+1)®SizeMaskUint32) + 1 + extra - // slower: dist = bitMask32[nb+1] + 2 + extra - default: - f.b, f.nb = fb, fnb - if debugDecode { - fmt.Println("dist too big:", dist, maxNumDist) - } - f.err = CorruptInputError(f.roffset) - return - } - - // No check on length; encoding can be prescient. - if dist > uint32(dict.histSize()) { - f.b, f.nb = fb, fnb - if debugDecode { - fmt.Println("dist > dict.histSize():", dist, dict.histSize()) - } - f.err = CorruptInputError(f.roffset) - return - } - - f.copyLen, f.copyDist = length, int(dist) - goto copyHistory - } - -copyHistory: - // Perform a backwards copy according to RFC section 3.2.3. - { - cnt := dict.tryWriteCopy(f.copyDist, f.copyLen) - if cnt == 0 { - cnt = dict.writeCopy(f.copyDist, f.copyLen) - } - f.copyLen -= cnt - - if dict.availWrite() == 0 || f.copyLen > 0 { - f.toRead = dict.readFlush() - f.step = $FUNCNAME$ // We need to continue this work - f.stepState = stateDict - f.b, f.nb = fb, fnb - return - } - goto readLiteral - } - // Not reached -} - -` - for i, t := range types { - s := strings.Replace(template, "$FUNCNAME$", "huffman"+names[i], -1) - s = strings.Replace(s, "$TYPE$", t, -1) - f.WriteString(s) - } - f.WriteString("func (f *decompressor) huffmanBlockDecoder() {\n") - f.WriteString("\tswitch f.r.(type) {\n") - for i, t := range types { - f.WriteString("\t\tcase " + t + ":\n") - f.WriteString("\t\t\tf.huffman" + names[i] + "()\n") - } - f.WriteString("\t\tdefault:\n") - f.WriteString("\t\t\tf.huffmanGenericReader()\n") - f.WriteString("\t}\n}\n") -} diff --git a/internal/compress/flate/deflate.go b/internal/compress/flate/deflate.go deleted file mode 100644 index d8a1ff2a..00000000 --- a/internal/compress/flate/deflate.go +++ /dev/null @@ -1,996 +0,0 @@ -// Copyright 2009 The Go Authors. All rights reserved. -// Copyright (c) 2015 Klaus Post -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package flate - -import ( - "errors" - "fmt" - "io" - "math" - - "codeberg.org/lindenii/furgit/internal/compress/internal/le" -) - -const ( - NoCompression = 0 - BestSpeed = 1 - BestCompression = 9 - DefaultCompression = -1 - - // HuffmanOnly disables Lempel-Ziv match searching and only performs Huffman - // entropy encoding. This mode is useful in compressing data that has - // already been compressed with an LZ style algorithm (e.g. Snappy or LZ4) - // that lacks an entropy encoder. Compression gains are achieved when - // certain bytes in the input stream occur more frequently than others. - // - // Note that HuffmanOnly produces a compressed output that is - // RFC 1951 compliant. That is, any valid DEFLATE decompressor will - // continue to be able to decompress this output. - HuffmanOnly = -2 - ConstantCompression = HuffmanOnly // compatibility alias. - - logWindowSize = 15 - windowSize = 1 << logWindowSize - windowMask = windowSize - 1 - logMaxOffsetSize = 15 // Standard DEFLATE - minMatchLength = 4 // The smallest match that the compressor looks for - maxMatchLength = 258 // The longest match for the compressor - minOffsetSize = 1 // The shortest offset that makes any sense - - // The maximum number of tokens we will encode at the time. - // Smaller sizes usually creates less optimal blocks. - // Bigger can make context switching slow. - // We use this for levels 7-9, so we make it big. - maxFlateBlockTokens = 1 << 15 - maxStoreBlockSize = 65535 - hashBits = 17 // After 17 performance degrades - hashSize = 1 << hashBits - hashMask = (1 << hashBits) - 1 - hashShift = (hashBits + minMatchLength - 1) / minMatchLength - maxHashOffset = 1 << 28 - - skipNever = math.MaxInt32 - - debugDeflate = false -) - -type compressionLevel struct { - good, lazy, nice, chain, fastSkipHashing, level int -} - -// Compression levels have been rebalanced from zlib deflate defaults -// to give a bigger spread in speed and compression. -// See https://blog.klauspost.com/rebalancing-deflate-compression-levels/ -var levels = []compressionLevel{ - {}, // 0 - // Level 1-6 uses specialized algorithm - values not used - {0, 0, 0, 0, 0, 1}, - {0, 0, 0, 0, 0, 2}, - {0, 0, 0, 0, 0, 3}, - {0, 0, 0, 0, 0, 4}, - {0, 0, 0, 0, 0, 5}, - {0, 0, 0, 0, 0, 6}, - // Levels 7-9 use increasingly more lazy matching - // and increasingly stringent conditions for "good enough". - {8, 12, 16, 24, skipNever, 7}, - {16, 30, 40, 64, skipNever, 8}, - {32, 258, 258, 1024, skipNever, 9}, -} - -// advancedState contains state for the advanced levels, with bigger hash tables, etc. -type advancedState struct { - // deflate state - length int - offset int - maxInsertIndex int - chainHead int - hashOffset int - - ii uint16 // position of last match, intended to overflow to reset. - - // input window: unprocessed data is window[index:windowEnd] - index int - hashMatch [maxMatchLength + minMatchLength]uint32 - - // Input hash chains - // hashHead[hashValue] contains the largest inputIndex with the specified hash value - // If hashHead[hashValue] is within the current window, then - // hashPrev[hashHead[hashValue] & windowMask] contains the previous index - // with the same hash value. - hashHead [hashSize]uint32 - hashPrev [windowSize]uint32 -} - -type compressor struct { - compressionLevel - - h *huffmanEncoder - w *huffmanBitWriter - - // compression algorithm - fill func(*compressor, []byte) int // copy data to window - step func(*compressor) // process window - - window []byte - windowEnd int - blockStart int // window index where current tokens start - err error - - // queued output tokens - tokens tokens - fast fastEnc - state *advancedState - - sync bool // requesting flush - byteAvailable bool // if true, still need to process window[index-1]. -} - -func (d *compressor) fillDeflate(b []byte) int { - s := d.state - if s.index >= 2*windowSize-(minMatchLength+maxMatchLength) { - // shift the window by windowSize - // copy(d.window[:], d.window[windowSize:2*windowSize]) - *(*[windowSize]byte)(d.window) = *(*[windowSize]byte)(d.window[windowSize:]) - s.index -= windowSize - d.windowEnd -= windowSize - if d.blockStart >= windowSize { - d.blockStart -= windowSize - } else { - d.blockStart = math.MaxInt32 - } - s.hashOffset += windowSize - if s.hashOffset > maxHashOffset { - delta := s.hashOffset - 1 - s.hashOffset -= delta - s.chainHead -= delta - // Iterate over slices instead of arrays to avoid copying - // the entire table onto the stack (Issue #18625). - for i, v := range s.hashPrev[:] { - if int(v) > delta { - s.hashPrev[i] = uint32(int(v) - delta) - } else { - s.hashPrev[i] = 0 - } - } - for i, v := range s.hashHead[:] { - if int(v) > delta { - s.hashHead[i] = uint32(int(v) - delta) - } else { - s.hashHead[i] = 0 - } - } - } - } - n := copy(d.window[d.windowEnd:], b) - d.windowEnd += n - return n -} - -func (d *compressor) writeBlock(tok *tokens, index int, eof bool) error { - if index > 0 || eof { - var window []byte - if d.blockStart <= index { - window = d.window[d.blockStart:index] - } - d.blockStart = index - // d.w.writeBlock(tok, eof, window) - d.w.writeBlockDynamic(tok, eof, window, d.sync) - return d.w.err - } - return nil -} - -// writeBlockSkip writes the current block and uses the number of tokens -// to determine if the block should be stored on no matches, or -// only huffman encoded. -func (d *compressor) writeBlockSkip(tok *tokens, index int, eof bool) error { - if index > 0 || eof { - if d.blockStart <= index { - window := d.window[d.blockStart:index] - // If we removed less than a 64th of all literals - // we huffman compress the block. - if int(tok.n) > len(window)-int(tok.n>>6) { - d.w.writeBlockHuff(eof, window, d.sync) - } else { - // Write a dynamic huffman block. - d.w.writeBlockDynamic(tok, eof, window, d.sync) - } - } else { - d.w.writeBlock(tok, eof, nil) - } - d.blockStart = index - return d.w.err - } - return nil -} - -// fillWindow will fill the current window with the supplied -// dictionary and calculate all hashes. -// This is much faster than doing a full encode. -// Should only be used after a start/reset. -func (d *compressor) fillWindow(b []byte) { - // Do not fill window if we are in store-only or huffman mode. - if d.level <= 0 && d.level > -MinCustomWindowSize { - return - } - if d.fast != nil { - // encode the last data, but discard the result - if len(b) > maxMatchOffset { - b = b[len(b)-maxMatchOffset:] - } - d.fast.Encode(&d.tokens, b) - d.tokens.Reset() - return - } - s := d.state - // If we are given too much, cut it. - if len(b) > windowSize { - b = b[len(b)-windowSize:] - } - // Add all to window. - n := copy(d.window[d.windowEnd:], b) - - // Calculate 256 hashes at the time (more L1 cache hits) - loops := (n + 256 - minMatchLength) / 256 - for j := range loops { - startindex := j * 256 - end := min(startindex+256+minMatchLength-1, n) - tocheck := d.window[startindex:end] - dstSize := len(tocheck) - minMatchLength + 1 - - if dstSize <= 0 { - continue - } - - dst := s.hashMatch[:dstSize] - bulkHash4(tocheck, dst) - var newH uint32 - for i, val := range dst { - di := i + startindex - newH = val & hashMask - // Get previous value with the same hash. - // Our chain should point to the previous value. - s.hashPrev[di&windowMask] = s.hashHead[newH] - // Set the head of the hash chain to us. - s.hashHead[newH] = uint32(di + s.hashOffset) - } - } - // Update window information. - d.windowEnd += n - s.index = n -} - -// Try to find a match starting at index whose length is greater than prevSize. -// We only look at chainCount possibilities before giving up. -// pos = s.index, prevHead = s.chainHead-s.hashOffset, prevLength=minMatchLength-1, lookahead -func (d *compressor) findMatch(pos int, prevHead int, lookahead int) (length, offset int, ok bool) { - minMatchLook := min(lookahead, maxMatchLength) - - win := d.window[0 : pos+minMatchLook] - - // We quit when we get a match that's at least nice long - nice := min(d.nice, len(win)-pos) - - // If we've got a match that's good enough, only look in 1/4 the chain. - tries := d.chain - length = minMatchLength - 1 - - wEnd := win[pos+length] - wPos := win[pos:] - minIndex := max(pos-windowSize, 0) - offset = 0 - - if d.chain < 100 { - for i := prevHead; tries > 0; tries-- { - if wEnd == win[i+length] { - n := matchLen(win[i:i+minMatchLook], wPos) - if n > length { - length = n - offset = pos - i - ok = true - if n >= nice { - // The match is good enough that we don't try to find a better one. - break - } - wEnd = win[pos+n] - } - } - if i <= minIndex { - // hashPrev[i & windowMask] has already been overwritten, so stop now. - break - } - i = int(d.state.hashPrev[i&windowMask]) - d.state.hashOffset - if i < minIndex { - break - } - } - return - } - - // Minimum gain to accept a match. - cGain := 4 - - // Some like it higher (CSV), some like it lower (JSON) - const baseCost = 3 - // Base is 4 bytes at with an additional cost. - // Matches must be better than this. - - for i := prevHead; tries > 0; tries-- { - if wEnd == win[i+length] { - n := matchLen(win[i:i+minMatchLook], wPos) - if n > length { - // Calculate gain. Estimate - newGain := d.h.bitLengthRaw(wPos[:n]) - int(offsetExtraBits[offsetCode(uint32(pos-i))]) - baseCost - int(lengthExtraBits[lengthCodes[(n-3)&255]]) - - // fmt.Println("gain:", newGain, "prev:", cGain, "raw:", d.h.bitLengthRaw(wPos[:n]), "this-len:", n, "prev-len:", length) - if newGain > cGain { - length = n - offset = pos - i - cGain = newGain - ok = true - if n >= nice { - // The match is good enough that we don't try to find a better one. - break - } - wEnd = win[pos+n] - } - } - } - if i <= minIndex { - // hashPrev[i & windowMask] has already been overwritten, so stop now. - break - } - i = int(d.state.hashPrev[i&windowMask]) - d.state.hashOffset - if i < minIndex { - break - } - } - return -} - -func (d *compressor) writeStoredBlock(buf []byte) error { - if d.w.writeStoredHeader(len(buf), false); d.w.err != nil { - return d.w.err - } - d.w.writeBytes(buf) - return d.w.err -} - -// hash4 returns a hash representation of the first 4 bytes -// of the supplied slice. -// The caller must ensure that len(b) >= 4. -func hash4(b []byte) uint32 { - return hash4u(le.Load32(b, 0), hashBits) -} - -// hash4 returns the hash of u to fit in a hash table with h bits. -// Preferably h should be a constant and should always be <32. -func hash4u(u uint32, h uint8) uint32 { - return (u * prime4bytes) >> (32 - h) -} - -// bulkHash4 will compute hashes using the same -// algorithm as hash4 -func bulkHash4(b []byte, dst []uint32) { - if len(b) < 4 { - return - } - hb := le.Load32(b, 0) - - dst[0] = hash4u(hb, hashBits) - end := len(b) - 4 + 1 - for i := 1; i < end; i++ { - hb = (hb >> 8) | uint32(b[i+3])<<24 - dst[i] = hash4u(hb, hashBits) - } -} - -func (d *compressor) initDeflate() { - d.window = make([]byte, 2*windowSize) - d.byteAvailable = false - d.err = nil - if d.state == nil { - return - } - s := d.state - s.index = 0 - s.hashOffset = 1 - s.length = minMatchLength - 1 - s.offset = 0 - s.chainHead = -1 -} - -// deflateLazy is the same as deflate, but with d.fastSkipHashing == skipNever, -// meaning it always has lazy matching on. -func (d *compressor) deflateLazy() { - s := d.state - // Sanity enables additional runtime tests. - // It's intended to be used during development - // to supplement the currently ad-hoc unit tests. - const sanity = debugDeflate - - if d.windowEnd-s.index < minMatchLength+maxMatchLength && !d.sync { - return - } - if d.windowEnd != s.index && d.chain > 100 { - // Get literal huffman coder. - if d.h == nil { - d.h = newHuffmanEncoder(maxFlateBlockTokens) - } - var tmp [256]uint16 - toIndex := d.window[s.index:d.windowEnd] - toIndex = toIndex[:min(len(toIndex), maxFlateBlockTokens)] - for _, v := range toIndex { - tmp[v]++ - } - d.h.generate(tmp[:], 15) - } - - s.maxInsertIndex = d.windowEnd - (minMatchLength - 1) - - for { - if sanity && s.index > d.windowEnd { - panic("index > windowEnd") - } - lookahead := d.windowEnd - s.index - if lookahead < minMatchLength+maxMatchLength { - if !d.sync { - return - } - if sanity && s.index > d.windowEnd { - panic("index > windowEnd") - } - if lookahead == 0 { - // Flush current output block if any. - if d.byteAvailable { - // There is still one pending token that needs to be flushed - d.tokens.AddLiteral(d.window[s.index-1]) - d.byteAvailable = false - } - if d.tokens.n > 0 { - if d.err = d.writeBlock(&d.tokens, s.index, false); d.err != nil { - return - } - d.tokens.Reset() - } - return - } - } - if s.index < s.maxInsertIndex { - // Update the hash - hash := hash4(d.window[s.index:]) - ch := s.hashHead[hash] - s.chainHead = int(ch) - s.hashPrev[s.index&windowMask] = ch - s.hashHead[hash] = uint32(s.index + s.hashOffset) - } - prevLength := s.length - prevOffset := s.offset - s.length = minMatchLength - 1 - s.offset = 0 - minIndex := max(s.index-windowSize, 0) - - if s.chainHead-s.hashOffset >= minIndex && lookahead > prevLength && prevLength < d.lazy { - if newLength, newOffset, ok := d.findMatch(s.index, s.chainHead-s.hashOffset, lookahead); ok { - s.length = newLength - s.offset = newOffset - } - } - - if prevLength >= minMatchLength && s.length <= prevLength { - // No better match, but check for better match at end... - // - // Skip forward a number of bytes. - // Offset of 2 seems to yield best results. 3 is sometimes better. - const checkOff = 2 - - // Check all, except full length - if prevLength < maxMatchLength-checkOff { - prevIndex := s.index - 1 - if prevIndex+prevLength < s.maxInsertIndex { - end := min(lookahead, maxMatchLength+checkOff) - end += prevIndex - - // Hash at match end. - h := hash4(d.window[prevIndex+prevLength:]) - ch2 := int(s.hashHead[h]) - s.hashOffset - prevLength - if prevIndex-ch2 != prevOffset && ch2 > minIndex+checkOff { - length := matchLen(d.window[prevIndex+checkOff:end], d.window[ch2+checkOff:]) - // It seems like a pure length metric is best. - if length > prevLength { - prevLength = length - prevOffset = prevIndex - ch2 - - // Extend back... - for i := checkOff - 1; i >= 0; i-- { - if prevLength >= maxMatchLength || d.window[prevIndex+i] != d.window[ch2+i] { - // Emit tokens we "owe" - for j := 0; j <= i; j++ { - d.tokens.AddLiteral(d.window[prevIndex+j]) - if d.tokens.n == maxFlateBlockTokens { - // The block includes the current character - if d.err = d.writeBlock(&d.tokens, s.index, false); d.err != nil { - return - } - d.tokens.Reset() - } - s.index++ - if s.index < s.maxInsertIndex { - h := hash4(d.window[s.index:]) - ch := s.hashHead[h] - s.chainHead = int(ch) - s.hashPrev[s.index&windowMask] = ch - s.hashHead[h] = uint32(s.index + s.hashOffset) - } - } - break - } else { - prevLength++ - } - } - } else if false { - // Check one further ahead. - // Only rarely better, disabled for now. - prevIndex++ - h := hash4(d.window[prevIndex+prevLength:]) - ch2 := int(s.hashHead[h]) - s.hashOffset - prevLength - if prevIndex-ch2 != prevOffset && ch2 > minIndex+checkOff { - length := matchLen(d.window[prevIndex+checkOff:end], d.window[ch2+checkOff:]) - // It seems like a pure length metric is best. - if length > prevLength+checkOff { - prevLength = length - prevOffset = prevIndex - ch2 - prevIndex-- - - // Extend back... - for i := checkOff; i >= 0; i-- { - if prevLength >= maxMatchLength || d.window[prevIndex+i] != d.window[ch2+i-1] { - // Emit tokens we "owe" - for j := 0; j <= i; j++ { - d.tokens.AddLiteral(d.window[prevIndex+j]) - if d.tokens.n == maxFlateBlockTokens { - // The block includes the current character - if d.err = d.writeBlock(&d.tokens, s.index, false); d.err != nil { - return - } - d.tokens.Reset() - } - s.index++ - if s.index < s.maxInsertIndex { - h := hash4(d.window[s.index:]) - ch := s.hashHead[h] - s.chainHead = int(ch) - s.hashPrev[s.index&windowMask] = ch - s.hashHead[h] = uint32(s.index + s.hashOffset) - } - } - break - } else { - prevLength++ - } - } - } - } - } - } - } - } - // There was a match at the previous step, and the current match is - // not better. Output the previous match. - d.tokens.AddMatch(uint32(prevLength-3), uint32(prevOffset-minOffsetSize)) - - // Insert in the hash table all strings up to the end of the match. - // index and index-1 are already inserted. If there is not enough - // lookahead, the last two strings are not inserted into the hash - // table. - newIndex := s.index + prevLength - 1 - // Calculate missing hashes - end := min(newIndex, s.maxInsertIndex) - end += minMatchLength - 1 - startindex := min(s.index+1, s.maxInsertIndex) - tocheck := d.window[startindex:end] - dstSize := len(tocheck) - minMatchLength + 1 - if dstSize > 0 { - dst := s.hashMatch[:dstSize] - bulkHash4(tocheck, dst) - var newH uint32 - for i, val := range dst { - di := i + startindex - newH = val & hashMask - // Get previous value with the same hash. - // Our chain should point to the previous value. - s.hashPrev[di&windowMask] = s.hashHead[newH] - // Set the head of the hash chain to us. - s.hashHead[newH] = uint32(di + s.hashOffset) - } - } - - s.index = newIndex - d.byteAvailable = false - s.length = minMatchLength - 1 - if d.tokens.n == maxFlateBlockTokens { - // The block includes the current character - if d.err = d.writeBlock(&d.tokens, s.index, false); d.err != nil { - return - } - d.tokens.Reset() - } - s.ii = 0 - } else { - // Reset, if we got a match this run. - if s.length >= minMatchLength { - s.ii = 0 - } - // We have a byte waiting. Emit it. - if d.byteAvailable { - s.ii++ - d.tokens.AddLiteral(d.window[s.index-1]) - if d.tokens.n == maxFlateBlockTokens { - if d.err = d.writeBlock(&d.tokens, s.index, false); d.err != nil { - return - } - d.tokens.Reset() - } - s.index++ - - // If we have a long run of no matches, skip additional bytes - // Resets when s.ii overflows after 64KB. - if n := int(s.ii) - d.chain; n > 0 { - n = 1 + int(n>>6) - for j := 0; j < n; j++ { - if s.index >= d.windowEnd-1 { - break - } - d.tokens.AddLiteral(d.window[s.index-1]) - if d.tokens.n == maxFlateBlockTokens { - if d.err = d.writeBlock(&d.tokens, s.index, false); d.err != nil { - return - } - d.tokens.Reset() - } - // Index... - if s.index < s.maxInsertIndex { - h := hash4(d.window[s.index:]) - ch := s.hashHead[h] - s.chainHead = int(ch) - s.hashPrev[s.index&windowMask] = ch - s.hashHead[h] = uint32(s.index + s.hashOffset) - } - s.index++ - } - // Flush last byte - d.tokens.AddLiteral(d.window[s.index-1]) - d.byteAvailable = false - // s.length = minMatchLength - 1 // not needed, since s.ii is reset above, so it should never be > minMatchLength - if d.tokens.n == maxFlateBlockTokens { - if d.err = d.writeBlock(&d.tokens, s.index, false); d.err != nil { - return - } - d.tokens.Reset() - } - } - } else { - s.index++ - d.byteAvailable = true - } - } - } -} - -func (d *compressor) store() { - if d.windowEnd > 0 && (d.windowEnd == maxStoreBlockSize || d.sync) { - d.err = d.writeStoredBlock(d.window[:d.windowEnd]) - d.windowEnd = 0 - } -} - -// fillWindow will fill the buffer with data for huffman-only compression. -// The number of bytes copied is returned. -func (d *compressor) fillBlock(b []byte) int { - n := copy(d.window[d.windowEnd:], b) - d.windowEnd += n - return n -} - -// storeHuff will compress and store the currently added data, -// if enough has been accumulated or we at the end of the stream. -// Any error that occurred will be in d.err -func (d *compressor) storeHuff() { - if d.windowEnd < len(d.window) && !d.sync || d.windowEnd == 0 { - return - } - d.w.writeBlockHuff(false, d.window[:d.windowEnd], d.sync) - d.err = d.w.err - d.windowEnd = 0 -} - -// storeFast will compress and store the currently added data, -// if enough has been accumulated or we at the end of the stream. -// Any error that occurred will be in d.err -func (d *compressor) storeFast() { - // We only compress if we have maxStoreBlockSize. - if d.windowEnd < len(d.window) { - if !d.sync { - return - } - // Handle extremely small sizes. - if d.windowEnd < 128 { - if d.windowEnd == 0 { - return - } - if d.windowEnd <= 32 { - d.err = d.writeStoredBlock(d.window[:d.windowEnd]) - } else { - d.w.writeBlockHuff(false, d.window[:d.windowEnd], true) - d.err = d.w.err - } - d.tokens.Reset() - d.windowEnd = 0 - d.fast.Reset() - return - } - } - - d.fast.Encode(&d.tokens, d.window[:d.windowEnd]) - // If we made zero matches, store the block as is. - if d.tokens.n == 0 { - d.err = d.writeStoredBlock(d.window[:d.windowEnd]) - // If we removed less than 1/16th, huffman compress the block. - } else if int(d.tokens.n) > d.windowEnd-(d.windowEnd>>4) { - d.w.writeBlockHuff(false, d.window[:d.windowEnd], d.sync) - d.err = d.w.err - } else { - d.w.writeBlockDynamic(&d.tokens, false, d.window[:d.windowEnd], d.sync) - d.err = d.w.err - } - d.tokens.Reset() - d.windowEnd = 0 -} - -// write will add input byte to the stream. -// Unless an error occurs all bytes will be consumed. -func (d *compressor) write(b []byte) (n int, err error) { - if d.err != nil { - return 0, d.err - } - n = len(b) - for len(b) > 0 { - if d.windowEnd == len(d.window) || d.sync { - d.step(d) - } - b = b[d.fill(d, b):] - if d.err != nil { - return 0, d.err - } - } - return n, d.err -} - -func (d *compressor) syncFlush() error { - d.sync = true - if d.err != nil { - return d.err - } - d.step(d) - if d.err == nil { - d.w.writeStoredHeader(0, false) - d.w.flush() - d.err = d.w.err - } - d.sync = false - return d.err -} - -func (d *compressor) init(w io.Writer, level int) (err error) { - d.w = newHuffmanBitWriter(w) - - switch { - case level == NoCompression: - d.window = make([]byte, maxStoreBlockSize) - d.fill = (*compressor).fillBlock - d.step = (*compressor).store - case level == ConstantCompression: - d.w.logNewTablePenalty = 10 - d.window = make([]byte, 32<<10) - d.fill = (*compressor).fillBlock - d.step = (*compressor).storeHuff - case level == DefaultCompression: - level = 5 - fallthrough - case level >= 1 && level <= 6: - d.w.logNewTablePenalty = 7 - d.fast = newFastEnc(level) - d.window = make([]byte, maxStoreBlockSize) - d.fill = (*compressor).fillBlock - d.step = (*compressor).storeFast - case 7 <= level && level <= 9: - d.w.logNewTablePenalty = 8 - d.state = &advancedState{} - d.compressionLevel = levels[level] - d.initDeflate() - d.fill = (*compressor).fillDeflate - d.step = (*compressor).deflateLazy - case -level >= MinCustomWindowSize && -level <= MaxCustomWindowSize: - d.w.logNewTablePenalty = 7 - d.fast = &fastEncL5Window{maxOffset: int32(-level), cur: maxStoreBlockSize} - d.window = make([]byte, maxStoreBlockSize) - d.fill = (*compressor).fillBlock - d.step = (*compressor).storeFast - default: - return fmt.Errorf("flate: invalid compression level %d: want value in range [-2, 9]", level) - } - d.level = level - return nil -} - -// reset the state of the compressor. -func (d *compressor) reset(w io.Writer) { - d.w.reset(w) - d.sync = false - d.err = nil - // We only need to reset a few things for Snappy. - if d.fast != nil { - d.fast.Reset() - d.windowEnd = 0 - d.tokens.Reset() - return - } - switch d.compressionLevel.chain { - case 0: - // level was NoCompression or ConstantCompression. - d.windowEnd = 0 - default: - s := d.state - s.chainHead = -1 - for i := range s.hashHead { - s.hashHead[i] = 0 - } - for i := range s.hashPrev { - s.hashPrev[i] = 0 - } - s.hashOffset = 1 - s.index, d.windowEnd = 0, 0 - d.blockStart, d.byteAvailable = 0, false - d.tokens.Reset() - s.length = minMatchLength - 1 - s.offset = 0 - s.ii = 0 - s.maxInsertIndex = 0 - } -} - -func (d *compressor) close() error { - if d.err != nil { - return d.err - } - d.sync = true - d.step(d) - if d.err != nil { - return d.err - } - if d.w.writeStoredHeader(0, true); d.w.err != nil { - return d.w.err - } - d.w.flush() - d.w.reset(nil) - return d.w.err -} - -// NewWriter returns a new Writer compressing data at the given level. -// Following zlib, levels range from 1 (BestSpeed) to 9 (BestCompression); -// higher levels typically run slower but compress more. -// Level 0 (NoCompression) does not attempt any compression; it only adds the -// necessary DEFLATE framing. -// Level -1 (DefaultCompression) uses the default compression level. -// Level -2 (ConstantCompression) will use Huffman compression only, giving -// a very fast compression for all types of input, but sacrificing considerable -// compression efficiency. -// -// If level is in the range [-2, 9] then the error returned will be nil. -// Otherwise the error returned will be non-nil. -func NewWriter(w io.Writer, level int) (*Writer, error) { - var dw Writer - if err := dw.d.init(w, level); err != nil { - return nil, err - } - return &dw, nil -} - -// NewWriterDict is like NewWriter but initializes the new -// Writer with a preset dictionary. The returned Writer behaves -// as if the dictionary had been written to it without producing -// any compressed output. The compressed data written to w -// can only be decompressed by a Reader initialized with the -// same dictionary. -func NewWriterDict(w io.Writer, level int, dict []byte) (*Writer, error) { - zw, err := NewWriter(w, level) - if err != nil { - return nil, err - } - zw.d.fillWindow(dict) - zw.dict = append(zw.dict, dict...) // duplicate dictionary for Reset method. - return zw, err -} - -// MinCustomWindowSize is the minimum window size that can be sent to NewWriterWindow. -const MinCustomWindowSize = 32 - -// MaxCustomWindowSize is the maximum custom window that can be sent to NewWriterWindow. -const MaxCustomWindowSize = windowSize - -// NewWriterWindow returns a new Writer compressing data with a custom window size. -// windowSize must be from MinCustomWindowSize to MaxCustomWindowSize. -func NewWriterWindow(w io.Writer, windowSize int) (*Writer, error) { - if windowSize < MinCustomWindowSize { - return nil, errors.New("flate: requested window size less than MinWindowSize") - } - if windowSize > MaxCustomWindowSize { - return nil, errors.New("flate: requested window size bigger than MaxCustomWindowSize") - } - var dw Writer - if err := dw.d.init(w, -windowSize); err != nil { - return nil, err - } - return &dw, nil -} - -// A Writer takes data written to it and writes the compressed -// form of that data to an underlying writer (see NewWriter). -type Writer struct { - d compressor - dict []byte -} - -// Write writes data to w, which will eventually write the -// compressed form of data to its underlying writer. -func (w *Writer) Write(data []byte) (n int, err error) { - return w.d.write(data) -} - -// Flush flushes any pending data to the underlying writer. -// It is useful mainly in compressed network protocols, to ensure that -// a remote reader has enough data to reconstruct a packet. -// Flush does not return until the data has been written. -// Calling Flush when there is no pending data still causes the Writer -// to emit a sync marker of at least 4 bytes. -// If the underlying writer returns an error, Flush returns that error. -// -// In the terminology of the zlib library, Flush is equivalent to Z_SYNC_FLUSH. -func (w *Writer) Flush() error { - // For more about flushing: - // http://www.bolet.org/~pornin/deflate-flush.html - return w.d.syncFlush() -} - -// Close flushes and closes the writer. -func (w *Writer) Close() error { - return w.d.close() -} - -// Reset discards the writer's state and makes it equivalent to -// the result of NewWriter or NewWriterDict called with dst -// and w's level and dictionary. -func (w *Writer) Reset(dst io.Writer) { - if len(w.dict) > 0 { - // w was created with NewWriterDict - w.d.reset(dst) - if dst != nil { - w.d.fillWindow(w.dict) - } - } else { - // w was created with NewWriter - w.d.reset(dst) - } -} - -// ResetDict discards the writer's state and makes it equivalent to -// the result of NewWriter or NewWriterDict called with dst -// and w's level, but sets a specific dictionary. -func (w *Writer) ResetDict(dst io.Writer, dict []byte) { - w.dict = dict - w.d.reset(dst) - w.d.fillWindow(w.dict) -} diff --git a/internal/compress/flate/deflate_test.go b/internal/compress/flate/deflate_test.go deleted file mode 100644 index 9ac3da1f..00000000 --- a/internal/compress/flate/deflate_test.go +++ /dev/null @@ -1,708 +0,0 @@ -// Copyright 2009 The Go Authors. All rights reserved. -// Copyright (c) 2015 Klaus Post -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package flate - -import ( - "bytes" - "fmt" - "io" - "os" - "reflect" - "strings" - "sync" - "testing" -) - -type deflateTest struct { - in []byte - level int - out []byte -} - -type deflateInflateTest struct { - in []byte -} - -type reverseBitsTest struct { - in uint16 - bitCount uint8 - out uint16 -} - -var deflateTests = []*deflateTest{ - 0: {[]byte{}, 0, []byte{0x3, 0x0}}, - 1: {[]byte{0x11}, BestCompression, []byte{0x12, 0x4, 0xc, 0x0}}, - 2: {[]byte{0x11}, BestCompression, []byte{0x12, 0x4, 0xc, 0x0}}, - 3: {[]byte{0x11}, BestCompression, []byte{0x12, 0x4, 0xc, 0x0}}, - - 4: {[]byte{0x11}, 0, []byte{0x0, 0x1, 0x0, 0xfe, 0xff, 0x11, 0x3, 0x0}}, - 5: {[]byte{0x11, 0x12}, 0, []byte{0x0, 0x2, 0x0, 0xfd, 0xff, 0x11, 0x12, 0x3, 0x0}}, - 6: { - []byte{0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11}, - 0, - []byte{0x0, 0x8, 0x0, 0xf7, 0xff, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x3, 0x0}, - }, - 7: {[]byte{}, 1, []byte{0x3, 0x0}}, - 8: {[]byte{0x11}, BestCompression, []byte{0x12, 0x4, 0xc, 0x0}}, - 9: {[]byte{0x11, 0x12}, BestCompression, []byte{0x12, 0x14, 0x2, 0xc, 0x0}}, - 10: {[]byte{0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11}, BestCompression, []byte{0x12, 0x84, 0x1, 0xc0, 0x0}}, - 11: {[]byte{}, 9, []byte{0x3, 0x0}}, - 12: {[]byte{0x11}, 9, []byte{0x12, 0x4, 0xc, 0x0}}, - 13: {[]byte{0x11, 0x12}, 9, []byte{0x12, 0x14, 0x2, 0xc, 0x0}}, - 14: {[]byte{0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11}, 9, []byte{0x12, 0x84, 0x1, 0xc0, 0x0}}, -} - -var deflateInflateTests = []*deflateInflateTest{ - {[]byte{}}, - {[]byte{0x11}}, - {[]byte{0x11, 0x12}}, - {[]byte{0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11}}, - {[]byte{0x11, 0x10, 0x13, 0x41, 0x21, 0x21, 0x41, 0x13, 0x87, 0x78, 0x13}}, - {largeDataChunk()}, -} - -var reverseBitsTests = []*reverseBitsTest{ - {1, 1, 1}, - {1, 2, 2}, - {1, 3, 4}, - {1, 4, 8}, - {1, 5, 16}, - {17, 5, 17}, - {257, 9, 257}, - {29, 5, 23}, -} - -func largeDataChunk() []byte { - result := make([]byte, 100000) - for i := range result { - result[i] = byte(i * i & 0xFF) - } - return result -} - -func TestBulkHash4(t *testing.T) { - for _, x := range deflateTests { - y := x.out - if len(y) >= minMatchLength { - y = append(y, y...) - for j := 4; j < len(y); j++ { - y := y[:j] - dst := make([]uint32, len(y)-minMatchLength+1) - for i := range dst { - dst[i] = uint32(i + 100) - } - bulkHash4(y, dst) - for i, val := range dst { - got := val - expect := hash4(y[i:]) - if got != expect && got == uint32(i)+100 { - t.Errorf("Len:%d Index:%d, expected 0x%08x but not modified", len(y), i, expect) - } else if got != expect { - t.Errorf("Len:%d Index:%d, got 0x%08x expected:0x%08x", len(y), i, got, expect) - } else { - // t.Logf("Len:%d Index:%d OK (0x%08x)", len(y), i, got) - } - } - } - } - } -} - -func TestDeflate(t *testing.T) { - for i, h := range deflateTests { - var buf bytes.Buffer - w, err := NewWriter(&buf, h.level) - if err != nil { - t.Errorf("NewWriter: %v", err) - continue - } - w.Write(h.in) - w.Close() - if !bytes.Equal(buf.Bytes(), h.out) { - t.Errorf("%d: Deflate(%d, %x) got \n%#v, want \n%#v", i, h.level, h.in, buf.Bytes(), h.out) - } - } -} - -// A sparseReader returns a stream consisting of 0s followed by 1<<16 1s. -// This tests missing hash references in a very large input. -type sparseReader struct { - l int64 - cur int64 -} - -func (r *sparseReader) Read(b []byte) (n int, err error) { - if r.cur >= r.l { - return 0, io.EOF - } - n = len(b) - cur := r.cur + int64(n) - if cur > r.l { - n -= int(cur - r.l) - cur = r.l - } - for i := range b[0:n] { - if r.cur+int64(i) >= r.l-1<<16 { - b[i] = 1 - } else { - b[i] = 0 - } - } - r.cur = cur - return -} - -func TestVeryLongSparseChunk(t *testing.T) { - if testing.Short() { - t.Skip("skipping sparse chunk during short test") - } - var buf bytes.Buffer - w, err := NewWriter(&buf, 1) - if err != nil { - t.Errorf("NewWriter: %v", err) - return - } - if _, err = io.Copy(w, &sparseReader{l: 23e8}); err != nil { - t.Errorf("Compress failed: %v", err) - return - } - t.Log("Length:", buf.Len()) -} - -func TestOneMByte(t *testing.T) { - var input [1024 * 1024]byte - - var compressedOutput bytes.Buffer - for level := HuffmanOnly; level <= BestCompression; level++ { - compressedOutput.Reset() - compressor, err := NewWriter(&compressedOutput, level) - if err != nil { - t.Fatalf("create: %s", err) - } - // Use single write... - if _, err := compressor.Write(input[:]); err != nil { - t.Fatalf("compress: %s", err) - } - - if err := compressor.Close(); err != nil { - t.Fatalf("close: %s", err) - } - - var decompressedOutput bytes.Buffer - - decompresser := NewReader(&compressedOutput) - t.Log("level:", level, "compressed:", compressedOutput.Len()) - if _, err := io.Copy(&decompressedOutput, decompresser); err != nil { - t.Fatalf("decompress: %s", err) - } - - if !bytes.Equal(input[:], decompressedOutput.Bytes()) { - t.Fatal("input and output do not match") - } - } -} - -type syncBuffer struct { - buf bytes.Buffer - mu sync.RWMutex - closed bool - ready chan bool -} - -func newSyncBuffer() *syncBuffer { - return &syncBuffer{ready: make(chan bool, 1)} -} - -func (b *syncBuffer) Read(p []byte) (n int, err error) { - for { - b.mu.RLock() - n, err = b.buf.Read(p) - b.mu.RUnlock() - if n > 0 || b.closed { - return - } - <-b.ready - } -} - -func (b *syncBuffer) signal() { - select { - case b.ready <- true: - default: - } -} - -func (b *syncBuffer) Write(p []byte) (n int, err error) { - n, err = b.buf.Write(p) - b.signal() - return -} - -func (b *syncBuffer) WriteMode() { - b.mu.Lock() -} - -func (b *syncBuffer) ReadMode() { - b.mu.Unlock() - b.signal() -} - -func (b *syncBuffer) Close() error { - b.closed = true - b.signal() - return nil -} - -func testSync(t *testing.T, level int, input []byte, name string) { - if len(input) == 0 { - return - } - - t.Logf("--testSync %d, %d, %s", level, len(input), name) - buf := newSyncBuffer() - buf1 := new(bytes.Buffer) - buf.WriteMode() - w, err := NewWriter(io.MultiWriter(buf, buf1), level) - if err != nil { - t.Errorf("NewWriter: %v", err) - return - } - r := NewReader(buf) - - // Write half the input and read back. - for i := range 2 { - var lo, hi int - if i == 0 { - lo, hi = 0, (len(input)+1)/2 - } else { - lo, hi = (len(input)+1)/2, len(input) - } - t.Logf("#%d: write %d-%d", i, lo, hi) - if _, err := w.Write(input[lo:hi]); err != nil { - t.Errorf("testSync: write: %v", err) - return - } - if i == 0 { - if err := w.Flush(); err != nil { - t.Errorf("testSync: flush: %v", err) - return - } - } else { - if err := w.Close(); err != nil { - t.Errorf("testSync: close: %v", err) - } - } - buf.ReadMode() - out := make([]byte, hi-lo+1) - m, err := io.ReadAtLeast(r, out, hi-lo) - t.Logf("#%d: read %d", i, m) - if m != hi-lo || err != nil { - t.Errorf("testSync/%d (%d, %d, %s): read %d: %d, %v (%d left)", i, level, len(input), name, hi-lo, m, err, buf.buf.Len()) - return - } - if !bytes.Equal(input[lo:hi], out[:hi-lo]) { - t.Errorf("testSync/%d: read wrong bytes: %x vs %x", i, input[lo:hi], out[:hi-lo]) - return - } - // This test originally checked that after reading - // the first half of the input, there was nothing left - // in the read buffer (buf.buf.Len() != 0) but that is - // not necessarily the case: the write Flush may emit - // some extra framing bits that are not necessary - // to process to obtain the first half of the uncompressed - // data. The test ran correctly most of the time, because - // the background goroutine had usually read even - // those extra bits by now, but it's not a useful thing to - // check. - buf.WriteMode() - } - buf.ReadMode() - out := make([]byte, 10) - if n, err := r.Read(out); n > 0 || err != io.EOF { - t.Errorf("testSync (%d, %d, %s): final Read: %d, %v (hex: %x)", level, len(input), name, n, err, out[0:n]) - } - if buf.buf.Len() != 0 { - t.Errorf("testSync (%d, %d, %s): extra data at end", level, len(input), name) - } - r.Close() - - // stream should work for ordinary reader too - r = NewReader(buf1) - out, err = io.ReadAll(r) - if err != nil { - t.Errorf("testSync: read: %s", err) - return - } - r.Close() - if !bytes.Equal(input, out) { - t.Errorf("testSync: decompress(compress(data)) != data: level=%d input=%s", level, name) - } -} - -func testToFromWithLevelAndLimit(t *testing.T, level int, input []byte, name string, limit int) { - var buffer bytes.Buffer - w, err := NewWriter(&buffer, level) - if err != nil { - t.Errorf("NewWriter: %v", err) - return - } - w.Write(input) - w.Close() - if limit > 0 { - t.Logf("level: %d - Size:%.2f%%, %d b\n", level, float64(buffer.Len()*100)/float64(limit), buffer.Len()) - } - if limit > 0 && buffer.Len() > limit { - t.Errorf("level: %d, len(compress(data)) = %d > limit = %d", level, buffer.Len(), limit) - } - - r := NewReader(&buffer) - out, err := io.ReadAll(r) - if err != nil { - t.Errorf("read: %s", err) - return - } - r.Close() - if !bytes.Equal(input, out) { - os.WriteFile("testdata/fails/"+t.Name()+".got", out, os.ModePerm) - os.WriteFile("testdata/fails/"+t.Name()+".want", input, os.ModePerm) - t.Errorf("decompress(compress(data)) != data: level=%d input=%s", level, name) - return - } - testSync(t, level, input, name) -} - -func testToFromWithLimit(t *testing.T, input []byte, name string, limit [11]int) { - for i := range 10 { - testToFromWithLevelAndLimit(t, i, input, name, limit[i]) - } - testToFromWithLevelAndLimit(t, -2, input, name, limit[10]) -} - -func TestDeflateInflate(t *testing.T) { - for i, h := range deflateInflateTests { - testToFromWithLimit(t, h.in, fmt.Sprintf("#%d", i), [11]int{}) - } -} - -func TestReverseBits(t *testing.T) { - for _, h := range reverseBitsTests { - if v := reverseBits(h.in, h.bitCount); v != h.out { - t.Errorf("reverseBits(%v,%v) = %v, want %v", - h.in, h.bitCount, v, h.out) - } - } -} - -type deflateInflateStringTest struct { - filename string - label string - limit [11]int // Number 11 is ConstantCompression -} - -var deflateInflateStringTests = []deflateInflateStringTest{ - { - "../testdata/e.txt", - "2.718281828...", - [...]int{100018, 67900, 50960, 51150, 50930, 50790, 50790, 50790, 50790, 50790, 43683 + 100}, - }, - { - "../testdata/Mark.Twain-Tom.Sawyer.txt", - "Mark.Twain-Tom.Sawyer", - [...]int{387999, 185000, 182361, 179974, 174124, 168819, 162936, 160506, 160295, 160295, 233460 + 100}, - }, -} - -func TestDeflateInflateString(t *testing.T) { - for _, test := range deflateInflateStringTests { - gold, err := os.ReadFile(test.filename) - if err != nil { - t.Error(err) - } - // Remove returns that may be present on Windows - neutral := strings.Map(func(r rune) rune { - if r != '\r' { - return r - } - return -1 - }, string(gold)) - - testToFromWithLimit(t, []byte(neutral), test.label, test.limit) - - if testing.Short() { - break - } - } -} - -func TestReaderDict(t *testing.T) { - const ( - dict = "hello world" - text = "hello again world" - ) - var b bytes.Buffer - w, err := NewWriter(&b, 5) - if err != nil { - t.Fatalf("NewWriter: %v", err) - } - w.Write([]byte(dict)) - w.Flush() - b.Reset() - w.Write([]byte(text)) - w.Close() - - r := NewReaderDict(&b, []byte(dict)) - data, err := io.ReadAll(r) - if err != nil { - t.Fatal(err) - } - if string(data) != "hello again world" { - t.Fatalf("read returned %q want %q", string(data), text) - } -} - -func TestWriterDict(t *testing.T) { - const ( - dict = "hello world Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." - text = "hello world Lorem ipsum dolor sit amet" - ) - // This test is sensitive to algorithm changes that skip - // data in favour of speed. Higher levels are less prone to this - // so we test level 4-9. - for l := 4; l < 9; l++ { - var b bytes.Buffer - w, err := NewWriter(&b, l) - if err != nil { - t.Fatalf("level %d, NewWriter: %v", l, err) - } - w.Write([]byte(dict)) - w.Flush() - b.Reset() - w.Write([]byte(text)) - w.Close() - - var b1 bytes.Buffer - w, _ = NewWriterDict(&b1, l, []byte(dict)) - w.Write([]byte(text)) - w.Close() - - if !bytes.Equal(b1.Bytes(), b.Bytes()) { - t.Errorf("level %d, writer wrote\n%v\n want\n%v", l, b1.Bytes(), b.Bytes()) - } - } -} - -// See http://code.google.com/p/go/issues/detail?id=2508 -func TestRegression2508(t *testing.T) { - if testing.Short() { - t.Logf("test disabled with -short") - return - } - w, err := NewWriter(io.Discard, 1) - if err != nil { - t.Fatalf("NewWriter: %v", err) - } - buf := make([]byte, 1024) - for range 131072 { - if _, err := w.Write(buf); err != nil { - t.Fatalf("writer failed: %v", err) - } - } - w.Close() -} - -func TestWriterReset(t *testing.T) { - for level := -2; level <= 9; level++ { - if level == -1 { - level++ - } - if testing.Short() && level > 1 { - break - } - w, err := NewWriter(io.Discard, level) - if err != nil { - t.Fatalf("NewWriter: %v", err) - } - buf := []byte("hello world") - for range 1024 { - w.Write(buf) - } - w.Reset(io.Discard) - - wref, err := NewWriter(io.Discard, level) - if err != nil { - t.Fatalf("NewWriter: %v", err) - } - - // DeepEqual doesn't compare functions. - w.d.fill, wref.d.fill = nil, nil - w.d.step, wref.d.step = nil, nil - w.d.state, wref.d.state = nil, nil - w.d.fast, wref.d.fast = nil, nil - - // hashMatch is always overwritten when used. - if w.d.tokens.n != 0 { - t.Errorf("level %d Writer not reset after Reset. %d tokens were present", level, w.d.tokens.n) - } - // As long as the length is 0, we don't care about the content. - w.d.tokens = wref.d.tokens - - // We don't care if there are values in the window, as long as it is at d.index is 0 - w.d.window = wref.d.window - if !reflect.DeepEqual(w, wref) { - t.Errorf("level %d Writer not reset after Reset", level) - } - } - - for i := HuffmanOnly; i <= BestCompression; i++ { - testResetOutput(t, fmt.Sprint("level-", i), func(w io.Writer) (*Writer, error) { return NewWriter(w, i) }) - } - dict := []byte(strings.Repeat("we are the world - how are you?", 3)) - for i := HuffmanOnly; i <= BestCompression; i++ { - testResetOutput(t, fmt.Sprint("dict-level-", i), func(w io.Writer) (*Writer, error) { return NewWriterDict(w, i, dict) }) - } - for i := HuffmanOnly; i <= BestCompression; i++ { - testResetOutput(t, fmt.Sprint("dict-reset-level-", i), func(w io.Writer) (*Writer, error) { - w2, err := NewWriter(nil, i) - if err != nil { - return w2, err - } - w2.ResetDict(w, dict) - return w2, nil - }) - } - testResetOutput(t, fmt.Sprint("dict-reset-window"), func(w io.Writer) (*Writer, error) { - w2, err := NewWriterWindow(nil, 1024) - if err != nil { - return w2, err - } - w2.ResetDict(w, dict) - return w2, nil - }) -} - -func testResetOutput(t *testing.T, name string, newWriter func(w io.Writer) (*Writer, error)) { - t.Run(name, func(t *testing.T) { - buf := new(bytes.Buffer) - w, err := newWriter(buf) - if err != nil { - t.Fatalf("NewWriter: %v", err) - } - b := []byte("hello world - how are you doing?") - for range 1024 { - w.Write(b) - } - w.Close() - out1 := buf.Bytes() - - buf2 := new(bytes.Buffer) - w.Reset(buf2) - for range 1024 { - w.Write(b) - } - w.Close() - out2 := buf2.Bytes() - - if len(out1) != len(out2) { - t.Errorf("got %d, expected %d bytes", len(out2), len(out1)) - } - if !bytes.Equal(out1, out2) { - mm := 0 - for i, b := range out1[:len(out2)] { - if b != out2[i] { - t.Errorf("mismatch index %d: %02x, expected %02x", i, out2[i], b) - } - mm++ - if mm == 10 { - t.Fatal("Stopping") - } - } - } - t.Logf("got %d bytes", len(out1)) - }) -} - -// TestBestSpeed tests that round-tripping through deflate and then inflate -// recovers the original input. The Write sizes are near the thresholds in the -// compressor.encSpeed method (0, 16, 128), as well as near maxStoreBlockSize -// (65535). -func TestBestSpeed(t *testing.T) { - abc := make([]byte, 128) - for i := range abc { - abc[i] = byte(i) - } - abcabc := bytes.Repeat(abc, 131072/len(abc)) - var want []byte - - testCases := [][]int{ - {65536, 0}, - {65536, 1}, - {65536, 1, 256}, - {65536, 1, 65536}, - {65536, 14}, - {65536, 15}, - {65536, 16}, - {65536, 16, 256}, - {65536, 16, 65536}, - {65536, 127}, - {65536, 128}, - {65536, 128, 256}, - {65536, 128, 65536}, - {65536, 129}, - {65536, 65536, 256}, - {65536, 65536, 65536}, - } - - for i, tc := range testCases { - if testing.Short() && i > 5 { - t.Skip() - } - for _, firstN := range []int{1, 65534, 65535, 65536, 65537, 131072} { - tc[0] = firstN - outer: - for _, flush := range []bool{false, true} { - buf := new(bytes.Buffer) - want = want[:0] - - w, err := NewWriter(buf, BestSpeed) - if err != nil { - t.Errorf("i=%d, firstN=%d, flush=%t: NewWriter: %v", i, firstN, flush, err) - continue - } - for _, n := range tc { - want = append(want, abcabc[:n]...) - if _, err := w.Write(abcabc[:n]); err != nil { - t.Errorf("i=%d, firstN=%d, flush=%t: Write: %v", i, firstN, flush, err) - continue outer - } - if !flush { - continue - } - if err := w.Flush(); err != nil { - t.Errorf("i=%d, firstN=%d, flush=%t: Flush: %v", i, firstN, flush, err) - continue outer - } - } - if err := w.Close(); err != nil { - t.Errorf("i=%d, firstN=%d, flush=%t: Close: %v", i, firstN, flush, err) - continue - } - - r := NewReader(buf) - got, err := io.ReadAll(r) - if err != nil { - t.Errorf("i=%d, firstN=%d, flush=%t: ReadAll: %v", i, firstN, flush, err) - continue - } - r.Close() - - if !bytes.Equal(got, want) { - t.Errorf("i=%d, firstN=%d, flush=%t: corruption during deflate-then-inflate", i, firstN, flush) - continue - } - } - } - } -} diff --git a/internal/compress/flate/dict_decoder.go b/internal/compress/flate/dict_decoder.go deleted file mode 100644 index cb855abc..00000000 --- a/internal/compress/flate/dict_decoder.go +++ /dev/null @@ -1,181 +0,0 @@ -// Copyright 2016 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package flate - -// dictDecoder implements the LZ77 sliding dictionary as used in decompression. -// LZ77 decompresses data through sequences of two forms of commands: -// -// - Literal insertions: Runs of one or more symbols are inserted into the data -// stream as is. This is accomplished through the writeByte method for a -// single symbol, or combinations of writeSlice/writeMark for multiple symbols. -// Any valid stream must start with a literal insertion if no preset dictionary -// is used. -// -// - Backward copies: Runs of one or more symbols are copied from previously -// emitted data. Backward copies come as the tuple (dist, length) where dist -// determines how far back in the stream to copy from and length determines how -// many bytes to copy. Note that it is valid for the length to be greater than -// the distance. Since LZ77 uses forward copies, that situation is used to -// perform a form of run-length encoding on repeated runs of symbols. -// The writeCopy and tryWriteCopy are used to implement this command. -// -// For performance reasons, this implementation performs little to no sanity -// checks about the arguments. As such, the invariants documented for each -// method call must be respected. -type dictDecoder struct { - hist []byte // Sliding window history - - // Invariant: 0 <= rdPos <= wrPos <= len(hist) - wrPos int // Current output position in buffer - rdPos int // Have emitted hist[:rdPos] already - full bool // Has a full window length been written yet? -} - -// init initializes dictDecoder to have a sliding window dictionary of the given -// size. If a preset dict is provided, it will initialize the dictionary with -// the contents of dict. -func (dd *dictDecoder) init(size int, dict []byte) { - *dd = dictDecoder{hist: dd.hist} - - if cap(dd.hist) < size { - dd.hist = make([]byte, size) - } - dd.hist = dd.hist[:size] - - if len(dict) > len(dd.hist) { - dict = dict[len(dict)-len(dd.hist):] - } - dd.wrPos = copy(dd.hist, dict) - if dd.wrPos == len(dd.hist) { - dd.wrPos = 0 - dd.full = true - } - dd.rdPos = dd.wrPos -} - -// histSize reports the total amount of historical data in the dictionary. -func (dd *dictDecoder) histSize() int { - if dd.full { - return len(dd.hist) - } - return dd.wrPos -} - -// availRead reports the number of bytes that can be flushed by readFlush. -func (dd *dictDecoder) availRead() int { - return dd.wrPos - dd.rdPos -} - -// availWrite reports the available amount of output buffer space. -func (dd *dictDecoder) availWrite() int { - return len(dd.hist) - dd.wrPos -} - -// writeSlice returns a slice of the available buffer to write data to. -// -// This invariant will be kept: len(s) <= availWrite() -func (dd *dictDecoder) writeSlice() []byte { - return dd.hist[dd.wrPos:] -} - -// writeMark advances the writer pointer by cnt. -// -// This invariant must be kept: 0 <= cnt <= availWrite() -func (dd *dictDecoder) writeMark(cnt int) { - dd.wrPos += cnt -} - -// writeByte writes a single byte to the dictionary. -// -// This invariant must be kept: 0 < availWrite() -func (dd *dictDecoder) writeByte(c byte) { - dd.hist[dd.wrPos] = c - dd.wrPos++ -} - -// writeCopy copies a string at a given (dist, length) to the output. -// This returns the number of bytes copied and may be less than the requested -// length if the available space in the output buffer is too small. -// -// This invariant must be kept: 0 < dist <= histSize() -func (dd *dictDecoder) writeCopy(dist, length int) int { - dstBase := dd.wrPos - dstPos := dstBase - srcPos := dstPos - dist - endPos := min(dstPos+length, len(dd.hist)) - - // Copy non-overlapping section after destination position. - // - // This section is non-overlapping in that the copy length for this section - // is always less than or equal to the backwards distance. This can occur - // if a distance refers to data that wraps-around in the buffer. - // Thus, a backwards copy is performed here; that is, the exact bytes in - // the source prior to the copy is placed in the destination. - if srcPos < 0 { - srcPos += len(dd.hist) - dstPos += copy(dd.hist[dstPos:endPos], dd.hist[srcPos:]) - srcPos = 0 - } - - // Copy possibly overlapping section before destination position. - // - // This section can overlap if the copy length for this section is larger - // than the backwards distance. This is allowed by LZ77 so that repeated - // strings can be succinctly represented using (dist, length) pairs. - // Thus, a forwards copy is performed here; that is, the bytes copied is - // possibly dependent on the resulting bytes in the destination as the copy - // progresses along. This is functionally equivalent to the following: - // - // for i := 0; i < endPos-dstPos; i++ { - // dd.hist[dstPos+i] = dd.hist[srcPos+i] - // } - // dstPos = endPos - // - for dstPos < endPos { - dstPos += copy(dd.hist[dstPos:endPos], dd.hist[srcPos:dstPos]) - } - - dd.wrPos = dstPos - return dstPos - dstBase -} - -// tryWriteCopy tries to copy a string at a given (distance, length) to the -// output. This specialized version is optimized for short distances. -// -// This method is designed to be inlined for performance reasons. -// -// This invariant must be kept: 0 < dist <= histSize() -func (dd *dictDecoder) tryWriteCopy(dist, length int) int { - dstPos := dd.wrPos - endPos := dstPos + length - if dstPos < dist || endPos > len(dd.hist) { - return 0 - } - dstBase := dstPos - srcPos := dstPos - dist - - // Copy possibly overlapping section before destination position. -loop: - dstPos += copy(dd.hist[dstPos:endPos], dd.hist[srcPos:dstPos]) - if dstPos < endPos { - goto loop // Avoid for-loop so that this function can be inlined - } - - dd.wrPos = dstPos - return dstPos - dstBase -} - -// readFlush returns a slice of the historical buffer that is ready to be -// emitted to the user. The data returned by readFlush must be fully consumed -// before calling any other dictDecoder methods. -func (dd *dictDecoder) readFlush() []byte { - toRead := dd.hist[dd.rdPos:dd.wrPos] - dd.rdPos = dd.wrPos - if dd.wrPos == len(dd.hist) { - dd.wrPos, dd.rdPos = 0, 0 - dd.full = true - } - return toRead -} diff --git a/internal/compress/flate/dict_decoder_test.go b/internal/compress/flate/dict_decoder_test.go deleted file mode 100644 index 8bc48a3e..00000000 --- a/internal/compress/flate/dict_decoder_test.go +++ /dev/null @@ -1,284 +0,0 @@ -// Copyright 2016 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package flate - -import ( - "bytes" - "strings" - "testing" -) - -func TestDictDecoder(t *testing.T) { - const ( - abc = "ABC\n" - fox = "The quick brown fox jumped over the lazy dog!\n" - poem = "The Road Not Taken\nRobert Frost\n" + - "\n" + - "Two roads diverged in a yellow wood,\n" + - "And sorry I could not travel both\n" + - "And be one traveler, long I stood\n" + - "And looked down one as far as I could\n" + - "To where it bent in the undergrowth;\n" + - "\n" + - "Then took the other, as just as fair,\n" + - "And having perhaps the better claim,\n" + - "Because it was grassy and wanted wear;\n" + - "Though as for that the passing there\n" + - "Had worn them really about the same,\n" + - "\n" + - "And both that morning equally lay\n" + - "In leaves no step had trodden black.\n" + - "Oh, I kept the first for another day!\n" + - "Yet knowing how way leads on to way,\n" + - "I doubted if I should ever come back.\n" + - "\n" + - "I shall be telling this with a sigh\n" + - "Somewhere ages and ages hence:\n" + - "Two roads diverged in a wood, and I-\n" + - "I took the one less traveled by,\n" + - "And that has made all the difference.\n" - ) - - poemRefs := []struct { - dist int // Backward distance (0 if this is an insertion) - length int // Length of copy or insertion - }{ - {0, 38}, - {33, 3}, - {0, 48}, - {79, 3}, - {0, 11}, - {34, 5}, - {0, 6}, - {23, 7}, - {0, 8}, - {50, 3}, - {0, 2}, - {69, 3}, - {34, 5}, - {0, 4}, - {97, 3}, - {0, 4}, - {43, 5}, - {0, 6}, - {7, 4}, - {88, 7}, - {0, 12}, - {80, 3}, - {0, 2}, - {141, 4}, - {0, 1}, - {196, 3}, - {0, 3}, - {157, 3}, - {0, 6}, - {181, 3}, - {0, 2}, - {23, 3}, - {77, 3}, - {28, 5}, - {128, 3}, - {110, 4}, - {70, 3}, - {0, 4}, - {85, 6}, - {0, 2}, - {182, 6}, - {0, 4}, - {133, 3}, - {0, 7}, - {47, 5}, - {0, 20}, - {112, 5}, - {0, 1}, - {58, 3}, - {0, 8}, - {59, 3}, - {0, 4}, - {173, 3}, - {0, 5}, - {114, 3}, - {0, 4}, - {92, 5}, - {0, 2}, - {71, 3}, - {0, 2}, - {76, 5}, - {0, 1}, - {46, 3}, - {96, 4}, - {130, 4}, - {0, 3}, - {360, 3}, - {0, 3}, - {178, 5}, - {0, 7}, - {75, 3}, - {0, 3}, - {45, 6}, - {0, 6}, - {299, 6}, - {180, 3}, - {70, 6}, - {0, 1}, - {48, 3}, - {66, 4}, - {0, 3}, - {47, 5}, - {0, 9}, - {325, 3}, - {0, 1}, - {359, 3}, - {318, 3}, - {0, 2}, - {199, 3}, - {0, 1}, - {344, 3}, - {0, 3}, - {248, 3}, - {0, 10}, - {310, 3}, - {0, 3}, - {93, 6}, - {0, 3}, - {252, 3}, - {157, 4}, - {0, 2}, - {273, 5}, - {0, 14}, - {99, 4}, - {0, 1}, - {464, 4}, - {0, 2}, - {92, 4}, - {495, 3}, - {0, 1}, - {322, 4}, - {16, 4}, - {0, 3}, - {402, 3}, - {0, 2}, - {237, 4}, - {0, 2}, - {432, 4}, - {0, 1}, - {483, 5}, - {0, 2}, - {294, 4}, - {0, 2}, - {306, 3}, - {113, 5}, - {0, 1}, - {26, 4}, - {164, 3}, - {488, 4}, - {0, 1}, - {542, 3}, - {248, 6}, - {0, 5}, - {205, 3}, - {0, 8}, - {48, 3}, - {449, 6}, - {0, 2}, - {192, 3}, - {328, 4}, - {9, 5}, - {433, 3}, - {0, 3}, - {622, 25}, - {615, 5}, - {46, 5}, - {0, 2}, - {104, 3}, - {475, 10}, - {549, 3}, - {0, 4}, - {597, 8}, - {314, 3}, - {0, 1}, - {473, 6}, - {317, 5}, - {0, 1}, - {400, 3}, - {0, 3}, - {109, 3}, - {151, 3}, - {48, 4}, - {0, 4}, - {125, 3}, - {108, 3}, - {0, 2}, - } - - var got, want bytes.Buffer - var dd dictDecoder - dd.init(1<<11, nil) - - writeCopy := func(dist, length int) { - for length > 0 { - cnt := dd.tryWriteCopy(dist, length) - if cnt == 0 { - cnt = dd.writeCopy(dist, length) - } - - length -= cnt - if dd.availWrite() == 0 { - got.Write(dd.readFlush()) - } - } - } - writeString := func(str string) { - for len(str) > 0 { - cnt := copy(dd.writeSlice(), str) - str = str[cnt:] - dd.writeMark(cnt) - if dd.availWrite() == 0 { - got.Write(dd.readFlush()) - } - } - } - - writeString(".") - want.WriteByte('.') - - str := poem - for _, ref := range poemRefs { - if ref.dist == 0 { - writeString(str[:ref.length]) - } else { - writeCopy(ref.dist, ref.length) - } - str = str[ref.length:] - } - want.WriteString(poem) - - writeCopy(dd.histSize(), 33) - want.Write(want.Bytes()[:33]) - - writeString(abc) - writeCopy(len(abc), 59*len(abc)) - want.WriteString(strings.Repeat(abc, 60)) - - writeString(fox) - writeCopy(len(fox), 9*len(fox)) - want.WriteString(strings.Repeat(fox, 10)) - - writeString(".") - writeCopy(1, 9) - want.WriteString(strings.Repeat(".", 10)) - - writeString(strings.ToUpper(poem)) - writeCopy(len(poem), 7*len(poem)) - want.WriteString(strings.Repeat(strings.ToUpper(poem), 8)) - - writeCopy(dd.histSize(), 10) - want.Write(want.Bytes()[want.Len()-dd.histSize():][:10]) - - got.Write(dd.readFlush()) - if got.String() != want.String() { - t.Errorf("final string mismatch:\ngot %q\nwant %q", got.String(), want.String()) - } -} diff --git a/internal/compress/flate/example_test.go b/internal/compress/flate/example_test.go deleted file mode 100644 index 0861c4da..00000000 --- a/internal/compress/flate/example_test.go +++ /dev/null @@ -1,240 +0,0 @@ -// Copyright 2016 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package flate_test - -import ( - "bytes" - "fmt" - "io" - "log" - "os" - "strings" - "sync" - - "codeberg.org/lindenii/furgit/internal/compress/flate" -) - -// In performance critical applications, Reset can be used to discard the -// current compressor or decompressor state and reinitialize them quickly -// by taking advantage of previously allocated memory. -func Example_reset() { - proverbs := []string{ - "Don't communicate by sharing memory, share memory by communicating.\n", - "Concurrency is not parallelism.\n", - "The bigger the interface, the weaker the abstraction.\n", - "Documentation is for users.\n", - } - - var r strings.Reader - var b bytes.Buffer - buf := make([]byte, 32<<10) - - zw, err := flate.NewWriter(nil, flate.DefaultCompression) - if err != nil { - log.Fatal(err) - } - zr := flate.NewReader(nil) - - for _, s := range proverbs { - r.Reset(s) - b.Reset() - - // Reset the compressor and encode from some input stream. - zw.Reset(&b) - if _, err := io.CopyBuffer(zw, &r, buf); err != nil { - log.Fatal(err) - } - if err := zw.Close(); err != nil { - log.Fatal(err) - } - - // Reset the decompressor and decode to some output stream. - if err := zr.(flate.Resetter).Reset(&b, nil); err != nil { - log.Fatal(err) - } - if _, err := io.CopyBuffer(os.Stdout, zr, buf); err != nil { - log.Fatal(err) - } - if err := zr.Close(); err != nil { - log.Fatal(err) - } - } - - // Output: - // Don't communicate by sharing memory, share memory by communicating. - // Concurrency is not parallelism. - // The bigger the interface, the weaker the abstraction. - // Documentation is for users. -} - -// A preset dictionary can be used to improve the compression ratio. -// The downside to using a dictionary is that the compressor and decompressor -// must agree in advance what dictionary to use. -func Example_dictionary() { - // The dictionary is a string of bytes. When compressing some input data, - // the compressor will attempt to substitute substrings with matches found - // in the dictionary. As such, the dictionary should only contain substrings - // that are expected to be found in the actual data stream. - const dict = `` + `` + `` + ` - - - - - - ... - -` - - var b bytes.Buffer - - // Compress the data using the specially crafted dictionary. - zw, err := flate.NewWriterDict(&b, flate.BestCompression, []byte(dict)) - if err != nil { - log.Fatal(err) - } - if _, err := io.Copy(zw, strings.NewReader(data)); err != nil { - log.Fatal(err) - } - if err := zw.Close(); err != nil { - log.Fatal(err) - } - - // The decompressor must use the same dictionary as the compressor. - // Otherwise, the input may appear as corrupted. - fmt.Println("Decompressed output using the dictionary:") - zr := flate.NewReaderDict(bytes.NewReader(b.Bytes()), []byte(dict)) - if _, err := io.Copy(os.Stdout, zr); err != nil { - log.Fatal(err) - } - if err := zr.Close(); err != nil { - log.Fatal(err) - } - - fmt.Println() - - // Substitute all of the bytes in the dictionary with a '#' to visually - // demonstrate the approximate effectiveness of using a preset dictionary. - fmt.Println("Substrings matched by the dictionary are marked with #:") - hashDict := []byte(dict) - for i := range hashDict { - hashDict[i] = '#' - } - zr = flate.NewReaderDict(&b, hashDict) - if _, err := io.Copy(os.Stdout, zr); err != nil { - log.Fatal(err) - } - if err := zr.Close(); err != nil { - log.Fatal(err) - } - - // Output: - // Decompressed output using the dictionary: - // - // - // - // - // - // - // ... - // - // - // Substrings matched by the dictionary are marked with #: - // ##################### - // ###### - // ############title###########The Go Programming Language"/# - // ############authors###########Alan Donovan and Brian Kernighan"/# - // ############published###########2015-10-26"/# - // ############isbn###########978-0134190440"/# - // ######... cap(e.hist) { - if cap(e.hist) == 0 { - e.hist = make([]byte, 0, allocHistory) - } else { - if cap(e.hist) < maxMatchOffset*2 { - panic("unexpected buffer size") - } - // Move down - offset := int32(len(e.hist)) - maxMatchOffset - // copy(e.hist[0:maxMatchOffset], e.hist[offset:]) - *(*[maxMatchOffset]byte)(e.hist) = *(*[maxMatchOffset]byte)(e.hist[offset:]) - e.cur += offset - e.hist = e.hist[:maxMatchOffset] - } - } - s := int32(len(e.hist)) - e.hist = append(e.hist, src...) - return s -} - -type tableEntryPrev struct { - Cur tableEntry - Prev tableEntry -} - -// hash7 returns the hash of the lowest 7 bytes of u to fit in a hash table with h bits. -// Preferably h should be a constant and should always be <64. -func hash7(u uint64, h uint8) uint32 { - return uint32(((u << (64 - 56)) * prime7bytes) >> ((64 - h) & reg8SizeMask64)) -} - -// hashLen returns a hash of the lowest mls bytes of with length output bits. -// mls must be >=3 and <=8. Any other value will return hash for 4 bytes. -// length should always be < 32. -// Preferably length and mls should be a constant for inlining. -func hashLen(u uint64, length, mls uint8) uint32 { - switch mls { - case 3: - return (uint32(u<<8) * prime3bytes) >> (32 - length) - case 5: - return uint32(((u << (64 - 40)) * prime5bytes) >> (64 - length)) - case 6: - return uint32(((u << (64 - 48)) * prime6bytes) >> (64 - length)) - case 7: - return uint32(((u << (64 - 56)) * prime7bytes) >> (64 - length)) - case 8: - return uint32((u * prime8bytes) >> (64 - length)) - default: - return (uint32(u) * prime4bytes) >> (32 - length) - } -} - -// matchlen will return the match length between offsets and t in src. -// The maximum length returned is maxMatchLength - 4. -// It is assumed that s > t, that t >=0 and s < len(src). -func (e *fastGen) matchlen(s, t int, src []byte) int32 { - if debugDeflate { - if t >= s { - panic(fmt.Sprint("t >=s:", t, s)) - } - if int(s) >= len(src) { - panic(fmt.Sprint("s >= len(src):", s, len(src))) - } - if t < 0 { - panic(fmt.Sprint("t < 0:", t)) - } - if s-t > maxMatchOffset { - panic(fmt.Sprint(s, "-", t, "(", s-t, ") > maxMatchLength (", maxMatchOffset, ")")) - } - } - a := src[s:min(s+maxMatchLength-4, len(src))] - b := src[t:] - return int32(matchLen(a, b)) -} - -// matchlenLong will return the match length between offsets and t in src. -// It is assumed that s > t, that t >=0 and s < len(src). -func (e *fastGen) matchlenLong(s, t int, src []byte) int32 { - if debugDeflate { - if t >= s { - panic(fmt.Sprint("t >=s:", t, s)) - } - if int(s) >= len(src) { - panic(fmt.Sprint("s >= len(src):", s, len(src))) - } - if t < 0 { - panic(fmt.Sprint("t < 0:", t)) - } - if s-t > maxMatchOffset { - panic(fmt.Sprint(s, "-", t, "(", s-t, ") > maxMatchLength (", maxMatchOffset, ")")) - } - } - return int32(matchLen(src[s:], src[t:])) -} - -// Reset the encoding table. -func (e *fastGen) Reset() { - if cap(e.hist) < allocHistory { - e.hist = make([]byte, 0, allocHistory) - } - // We offset current position so everything will be out of reach. - // If we are above the buffer reset it will be cleared anyway since len(hist) == 0. - if e.cur <= bufferReset { - e.cur += maxMatchOffset + int32(len(e.hist)) - } - e.hist = e.hist[:0] -} diff --git a/internal/compress/flate/flate_test.go b/internal/compress/flate/flate_test.go deleted file mode 100644 index 7b019548..00000000 --- a/internal/compress/flate/flate_test.go +++ /dev/null @@ -1,370 +0,0 @@ -// Copyright 2009 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// This test tests some internals of the flate package. -// The tests in package compress/gzip serve as the -// end-to-end test of the decompressor. - -package flate - -import ( - "archive/zip" - "bytes" - "compress/flate" - "encoding/hex" - "fmt" - "io" - "os" - "testing" -) - -// The following test should not panic. -func TestIssue5915(t *testing.T) { - bits := []int{ - 4, 0, 0, 6, 4, 3, 2, 3, 3, 4, 4, 5, 0, 0, 0, 0, 5, 5, 6, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 8, 6, 0, 11, 0, 8, 0, 6, 6, 10, 8, - } - var h huffmanDecoder - if h.init(bits) { - t.Fatalf("Given sequence of bits is bad, and should not succeed.") - } -} - -// The following test should not panic. -func TestIssue5962(t *testing.T) { - bits := []int{ - 4, 0, 0, 6, 4, 3, 2, 3, 3, 4, 4, 5, 0, 0, 0, 0, - 5, 5, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, - } - var h huffmanDecoder - if h.init(bits) { - t.Fatalf("Given sequence of bits is bad, and should not succeed.") - } -} - -// The following test should not panic. -func TestIssue6255(t *testing.T) { - bits1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 11} - bits2 := []int{11, 13} - var h huffmanDecoder - if !h.init(bits1) { - t.Fatalf("Given sequence of bits is good and should succeed.") - } - if h.init(bits2) { - t.Fatalf("Given sequence of bits is bad and should not succeed.") - } -} - -func TestInvalidEncoding(t *testing.T) { - // Initialize Huffman decoder to recognize "0". - var h huffmanDecoder - if !h.init([]int{1}) { - t.Fatal("Failed to initialize Huffman decoder") - } - - // Initialize decompressor with invalid Huffman coding. - var f decompressor - f.r = bytes.NewReader([]byte{0xff}) - - _, err := f.huffSym(&h) - if err == nil { - t.Fatal("Should have rejected invalid bit sequence") - } -} - -func TestRegressions(t *testing.T) { - // Test fuzzer regressions - data, err := os.ReadFile("testdata/regression.zip") - if err != nil { - t.Fatal(err) - } - zr, err := zip.NewReader(bytes.NewReader(data), int64(len(data))) - if err != nil { - t.Fatal(err) - } - for _, tt := range zr.File { - data, err := tt.Open() - if err != nil { - t.Fatal(err) - } - data1, err := io.ReadAll(data) - if err != nil { - t.Fatal(err) - } - t.Run(tt.Name, func(t *testing.T) { - if testing.Short() && len(data1) > 10000 { - t.SkipNow() - } - for level := 0; level <= 9; level++ { - t.Run(fmt.Sprint(tt.Name+"-level", 1), func(t *testing.T) { - buf := new(bytes.Buffer) - fw, err := NewWriter(buf, level) - if err != nil { - t.Error(err) - } - n, err := fw.Write(data1) - if n != len(data1) { - t.Error("short write") - } - if err != nil { - t.Error(err) - } - err = fw.Close() - if err != nil { - t.Error(err) - } - fr1 := NewReader(buf) - data2, err := io.ReadAll(fr1) - if err != nil { - t.Error(err) - } - if !bytes.Equal(data1, data2) { - t.Error("not equal") - } - // Do it again... - buf.Reset() - fw.Reset(buf) - n, err = fw.Write(data1) - if n != len(data1) { - t.Error("short write") - } - if err != nil { - t.Error(err) - } - err = fw.Close() - if err != nil { - t.Error(err) - } - fr1 = flate.NewReader(buf) - data2, err = io.ReadAll(fr1) - if err != nil { - t.Error(err) - } - if !bytes.Equal(data1, data2) { - t.Error("not equal") - } - }) - } - t.Run(tt.Name+"stateless", func(t *testing.T) { - // Split into two and use history... - buf := new(bytes.Buffer) - err = StatelessDeflate(buf, data1[:len(data1)/2], false, nil) - if err != nil { - t.Error(err) - } - - // Use top half as dictionary... - dict := data1[:len(data1)/2] - err = StatelessDeflate(buf, data1[len(data1)/2:], true, dict) - if err != nil { - t.Error(err) - } - t.Log(buf.Len()) - fr1 := NewReader(buf) - data2, err := io.ReadAll(fr1) - if err != nil { - t.Error(err) - } - if !bytes.Equal(data1, data2) { - // fmt.Printf("want:%x\ngot: %x\n", data1, data2) - t.Error("not equal") - } - }) - }) - } -} - -func TestInvalidBits(t *testing.T) { - oversubscribed := []int{1, 2, 3, 4, 4, 5} - incomplete := []int{1, 2, 4, 4} - var h huffmanDecoder - if h.init(oversubscribed) { - t.Fatal("Should reject oversubscribed bit-length set") - } - if h.init(incomplete) { - t.Fatal("Should reject incomplete bit-length set") - } -} - -func TestStreams(t *testing.T) { - // To verify any of these hexstrings as valid or invalid flate streams - // according to the C zlib library, you can use the Python wrapper library: - // >>> hex_string = "010100feff11" - // >>> import zlib - // >>> zlib.decompress(hex_string.decode("hex"), -15) # Negative means raw DEFLATE - // '\x11' - - testCases := []struct { - desc string // Description of the stream - stream string // Hexstring of the input DEFLATE stream - want string // Expected result. Use "fail" to expect failure - }{{ - "degenerate HCLenTree", - "05e0010000000000100000000000000000000000000000000000000000000000" + - "00000000000000000004", - "fail", - }, { - "complete HCLenTree, empty HLitTree, empty HDistTree", - "05e0010400000000000000000000000000000000000000000000000000000000" + - "00000000000000000010", - "fail", - }, { - "empty HCLenTree", - "05e0010000000000000000000000000000000000000000000000000000000000" + - "00000000000000000010", - "fail", - }, { - "complete HCLenTree, complete HLitTree, empty HDistTree, use missing HDist symbol", - "000100feff000de0010400000000100000000000000000000000000000000000" + - "0000000000000000000000000000002c", - "fail", - }, { - "complete HCLenTree, complete HLitTree, degenerate HDistTree, use missing HDist symbol", - "000100feff000de0010000000000000000000000000000000000000000000000" + - "00000000000000000610000000004070", - "fail", - }, { - "complete HCLenTree, empty HLitTree, empty HDistTree", - "05e0010400000000100400000000000000000000000000000000000000000000" + - "0000000000000000000000000008", - "fail", - }, { - "complete HCLenTree, empty HLitTree, degenerate HDistTree", - "05e0010400000000100400000000000000000000000000000000000000000000" + - "0000000000000000000800000008", - "fail", - }, { - "complete HCLenTree, degenerate HLitTree, degenerate HDistTree, use missing HLit symbol", - "05e0010400000000100000000000000000000000000000000000000000000000" + - "0000000000000000001c", - "fail", - }, { - "complete HCLenTree, complete HLitTree, too large HDistTree", - "edff870500000000200400000000000000000000000000000000000000000000" + - "000000000000000000080000000000000004", - "fail", - }, { - "complete HCLenTree, complete HLitTree, empty HDistTree, excessive repeater code", - "edfd870500000000200400000000000000000000000000000000000000000000" + - "000000000000000000e8b100", - "fail", - }, { - "complete HCLenTree, complete HLitTree, empty HDistTree of normal length 30", - "05fd01240000000000f8ffffffffffffffffffffffffffffffffffffffffffff" + - "ffffffffffffffffff07000000fe01", - "", - }, { - "complete HCLenTree, complete HLitTree, empty HDistTree of excessive length 31", - "05fe01240000000000f8ffffffffffffffffffffffffffffffffffffffffffff" + - "ffffffffffffffffff07000000fc03", - "fail", - }, { - "complete HCLenTree, over-subscribed HLitTree, empty HDistTree", - "05e001240000000000fcffffffffffffffffffffffffffffffffffffffffffff" + - "ffffffffffffffffff07f00f", - "fail", - }, { - "complete HCLenTree, under-subscribed HLitTree, empty HDistTree", - "05e001240000000000fcffffffffffffffffffffffffffffffffffffffffffff" + - "fffffffffcffffffff07f00f", - "fail", - }, { - "complete HCLenTree, complete HLitTree with single code, empty HDistTree", - "05e001240000000000f8ffffffffffffffffffffffffffffffffffffffffffff" + - "ffffffffffffffffff07f00f", - "01", - }, { - "complete HCLenTree, complete HLitTree with multiple codes, empty HDistTree", - "05e301240000000000f8ffffffffffffffffffffffffffffffffffffffffffff" + - "ffffffffffffffffff07807f", - "01", - }, { - "complete HCLenTree, complete HLitTree, degenerate HDistTree, use valid HDist symbol", - "000100feff000de0010400000000100000000000000000000000000000000000" + - "0000000000000000000000000000003c", - "00000000", - }, { - "complete HCLenTree, degenerate HLitTree, degenerate HDistTree", - "05e0010400000000100000000000000000000000000000000000000000000000" + - "0000000000000000000c", - "", - }, { - "complete HCLenTree, degenerate HLitTree, empty HDistTree", - "05e0010400000000100000000000000000000000000000000000000000000000" + - "00000000000000000004", - "", - }, { - "complete HCLenTree, complete HLitTree, empty HDistTree, spanning repeater code", - "edfd870500000000200400000000000000000000000000000000000000000000" + - "000000000000000000e8b000", - "", - }, { - "complete HCLenTree with length codes, complete HLitTree, empty HDistTree", - "ede0010400000000100000000000000000000000000000000000000000000000" + - "0000000000000000000400004000", - "", - }, { - "complete HCLenTree, complete HLitTree, degenerate HDistTree, use valid HLit symbol 284 with count 31", - "000100feff00ede0010400000000100000000000000000000000000000000000" + - "000000000000000000000000000000040000407f00", - "0000000000000000000000000000000000000000000000000000000000000000" + - "0000000000000000000000000000000000000000000000000000000000000000" + - "0000000000000000000000000000000000000000000000000000000000000000" + - "0000000000000000000000000000000000000000000000000000000000000000" + - "0000000000000000000000000000000000000000000000000000000000000000" + - "0000000000000000000000000000000000000000000000000000000000000000" + - "0000000000000000000000000000000000000000000000000000000000000000" + - "0000000000000000000000000000000000000000000000000000000000000000" + - "000000", - }, { - "complete HCLenTree, complete HLitTree, degenerate HDistTree, use valid HLit and HDist symbols", - "0cc2010d00000082b0ac4aff0eb07d27060000ffff", - "616263616263", - }, { - "fixed block, use reserved symbol 287", - "33180700", - "fail", - }, { - "raw block", - "010100feff11", - "11", - }, { - "issue 10426 - over-subscribed HCLenTree causes a hang", - "344c4a4e494d4b070000ff2e2eff2e2e2e2e2eff", - "fail", - }, { - "issue 11030 - empty HDistTree unexpectedly leads to error", - "05c0070600000080400fff37a0ca", - "", - }, { - "issue 11033 - empty HDistTree unexpectedly leads to error", - "050fb109c020cca5d017dcbca044881ee1034ec149c8980bbc413c2ab35be9dc" + - "b1473449922449922411202306ee97b0383a521b4ffdcf3217f9f7d3adb701", - "3130303634342068652e706870005d05355f7ed957ff084a90925d19e3ebc6d0" + - "c6d7", - }} - - for i, tc := range testCases { - data, err := hex.DecodeString(tc.stream) - if err != nil { - t.Fatal(err) - } - data, err = io.ReadAll(NewReader(bytes.NewReader(data))) - if tc.want == "fail" { - if err == nil { - t.Errorf("#%d (%s): got nil error, want non-nil", i, tc.desc) - } - } else { - if err != nil { - t.Errorf("#%d (%s): %v", i, tc.desc, err) - continue - } - if got := hex.EncodeToString(data); got != tc.want { - t.Errorf("#%d (%s):\ngot %q\nwant %q", i, tc.desc, got, tc.want) - } - - } - } -} diff --git a/internal/compress/flate/fuzz_test.go b/internal/compress/flate/fuzz_test.go deleted file mode 100644 index 5c361956..00000000 --- a/internal/compress/flate/fuzz_test.go +++ /dev/null @@ -1,176 +0,0 @@ -//go:build go1.18 - -package flate - -import ( - "bytes" - "flag" - "io" - "os" - "strconv" - "testing" - - "codeberg.org/lindenii/furgit/internal/compress/internal/fuzz" -) - -// Fuzzing tweaks: -var ( - fuzzStartF = flag.Int("start", HuffmanOnly, "Start fuzzing at this level") - fuzzEndF = flag.Int("end", BestCompression, "End fuzzing at this level (inclusive)") - fuzzMaxF = flag.Int("max", 1<<20, "Maximum input size") - fuzzSLF = flag.Bool("sl", true, "Include stateless encodes") - fuzzWindow = flag.Bool("windows", true, "Include windowed encodes") -) - -func TestMain(m *testing.M) { - flag.Parse() - os.Exit(m.Run()) -} - -func FuzzEncoding(f *testing.F) { - fuzz.AddFromZip(f, "testdata/regression.zip", fuzz.TypeRaw, false) - fuzz.AddFromZip(f, "testdata/fuzz/encode-raw-corpus.zip", fuzz.TypeRaw, testing.Short()) - fuzz.AddFromZip(f, "testdata/fuzz/FuzzEncoding.zip", fuzz.TypeGoFuzz, testing.Short()) - - startFuzz := *fuzzStartF - endFuzz := *fuzzEndF - maxSize := *fuzzMaxF - stateless := *fuzzSLF - fuzzWindow := *fuzzWindow - - decoder := NewReader(nil) - buf := new(bytes.Buffer) - encs := make([]*Writer, endFuzz-startFuzz+1) - for i := range encs { - var err error - encs[i], err = NewWriter(nil, i+startFuzz) - if err != nil { - f.Fatal(err.Error()) - } - } - - f.Fuzz(func(t *testing.T, data []byte) { - if len(data) > maxSize { - return - } - for level := startFuzz; level <= endFuzz; level++ { - msg := "level " + strconv.Itoa(level) + ":" - buf.Reset() - fw := encs[level-startFuzz] - fw.Reset(buf) - n, err := fw.Write(data) - if n != len(data) { - t.Fatal(msg + "short write") - } - if err != nil { - t.Fatal(msg + err.Error()) - } - err = fw.Close() - if err != nil { - t.Fatal(msg + err.Error()) - } - decoder.(Resetter).Reset(buf, nil) - data2, err := io.ReadAll(decoder) - if err != nil { - t.Fatal(msg + err.Error()) - } - if !bytes.Equal(data, data2) { - t.Fatal(msg + "not equal") - } - // Do it again... (also uses copy) - msg = "level " + strconv.Itoa(level) + " (reset):" - buf.Reset() - fw.Reset(buf) - _, err = io.Copy(fw, bytes.NewReader(data)) - if err != nil { - t.Fatal(msg + err.Error()) - } - err = fw.Close() - if err != nil { - t.Fatal(msg + err.Error()) - } - decoder.(Resetter).Reset(buf, nil) - data2, err = io.ReadAll(decoder) - if err != nil { - t.Fatal(msg + err.Error()) - } - if !bytes.Equal(data, data2) { - t.Fatal(msg + "not equal") - } - } - if stateless { - // Split into two and use history... - msg := "stateless:" - buf.Reset() - err := StatelessDeflate(buf, data[:len(data)/2], false, nil) - if err != nil { - t.Error(err) - } - - // Use top half as dictionary... - dict := data[:len(data)/2] - err = StatelessDeflate(buf, data[len(data)/2:], true, dict) - if err != nil { - t.Error(err) - } - - decoder.(Resetter).Reset(buf, nil) - data2, err := io.ReadAll(decoder) - if err != nil { - t.Error(err) - } - if !bytes.Equal(data, data2) { - // fmt.Printf("want:%x\ngot: %x\n", data1, data2) - t.Error(msg + "not equal") - } - } - if fuzzWindow { - msg := "windowed:" - buf.Reset() - fw, err := NewWriterWindow(buf, 1000) - if err != nil { - t.Fatal(msg + err.Error()) - } - fw.Reset(buf) - n, err := fw.Write(data) - if n != len(data) { - t.Fatal(msg + "short write") - } - if err != nil { - t.Fatal(msg + err.Error()) - } - err = fw.Close() - if err != nil { - t.Fatal(msg + err.Error()) - } - decoder.(Resetter).Reset(buf, nil) - data2, err := io.ReadAll(decoder) - if err != nil { - t.Fatal(msg + err.Error()) - } - if !bytes.Equal(data, data2) { - t.Fatal(msg + "not equal") - } - // Do it again... - msg = msg + " (reset):" - buf.Reset() - fw.Reset(buf) - n, err = fw.Write(data) - if n != len(data) { - t.Fatal(msg + "short write") - } - if err != nil { - t.Fatal(msg + err.Error()) - } - err = fw.Close() - if err != nil { - t.Fatal(msg + err.Error()) - } - decoder.(Resetter).Reset(buf, nil) - data2, err = io.ReadAll(decoder) - if err != nil { - t.Fatal(msg + err.Error()) - } - } - }) -} diff --git a/internal/compress/flate/huffman_bit_writer.go b/internal/compress/flate/huffman_bit_writer.go deleted file mode 100644 index aff3c960..00000000 --- a/internal/compress/flate/huffman_bit_writer.go +++ /dev/null @@ -1,1174 +0,0 @@ -// Copyright 2009 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package flate - -import ( - "fmt" - "io" - "math" - - "codeberg.org/lindenii/furgit/internal/compress/internal/le" -) - -const ( - // The largest offset code. - offsetCodeCount = 30 - - // The special code used to mark the end of a block. - endBlockMarker = 256 - - // The first length code. - lengthCodesStart = 257 - - // The number of codegen codes. - codegenCodeCount = 19 - badCode = 255 - - // maxPredefinedTokens is the maximum number of tokens - // where we check if fixed size is smaller. - maxPredefinedTokens = 250 - - // bufferFlushSize indicates the buffer size - // after which bytes are flushed to the writer. - // Should preferably be a multiple of 6, since - // we accumulate 6 bytes between writes to the buffer. - bufferFlushSize = 246 -) - -// Minimum length code that emits bits. -const lengthExtraBitsMinCode = 8 - -// The number of extra bits needed by length code X - LENGTH_CODES_START. -var lengthExtraBits = [32]uint8{ - /* 257 */ 0, 0, 0, - /* 260 */ 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, - /* 270 */ 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, - /* 280 */ 4, 5, 5, 5, 5, 0, -} - -// The length indicated by length code X - LENGTH_CODES_START. -var lengthBase = [32]uint8{ - 0, 1, 2, 3, 4, 5, 6, 7, 8, 10, - 12, 14, 16, 20, 24, 28, 32, 40, 48, 56, - 64, 80, 96, 112, 128, 160, 192, 224, 255, -} - -// Minimum offset code that emits bits. -const offsetExtraBitsMinCode = 4 - -// offset code word extra bits. -var offsetExtraBits = [32]int8{ - 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, - 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, - 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, - /* extended window */ - 14, 14, -} - -var offsetCombined = [32]uint32{} - -func init() { - offsetBase := [32]uint32{ - /* normal deflate */ - 0x000000, 0x000001, 0x000002, 0x000003, 0x000004, - 0x000006, 0x000008, 0x00000c, 0x000010, 0x000018, - 0x000020, 0x000030, 0x000040, 0x000060, 0x000080, - 0x0000c0, 0x000100, 0x000180, 0x000200, 0x000300, - 0x000400, 0x000600, 0x000800, 0x000c00, 0x001000, - 0x001800, 0x002000, 0x003000, 0x004000, 0x006000, - - /* extended window */ - 0x008000, 0x00c000, - } - - for i := range offsetCombined[:] { - // Don't use extended window values... - if offsetExtraBits[i] == 0 || offsetBase[i] > 0x006000 { - continue - } - offsetCombined[i] = uint32(offsetExtraBits[i]) | (offsetBase[i] << 8) - } -} - -// The odd order in which the codegen code sizes are written. -var codegenOrder = []uint32{16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15} - -type huffmanBitWriter struct { - // writer is the underlying writer. - // Do not use it directly; use the write method, which ensures - // that Write errors are sticky. - writer io.Writer - - // Data waiting to be written is bytes[0:nbytes] - // and then the low nbits of bits. - bits uint64 - nbits uint8 - nbytes uint8 - lastHuffMan bool - literalEncoding *huffmanEncoder - tmpLitEncoding *huffmanEncoder - offsetEncoding *huffmanEncoder - codegenEncoding *huffmanEncoder - err error - lastHeader int - // Set between 0 (reused block can be up to 2x the size) - logNewTablePenalty uint - bytes [256 + 8]byte - literalFreq [lengthCodesStart + 32]uint16 - offsetFreq [32]uint16 - codegenFreq [codegenCodeCount]uint16 - - // codegen must have an extra space for the final symbol. - codegen [literalCount + offsetCodeCount + 1]uint8 -} - -// Huffman reuse. -// -// The huffmanBitWriter supports reusing huffman tables and thereby combining block sections. -// -// This is controlled by several variables: -// -// If lastHeader is non-zero the Huffman table can be reused. -// This also indicates that a Huffman table has been generated that can output all -// possible symbols. -// It also indicates that an EOB has not yet been emitted, so if a new tabel is generated -// an EOB with the previous table must be written. -// -// If lastHuffMan is set, a table for outputting literals has been generated and offsets are invalid. -// -// An incoming block estimates the output size of a new table using a 'fresh' by calculating the -// optimal size and adding a penalty in 'logNewTablePenalty'. -// A Huffman table is not optimal, which is why we add a penalty, and generating a new table -// is slower both for compression and decompression. - -func newHuffmanBitWriter(w io.Writer) *huffmanBitWriter { - return &huffmanBitWriter{ - writer: w, - literalEncoding: newHuffmanEncoder(literalCount), - tmpLitEncoding: newHuffmanEncoder(literalCount), - codegenEncoding: newHuffmanEncoder(codegenCodeCount), - offsetEncoding: newHuffmanEncoder(offsetCodeCount), - } -} - -func (w *huffmanBitWriter) reset(writer io.Writer) { - w.writer = writer - w.bits, w.nbits, w.nbytes, w.err = 0, 0, 0, nil - w.lastHeader = 0 - w.lastHuffMan = false -} - -func (w *huffmanBitWriter) canReuse(t *tokens) (ok bool) { - a := t.offHist[:offsetCodeCount] - b := w.offsetEncoding.codes - b = b[:len(a)] - for i, v := range a { - if v != 0 && b[i].zero() { - return false - } - } - - a = t.extraHist[:literalCount-256] - b = w.literalEncoding.codes[256:literalCount] - b = b[:len(a)] - for i, v := range a { - if v != 0 && b[i].zero() { - return false - } - } - - a = t.litHist[:256] - b = w.literalEncoding.codes[:len(a)] - for i, v := range a { - if v != 0 && b[i].zero() { - return false - } - } - return true -} - -func (w *huffmanBitWriter) flush() { - if w.err != nil { - w.nbits = 0 - return - } - if w.lastHeader > 0 { - // We owe an EOB - w.writeCode(w.literalEncoding.codes[endBlockMarker]) - w.lastHeader = 0 - } - n := w.nbytes - for w.nbits != 0 { - w.bytes[n] = byte(w.bits) - w.bits >>= 8 - if w.nbits > 8 { // Avoid underflow - w.nbits -= 8 - } else { - w.nbits = 0 - } - n++ - } - w.bits = 0 - if n > 0 { - w.write(w.bytes[:n]) - } - w.nbytes = 0 -} - -func (w *huffmanBitWriter) write(b []byte) { - if w.err != nil { - return - } - _, w.err = w.writer.Write(b) -} - -func (w *huffmanBitWriter) writeBits(b int32, nb uint8) { - w.bits |= uint64(b) << (w.nbits & 63) - w.nbits += nb - if w.nbits >= 48 { - w.writeOutBits() - } -} - -func (w *huffmanBitWriter) writeBytes(bytes []byte) { - if w.err != nil { - return - } - n := w.nbytes - if w.nbits&7 != 0 { - w.err = InternalError("writeBytes with unfinished bits") - return - } - for w.nbits != 0 { - w.bytes[n] = byte(w.bits) - w.bits >>= 8 - w.nbits -= 8 - n++ - } - if n != 0 { - w.write(w.bytes[:n]) - } - w.nbytes = 0 - w.write(bytes) -} - -// RFC 1951 3.2.7 specifies a special run-length encoding for specifying -// the literal and offset lengths arrays (which are concatenated into a single -// array). This method generates that run-length encoding. -// -// The result is written into the codegen array, and the frequencies -// of each code is written into the codegenFreq array. -// Codes 0-15 are single byte codes. Codes 16-18 are followed by additional -// information. Code badCode is an end marker -// -// numLiterals The number of literals in literalEncoding -// numOffsets The number of offsets in offsetEncoding -// litenc, offenc The literal and offset encoder to use -func (w *huffmanBitWriter) generateCodegen(numLiterals int, numOffsets int, litEnc, offEnc *huffmanEncoder) { - for i := range w.codegenFreq { - w.codegenFreq[i] = 0 - } - // Note that we are using codegen both as a temporary variable for holding - // a copy of the frequencies, and as the place where we put the result. - // This is fine because the output is always shorter than the input used - // so far. - codegen := w.codegen[:] // cache - // Copy the concatenated code sizes to codegen. Put a marker at the end. - cgnl := codegen[:numLiterals] - for i := range cgnl { - cgnl[i] = litEnc.codes[i].len() - } - - cgnl = codegen[numLiterals : numLiterals+numOffsets] - for i := range cgnl { - cgnl[i] = offEnc.codes[i].len() - } - codegen[numLiterals+numOffsets] = badCode - - size := codegen[0] - count := 1 - outIndex := 0 - for inIndex := 1; size != badCode; inIndex++ { - // INVARIANT: We have seen "count" copies of size that have not yet - // had output generated for them. - nextSize := codegen[inIndex] - if nextSize == size { - count++ - continue - } - // We need to generate codegen indicating "count" of size. - if size != 0 { - codegen[outIndex] = size - outIndex++ - w.codegenFreq[size]++ - count-- - for count >= 3 { - n := min(6, count) - codegen[outIndex] = 16 - outIndex++ - codegen[outIndex] = uint8(n - 3) - outIndex++ - w.codegenFreq[16]++ - count -= n - } - } else { - for count >= 11 { - n := min(138, count) - codegen[outIndex] = 18 - outIndex++ - codegen[outIndex] = uint8(n - 11) - outIndex++ - w.codegenFreq[18]++ - count -= n - } - if count >= 3 { - // count >= 3 && count <= 10 - codegen[outIndex] = 17 - outIndex++ - codegen[outIndex] = uint8(count - 3) - outIndex++ - w.codegenFreq[17]++ - count = 0 - } - } - count-- - for ; count >= 0; count-- { - codegen[outIndex] = size - outIndex++ - w.codegenFreq[size]++ - } - // Set up invariant for next time through the loop. - size = nextSize - count = 1 - } - // Marker indicating the end of the codegen. - codegen[outIndex] = badCode -} - -func (w *huffmanBitWriter) codegens() int { - numCodegens := len(w.codegenFreq) - for numCodegens > 4 && w.codegenFreq[codegenOrder[numCodegens-1]] == 0 { - numCodegens-- - } - return numCodegens -} - -func (w *huffmanBitWriter) headerSize() (size, numCodegens int) { - numCodegens = len(w.codegenFreq) - for numCodegens > 4 && w.codegenFreq[codegenOrder[numCodegens-1]] == 0 { - numCodegens-- - } - return 3 + 5 + 5 + 4 + (3 * numCodegens) + - w.codegenEncoding.bitLength(w.codegenFreq[:]) + - int(w.codegenFreq[16])*2 + - int(w.codegenFreq[17])*3 + - int(w.codegenFreq[18])*7, numCodegens -} - -// dynamicSize returns the size of dynamically encoded data in bits. -func (w *huffmanBitWriter) dynamicReuseSize(litEnc, offEnc *huffmanEncoder) (size int) { - size = litEnc.bitLength(w.literalFreq[:]) + - offEnc.bitLength(w.offsetFreq[:]) - return size -} - -// dynamicSize returns the size of dynamically encoded data in bits. -func (w *huffmanBitWriter) dynamicSize(litEnc, offEnc *huffmanEncoder, extraBits int) (size, numCodegens int) { - header, numCodegens := w.headerSize() - size = header + - litEnc.bitLength(w.literalFreq[:]) + - offEnc.bitLength(w.offsetFreq[:]) + - extraBits - return size, numCodegens -} - -// extraBitSize will return the number of bits that will be written -// as "extra" bits on matches. -func (w *huffmanBitWriter) extraBitSize() int { - total := 0 - for i, n := range w.literalFreq[257:literalCount] { - total += int(n) * int(lengthExtraBits[i&31]) - } - for i, n := range w.offsetFreq[:offsetCodeCount] { - total += int(n) * int(offsetExtraBits[i&31]) - } - return total -} - -// fixedSize returns the size of dynamically encoded data in bits. -func (w *huffmanBitWriter) fixedSize(extraBits int) int { - return 3 + - fixedLiteralEncoding.bitLength(w.literalFreq[:]) + - fixedOffsetEncoding.bitLength(w.offsetFreq[:]) + - extraBits -} - -// storedSize calculates the stored size, including header. -// The function returns the size in bits and whether the block -// fits inside a single block. -func (w *huffmanBitWriter) storedSize(in []byte) (int, bool) { - if in == nil { - return 0, false - } - if len(in) <= maxStoreBlockSize { - return (len(in) + 5) * 8, true - } - return 0, false -} - -func (w *huffmanBitWriter) writeCode(c hcode) { - // The function does not get inlined if we "& 63" the shift. - w.bits |= c.code64() << (w.nbits & 63) - w.nbits += c.len() - if w.nbits >= 48 { - w.writeOutBits() - } -} - -// writeOutBits will write bits to the buffer. -func (w *huffmanBitWriter) writeOutBits() { - bits := w.bits - w.bits >>= 48 - w.nbits -= 48 - n := w.nbytes - - // We overwrite, but faster... - le.Store64(w.bytes[:], n, bits) - n += 6 - - if n >= bufferFlushSize { - if w.err != nil { - n = 0 - return - } - w.write(w.bytes[:n]) - n = 0 - } - - w.nbytes = n -} - -// Write the header of a dynamic Huffman block to the output stream. -// -// numLiterals The number of literals specified in codegen -// numOffsets The number of offsets specified in codegen -// numCodegens The number of codegens used in codegen -func (w *huffmanBitWriter) writeDynamicHeader(numLiterals int, numOffsets int, numCodegens int, isEof bool) { - if w.err != nil { - return - } - var firstBits int32 = 4 - if isEof { - firstBits = 5 - } - w.writeBits(firstBits, 3) - w.writeBits(int32(numLiterals-257), 5) - w.writeBits(int32(numOffsets-1), 5) - w.writeBits(int32(numCodegens-4), 4) - - for i := range numCodegens { - value := uint(w.codegenEncoding.codes[codegenOrder[i]].len()) - w.writeBits(int32(value), 3) - } - - i := 0 - for { - codeWord := uint32(w.codegen[i]) - i++ - if codeWord == badCode { - break - } - w.writeCode(w.codegenEncoding.codes[codeWord]) - - switch codeWord { - case 16: - w.writeBits(int32(w.codegen[i]), 2) - i++ - case 17: - w.writeBits(int32(w.codegen[i]), 3) - i++ - case 18: - w.writeBits(int32(w.codegen[i]), 7) - i++ - } - } -} - -// writeStoredHeader will write a stored header. -// If the stored block is only used for EOF, -// it is replaced with a fixed huffman block. -func (w *huffmanBitWriter) writeStoredHeader(length int, isEof bool) { - if w.err != nil { - return - } - if w.lastHeader > 0 { - // We owe an EOB - w.writeCode(w.literalEncoding.codes[endBlockMarker]) - w.lastHeader = 0 - } - - // To write EOF, use a fixed encoding block. 10 bits instead of 5 bytes. - if length == 0 && isEof { - w.writeFixedHeader(isEof) - // EOB: 7 bits, value: 0 - w.writeBits(0, 7) - w.flush() - return - } - - var flag int32 - if isEof { - flag = 1 - } - w.writeBits(flag, 3) - w.flush() - w.writeBits(int32(length), 16) - w.writeBits(int32(^uint16(length)), 16) -} - -func (w *huffmanBitWriter) writeFixedHeader(isEof bool) { - if w.err != nil { - return - } - if w.lastHeader > 0 { - // We owe an EOB - w.writeCode(w.literalEncoding.codes[endBlockMarker]) - w.lastHeader = 0 - } - - // Indicate that we are a fixed Huffman block - var value int32 = 2 - if isEof { - value = 3 - } - w.writeBits(value, 3) -} - -// writeBlock will write a block of tokens with the smallest encoding. -// The original input can be supplied, and if the huffman encoded data -// is larger than the original bytes, the data will be written as a -// stored block. -// If the input is nil, the tokens will always be Huffman encoded. -func (w *huffmanBitWriter) writeBlock(tokens *tokens, eof bool, input []byte) { - if w.err != nil { - return - } - - tokens.AddEOB() - if w.lastHeader > 0 { - // We owe an EOB - w.writeCode(w.literalEncoding.codes[endBlockMarker]) - w.lastHeader = 0 - } - numLiterals, numOffsets := w.indexTokens(tokens, false) - w.generate() - var extraBits int - storedSize, storable := w.storedSize(input) - if storable { - extraBits = w.extraBitSize() - } - - // Figure out smallest code. - // Fixed Huffman baseline. - literalEncoding := fixedLiteralEncoding - offsetEncoding := fixedOffsetEncoding - size := math.MaxInt32 - if tokens.n < maxPredefinedTokens { - size = w.fixedSize(extraBits) - } - - // Dynamic Huffman? - var numCodegens int - - // Generate codegen and codegenFrequencies, which indicates how to encode - // the literalEncoding and the offsetEncoding. - w.generateCodegen(numLiterals, numOffsets, w.literalEncoding, w.offsetEncoding) - w.codegenEncoding.generate(w.codegenFreq[:], 7) - dynamicSize, numCodegens := w.dynamicSize(w.literalEncoding, w.offsetEncoding, extraBits) - - if dynamicSize < size { - size = dynamicSize - literalEncoding = w.literalEncoding - offsetEncoding = w.offsetEncoding - } - - // Stored bytes? - if storable && storedSize <= size { - w.writeStoredHeader(len(input), eof) - w.writeBytes(input) - return - } - - // Huffman. - if literalEncoding == fixedLiteralEncoding { - w.writeFixedHeader(eof) - } else { - w.writeDynamicHeader(numLiterals, numOffsets, numCodegens, eof) - } - - // Write the tokens. - w.writeTokens(tokens.Slice(), literalEncoding.codes, offsetEncoding.codes) -} - -// writeBlockDynamic encodes a block using a dynamic Huffman table. -// This should be used if the symbols used have a disproportionate -// histogram distribution. -// If input is supplied and the compression savings are below 1/16th of the -// input size the block is stored. -func (w *huffmanBitWriter) writeBlockDynamic(tokens *tokens, eof bool, input []byte, sync bool) { - if w.err != nil { - return - } - - sync = sync || eof - if sync { - tokens.AddEOB() - } - - // We cannot reuse pure huffman table, and must mark as EOF. - if (w.lastHuffMan || eof) && w.lastHeader > 0 { - // We will not try to reuse. - w.writeCode(w.literalEncoding.codes[endBlockMarker]) - w.lastHeader = 0 - w.lastHuffMan = false - } - - // fillReuse enables filling of empty values. - // This will make encodings always reusable without testing. - // However, this does not appear to benefit on most cases. - const fillReuse = false - - // Check if we can reuse... - if !fillReuse && w.lastHeader > 0 && !w.canReuse(tokens) { - w.writeCode(w.literalEncoding.codes[endBlockMarker]) - w.lastHeader = 0 - } - - numLiterals, numOffsets := w.indexTokens(tokens, true) - extraBits := 0 - ssize, storable := w.storedSize(input) - - const usePrefs = true - if storable || w.lastHeader > 0 { - extraBits = w.extraBitSize() - } - - var size int - - // Check if we should reuse. - if w.lastHeader > 0 { - // Estimate size for using a new table. - // Use the previous header size as the best estimate. - newSize := w.lastHeader + tokens.EstimatedBits() - newSize += int(w.literalEncoding.codes[endBlockMarker].len()) + newSize>>w.logNewTablePenalty - - // The estimated size is calculated as an optimal table. - // We add a penalty to make it more realistic and re-use a bit more. - reuseSize := w.dynamicReuseSize(w.literalEncoding, w.offsetEncoding) + extraBits - - // Check if a new table is better. - if newSize < reuseSize { - // Write the EOB we owe. - w.writeCode(w.literalEncoding.codes[endBlockMarker]) - size = newSize - w.lastHeader = 0 - } else { - size = reuseSize - } - - if tokens.n < maxPredefinedTokens { - if preSize := w.fixedSize(extraBits) + 7; usePrefs && preSize < size { - // Check if we get a reasonable size decrease. - if storable && ssize <= size { - w.writeStoredHeader(len(input), eof) - w.writeBytes(input) - return - } - w.writeFixedHeader(eof) - if !sync { - tokens.AddEOB() - } - w.writeTokens(tokens.Slice(), fixedLiteralEncoding.codes, fixedOffsetEncoding.codes) - return - } - } - // Check if we get a reasonable size decrease. - if storable && ssize <= size { - w.writeStoredHeader(len(input), eof) - w.writeBytes(input) - return - } - } - - // We want a new block/table - if w.lastHeader == 0 { - if fillReuse && !sync { - w.fillTokens() - numLiterals, numOffsets = maxNumLit, maxNumDist - } else { - w.literalFreq[endBlockMarker] = 1 - } - - w.generate() - // Generate codegen and codegenFrequencies, which indicates how to encode - // the literalEncoding and the offsetEncoding. - w.generateCodegen(numLiterals, numOffsets, w.literalEncoding, w.offsetEncoding) - w.codegenEncoding.generate(w.codegenFreq[:], 7) - - var numCodegens int - if fillReuse && !sync { - // Reindex for accurate size... - w.indexTokens(tokens, true) - } - size, numCodegens = w.dynamicSize(w.literalEncoding, w.offsetEncoding, extraBits) - - // Store predefined, if we don't get a reasonable improvement. - if tokens.n < maxPredefinedTokens { - if preSize := w.fixedSize(extraBits); usePrefs && preSize <= size { - // Store bytes, if we don't get an improvement. - if storable && ssize <= preSize { - w.writeStoredHeader(len(input), eof) - w.writeBytes(input) - return - } - w.writeFixedHeader(eof) - if !sync { - tokens.AddEOB() - } - w.writeTokens(tokens.Slice(), fixedLiteralEncoding.codes, fixedOffsetEncoding.codes) - return - } - } - - if storable && ssize <= size { - // Store bytes, if we don't get an improvement. - w.writeStoredHeader(len(input), eof) - w.writeBytes(input) - return - } - - // Write Huffman table. - w.writeDynamicHeader(numLiterals, numOffsets, numCodegens, eof) - if !sync { - w.lastHeader, _ = w.headerSize() - } - w.lastHuffMan = false - } - - if sync { - w.lastHeader = 0 - } - // Write the tokens. - w.writeTokens(tokens.Slice(), w.literalEncoding.codes, w.offsetEncoding.codes) -} - -func (w *huffmanBitWriter) fillTokens() { - for i, v := range w.literalFreq[:literalCount] { - if v == 0 { - w.literalFreq[i] = 1 - } - } - for i, v := range w.offsetFreq[:offsetCodeCount] { - if v == 0 { - w.offsetFreq[i] = 1 - } - } -} - -// indexTokens indexes a slice of tokens, and updates -// literalFreq and offsetFreq, and generates literalEncoding -// and offsetEncoding. -// The number of literal and offset tokens is returned. -func (w *huffmanBitWriter) indexTokens(t *tokens, alwaysEOB bool) (numLiterals, numOffsets int) { - // copy(w.literalFreq[:], t.litHist[:]) - *(*[256]uint16)(w.literalFreq[:]) = t.litHist - // copy(w.literalFreq[256:], t.extraHist[:]) - *(*[32]uint16)(w.literalFreq[256:]) = t.extraHist - w.offsetFreq = t.offHist - - if t.n == 0 { - return - } - if alwaysEOB { - w.literalFreq[endBlockMarker] = 1 - } - - // get the number of literals - numLiterals = len(w.literalFreq) - for w.literalFreq[numLiterals-1] == 0 { - numLiterals-- - } - // get the number of offsets - numOffsets = len(w.offsetFreq) - for numOffsets > 0 && w.offsetFreq[numOffsets-1] == 0 { - numOffsets-- - } - if numOffsets == 0 { - // We haven't found a single match. If we want to go with the dynamic encoding, - // we should count at least one offset to be sure that the offset huffman tree could be encoded. - w.offsetFreq[0] = 1 - numOffsets = 1 - } - return -} - -func (w *huffmanBitWriter) generate() { - w.literalEncoding.generate(w.literalFreq[:literalCount], 15) - w.offsetEncoding.generate(w.offsetFreq[:offsetCodeCount], 15) -} - -// writeTokens writes a slice of tokens to the output. -// codes for literal and offset encoding must be supplied. -func (w *huffmanBitWriter) writeTokens(tokens []token, leCodes, oeCodes []hcode) { - if w.err != nil { - return - } - if len(tokens) == 0 { - return - } - - // Only last token should be endBlockMarker. - var deferEOB bool - if tokens[len(tokens)-1] == endBlockMarker { - tokens = tokens[:len(tokens)-1] - deferEOB = true - } - - // Create slices up to the next power of two to avoid bounds checks. - lits := leCodes[:256] - offs := oeCodes[:32] - lengths := leCodes[lengthCodesStart:] - lengths = lengths[:32] - - // Go 1.16 LOVES having these on stack. - bits, nbits, nbytes := w.bits, w.nbits, w.nbytes - - for _, t := range tokens { - if t < 256 { - // w.writeCode(lits[t.literal()]) - c := lits[t] - bits |= c.code64() << (nbits & 63) - nbits += c.len() - if nbits >= 48 { - le.Store64(w.bytes[:], nbytes, bits) - bits >>= 48 - nbits -= 48 - nbytes += 6 - if nbytes >= bufferFlushSize { - if w.err != nil { - nbytes = 0 - return - } - _, w.err = w.writer.Write(w.bytes[:nbytes]) - nbytes = 0 - } - } - continue - } - - // Write the length - length := t.length() - lengthCode := lengthCode(length) & 31 - if false { - w.writeCode(lengths[lengthCode]) - } else { - // inlined - c := lengths[lengthCode] - bits |= c.code64() << (nbits & 63) - nbits += c.len() - if nbits >= 48 { - le.Store64(w.bytes[:], nbytes, bits) - bits >>= 48 - nbits -= 48 - nbytes += 6 - if nbytes >= bufferFlushSize { - if w.err != nil { - nbytes = 0 - return - } - _, w.err = w.writer.Write(w.bytes[:nbytes]) - nbytes = 0 - } - } - } - - if lengthCode >= lengthExtraBitsMinCode { - extraLengthBits := lengthExtraBits[lengthCode] - // w.writeBits(extraLength, extraLengthBits) - extraLength := int32(length - lengthBase[lengthCode]) - bits |= uint64(extraLength) << (nbits & 63) - nbits += extraLengthBits - if nbits >= 48 { - le.Store64(w.bytes[:], nbytes, bits) - bits >>= 48 - nbits -= 48 - nbytes += 6 - if nbytes >= bufferFlushSize { - if w.err != nil { - nbytes = 0 - return - } - _, w.err = w.writer.Write(w.bytes[:nbytes]) - nbytes = 0 - } - } - } - // Write the offset - offset := t.offset() - offsetCode := (offset >> 16) & 31 - if false { - w.writeCode(offs[offsetCode]) - } else { - // inlined - c := offs[offsetCode] - bits |= c.code64() << (nbits & 63) - nbits += c.len() - if nbits >= 48 { - le.Store64(w.bytes[:], nbytes, bits) - bits >>= 48 - nbits -= 48 - nbytes += 6 - if nbytes >= bufferFlushSize { - if w.err != nil { - nbytes = 0 - return - } - _, w.err = w.writer.Write(w.bytes[:nbytes]) - nbytes = 0 - } - } - } - - if offsetCode >= offsetExtraBitsMinCode { - offsetComb := offsetCombined[offsetCode] - // w.writeBits(extraOffset, extraOffsetBits) - bits |= uint64((offset-(offsetComb>>8))&matchOffsetOnlyMask) << (nbits & 63) - nbits += uint8(offsetComb) - if nbits >= 48 { - le.Store64(w.bytes[:], nbytes, bits) - bits >>= 48 - nbits -= 48 - nbytes += 6 - if nbytes >= bufferFlushSize { - if w.err != nil { - nbytes = 0 - return - } - _, w.err = w.writer.Write(w.bytes[:nbytes]) - nbytes = 0 - } - } - } - } - // Restore... - w.bits, w.nbits, w.nbytes = bits, nbits, nbytes - - if deferEOB { - w.writeCode(leCodes[endBlockMarker]) - } -} - -// huffOffset is a static offset encoder used for huffman only encoding. -// It can be reused since we will not be encoding offset values. -var huffOffset *huffmanEncoder - -func init() { - w := newHuffmanBitWriter(nil) - w.offsetFreq[0] = 1 - huffOffset = newHuffmanEncoder(offsetCodeCount) - huffOffset.generate(w.offsetFreq[:offsetCodeCount], 15) -} - -// writeBlockHuff encodes a block of bytes as either -// Huffman encoded literals or uncompressed bytes if the -// results only gains very little from compression. -func (w *huffmanBitWriter) writeBlockHuff(eof bool, input []byte, sync bool) { - if w.err != nil { - return - } - - // Clear histogram - for i := range w.literalFreq[:] { - w.literalFreq[i] = 0 - } - if !w.lastHuffMan { - for i := range w.offsetFreq[:] { - w.offsetFreq[i] = 0 - } - } - - const numLiterals = endBlockMarker + 1 - const numOffsets = 1 - - // Add everything as literals - // We have to estimate the header size. - // Assume header is around 70 bytes: - // https://stackoverflow.com/a/25454430 - const guessHeaderSizeBits = 70 * 8 - histogram(input, w.literalFreq[:numLiterals]) - ssize, storable := w.storedSize(input) - if storable && len(input) > 1024 { - // Quick check for incompressible content. - abs := float64(0) - avg := float64(len(input)) / 256 - max := float64(len(input) * 2) - for _, v := range w.literalFreq[:256] { - diff := float64(v) - avg - abs += diff * diff - if abs > max { - break - } - } - if abs < max { - if debugDeflate { - fmt.Println("stored", abs, "<", max) - } - // No chance we can compress this... - w.writeStoredHeader(len(input), eof) - w.writeBytes(input) - return - } - } - w.literalFreq[endBlockMarker] = 1 - w.tmpLitEncoding.generate(w.literalFreq[:numLiterals], 15) - estBits := w.tmpLitEncoding.canReuseBits(w.literalFreq[:numLiterals]) - if estBits < math.MaxInt32 { - estBits += w.lastHeader - if w.lastHeader == 0 { - estBits += guessHeaderSizeBits - } - estBits += estBits >> w.logNewTablePenalty - } - - // Store bytes, if we don't get a reasonable improvement. - if storable && ssize <= estBits { - if debugDeflate { - fmt.Println("stored,", ssize, "<=", estBits) - } - w.writeStoredHeader(len(input), eof) - w.writeBytes(input) - return - } - - if w.lastHeader > 0 { - reuseSize := w.literalEncoding.canReuseBits(w.literalFreq[:256]) - - if estBits < reuseSize { - if debugDeflate { - fmt.Println("NOT reusing, reuse:", reuseSize/8, "> new:", estBits/8, "header est:", w.lastHeader/8, "bytes") - } - // We owe an EOB - w.writeCode(w.literalEncoding.codes[endBlockMarker]) - w.lastHeader = 0 - } else if debugDeflate { - fmt.Println("reusing, reuse:", reuseSize/8, "> new:", estBits/8, "- header est:", w.lastHeader/8) - } - } - - count := 0 - if w.lastHeader == 0 { - // Use the temp encoding, so swap. - w.literalEncoding, w.tmpLitEncoding = w.tmpLitEncoding, w.literalEncoding - // Generate codegen and codegenFrequencies, which indicates how to encode - // the literalEncoding and the offsetEncoding. - w.generateCodegen(numLiterals, numOffsets, w.literalEncoding, huffOffset) - w.codegenEncoding.generate(w.codegenFreq[:], 7) - numCodegens := w.codegens() - - // Huffman. - w.writeDynamicHeader(numLiterals, numOffsets, numCodegens, eof) - w.lastHuffMan = true - w.lastHeader, _ = w.headerSize() - if debugDeflate { - count += w.lastHeader - fmt.Println("header:", count/8) - } - } - - encoding := w.literalEncoding.codes[:256] - // Go 1.16 LOVES having these on stack. At least 1.5x the speed. - bits, nbits, nbytes := w.bits, w.nbits, w.nbytes - - if debugDeflate { - count -= int(nbytes)*8 + int(nbits) - } - // Unroll, write 3 codes/loop. - // Fastest number of unrolls. - for len(input) > 3 { - // We must have at least 48 bits free. - if nbits >= 8 { - n := nbits >> 3 - le.Store64(w.bytes[:], nbytes, bits) - bits >>= (n * 8) & 63 - nbits -= n * 8 - nbytes += n - } - if nbytes >= bufferFlushSize { - if w.err != nil { - nbytes = 0 - return - } - if debugDeflate { - count += int(nbytes) * 8 - } - _, w.err = w.writer.Write(w.bytes[:nbytes]) - nbytes = 0 - } - a, b := encoding[input[0]], encoding[input[1]] - bits |= a.code64() << (nbits & 63) - bits |= b.code64() << ((nbits + a.len()) & 63) - c := encoding[input[2]] - nbits += b.len() + a.len() - bits |= c.code64() << (nbits & 63) - nbits += c.len() - input = input[3:] - } - - // Remaining... - for _, t := range input { - if nbits >= 48 { - le.Store64(w.bytes[:], nbytes, bits) - bits >>= 48 - nbits -= 48 - nbytes += 6 - if nbytes >= bufferFlushSize { - if w.err != nil { - nbytes = 0 - return - } - if debugDeflate { - count += int(nbytes) * 8 - } - _, w.err = w.writer.Write(w.bytes[:nbytes]) - nbytes = 0 - } - } - // Bitwriting inlined, ~30% speedup - c := encoding[t] - bits |= c.code64() << (nbits & 63) - - nbits += c.len() - if debugDeflate { - count += int(c.len()) - } - } - // Restore... - w.bits, w.nbits, w.nbytes = bits, nbits, nbytes - - if debugDeflate { - nb := count + int(nbytes)*8 + int(nbits) - fmt.Println("wrote", nb, "bits,", nb/8, "bytes.") - } - // Flush if needed to have space. - if w.nbits >= 48 { - w.writeOutBits() - } - - if eof || sync { - w.writeCode(w.literalEncoding.codes[endBlockMarker]) - w.lastHeader = 0 - w.lastHuffMan = false - } -} diff --git a/internal/compress/flate/huffman_bit_writer_test.go b/internal/compress/flate/huffman_bit_writer_test.go deleted file mode 100644 index 3fc414e2..00000000 --- a/internal/compress/flate/huffman_bit_writer_test.go +++ /dev/null @@ -1,381 +0,0 @@ -// Copyright 2016 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package flate - -import ( - "bytes" - "flag" - "fmt" - "os" - "path/filepath" - "strings" - "testing" -) - -var update = flag.Bool("update", false, "update reference files") - -// TestBlockHuff tests huffman encoding against reference files -// to detect possible regressions. -// If encoding/bit allocation changes you can regenerate these files -// by using the -update flag. -func TestBlockHuff(t *testing.T) { - // determine input files - match, err := filepath.Glob("testdata/huffman-*.in") - if err != nil { - t.Fatal(err) - } - - for _, in := range match { - out := in // for files where input and output are identical - if strings.HasSuffix(in, ".in") { - out = in[:len(in)-len(".in")] + ".golden" - } - t.Run(in, func(t *testing.T) { - testBlockHuff(t, in, out) - }) - } -} - -func testBlockHuff(t *testing.T, in, out string) { - all, err := os.ReadFile(in) - if err != nil { - t.Error(err) - return - } - var buf bytes.Buffer - bw := newHuffmanBitWriter(&buf) - bw.logNewTablePenalty = 8 - bw.writeBlockHuff(false, all, false) - bw.flush() - got := buf.Bytes() - - want, err := os.ReadFile(out) - if err != nil && !*update { - t.Error(err) - return - } - - t.Logf("Testing %q", in) - if !bytes.Equal(got, want) { - if *update { - if in != out { - t.Logf("Updating %q", out) - if err := os.WriteFile(out, got, 0o666); err != nil { - t.Error(err) - } - return - } - // in == out: don't accidentally destroy input - t.Errorf("WARNING: -update did not rewrite input file %s", in) - } - - t.Errorf("%q != %q (see %q)", in, out, in+".got") - if err := os.WriteFile(in+".got", got, 0o666); err != nil { - t.Error(err) - } - return - } - t.Log("Output ok") - - // Test if the writer produces the same output after reset. - buf.Reset() - bw.reset(&buf) - bw.writeBlockHuff(false, all, false) - bw.flush() - got = buf.Bytes() - if !bytes.Equal(got, want) { - t.Errorf("after reset %q != %q (see %q)", in, out, in+".reset.got") - if err := os.WriteFile(in+".reset.got", got, 0o666); err != nil { - t.Error(err) - } - return - } - t.Log("Reset ok") - testWriterEOF(t, "huff", huffTest{input: in}, true) -} - -type huffTest struct { - tokens []token - input string // File name of input data matching the tokens. - want string // File name of data with the expected output with input available. - wantNoInput string // File name of the expected output when no input is available. -} - -const ml = 0x7fc00000 // Maximum length token. Used to reduce the size of writeBlockTests - -var writeBlockTests = []huffTest{ - { - input: "testdata/huffman-null-max.in", - want: "testdata/huffman-null-max.%s.expect", - wantNoInput: "testdata/huffman-null-max.%s.expect-noinput", - tokens: []token{0x0, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, 0x0, 0x0}, - }, - { - input: "testdata/huffman-pi.in", - want: "testdata/huffman-pi.%s.expect", - wantNoInput: "testdata/huffman-pi.%s.expect-noinput", - tokens: []token{0x33, 0x2e, 0x31, 0x34, 0x31, 0x35, 0x39, 0x32, 0x36, 0x35, 0x33, 0x35, 0x38, 0x39, 0x37, 0x39, 0x33, 0x32, 0x33, 0x38, 0x34, 0x36, 0x32, 0x36, 0x34, 0x33, 0x33, 0x38, 0x33, 0x32, 0x37, 0x39, 0x35, 0x30, 0x32, 0x38, 0x38, 0x34, 0x31, 0x39, 0x37, 0x31, 0x36, 0x39, 0x33, 0x39, 0x39, 0x33, 0x37, 0x35, 0x31, 0x30, 0x35, 0x38, 0x32, 0x30, 0x39, 0x37, 0x34, 0x39, 0x34, 0x34, 0x35, 0x39, 0x32, 0x33, 0x30, 0x37, 0x38, 0x31, 0x36, 0x34, 0x30, 0x36, 0x32, 0x38, 0x36, 0x32, 0x30, 0x38, 0x39, 0x39, 0x38, 0x36, 0x32, 0x38, 0x30, 0x33, 0x34, 0x38, 0x32, 0x35, 0x33, 0x34, 0x32, 0x31, 0x31, 0x37, 0x30, 0x36, 0x37, 0x39, 0x38, 0x32, 0x31, 0x34, 0x38, 0x30, 0x38, 0x36, 0x35, 0x31, 0x33, 0x32, 0x38, 0x32, 0x33, 0x30, 0x36, 0x36, 0x34, 0x37, 0x30, 0x39, 0x33, 0x38, 0x34, 0x34, 0x36, 0x30, 0x39, 0x35, 0x35, 0x30, 0x35, 0x38, 0x32, 0x32, 0x33, 0x31, 0x37, 0x32, 0x35, 0x33, 0x35, 0x39, 0x34, 0x30, 0x38, 0x31, 0x32, 0x38, 0x34, 0x38, 0x31, 0x31, 0x31, 0x37, 0x34, 0x4040007e, 0x34, 0x31, 0x30, 0x32, 0x37, 0x30, 0x31, 0x39, 0x33, 0x38, 0x35, 0x32, 0x31, 0x31, 0x30, 0x35, 0x35, 0x35, 0x39, 0x36, 0x34, 0x34, 0x36, 0x32, 0x32, 0x39, 0x34, 0x38, 0x39, 0x35, 0x34, 0x39, 0x33, 0x30, 0x33, 0x38, 0x31, 0x40400012, 0x32, 0x38, 0x38, 0x31, 0x30, 0x39, 0x37, 0x35, 0x36, 0x36, 0x35, 0x39, 0x33, 0x33, 0x34, 0x34, 0x36, 0x40400047, 0x37, 0x35, 0x36, 0x34, 0x38, 0x32, 0x33, 0x33, 0x37, 0x38, 0x36, 0x37, 0x38, 0x33, 0x31, 0x36, 0x35, 0x32, 0x37, 0x31, 0x32, 0x30, 0x31, 0x39, 0x30, 0x39, 0x31, 0x34, 0x4040001a, 0x35, 0x36, 0x36, 0x39, 0x32, 0x33, 0x34, 0x36, 0x404000b2, 0x36, 0x31, 0x30, 0x34, 0x35, 0x34, 0x33, 0x32, 0x36, 0x40400032, 0x31, 0x33, 0x33, 0x39, 0x33, 0x36, 0x30, 0x37, 0x32, 0x36, 0x30, 0x32, 0x34, 0x39, 0x31, 0x34, 0x31, 0x32, 0x37, 0x33, 0x37, 0x32, 0x34, 0x35, 0x38, 0x37, 0x30, 0x30, 0x36, 0x36, 0x30, 0x36, 0x33, 0x31, 0x35, 0x35, 0x38, 0x38, 0x31, 0x37, 0x34, 0x38, 0x38, 0x31, 0x35, 0x32, 0x30, 0x39, 0x32, 0x30, 0x39, 0x36, 0x32, 0x38, 0x32, 0x39, 0x32, 0x35, 0x34, 0x30, 0x39, 0x31, 0x37, 0x31, 0x35, 0x33, 0x36, 0x34, 0x33, 0x36, 0x37, 0x38, 0x39, 0x32, 0x35, 0x39, 0x30, 0x33, 0x36, 0x30, 0x30, 0x31, 0x31, 0x33, 0x33, 0x30, 0x35, 0x33, 0x30, 0x35, 0x34, 0x38, 0x38, 0x32, 0x30, 0x34, 0x36, 0x36, 0x35, 0x32, 0x31, 0x33, 0x38, 0x34, 0x31, 0x34, 0x36, 0x39, 0x35, 0x31, 0x39, 0x34, 0x31, 0x35, 0x31, 0x31, 0x36, 0x30, 0x39, 0x34, 0x33, 0x33, 0x30, 0x35, 0x37, 0x32, 0x37, 0x30, 0x33, 0x36, 0x35, 0x37, 0x35, 0x39, 0x35, 0x39, 0x31, 0x39, 0x35, 0x33, 0x30, 0x39, 0x32, 0x31, 0x38, 0x36, 0x31, 0x31, 0x37, 0x404000e9, 0x33, 0x32, 0x40400009, 0x39, 0x33, 0x31, 0x30, 0x35, 0x31, 0x31, 0x38, 0x35, 0x34, 0x38, 0x30, 0x37, 0x4040010e, 0x33, 0x37, 0x39, 0x39, 0x36, 0x32, 0x37, 0x34, 0x39, 0x35, 0x36, 0x37, 0x33, 0x35, 0x31, 0x38, 0x38, 0x35, 0x37, 0x35, 0x32, 0x37, 0x32, 0x34, 0x38, 0x39, 0x31, 0x32, 0x32, 0x37, 0x39, 0x33, 0x38, 0x31, 0x38, 0x33, 0x30, 0x31, 0x31, 0x39, 0x34, 0x39, 0x31, 0x32, 0x39, 0x38, 0x33, 0x33, 0x36, 0x37, 0x33, 0x33, 0x36, 0x32, 0x34, 0x34, 0x30, 0x36, 0x35, 0x36, 0x36, 0x34, 0x33, 0x30, 0x38, 0x36, 0x30, 0x32, 0x31, 0x33, 0x39, 0x34, 0x39, 0x34, 0x36, 0x33, 0x39, 0x35, 0x32, 0x32, 0x34, 0x37, 0x33, 0x37, 0x31, 0x39, 0x30, 0x37, 0x30, 0x32, 0x31, 0x37, 0x39, 0x38, 0x40800099, 0x37, 0x30, 0x32, 0x37, 0x37, 0x30, 0x35, 0x33, 0x39, 0x32, 0x31, 0x37, 0x31, 0x37, 0x36, 0x32, 0x39, 0x33, 0x31, 0x37, 0x36, 0x37, 0x35, 0x40800232, 0x37, 0x34, 0x38, 0x31, 0x40400006, 0x36, 0x36, 0x39, 0x34, 0x30, 0x404001e7, 0x30, 0x30, 0x30, 0x35, 0x36, 0x38, 0x31, 0x32, 0x37, 0x31, 0x34, 0x35, 0x32, 0x36, 0x33, 0x35, 0x36, 0x30, 0x38, 0x32, 0x37, 0x37, 0x38, 0x35, 0x37, 0x37, 0x31, 0x33, 0x34, 0x32, 0x37, 0x35, 0x37, 0x37, 0x38, 0x39, 0x36, 0x40400129, 0x33, 0x36, 0x33, 0x37, 0x31, 0x37, 0x38, 0x37, 0x32, 0x31, 0x34, 0x36, 0x38, 0x34, 0x34, 0x30, 0x39, 0x30, 0x31, 0x32, 0x32, 0x34, 0x39, 0x35, 0x33, 0x34, 0x33, 0x30, 0x31, 0x34, 0x36, 0x35, 0x34, 0x39, 0x35, 0x38, 0x35, 0x33, 0x37, 0x31, 0x30, 0x35, 0x30, 0x37, 0x39, 0x404000ca, 0x36, 0x40400153, 0x38, 0x39, 0x32, 0x33, 0x35, 0x34, 0x404001c9, 0x39, 0x35, 0x36, 0x31, 0x31, 0x32, 0x31, 0x32, 0x39, 0x30, 0x32, 0x31, 0x39, 0x36, 0x30, 0x38, 0x36, 0x34, 0x30, 0x33, 0x34, 0x34, 0x31, 0x38, 0x31, 0x35, 0x39, 0x38, 0x31, 0x33, 0x36, 0x32, 0x39, 0x37, 0x37, 0x34, 0x40400074, 0x30, 0x39, 0x39, 0x36, 0x30, 0x35, 0x31, 0x38, 0x37, 0x30, 0x37, 0x32, 0x31, 0x31, 0x33, 0x34, 0x39, 0x40800000, 0x38, 0x33, 0x37, 0x32, 0x39, 0x37, 0x38, 0x30, 0x34, 0x39, 0x39, 0x404002da, 0x39, 0x37, 0x33, 0x31, 0x37, 0x33, 0x32, 0x38, 0x4040018a, 0x36, 0x33, 0x31, 0x38, 0x35, 0x40400301, 0x404002e8, 0x34, 0x35, 0x35, 0x33, 0x34, 0x36, 0x39, 0x30, 0x38, 0x33, 0x30, 0x32, 0x36, 0x34, 0x32, 0x35, 0x32, 0x32, 0x33, 0x30, 0x404002e3, 0x40400267, 0x38, 0x35, 0x30, 0x33, 0x35, 0x32, 0x36, 0x31, 0x39, 0x33, 0x31, 0x31, 0x40400212, 0x31, 0x30, 0x31, 0x30, 0x30, 0x30, 0x33, 0x31, 0x33, 0x37, 0x38, 0x33, 0x38, 0x37, 0x35, 0x32, 0x38, 0x38, 0x36, 0x35, 0x38, 0x37, 0x35, 0x33, 0x33, 0x32, 0x30, 0x38, 0x33, 0x38, 0x31, 0x34, 0x32, 0x30, 0x36, 0x40400140, 0x4040012b, 0x31, 0x34, 0x37, 0x33, 0x30, 0x33, 0x35, 0x39, 0x4080032e, 0x39, 0x30, 0x34, 0x32, 0x38, 0x37, 0x35, 0x35, 0x34, 0x36, 0x38, 0x37, 0x33, 0x31, 0x31, 0x35, 0x39, 0x35, 0x40400355, 0x33, 0x38, 0x38, 0x32, 0x33, 0x35, 0x33, 0x37, 0x38, 0x37, 0x35, 0x4080037f, 0x39, 0x4040013a, 0x31, 0x40400148, 0x38, 0x30, 0x35, 0x33, 0x4040018a, 0x32, 0x32, 0x36, 0x38, 0x30, 0x36, 0x36, 0x31, 0x33, 0x30, 0x30, 0x31, 0x39, 0x32, 0x37, 0x38, 0x37, 0x36, 0x36, 0x31, 0x31, 0x31, 0x39, 0x35, 0x39, 0x40400237, 0x36, 0x40800124, 0x38, 0x39, 0x33, 0x38, 0x30, 0x39, 0x35, 0x32, 0x35, 0x37, 0x32, 0x30, 0x31, 0x30, 0x36, 0x35, 0x34, 0x38, 0x35, 0x38, 0x36, 0x33, 0x32, 0x37, 0x4040009a, 0x39, 0x33, 0x36, 0x31, 0x35, 0x33, 0x40400220, 0x4080015c, 0x32, 0x33, 0x30, 0x33, 0x30, 0x31, 0x39, 0x35, 0x32, 0x30, 0x33, 0x35, 0x33, 0x30, 0x31, 0x38, 0x35, 0x32, 0x40400171, 0x40400075, 0x33, 0x36, 0x32, 0x32, 0x35, 0x39, 0x39, 0x34, 0x31, 0x33, 0x40400254, 0x34, 0x39, 0x37, 0x32, 0x31, 0x37, 0x404000de, 0x33, 0x34, 0x37, 0x39, 0x31, 0x33, 0x31, 0x35, 0x31, 0x35, 0x35, 0x37, 0x34, 0x38, 0x35, 0x37, 0x32, 0x34, 0x32, 0x34, 0x35, 0x34, 0x31, 0x35, 0x30, 0x36, 0x39, 0x4040013f, 0x38, 0x32, 0x39, 0x35, 0x33, 0x33, 0x31, 0x31, 0x36, 0x38, 0x36, 0x31, 0x37, 0x32, 0x37, 0x38, 0x40400337, 0x39, 0x30, 0x37, 0x35, 0x30, 0x39, 0x4040010d, 0x37, 0x35, 0x34, 0x36, 0x33, 0x37, 0x34, 0x36, 0x34, 0x39, 0x33, 0x39, 0x33, 0x31, 0x39, 0x32, 0x35, 0x35, 0x30, 0x36, 0x30, 0x34, 0x30, 0x30, 0x39, 0x4040026b, 0x31, 0x36, 0x37, 0x31, 0x31, 0x33, 0x39, 0x30, 0x30, 0x39, 0x38, 0x40400335, 0x34, 0x30, 0x31, 0x32, 0x38, 0x35, 0x38, 0x33, 0x36, 0x31, 0x36, 0x30, 0x33, 0x35, 0x36, 0x33, 0x37, 0x30, 0x37, 0x36, 0x36, 0x30, 0x31, 0x30, 0x34, 0x40400172, 0x38, 0x31, 0x39, 0x34, 0x32, 0x39, 0x4080041e, 0x404000ef, 0x4040028b, 0x37, 0x38, 0x33, 0x37, 0x34, 0x404004a8, 0x38, 0x32, 0x35, 0x35, 0x33, 0x37, 0x40800209, 0x32, 0x36, 0x38, 0x4040002e, 0x34, 0x30, 0x34, 0x37, 0x404001d1, 0x34, 0x404004b5, 0x4040038d, 0x38, 0x34, 0x404003a8, 0x36, 0x40c0031f, 0x33, 0x33, 0x31, 0x33, 0x36, 0x37, 0x37, 0x30, 0x32, 0x38, 0x39, 0x38, 0x39, 0x31, 0x35, 0x32, 0x40400062, 0x35, 0x32, 0x31, 0x36, 0x32, 0x30, 0x35, 0x36, 0x39, 0x36, 0x40400411, 0x30, 0x35, 0x38, 0x40400477, 0x35, 0x40400498, 0x35, 0x31, 0x31, 0x40400209, 0x38, 0x32, 0x34, 0x33, 0x30, 0x30, 0x33, 0x35, 0x35, 0x38, 0x37, 0x36, 0x34, 0x30, 0x32, 0x34, 0x37, 0x34, 0x39, 0x36, 0x34, 0x37, 0x33, 0x32, 0x36, 0x33, 0x4040043e, 0x39, 0x39, 0x32, 0x4040044b, 0x34, 0x32, 0x36, 0x39, 0x40c002c5, 0x37, 0x404001d6, 0x34, 0x4040053d, 0x4040041d, 0x39, 0x33, 0x34, 0x31, 0x37, 0x404001ad, 0x31, 0x32, 0x4040002a, 0x34, 0x4040019e, 0x31, 0x35, 0x30, 0x33, 0x30, 0x32, 0x38, 0x36, 0x31, 0x38, 0x32, 0x39, 0x37, 0x34, 0x35, 0x35, 0x35, 0x37, 0x30, 0x36, 0x37, 0x34, 0x40400135, 0x35, 0x30, 0x35, 0x34, 0x39, 0x34, 0x35, 0x38, 0x404001c5, 0x39, 0x40400051, 0x35, 0x36, 0x404001ec, 0x37, 0x32, 0x31, 0x30, 0x37, 0x39, 0x40400159, 0x33, 0x30, 0x4040010a, 0x33, 0x32, 0x31, 0x31, 0x36, 0x35, 0x33, 0x34, 0x34, 0x39, 0x38, 0x37, 0x32, 0x30, 0x32, 0x37, 0x4040011b, 0x30, 0x32, 0x33, 0x36, 0x34, 0x4040022e, 0x35, 0x34, 0x39, 0x39, 0x31, 0x31, 0x39, 0x38, 0x40400418, 0x34, 0x4040011b, 0x35, 0x33, 0x35, 0x36, 0x36, 0x33, 0x36, 0x39, 0x40400450, 0x32, 0x36, 0x35, 0x404002e4, 0x37, 0x38, 0x36, 0x32, 0x35, 0x35, 0x31, 0x404003da, 0x31, 0x37, 0x35, 0x37, 0x34, 0x36, 0x37, 0x32, 0x38, 0x39, 0x30, 0x39, 0x37, 0x37, 0x37, 0x37, 0x40800453, 0x30, 0x30, 0x30, 0x404005fd, 0x37, 0x30, 0x404004df, 0x36, 0x404003e9, 0x34, 0x39, 0x31, 0x4040041e, 0x40400297, 0x32, 0x31, 0x34, 0x37, 0x37, 0x32, 0x33, 0x35, 0x30, 0x31, 0x34, 0x31, 0x34, 0x40400643, 0x33, 0x35, 0x36, 0x404004af, 0x31, 0x36, 0x31, 0x33, 0x36, 0x31, 0x31, 0x35, 0x37, 0x33, 0x35, 0x32, 0x35, 0x40400504, 0x33, 0x34, 0x4040005b, 0x31, 0x38, 0x4040047b, 0x38, 0x34, 0x404005e7, 0x33, 0x33, 0x32, 0x33, 0x39, 0x30, 0x37, 0x33, 0x39, 0x34, 0x31, 0x34, 0x33, 0x33, 0x33, 0x34, 0x35, 0x34, 0x37, 0x37, 0x36, 0x32, 0x34, 0x40400242, 0x32, 0x35, 0x31, 0x38, 0x39, 0x38, 0x33, 0x35, 0x36, 0x39, 0x34, 0x38, 0x35, 0x35, 0x36, 0x32, 0x30, 0x39, 0x39, 0x32, 0x31, 0x39, 0x32, 0x32, 0x32, 0x31, 0x38, 0x34, 0x32, 0x37, 0x4040023e, 0x32, 0x404000ba, 0x36, 0x38, 0x38, 0x37, 0x36, 0x37, 0x31, 0x37, 0x39, 0x30, 0x40400055, 0x30, 0x40800106, 0x36, 0x36, 0x404003e7, 0x38, 0x38, 0x36, 0x32, 0x37, 0x32, 0x404006dc, 0x31, 0x37, 0x38, 0x36, 0x30, 0x38, 0x35, 0x37, 0x40400073, 0x33, 0x408002fc, 0x37, 0x39, 0x37, 0x36, 0x36, 0x38, 0x31, 0x404002bd, 0x30, 0x30, 0x39, 0x35, 0x33, 0x38, 0x38, 0x40400638, 0x33, 0x404006a5, 0x30, 0x36, 0x38, 0x30, 0x30, 0x36, 0x34, 0x32, 0x32, 0x35, 0x31, 0x32, 0x35, 0x32, 0x4040057b, 0x37, 0x33, 0x39, 0x32, 0x40400297, 0x40400474, 0x34, 0x408006b3, 0x38, 0x36, 0x32, 0x36, 0x39, 0x34, 0x35, 0x404001e5, 0x34, 0x31, 0x39, 0x36, 0x35, 0x32, 0x38, 0x35, 0x30, 0x40400099, 0x4040039c, 0x31, 0x38, 0x36, 0x33, 0x404001be, 0x34, 0x40800154, 0x32, 0x30, 0x33, 0x39, 0x4040058b, 0x34, 0x35, 0x404002bc, 0x32, 0x33, 0x37, 0x4040042c, 0x36, 0x40400510, 0x35, 0x36, 0x40400638, 0x37, 0x31, 0x39, 0x31, 0x37, 0x32, 0x38, 0x40400171, 0x37, 0x36, 0x34, 0x36, 0x35, 0x37, 0x35, 0x37, 0x33, 0x39, 0x40400101, 0x33, 0x38, 0x39, 0x40400748, 0x38, 0x33, 0x32, 0x36, 0x34, 0x35, 0x39, 0x39, 0x35, 0x38, 0x404006a7, 0x30, 0x34, 0x37, 0x38, 0x404001de, 0x40400328, 0x39, 0x4040002d, 0x36, 0x34, 0x30, 0x37, 0x38, 0x39, 0x35, 0x31, 0x4040008e, 0x36, 0x38, 0x33, 0x4040012f, 0x32, 0x35, 0x39, 0x35, 0x37, 0x30, 0x40400468, 0x38, 0x32, 0x32, 0x404002c8, 0x32, 0x4040061b, 0x34, 0x30, 0x37, 0x37, 0x32, 0x36, 0x37, 0x31, 0x39, 0x34, 0x37, 0x38, 0x40400319, 0x38, 0x32, 0x36, 0x30, 0x31, 0x34, 0x37, 0x36, 0x39, 0x39, 0x30, 0x39, 0x404004e8, 0x30, 0x31, 0x33, 0x36, 0x33, 0x39, 0x34, 0x34, 0x33, 0x4040027f, 0x33, 0x30, 0x40400105, 0x32, 0x30, 0x33, 0x34, 0x39, 0x36, 0x32, 0x35, 0x32, 0x34, 0x35, 0x31, 0x37, 0x404003b5, 0x39, 0x36, 0x35, 0x31, 0x34, 0x33, 0x31, 0x34, 0x32, 0x39, 0x38, 0x30, 0x39, 0x31, 0x39, 0x30, 0x36, 0x35, 0x39, 0x32, 0x40400282, 0x37, 0x32, 0x32, 0x31, 0x36, 0x39, 0x36, 0x34, 0x36, 0x40400419, 0x4040007a, 0x35, 0x4040050e, 0x34, 0x40800565, 0x38, 0x40400559, 0x39, 0x37, 0x4040057b, 0x35, 0x34, 0x4040049d, 0x4040023e, 0x37, 0x4040065a, 0x38, 0x34, 0x36, 0x38, 0x31, 0x33, 0x4040008c, 0x36, 0x38, 0x33, 0x38, 0x36, 0x38, 0x39, 0x34, 0x32, 0x37, 0x37, 0x34, 0x31, 0x35, 0x35, 0x39, 0x39, 0x31, 0x38, 0x35, 0x4040005a, 0x32, 0x34, 0x35, 0x39, 0x35, 0x33, 0x39, 0x35, 0x39, 0x34, 0x33, 0x31, 0x404005b7, 0x37, 0x40400012, 0x36, 0x38, 0x30, 0x38, 0x34, 0x35, 0x404002e7, 0x37, 0x33, 0x4040081e, 0x39, 0x35, 0x38, 0x34, 0x38, 0x36, 0x35, 0x33, 0x38, 0x404006e8, 0x36, 0x32, 0x404000f2, 0x36, 0x30, 0x39, 0x404004b6, 0x36, 0x30, 0x38, 0x30, 0x35, 0x31, 0x32, 0x34, 0x33, 0x38, 0x38, 0x34, 0x4040013a, 0x4040000b, 0x34, 0x31, 0x33, 0x4040030f, 0x37, 0x36, 0x32, 0x37, 0x38, 0x40400341, 0x37, 0x31, 0x35, 0x4040059b, 0x33, 0x35, 0x39, 0x39, 0x37, 0x37, 0x30, 0x30, 0x31, 0x32, 0x39, 0x40400472, 0x38, 0x39, 0x34, 0x34, 0x31, 0x40400277, 0x36, 0x38, 0x35, 0x35, 0x4040005f, 0x34, 0x30, 0x36, 0x33, 0x404008e6, 0x32, 0x30, 0x37, 0x32, 0x32, 0x40400158, 0x40800203, 0x34, 0x38, 0x31, 0x35, 0x38, 0x40400205, 0x404001fe, 0x4040027a, 0x40400298, 0x33, 0x39, 0x34, 0x35, 0x32, 0x32, 0x36, 0x37, 0x40c00496, 0x38, 0x4040058a, 0x32, 0x31, 0x404002ea, 0x32, 0x40400387, 0x35, 0x34, 0x36, 0x36, 0x36, 0x4040051b, 0x32, 0x33, 0x39, 0x38, 0x36, 0x34, 0x35, 0x36, 0x404004c4, 0x31, 0x36, 0x33, 0x35, 0x40800253, 0x40400811, 0x37, 0x404008ad, 0x39, 0x38, 0x4040045e, 0x39, 0x33, 0x36, 0x33, 0x34, 0x4040075b, 0x37, 0x34, 0x33, 0x32, 0x34, 0x4040047b, 0x31, 0x35, 0x30, 0x37, 0x36, 0x404004bb, 0x37, 0x39, 0x34, 0x35, 0x31, 0x30, 0x39, 0x4040003e, 0x30, 0x39, 0x34, 0x30, 0x404006a6, 0x38, 0x38, 0x37, 0x39, 0x37, 0x31, 0x30, 0x38, 0x39, 0x33, 0x404008f0, 0x36, 0x39, 0x31, 0x33, 0x36, 0x38, 0x36, 0x37, 0x32, 0x4040025b, 0x404001fe, 0x35, 0x4040053f, 0x40400468, 0x40400801, 0x31, 0x37, 0x39, 0x32, 0x38, 0x36, 0x38, 0x404008cc, 0x38, 0x37, 0x34, 0x37, 0x4080079e, 0x38, 0x32, 0x34, 0x4040097a, 0x38, 0x4040025b, 0x37, 0x31, 0x34, 0x39, 0x30, 0x39, 0x36, 0x37, 0x35, 0x39, 0x38, 0x404006ef, 0x33, 0x36, 0x35, 0x40400134, 0x38, 0x31, 0x4040005c, 0x40400745, 0x40400936, 0x36, 0x38, 0x32, 0x39, 0x4040057e, 0x38, 0x37, 0x32, 0x32, 0x36, 0x35, 0x38, 0x38, 0x30, 0x40400611, 0x35, 0x40400249, 0x34, 0x32, 0x37, 0x30, 0x34, 0x37, 0x37, 0x35, 0x35, 0x4040081e, 0x33, 0x37, 0x39, 0x36, 0x34, 0x31, 0x34, 0x35, 0x31, 0x35, 0x32, 0x404005fd, 0x32, 0x33, 0x34, 0x33, 0x36, 0x34, 0x35, 0x34, 0x404005de, 0x34, 0x34, 0x34, 0x37, 0x39, 0x35, 0x4040003c, 0x40400523, 0x408008e6, 0x34, 0x31, 0x4040052a, 0x33, 0x40400304, 0x35, 0x32, 0x33, 0x31, 0x40800841, 0x31, 0x36, 0x36, 0x31, 0x404008b2, 0x35, 0x39, 0x36, 0x39, 0x35, 0x33, 0x36, 0x32, 0x33, 0x31, 0x34, 0x404005ff, 0x32, 0x34, 0x38, 0x34, 0x39, 0x33, 0x37, 0x31, 0x38, 0x37, 0x31, 0x31, 0x30, 0x31, 0x34, 0x35, 0x37, 0x36, 0x35, 0x34, 0x40400761, 0x30, 0x32, 0x37, 0x39, 0x39, 0x33, 0x34, 0x34, 0x30, 0x33, 0x37, 0x34, 0x32, 0x30, 0x30, 0x37, 0x4040093f, 0x37, 0x38, 0x35, 0x33, 0x39, 0x30, 0x36, 0x32, 0x31, 0x39, 0x40800299, 0x40400345, 0x38, 0x34, 0x37, 0x408003d2, 0x38, 0x33, 0x33, 0x32, 0x31, 0x34, 0x34, 0x35, 0x37, 0x31, 0x40400284, 0x40400776, 0x34, 0x33, 0x35, 0x30, 0x40400928, 0x40400468, 0x35, 0x33, 0x31, 0x39, 0x31, 0x30, 0x34, 0x38, 0x34, 0x38, 0x31, 0x30, 0x30, 0x35, 0x33, 0x37, 0x30, 0x36, 0x404008bc, 0x4080059d, 0x40800781, 0x31, 0x40400559, 0x37, 0x4040031b, 0x35, 0x404007ec, 0x4040040c, 0x36, 0x33, 0x408007dc, 0x34, 0x40400971, 0x4080034e, 0x408003f5, 0x38, 0x4080052d, 0x40800887, 0x39, 0x40400187, 0x39, 0x31, 0x404008ce, 0x38, 0x31, 0x34, 0x36, 0x37, 0x35, 0x31, 0x4040062b, 0x31, 0x32, 0x33, 0x39, 0x40c001a9, 0x39, 0x30, 0x37, 0x31, 0x38, 0x36, 0x34, 0x39, 0x34, 0x32, 0x33, 0x31, 0x39, 0x36, 0x31, 0x35, 0x36, 0x404001ec, 0x404006bc, 0x39, 0x35, 0x40400926, 0x40400469, 0x4040011b, 0x36, 0x30, 0x33, 0x38, 0x40400a25, 0x4040016f, 0x40400384, 0x36, 0x32, 0x4040045a, 0x35, 0x4040084c, 0x36, 0x33, 0x38, 0x39, 0x33, 0x37, 0x37, 0x38, 0x37, 0x404008c5, 0x404000f8, 0x39, 0x37, 0x39, 0x32, 0x30, 0x37, 0x37, 0x33, 0x404005d7, 0x32, 0x31, 0x38, 0x32, 0x35, 0x36, 0x404007df, 0x36, 0x36, 0x404006d6, 0x34, 0x32, 0x4080067e, 0x36, 0x404006e6, 0x34, 0x34, 0x40400024, 0x35, 0x34, 0x39, 0x32, 0x30, 0x32, 0x36, 0x30, 0x35, 0x40400ab3, 0x408003e4, 0x32, 0x30, 0x31, 0x34, 0x39, 0x404004d2, 0x38, 0x35, 0x30, 0x37, 0x33, 0x40400599, 0x36, 0x36, 0x36, 0x30, 0x40400194, 0x32, 0x34, 0x33, 0x34, 0x30, 0x40400087, 0x30, 0x4040076b, 0x38, 0x36, 0x33, 0x40400956, 0x404007e4, 0x4040042b, 0x40400174, 0x35, 0x37, 0x39, 0x36, 0x32, 0x36, 0x38, 0x35, 0x36, 0x40400140, 0x35, 0x30, 0x38, 0x40400523, 0x35, 0x38, 0x37, 0x39, 0x36, 0x39, 0x39, 0x40400711, 0x35, 0x37, 0x34, 0x40400a18, 0x38, 0x34, 0x30, 0x404008b3, 0x31, 0x34, 0x35, 0x39, 0x31, 0x4040078c, 0x37, 0x30, 0x40400234, 0x30, 0x31, 0x40400be7, 0x31, 0x32, 0x40400c74, 0x30, 0x404003c3, 0x33, 0x39, 0x40400b2a, 0x40400112, 0x37, 0x31, 0x35, 0x404003b0, 0x34, 0x32, 0x30, 0x40800bf2, 0x39, 0x40400bc2, 0x30, 0x37, 0x40400341, 0x40400795, 0x40400aaf, 0x40400c62, 0x32, 0x31, 0x40400960, 0x32, 0x35, 0x31, 0x4040057b, 0x40400944, 0x39, 0x32, 0x404001b2, 0x38, 0x32, 0x36, 0x40400b66, 0x32, 0x40400278, 0x33, 0x32, 0x31, 0x35, 0x37, 0x39, 0x31, 0x39, 0x38, 0x34, 0x31, 0x34, 0x4080087b, 0x39, 0x31, 0x36, 0x34, 0x408006e8, 0x39, 0x40800b58, 0x404008db, 0x37, 0x32, 0x32, 0x40400321, 0x35, 0x404008a4, 0x40400141, 0x39, 0x31, 0x30, 0x404000bc, 0x40400c5b, 0x35, 0x32, 0x38, 0x30, 0x31, 0x37, 0x40400231, 0x37, 0x31, 0x32, 0x40400914, 0x38, 0x33, 0x32, 0x40400373, 0x31, 0x40400589, 0x30, 0x39, 0x33, 0x35, 0x33, 0x39, 0x36, 0x35, 0x37, 0x4040064b, 0x31, 0x30, 0x38, 0x33, 0x40400069, 0x35, 0x31, 0x4040077a, 0x40400d5a, 0x31, 0x34, 0x34, 0x34, 0x32, 0x31, 0x30, 0x30, 0x40400202, 0x30, 0x33, 0x4040019c, 0x31, 0x31, 0x30, 0x33, 0x40400c81, 0x40400009, 0x40400026, 0x40c00602, 0x35, 0x31, 0x36, 0x404005d9, 0x40800883, 0x4040092a, 0x35, 0x40800c42, 0x38, 0x35, 0x31, 0x37, 0x31, 0x34, 0x33, 0x37, 0x40400605, 0x4040006d, 0x31, 0x35, 0x35, 0x36, 0x35, 0x30, 0x38, 0x38, 0x404003b9, 0x39, 0x38, 0x39, 0x38, 0x35, 0x39, 0x39, 0x38, 0x32, 0x33, 0x38, 0x404001cf, 0x404009ba, 0x33, 0x4040016c, 0x4040043e, 0x404009c3, 0x38, 0x40800e05, 0x33, 0x32, 0x40400107, 0x35, 0x40400305, 0x33, 0x404001ca, 0x39, 0x4040041b, 0x39, 0x38, 0x4040087d, 0x34, 0x40400cb8, 0x37, 0x4040064b, 0x30, 0x37, 0x404000e5, 0x34, 0x38, 0x31, 0x34, 0x31, 0x40400539, 0x38, 0x35, 0x39, 0x34, 0x36, 0x31, 0x40400bc9, 0x38, 0x30}, - }, - { - input: "testdata/huffman-rand-1k.in", - want: "testdata/huffman-rand-1k.%s.expect", - wantNoInput: "testdata/huffman-rand-1k.%s.expect-noinput", - tokens: []token{0xf8, 0x8b, 0x96, 0x76, 0x48, 0xd, 0x85, 0x94, 0x25, 0x80, 0xaf, 0xc2, 0xfe, 0x8d, 0xe8, 0x20, 0xeb, 0x17, 0x86, 0xc9, 0xb7, 0xc5, 0xde, 0x6, 0xea, 0x7d, 0x18, 0x8b, 0xe7, 0x3e, 0x7, 0xda, 0xdf, 0xff, 0x6c, 0x73, 0xde, 0xcc, 0xe7, 0x6d, 0x8d, 0x4, 0x19, 0x49, 0x7f, 0x47, 0x1f, 0x48, 0x15, 0xb0, 0xe8, 0x9e, 0xf2, 0x31, 0x59, 0xde, 0x34, 0xb4, 0x5b, 0xe5, 0xe0, 0x9, 0x11, 0x30, 0xc2, 0x88, 0x5b, 0x7c, 0x5d, 0x14, 0x13, 0x6f, 0x23, 0xa9, 0xd, 0xbc, 0x2d, 0x23, 0xbe, 0xd9, 0xed, 0x75, 0x4, 0x6c, 0x99, 0xdf, 0xfd, 0x70, 0x66, 0xe6, 0xee, 0xd9, 0xb1, 0x9e, 0x6e, 0x83, 0x59, 0xd5, 0xd4, 0x80, 0x59, 0x98, 0x77, 0x89, 0x43, 0x38, 0xc9, 0xaf, 0x30, 0x32, 0x9a, 0x20, 0x1b, 0x46, 0x3d, 0x67, 0x6e, 0xd7, 0x72, 0x9e, 0x4e, 0x21, 0x4f, 0xc6, 0xe0, 0xd4, 0x7b, 0x4, 0x8d, 0xa5, 0x3, 0xf6, 0x5, 0x9b, 0x6b, 0xdc, 0x2a, 0x93, 0x77, 0x28, 0xfd, 0xb4, 0x62, 0xda, 0x20, 0xe7, 0x1f, 0xab, 0x6b, 0x51, 0x43, 0x39, 0x2f, 0xa0, 0x92, 0x1, 0x6c, 0x75, 0x3e, 0xf4, 0x35, 0xfd, 0x43, 0x2e, 0xf7, 0xa4, 0x75, 0xda, 0xea, 0x9b, 0xa, 0x64, 0xb, 0xe0, 0x23, 0x29, 0xbd, 0xf7, 0xe7, 0x83, 0x3c, 0xfb, 0xdf, 0xb3, 0xae, 0x4f, 0xa4, 0x47, 0x55, 0x99, 0xde, 0x2f, 0x96, 0x6e, 0x1c, 0x43, 0x4c, 0x87, 0xe2, 0x7c, 0xd9, 0x5f, 0x4c, 0x7c, 0xe8, 0x90, 0x3, 0xdb, 0x30, 0x95, 0xd6, 0x22, 0xc, 0x47, 0xb8, 0x4d, 0x6b, 0xbd, 0x24, 0x11, 0xab, 0x2c, 0xd7, 0xbe, 0x6e, 0x7a, 0xd6, 0x8, 0xa3, 0x98, 0xd8, 0xdd, 0x15, 0x6a, 0xfa, 0x93, 0x30, 0x1, 0x25, 0x1d, 0xa2, 0x74, 0x86, 0x4b, 0x6a, 0x95, 0xe8, 0xe1, 0x4e, 0xe, 0x76, 0xb9, 0x49, 0xa9, 0x5f, 0xa0, 0xa6, 0x63, 0x3c, 0x7e, 0x7e, 0x20, 0x13, 0x4f, 0xbb, 0x66, 0x92, 0xb8, 0x2e, 0xa4, 0xfa, 0x48, 0xcb, 0xae, 0xb9, 0x3c, 0xaf, 0xd3, 0x1f, 0xe1, 0xd5, 0x8d, 0x42, 0x6d, 0xf0, 0xfc, 0x8c, 0xc, 0x0, 0xde, 0x40, 0xab, 0x8b, 0x47, 0x97, 0x4e, 0xa8, 0xcf, 0x8e, 0xdb, 0xa6, 0x8b, 0x20, 0x9, 0x84, 0x7a, 0x66, 0xe5, 0x98, 0x29, 0x2, 0x95, 0xe6, 0x38, 0x32, 0x60, 0x3, 0xe3, 0x9a, 0x1e, 0x54, 0xe8, 0x63, 0x80, 0x48, 0x9c, 0xe7, 0x63, 0x33, 0x6e, 0xa0, 0x65, 0x83, 0xfa, 0xc6, 0xba, 0x7a, 0x43, 0x71, 0x5, 0xf5, 0x68, 0x69, 0x85, 0x9c, 0xba, 0x45, 0xcd, 0x6b, 0xb, 0x19, 0xd1, 0xbb, 0x7f, 0x70, 0x85, 0x92, 0xd1, 0xb4, 0x64, 0x82, 0xb1, 0xe4, 0x62, 0xc5, 0x3c, 0x46, 0x1f, 0x92, 0x31, 0x1c, 0x4e, 0x41, 0x77, 0xf7, 0xe7, 0x87, 0xa2, 0xf, 0x6e, 0xe8, 0x92, 0x3, 0x6b, 0xa, 0xe7, 0xa9, 0x3b, 0x11, 0xda, 0x66, 0x8a, 0x29, 0xda, 0x79, 0xe1, 0x64, 0x8d, 0xe3, 0x54, 0xd4, 0xf5, 0xef, 0x64, 0x87, 0x3b, 0xf4, 0xc2, 0xf4, 0x71, 0x13, 0xa9, 0xe9, 0xe0, 0xa2, 0x6, 0x14, 0xab, 0x5d, 0xa7, 0x96, 0x0, 0xd6, 0xc3, 0xcc, 0x57, 0xed, 0x39, 0x6a, 0x25, 0xcd, 0x76, 0xea, 0xba, 0x3a, 0xf2, 0xa1, 0x95, 0x5d, 0xe5, 0x71, 0xcf, 0x9c, 0x62, 0x9e, 0x6a, 0xfa, 0xd5, 0x31, 0xd1, 0xa8, 0x66, 0x30, 0x33, 0xaa, 0x51, 0x17, 0x13, 0x82, 0x99, 0xc8, 0x14, 0x60, 0x9f, 0x4d, 0x32, 0x6d, 0xda, 0x19, 0x26, 0x21, 0xdc, 0x7e, 0x2e, 0x25, 0x67, 0x72, 0xca, 0xf, 0x92, 0xcd, 0xf6, 0xd6, 0xcb, 0x97, 0x8a, 0x33, 0x58, 0x73, 0x70, 0x91, 0x1d, 0xbf, 0x28, 0x23, 0xa3, 0xc, 0xf1, 0x83, 0xc3, 0xc8, 0x56, 0x77, 0x68, 0xe3, 0x82, 0xba, 0xb9, 0x57, 0x56, 0x57, 0x9c, 0xc3, 0xd6, 0x14, 0x5, 0x3c, 0xb1, 0xaf, 0x93, 0xc8, 0x8a, 0x57, 0x7f, 0x53, 0xfa, 0x2f, 0xaa, 0x6e, 0x66, 0x83, 0xfa, 0x33, 0xd1, 0x21, 0xab, 0x1b, 0x71, 0xb4, 0x7c, 0xda, 0xfd, 0xfb, 0x7f, 0x20, 0xab, 0x5e, 0xd5, 0xca, 0xfd, 0xdd, 0xe0, 0xee, 0xda, 0xba, 0xa8, 0x27, 0x99, 0x97, 0x69, 0xc1, 0x3c, 0x82, 0x8c, 0xa, 0x5c, 0x2d, 0x5b, 0x88, 0x3e, 0x34, 0x35, 0x86, 0x37, 0x46, 0x79, 0xe1, 0xaa, 0x19, 0xfb, 0xaa, 0xde, 0x15, 0x9, 0xd, 0x1a, 0x57, 0xff, 0xb5, 0xf, 0xf3, 0x2b, 0x5a, 0x6a, 0x4d, 0x19, 0x77, 0x71, 0x45, 0xdf, 0x4f, 0xb3, 0xec, 0xf1, 0xeb, 0x18, 0x53, 0x3e, 0x3b, 0x47, 0x8, 0x9a, 0x73, 0xa0, 0x5c, 0x8c, 0x5f, 0xeb, 0xf, 0x3a, 0xc2, 0x43, 0x67, 0xb4, 0x66, 0x67, 0x80, 0x58, 0xe, 0xc1, 0xec, 0x40, 0xd4, 0x22, 0x94, 0xca, 0xf9, 0xe8, 0x92, 0xe4, 0x69, 0x38, 0xbe, 0x67, 0x64, 0xca, 0x50, 0xc7, 0x6, 0x67, 0x42, 0x6e, 0xa3, 0xf0, 0xb7, 0x6c, 0xf2, 0xe8, 0x5f, 0xb1, 0xaf, 0xe7, 0xdb, 0xbb, 0x77, 0xb5, 0xf8, 0xcb, 0x8, 0xc4, 0x75, 0x7e, 0xc0, 0xf9, 0x1c, 0x7f, 0x3c, 0x89, 0x2f, 0xd2, 0x58, 0x3a, 0xe2, 0xf8, 0x91, 0xb6, 0x7b, 0x24, 0x27, 0xe9, 0xae, 0x84, 0x8b, 0xde, 0x74, 0xac, 0xfd, 0xd9, 0xb7, 0x69, 0x2a, 0xec, 0x32, 0x6f, 0xf0, 0x92, 0x84, 0xf1, 0x40, 0xc, 0x8a, 0xbc, 0x39, 0x6e, 0x2e, 0x73, 0xd4, 0x6e, 0x8a, 0x74, 0x2a, 0xdc, 0x60, 0x1f, 0xa3, 0x7, 0xde, 0x75, 0x8b, 0x74, 0xc8, 0xfe, 0x63, 0x75, 0xf6, 0x3d, 0x63, 0xac, 0x33, 0x89, 0xc3, 0xf0, 0xf8, 0x2d, 0x6b, 0xb4, 0x9e, 0x74, 0x8b, 0x5c, 0x33, 0xb4, 0xca, 0xa8, 0xe4, 0x99, 0xb6, 0x90, 0xa1, 0xef, 0xf, 0xd3, 0x61, 0xb2, 0xc6, 0x1a, 0x94, 0x7c, 0x44, 0x55, 0xf4, 0x45, 0xff, 0x9e, 0xa5, 0x5a, 0xc6, 0xa0, 0xe8, 0x2a, 0xc1, 0x8d, 0x6f, 0x34, 0x11, 0xb9, 0xbe, 0x4e, 0xd9, 0x87, 0x97, 0x73, 0xcf, 0x3d, 0x23, 0xae, 0xd5, 0x1a, 0x5e, 0xae, 0x5d, 0x6a, 0x3, 0xf9, 0x22, 0xd, 0x10, 0xd9, 0x47, 0x69, 0x15, 0x3f, 0xee, 0x52, 0xa3, 0x8, 0xd2, 0x3c, 0x51, 0xf4, 0xf8, 0x9d, 0xe4, 0x98, 0x89, 0xc8, 0x67, 0x39, 0xd5, 0x5e, 0x35, 0x78, 0x27, 0xe8, 0x3c, 0x80, 0xae, 0x79, 0x71, 0xd2, 0x93, 0xf4, 0xaa, 0x51, 0x12, 0x1c, 0x4b, 0x1b, 0xe5, 0x6e, 0x15, 0x6f, 0xe4, 0xbb, 0x51, 0x9b, 0x45, 0x9f, 0xf9, 0xc4, 0x8c, 0x2a, 0xfb, 0x1a, 0xdf, 0x55, 0xd3, 0x48, 0x93, 0x27, 0x1, 0x26, 0xc2, 0x6b, 0x55, 0x6d, 0xa2, 0xfb, 0x84, 0x8b, 0xc9, 0x9e, 0x28, 0xc2, 0xef, 0x1a, 0x24, 0xec, 0x9b, 0xae, 0xbd, 0x60, 0xe9, 0x15, 0x35, 0xee, 0x42, 0xa4, 0x33, 0x5b, 0xfa, 0xf, 0xb6, 0xf7, 0x1, 0xa6, 0x2, 0x4c, 0xca, 0x90, 0x58, 0x3a, 0x96, 0x41, 0xe7, 0xcb, 0x9, 0x8c, 0xdb, 0x85, 0x4d, 0xa8, 0x89, 0xf3, 0xb5, 0x8e, 0xfd, 0x75, 0x5b, 0x4f, 0xed, 0xde, 0x3f, 0xeb, 0x38, 0xa3, 0xbe, 0xb0, 0x73, 0xfc, 0xb8, 0x54, 0xf7, 0x4c, 0x30, 0x67, 0x2e, 0x38, 0xa2, 0x54, 0x18, 0xba, 0x8, 0xbf, 0xf2, 0x39, 0xd5, 0xfe, 0xa5, 0x41, 0xc6, 0x66, 0x66, 0xba, 0x81, 0xef, 0x67, 0xe4, 0xe6, 0x3c, 0xc, 0xca, 0xa4, 0xa, 0x79, 0xb3, 0x57, 0x8b, 0x8a, 0x75, 0x98, 0x18, 0x42, 0x2f, 0x29, 0xa3, 0x82, 0xef, 0x9f, 0x86, 0x6, 0x23, 0xe1, 0x75, 0xfa, 0x8, 0xb1, 0xde, 0x17, 0x4a}, - }, - { - input: "testdata/huffman-rand-limit.in", - want: "testdata/huffman-rand-limit.%s.expect", - wantNoInput: "testdata/huffman-rand-limit.%s.expect-noinput", - tokens: []token{0x61, 0x51c00000, 0xa, 0xf8, 0x8b, 0x96, 0x76, 0x48, 0xa, 0x85, 0x94, 0x25, 0x80, 0xaf, 0xc2, 0xfe, 0x8d, 0xe8, 0x20, 0xeb, 0x17, 0x86, 0xc9, 0xb7, 0xc5, 0xde, 0x6, 0xea, 0x7d, 0x18, 0x8b, 0xe7, 0x3e, 0x7, 0xda, 0xdf, 0xff, 0x6c, 0x73, 0xde, 0xcc, 0xe7, 0x6d, 0x8d, 0x4, 0x19, 0x49, 0x7f, 0x47, 0x1f, 0x48, 0x15, 0xb0, 0xe8, 0x9e, 0xf2, 0x31, 0x59, 0xde, 0x34, 0xb4, 0x5b, 0xe5, 0xe0, 0x9, 0x11, 0x30, 0xc2, 0x88, 0x5b, 0x7c, 0x5d, 0x14, 0x13, 0x6f, 0x23, 0xa9, 0xa, 0xbc, 0x2d, 0x23, 0xbe, 0xd9, 0xed, 0x75, 0x4, 0x6c, 0x99, 0xdf, 0xfd, 0x70, 0x66, 0xe6, 0xee, 0xd9, 0xb1, 0x9e, 0x6e, 0x83, 0x59, 0xd5, 0xd4, 0x80, 0x59, 0x98, 0x77, 0x89, 0x43, 0x38, 0xc9, 0xaf, 0x30, 0x32, 0x9a, 0x20, 0x1b, 0x46, 0x3d, 0x67, 0x6e, 0xd7, 0x72, 0x9e, 0x4e, 0x21, 0x4f, 0xc6, 0xe0, 0xd4, 0x7b, 0x4, 0x8d, 0xa5, 0x3, 0xf6, 0x5, 0x9b, 0x6b, 0xdc, 0x2a, 0x93, 0x77, 0x28, 0xfd, 0xb4, 0x62, 0xda, 0x20, 0xe7, 0x1f, 0xab, 0x6b, 0x51, 0x43, 0x39, 0x2f, 0xa0, 0x92, 0x1, 0x6c, 0x75, 0x3e, 0xf4, 0x35, 0xfd, 0x43, 0x2e, 0xf7, 0xa4, 0x75, 0xda, 0xea, 0x9b, 0xa}, - }, - { - input: "testdata/huffman-shifts.in", - want: "testdata/huffman-shifts.%s.expect", - wantNoInput: "testdata/huffman-shifts.%s.expect-noinput", - tokens: []token{0x31, 0x30, 0x7fc00001, 0x7fc00001, 0x7fc00001, 0x7fc00001, 0x7fc00001, 0x7fc00001, 0x7fc00001, 0x7fc00001, 0x7fc00001, 0x7fc00001, 0x7fc00001, 0x7fc00001, 0x7fc00001, 0x7fc00001, 0x7fc00001, 0x52400001, 0xd, 0xa, 0x32, 0x33, 0x7fc00001, 0x7fc00001, 0x7fc00001, 0x7fc00001, 0x7fc00001, 0x7fc00001, 0x7fc00001, 0x7fc00001, 0x7fc00001, 0x7f400001}, - }, - { - input: "testdata/huffman-text-shift.in", - want: "testdata/huffman-text-shift.%s.expect", - wantNoInput: "testdata/huffman-text-shift.%s.expect-noinput", - tokens: []token{0x2f, 0x2f, 0x43, 0x6f, 0x70, 0x79, 0x72, 0x69, 0x67, 0x68, 0x74, 0x32, 0x30, 0x30, 0x39, 0x54, 0x68, 0x47, 0x6f, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x2e, 0x41, 0x6c, 0x6c, 0x40800016, 0x72, 0x72, 0x76, 0x64, 0x2e, 0xd, 0xa, 0x2f, 0x2f, 0x55, 0x6f, 0x66, 0x74, 0x68, 0x69, 0x6f, 0x75, 0x72, 0x63, 0x63, 0x6f, 0x64, 0x69, 0x67, 0x6f, 0x76, 0x72, 0x6e, 0x64, 0x62, 0x79, 0x42, 0x53, 0x44, 0x2d, 0x74, 0x79, 0x6c, 0x40400020, 0x6c, 0x69, 0x63, 0x6e, 0x74, 0x68, 0x74, 0x63, 0x6e, 0x62, 0x66, 0x6f, 0x75, 0x6e, 0x64, 0x69, 0x6e, 0x74, 0x68, 0x4c, 0x49, 0x43, 0x45, 0x4e, 0x53, 0x45, 0x66, 0x69, 0x6c, 0x2e, 0xd, 0xa, 0xd, 0xa, 0x70, 0x63, 0x6b, 0x67, 0x6d, 0x69, 0x6e, 0x4040000a, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x22, 0x6f, 0x22, 0x4040000c, 0x66, 0x75, 0x6e, 0x63, 0x6d, 0x69, 0x6e, 0x28, 0x29, 0x7b, 0xd, 0xa, 0x9, 0x76, 0x72, 0x62, 0x3d, 0x6d, 0x6b, 0x28, 0x5b, 0x5d, 0x62, 0x79, 0x74, 0x2c, 0x36, 0x35, 0x35, 0x33, 0x35, 0x29, 0xd, 0xa, 0x9, 0x66, 0x2c, 0x5f, 0x3a, 0x3d, 0x6f, 0x2e, 0x43, 0x72, 0x74, 0x28, 0x22, 0x68, 0x75, 0x66, 0x66, 0x6d, 0x6e, 0x2d, 0x6e, 0x75, 0x6c, 0x6c, 0x2d, 0x6d, 0x78, 0x2e, 0x69, 0x6e, 0x22, 0x40800021, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x28, 0x62, 0x29, 0xd, 0xa, 0x7d, 0xd, 0xa, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x58, 0x78, 0x79, 0x7a, 0x21, 0x22, 0x23, 0xc2, 0xa4, 0x25, 0x26, 0x2f, 0x3f, 0x22}, - }, - { - input: "testdata/huffman-text.in", - want: "testdata/huffman-text.%s.expect", - wantNoInput: "testdata/huffman-text.%s.expect-noinput", - tokens: []token{0x2f, 0x2f, 0x20, 0x43, 0x6f, 0x70, 0x79, 0x72, 0x69, 0x67, 0x68, 0x74, 0x20, 0x32, 0x30, 0x30, 0x39, 0x20, 0x54, 0x68, 0x65, 0x20, 0x47, 0x6f, 0x20, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x73, 0x2e, 0x20, 0x41, 0x6c, 0x6c, 0x20, 0x4080001e, 0x73, 0x20, 0x72, 0x65, 0x73, 0x65, 0x72, 0x76, 0x65, 0x64, 0x2e, 0xd, 0xa, 0x2f, 0x2f, 0x20, 0x55, 0x73, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x20, 0x63, 0x6f, 0x64, 0x65, 0x20, 0x69, 0x73, 0x20, 0x67, 0x6f, 0x76, 0x65, 0x72, 0x6e, 0x65, 0x64, 0x20, 0x62, 0x79, 0x20, 0x61, 0x20, 0x42, 0x53, 0x44, 0x2d, 0x73, 0x74, 0x79, 0x6c, 0x65, 0x40800036, 0x6c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x20, 0x74, 0x68, 0x61, 0x74, 0x20, 0x63, 0x61, 0x6e, 0x20, 0x62, 0x65, 0x20, 0x66, 0x6f, 0x75, 0x6e, 0x64, 0x20, 0x69, 0x6e, 0x20, 0x74, 0x68, 0x65, 0x20, 0x4c, 0x49, 0x43, 0x45, 0x4e, 0x53, 0x45, 0x20, 0x66, 0x69, 0x6c, 0x65, 0x2e, 0xd, 0xa, 0xd, 0xa, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x20, 0x6d, 0x61, 0x69, 0x6e, 0x4040000f, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x20, 0x22, 0x6f, 0x73, 0x22, 0x4040000e, 0x66, 0x75, 0x6e, 0x63, 0x4080001b, 0x28, 0x29, 0x20, 0x7b, 0xd, 0xa, 0x9, 0x76, 0x61, 0x72, 0x20, 0x62, 0x20, 0x3d, 0x20, 0x6d, 0x61, 0x6b, 0x65, 0x28, 0x5b, 0x5d, 0x62, 0x79, 0x74, 0x65, 0x2c, 0x20, 0x36, 0x35, 0x35, 0x33, 0x35, 0x29, 0xd, 0xa, 0x9, 0x66, 0x2c, 0x20, 0x5f, 0x20, 0x3a, 0x3d, 0x20, 0x6f, 0x73, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x28, 0x22, 0x68, 0x75, 0x66, 0x66, 0x6d, 0x61, 0x6e, 0x2d, 0x6e, 0x75, 0x6c, 0x6c, 0x2d, 0x6d, 0x61, 0x78, 0x2e, 0x69, 0x6e, 0x22, 0x4080002a, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x28, 0x62, 0x29, 0xd, 0xa, 0x7d, 0xd, 0xa}, - }, - { - input: "testdata/huffman-zero.in", - want: "testdata/huffman-zero.%s.expect", - wantNoInput: "testdata/huffman-zero.%s.expect-noinput", - tokens: []token{0x30, ml, 0x4b800000}, - }, - { - input: "", - want: "", - wantNoInput: "testdata/null-long-match.%s.expect-noinput", - tokens: []token{0x0, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, ml, 0x41400000}, - }, -} - -// TestWriteBlock tests if the writeBlock encoding has changed. -// To update the reference files use the "-update" flag on the test. -func TestWriteBlock(t *testing.T) { - for _, test := range writeBlockTests { - testBlock(t, test, "wb") - } -} - -// TestWriteBlockDynamic tests if the writeBlockDynamic encoding has changed. -// To update the reference files use the "-update" flag on the test. -func TestWriteBlockDynamic(t *testing.T) { - for _, test := range writeBlockTests { - testBlock(t, test, "dyn") - } -} - -// TestWriteBlockDynamic tests if the writeBlockDynamic encoding has changed. -// To update the reference files use the "-update" flag on the test. -func TestWriteBlockDynamicSync(t *testing.T) { - for _, test := range writeBlockTests { - testBlock(t, test, "sync") - } -} - -// testBlock tests a block against its references, -// or regenerate the references, if "-update" flag is set. -func testBlock(t *testing.T, test huffTest, ttype string) { - if test.want != "" { - test.want = fmt.Sprintf(test.want, ttype) - } - const gotSuffix = ".got" - test.wantNoInput = fmt.Sprintf(test.wantNoInput, ttype) - tokens := indexTokens(test.tokens) - if *update { - if test.input != "" { - t.Logf("Updating %q", test.want) - input, err := os.ReadFile(test.input) - if err != nil { - t.Error(err) - return - } - - f, err := os.Create(test.want) - if err != nil { - t.Error(err) - return - } - defer f.Close() - bw := newHuffmanBitWriter(f) - writeToType(t, ttype, bw, tokens, input) - } - - t.Logf("Updating %q", test.wantNoInput) - f, err := os.Create(test.wantNoInput) - if err != nil { - t.Error(err) - return - } - defer f.Close() - bw := newHuffmanBitWriter(f) - writeToType(t, ttype, bw, tokens, nil) - return - } - - if test.input != "" { - t.Logf("Testing %q", test.want) - input, err := os.ReadFile(test.input) - if err != nil { - t.Error(err) - return - } - want, err := os.ReadFile(test.want) - if err != nil { - t.Error(err) - return - } - var buf bytes.Buffer - bw := newHuffmanBitWriter(&buf) - writeToType(t, ttype, bw, tokens, input) - - got := buf.Bytes() - if !bytes.Equal(got, want) { - t.Errorf("writeBlock did not yield expected result for file %q with input. See %q", test.want, test.want+gotSuffix) - if err := os.WriteFile(test.want+gotSuffix, got, 0o666); err != nil { - t.Error(err) - } - } - t.Log("Output ok") - - // Test if the writer produces the same output after reset. - buf.Reset() - bw.reset(&buf) - writeToType(t, ttype, bw, tokens, input) - bw.flush() - got = buf.Bytes() - if !bytes.Equal(got, want) { - t.Errorf("reset: writeBlock did not yield expected result for file %q with input. See %q", test.want, test.want+".reset"+gotSuffix) - if err := os.WriteFile(test.want+".reset"+gotSuffix, got, 0o666); err != nil { - t.Error(err) - } - return - } - t.Log("Reset ok") - testWriterEOF(t, "wb", test, true) - } - t.Logf("Testing %q", test.wantNoInput) - wantNI, err := os.ReadFile(test.wantNoInput) - if err != nil { - t.Error(err) - return - } - var buf bytes.Buffer - bw := newHuffmanBitWriter(&buf) - writeToType(t, ttype, bw, tokens, nil) - - got := buf.Bytes() - if !bytes.Equal(got, wantNI) { - t.Errorf("writeBlock did not yield expected result for file %q with input. See %q", test.wantNoInput, test.wantNoInput+gotSuffix) - if err := os.WriteFile(test.wantNoInput+gotSuffix, got, 0o666); err != nil { - t.Error(err) - } - } else if got[0]&1 == 1 { - t.Error("got unexpected EOF") - return - } - - t.Log("Output ok") - - // Test if the writer produces the same output after reset. - buf.Reset() - bw.reset(&buf) - writeToType(t, ttype, bw, tokens, nil) - bw.flush() - got = buf.Bytes() - if !bytes.Equal(got, wantNI) { - t.Errorf("reset: writeBlock did not yield expected result for file %q without input. See %q", test.wantNoInput, test.wantNoInput+".reset"+gotSuffix) - if err := os.WriteFile(test.wantNoInput+".reset"+gotSuffix, got, 0o666); err != nil { - t.Error(err) - } - return - } - t.Log("Reset ok") - testWriterEOF(t, "wb", test, false) -} - -func writeToType(t *testing.T, ttype string, bw *huffmanBitWriter, tok tokens, input []byte) { - switch ttype { - case "wb": - bw.writeBlock(&tok, false, input) - case "dyn": - bw.writeBlockDynamic(&tok, false, input, false) - case "sync": - bw.writeBlockDynamic(&tok, false, input, true) - default: - panic("unknown test type") - } - - if bw.err != nil { - t.Error(bw.err) - return - } - - bw.flush() - if bw.err != nil { - t.Error(bw.err) - return - } -} - -// testWriterEOF tests if the written block contains an EOF marker. -func testWriterEOF(t *testing.T, ttype string, test huffTest, useInput bool) { - if useInput && test.input == "" { - return - } - var input []byte - if useInput { - var err error - input, err = os.ReadFile(test.input) - if err != nil { - t.Error(err) - return - } - } - var buf bytes.Buffer - bw := newHuffmanBitWriter(&buf) - tokens := indexTokens(test.tokens) - switch ttype { - case "wb": - bw.writeBlock(&tokens, true, input) - case "dyn": - bw.writeBlockDynamic(&tokens, true, input, true) - case "huff": - bw.writeBlockHuff(true, input, true) - default: - panic("unknown test type") - } - if bw.err != nil { - t.Error(bw.err) - return - } - - bw.flush() - if bw.err != nil { - t.Error(bw.err) - return - } - b := buf.Bytes() - if len(b) == 0 { - t.Error("no output received") - return - } - if b[0]&1 != 1 { - t.Errorf("block not marked with EOF for input %q", test.input) - return - } - t.Log("EOF ok") -} diff --git a/internal/compress/flate/huffman_code.go b/internal/compress/flate/huffman_code.go deleted file mode 100644 index 42da87e8..00000000 --- a/internal/compress/flate/huffman_code.go +++ /dev/null @@ -1,419 +0,0 @@ -// Copyright 2009 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package flate - -import ( - "math" - "math/bits" -) - -const ( - maxBitsLimit = 16 - // number of valid literals - literalCount = 286 -) - -// hcode is a huffman code with a bit code and bit length. -type hcode uint32 - -func (h hcode) len() uint8 { - return uint8(h) -} - -func (h hcode) code64() uint64 { - return uint64(h >> 8) -} - -func (h hcode) zero() bool { - return h == 0 -} - -type huffmanEncoder struct { - codes []hcode - bitCount [17]int32 - - // Allocate a reusable buffer with the longest possible frequency table. - // Possible lengths are codegenCodeCount, offsetCodeCount and literalCount. - // The largest of these is literalCount, so we allocate for that case. - freqcache [literalCount + 1]literalNode -} - -type literalNode struct { - literal uint16 - freq uint16 -} - -// A levelInfo describes the state of the constructed tree for a given depth. -type levelInfo struct { - // Our level. for better printing - level int32 - - // The frequency of the last node at this level - lastFreq int32 - - // The frequency of the next character to add to this level - nextCharFreq int32 - - // The frequency of the next pair (from level below) to add to this level. - // Only valid if the "needed" value of the next lower level is 0. - nextPairFreq int32 - - // The number of chains remaining to generate for this level before moving - // up to the next level - needed int32 -} - -// set sets the code and length of an hcode. -func (h *hcode) set(code uint16, length uint8) { - *h = hcode(length) | (hcode(code) << 8) -} - -func newhcode(code uint16, length uint8) hcode { - return hcode(length) | (hcode(code) << 8) -} - -func reverseBits(number uint16, bitLength byte) uint16 { - return bits.Reverse16(number << ((16 - bitLength) & 15)) -} - -func maxNode() literalNode { return literalNode{math.MaxUint16, math.MaxUint16} } - -func newHuffmanEncoder(size int) *huffmanEncoder { - // Make capacity to next power of two. - c := uint(bits.Len32(uint32(size - 1))) - return &huffmanEncoder{codes: make([]hcode, size, 1<= 3 -// The cases of 0, 1, and 2 literals are handled by special case code. -// -// list An array of the literals with non-zero frequencies -// -// and their associated frequencies. The array is in order of increasing -// frequency, and has as its last element a special element with frequency -// MaxInt32 -// -// maxBits The maximum number of bits that should be used to encode any literal. -// -// Must be less than 16. -// -// return An integer array in which array[i] indicates the number of literals -// -// that should be encoded in i bits. -func (h *huffmanEncoder) bitCounts(list []literalNode, maxBits int32) []int32 { - if maxBits >= maxBitsLimit { - panic("flate: maxBits too large") - } - n := int32(len(list)) - list = list[0 : n+1] - list[n] = maxNode() - - // The tree can't have greater depth than n - 1, no matter what. This - // saves a little bit of work in some small cases - if maxBits > n-1 { - maxBits = n - 1 - } - - // Create information about each of the levels. - // A bogus "Level 0" whose sole purpose is so that - // level1.prev.needed==0. This makes level1.nextPairFreq - // be a legitimate value that never gets chosen. - var levels [maxBitsLimit]levelInfo - // leafCounts[i] counts the number of literals at the left - // of ancestors of the rightmost node at level i. - // leafCounts[i][j] is the number of literals at the left - // of the level j ancestor. - var leafCounts [maxBitsLimit][maxBitsLimit]int32 - - // Descending to only have 1 bounds check. - l2f := int32(list[2].freq) - l1f := int32(list[1].freq) - l0f := int32(list[0].freq) + int32(list[1].freq) - - for level := int32(1); level <= maxBits; level++ { - // For every level, the first two items are the first two characters. - // We initialize the levels as if we had already figured this out. - levels[level] = levelInfo{ - level: level, - lastFreq: l1f, - nextCharFreq: l2f, - nextPairFreq: l0f, - } - leafCounts[level][level] = 2 - if level == 1 { - levels[level].nextPairFreq = math.MaxInt32 - } - } - - // We need a total of 2*n - 2 items at top level and have already generated 2. - levels[maxBits].needed = 2*n - 4 - - level := uint32(maxBits) - for level < 16 { - l := &levels[level] - if l.nextPairFreq == math.MaxInt32 && l.nextCharFreq == math.MaxInt32 { - // We've run out of both leafs and pairs. - // End all calculations for this level. - // To make sure we never come back to this level or any lower level, - // set nextPairFreq impossibly large. - l.needed = 0 - levels[level+1].nextPairFreq = math.MaxInt32 - level++ - continue - } - - prevFreq := l.lastFreq - if l.nextCharFreq < l.nextPairFreq { - // The next item on this row is a leaf node. - n := leafCounts[level][level] + 1 - l.lastFreq = l.nextCharFreq - // Lower leafCounts are the same of the previous node. - leafCounts[level][level] = n - e := list[n] - if e.literal < math.MaxUint16 { - l.nextCharFreq = int32(e.freq) - } else { - l.nextCharFreq = math.MaxInt32 - } - } else { - // The next item on this row is a pair from the previous row. - // nextPairFreq isn't valid until we generate two - // more values in the level below - l.lastFreq = l.nextPairFreq - // Take leaf counts from the lower level, except counts[level] remains the same. - if true { - save := leafCounts[level][level] - leafCounts[level] = leafCounts[level-1] - leafCounts[level][level] = save - } else { - copy(leafCounts[level][:level], leafCounts[level-1][:level]) - } - levels[l.level-1].needed = 2 - } - - if l.needed--; l.needed == 0 { - // We've done everything we need to do for this level. - // Continue calculating one level up. Fill in nextPairFreq - // of that level with the sum of the two nodes we've just calculated on - // this level. - if l.level == maxBits { - // All done! - break - } - levels[l.level+1].nextPairFreq = prevFreq + l.lastFreq - level++ - } else { - // If we stole from below, move down temporarily to replenish it. - for levels[level-1].needed > 0 { - level-- - } - } - } - - // Somethings is wrong if at the end, the top level is null or hasn't used - // all of the leaves. - if leafCounts[maxBits][maxBits] != n { - panic("leafCounts[maxBits][maxBits] != n") - } - - bitCount := h.bitCount[:maxBits+1] - bits := 1 - counts := &leafCounts[maxBits] - for level := maxBits; level > 0; level-- { - // chain.leafCount gives the number of literals requiring at least "bits" - // bits to encode. - bitCount[bits] = counts[level] - counts[level-1] - bits++ - } - return bitCount -} - -// Look at the leaves and assign them a bit count and an encoding as specified -// in RFC 1951 3.2.2 -func (h *huffmanEncoder) assignEncodingAndSize(bitCount []int32, list []literalNode) { - code := uint16(0) - for n, bits := range bitCount { - code <<= 1 - if n == 0 || bits == 0 { - continue - } - // The literals list[len(list)-bits] .. list[len(list)-bits] - // are encoded using "bits" bits, and get the values - // code, code + 1, .... The code values are - // assigned in literal order (not frequency order). - chunk := list[len(list)-int(bits):] - - sortByLiteral(chunk) - for _, node := range chunk { - h.codes[node.literal] = newhcode(reverseBits(code, uint8(n)), uint8(n)) - code++ - } - list = list[0 : len(list)-int(bits)] - } -} - -// Update this Huffman Code object to be the minimum code for the specified frequency count. -// -// freq An array of frequencies, in which frequency[i] gives the frequency of literal i. -// maxBits The maximum number of bits to use for any literal. -func (h *huffmanEncoder) generate(freq []uint16, maxBits int32) { - list := h.freqcache[:len(freq)+1] - codes := h.codes[:len(freq)] - // Number of non-zero literals - count := 0 - // Set list to be the set of all non-zero literals and their frequencies - for i, f := range freq { - if f != 0 { - list[count] = literalNode{uint16(i), f} - count++ - } else { - codes[i] = 0 - } - } - list[count] = literalNode{} - - list = list[:count] - if count <= 2 { - // Handle the small cases here, because they are awkward for the general case code. With - // two or fewer literals, everything has bit length 1. - for i, node := range list { - // "list" is in order of increasing literal value. - h.codes[node.literal].set(uint16(i), 1) - } - return - } - sortByFreq(list) - - // Get the number of literals for each bit count - bitCount := h.bitCounts(list, maxBits) - // And do the assignment - h.assignEncodingAndSize(bitCount, list) -} - -// atLeastOne clamps the result between 1 and 15. -func atLeastOne(v float32) float32 { - if v < 1 { - return 1 - } - if v > 15 { - return 15 - } - return v -} - -func histogram(b []byte, h []uint16) { - if true && len(b) >= 8<<10 { - // Split for bigger inputs - histogramSplit(b, h) - } else { - h = h[:256] - for _, t := range b { - h[t]++ - } - } -} - -func histogramSplit(b []byte, h []uint16) { - // Tested, and slightly faster than 2-way. - // Writing to separate arrays and combining is also slightly slower. - h = h[:256] - for len(b)&3 != 0 { - h[b[0]]++ - b = b[1:] - } - n := len(b) / 4 - x, y, z, w := b[:n], b[n:], b[n+n:], b[n+n+n:] - y, z, w = y[:len(x)], z[:len(x)], w[:len(x)] - for i, t := range x { - v0 := &h[t] - v1 := &h[y[i]] - v3 := &h[w[i]] - v2 := &h[z[i]] - *v0++ - *v1++ - *v2++ - *v3++ - } -} diff --git a/internal/compress/flate/huffman_sortByFreq.go b/internal/compress/flate/huffman_sortByFreq.go deleted file mode 100644 index 6c05ba8c..00000000 --- a/internal/compress/flate/huffman_sortByFreq.go +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright 2009 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package flate - -// Sort sorts data. -// It makes one call to data.Len to determine n, and O(n*log(n)) calls to -// data.Less and data.Swap. The sort is not guaranteed to be stable. -func sortByFreq(data []literalNode) { - n := len(data) - quickSortByFreq(data, 0, n, maxDepth(n)) -} - -func quickSortByFreq(data []literalNode, a, b, maxDepth int) { - for b-a > 12 { // Use ShellSort for slices <= 12 elements - if maxDepth == 0 { - heapSort(data, a, b) - return - } - maxDepth-- - mlo, mhi := doPivotByFreq(data, a, b) - // Avoiding recursion on the larger subproblem guarantees - // a stack depth of at most lg(b-a). - if mlo-a < b-mhi { - quickSortByFreq(data, a, mlo, maxDepth) - a = mhi // i.e., quickSortByFreq(data, mhi, b) - } else { - quickSortByFreq(data, mhi, b, maxDepth) - b = mlo // i.e., quickSortByFreq(data, a, mlo) - } - } - if b-a > 1 { - // Do ShellSort pass with gap 6 - // It could be written in this simplified form cause b-a <= 12 - for i := a + 6; i < b; i++ { - if data[i].freq == data[i-6].freq && data[i].literal < data[i-6].literal || data[i].freq < data[i-6].freq { - data[i], data[i-6] = data[i-6], data[i] - } - } - insertionSortByFreq(data, a, b) - } -} - -func doPivotByFreq(data []literalNode, lo, hi int) (midlo, midhi int) { - m := int(uint(lo+hi) >> 1) // Written like this to avoid integer overflow. - if hi-lo > 40 { - // Tukey's ``Ninther,'' median of three medians of three. - s := (hi - lo) / 8 - medianOfThreeSortByFreq(data, lo, lo+s, lo+2*s) - medianOfThreeSortByFreq(data, m, m-s, m+s) - medianOfThreeSortByFreq(data, hi-1, hi-1-s, hi-1-2*s) - } - medianOfThreeSortByFreq(data, lo, m, hi-1) - - // Invariants are: - // data[lo] = pivot (set up by ChoosePivot) - // data[lo < i < a] < pivot - // data[a <= i < b] <= pivot - // data[b <= i < c] unexamined - // data[c <= i < hi-1] > pivot - // data[hi-1] >= pivot - pivot := lo - a, c := lo+1, hi-1 - - for ; a < c && (data[a].freq == data[pivot].freq && data[a].literal < data[pivot].literal || data[a].freq < data[pivot].freq); a++ { - } - b := a - for { - for ; b < c && (data[pivot].freq == data[b].freq && data[pivot].literal > data[b].literal || data[pivot].freq > data[b].freq); b++ { // data[b] <= pivot - } - for ; b < c && (data[pivot].freq == data[c-1].freq && data[pivot].literal < data[c-1].literal || data[pivot].freq < data[c-1].freq); c-- { // data[c-1] > pivot - } - if b >= c { - break - } - // data[b] > pivot; data[c-1] <= pivot - data[b], data[c-1] = data[c-1], data[b] - b++ - c-- - } - // If hi-c<3 then there are duplicates (by property of median of nine). - // Let's be a bit more conservative, and set border to 5. - protect := hi-c < 5 - if !protect && hi-c < (hi-lo)/4 { - // Lets test some points for equality to pivot - dups := 0 - if data[pivot].freq == data[hi-1].freq && data[pivot].literal > data[hi-1].literal || data[pivot].freq > data[hi-1].freq { // data[hi-1] = pivot - data[c], data[hi-1] = data[hi-1], data[c] - c++ - dups++ - } - if data[b-1].freq == data[pivot].freq && data[b-1].literal > data[pivot].literal || data[b-1].freq > data[pivot].freq { // data[b-1] = pivot - b-- - dups++ - } - // m-lo = (hi-lo)/2 > 6 - // b-lo > (hi-lo)*3/4-1 > 8 - // ==> m < b ==> data[m] <= pivot - if data[m].freq == data[pivot].freq && data[m].literal > data[pivot].literal || data[m].freq > data[pivot].freq { // data[m] = pivot - data[m], data[b-1] = data[b-1], data[m] - b-- - dups++ - } - // if at least 2 points are equal to pivot, assume skewed distribution - protect = dups > 1 - } - if protect { - // Protect against a lot of duplicates - // Add invariant: - // data[a <= i < b] unexamined - // data[b <= i < c] = pivot - for { - for ; a < b && (data[b-1].freq == data[pivot].freq && data[b-1].literal > data[pivot].literal || data[b-1].freq > data[pivot].freq); b-- { // data[b] == pivot - } - for ; a < b && (data[a].freq == data[pivot].freq && data[a].literal < data[pivot].literal || data[a].freq < data[pivot].freq); a++ { // data[a] < pivot - } - if a >= b { - break - } - // data[a] == pivot; data[b-1] < pivot - data[a], data[b-1] = data[b-1], data[a] - a++ - b-- - } - } - // Swap pivot into middle - data[pivot], data[b-1] = data[b-1], data[pivot] - return b - 1, c -} - -// Insertion sort -func insertionSortByFreq(data []literalNode, a, b int) { - for i := a + 1; i < b; i++ { - for j := i; j > a && (data[j].freq == data[j-1].freq && data[j].literal < data[j-1].literal || data[j].freq < data[j-1].freq); j-- { - data[j], data[j-1] = data[j-1], data[j] - } - } -} - -// quickSortByFreq, loosely following Bentley and McIlroy, -// ``Engineering a Sort Function,'' SP&E November 1993. - -// medianOfThreeSortByFreq moves the median of the three values data[m0], data[m1], data[m2] into data[m1]. -func medianOfThreeSortByFreq(data []literalNode, m1, m0, m2 int) { - // sort 3 elements - if data[m1].freq == data[m0].freq && data[m1].literal < data[m0].literal || data[m1].freq < data[m0].freq { - data[m1], data[m0] = data[m0], data[m1] - } - // data[m0] <= data[m1] - if data[m2].freq == data[m1].freq && data[m2].literal < data[m1].literal || data[m2].freq < data[m1].freq { - data[m2], data[m1] = data[m1], data[m2] - // data[m0] <= data[m2] && data[m1] < data[m2] - if data[m1].freq == data[m0].freq && data[m1].literal < data[m0].literal || data[m1].freq < data[m0].freq { - data[m1], data[m0] = data[m0], data[m1] - } - } - // now data[m0] <= data[m1] <= data[m2] -} diff --git a/internal/compress/flate/huffman_sortByLiteral.go b/internal/compress/flate/huffman_sortByLiteral.go deleted file mode 100644 index f6d0a404..00000000 --- a/internal/compress/flate/huffman_sortByLiteral.go +++ /dev/null @@ -1,203 +0,0 @@ -// Copyright 2009 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package flate - -// Sort sorts data. -// It makes one call to data.Len to determine n, and O(n*log(n)) calls to -// data.Less and data.Swap. The sort is not guaranteed to be stable. -func sortByLiteral(data []literalNode) { - n := len(data) - quickSort(data, 0, n, maxDepth(n)) -} - -func quickSort(data []literalNode, a, b, maxDepth int) { - for b-a > 12 { // Use ShellSort for slices <= 12 elements - if maxDepth == 0 { - heapSort(data, a, b) - return - } - maxDepth-- - mlo, mhi := doPivot(data, a, b) - // Avoiding recursion on the larger subproblem guarantees - // a stack depth of at most lg(b-a). - if mlo-a < b-mhi { - quickSort(data, a, mlo, maxDepth) - a = mhi // i.e., quickSort(data, mhi, b) - } else { - quickSort(data, mhi, b, maxDepth) - b = mlo // i.e., quickSort(data, a, mlo) - } - } - if b-a > 1 { - // Do ShellSort pass with gap 6 - // It could be written in this simplified form cause b-a <= 12 - for i := a + 6; i < b; i++ { - if data[i].literal < data[i-6].literal { - data[i], data[i-6] = data[i-6], data[i] - } - } - insertionSort(data, a, b) - } -} - -func heapSort(data []literalNode, a, b int) { - first := a - lo := 0 - hi := b - a - - // Build heap with greatest element at top. - for i := (hi - 1) / 2; i >= 0; i-- { - siftDown(data, i, hi, first) - } - - // Pop elements, largest first, into end of data. - for i := hi - 1; i >= 0; i-- { - data[first], data[first+i] = data[first+i], data[first] - siftDown(data, lo, i, first) - } -} - -// siftDown implements the heap property on data[lo, hi). -// first is an offset into the array where the root of the heap lies. -func siftDown(data []literalNode, lo, hi, first int) { - root := lo - for { - child := 2*root + 1 - if child >= hi { - break - } - if child+1 < hi && data[first+child].literal < data[first+child+1].literal { - child++ - } - if data[first+root].literal > data[first+child].literal { - return - } - data[first+root], data[first+child] = data[first+child], data[first+root] - root = child - } -} - -func doPivot(data []literalNode, lo, hi int) (midlo, midhi int) { - m := int(uint(lo+hi) >> 1) // Written like this to avoid integer overflow. - if hi-lo > 40 { - // Tukey's ``Ninther,'' median of three medians of three. - s := (hi - lo) / 8 - medianOfThree(data, lo, lo+s, lo+2*s) - medianOfThree(data, m, m-s, m+s) - medianOfThree(data, hi-1, hi-1-s, hi-1-2*s) - } - medianOfThree(data, lo, m, hi-1) - - // Invariants are: - // data[lo] = pivot (set up by ChoosePivot) - // data[lo < i < a] < pivot - // data[a <= i < b] <= pivot - // data[b <= i < c] unexamined - // data[c <= i < hi-1] > pivot - // data[hi-1] >= pivot - pivot := lo - a, c := lo+1, hi-1 - - for ; a < c && data[a].literal < data[pivot].literal; a++ { - } - b := a - for { - for ; b < c && data[pivot].literal > data[b].literal; b++ { // data[b] <= pivot - } - for ; b < c && data[pivot].literal < data[c-1].literal; c-- { // data[c-1] > pivot - } - if b >= c { - break - } - // data[b] > pivot; data[c-1] <= pivot - data[b], data[c-1] = data[c-1], data[b] - b++ - c-- - } - // If hi-c<3 then there are duplicates (by property of median of nine). - // Let's be a bit more conservative, and set border to 5. - protect := hi-c < 5 - if !protect && hi-c < (hi-lo)/4 { - // Lets test some points for equality to pivot - dups := 0 - if data[pivot].literal > data[hi-1].literal { // data[hi-1] = pivot - data[c], data[hi-1] = data[hi-1], data[c] - c++ - dups++ - } - if data[b-1].literal > data[pivot].literal { // data[b-1] = pivot - b-- - dups++ - } - // m-lo = (hi-lo)/2 > 6 - // b-lo > (hi-lo)*3/4-1 > 8 - // ==> m < b ==> data[m] <= pivot - if data[m].literal > data[pivot].literal { // data[m] = pivot - data[m], data[b-1] = data[b-1], data[m] - b-- - dups++ - } - // if at least 2 points are equal to pivot, assume skewed distribution - protect = dups > 1 - } - if protect { - // Protect against a lot of duplicates - // Add invariant: - // data[a <= i < b] unexamined - // data[b <= i < c] = pivot - for { - for ; a < b && data[b-1].literal > data[pivot].literal; b-- { // data[b] == pivot - } - for ; a < b && data[a].literal < data[pivot].literal; a++ { // data[a] < pivot - } - if a >= b { - break - } - // data[a] == pivot; data[b-1] < pivot - data[a], data[b-1] = data[b-1], data[a] - a++ - b-- - } - } - // Swap pivot into middle - data[pivot], data[b-1] = data[b-1], data[pivot] - return b - 1, c -} - -// Insertion sort -func insertionSort(data []literalNode, a, b int) { - for i := a + 1; i < b; i++ { - for j := i; j > a && data[j].literal < data[j-1].literal; j-- { - data[j], data[j-1] = data[j-1], data[j] - } - } -} - -// maxDepth returns a threshold at which quicksort should switch -// to heapsort. It returns 2*ceil(lg(n+1)). -func maxDepth(n int) int { - var depth int - for i := n; i > 0; i >>= 1 { - depth++ - } - return depth * 2 -} - -// medianOfThree moves the median of the three values data[m0], data[m1], data[m2] into data[m1]. -func medianOfThree(data []literalNode, m1, m0, m2 int) { - // sort 3 elements - if data[m1].literal < data[m0].literal { - data[m1], data[m0] = data[m0], data[m1] - } - // data[m0] <= data[m1] - if data[m2].literal < data[m1].literal { - data[m2], data[m1] = data[m1], data[m2] - // data[m0] <= data[m2] && data[m1] < data[m2] - if data[m1].literal < data[m0].literal { - data[m1], data[m0] = data[m0], data[m1] - } - } - // now data[m0] <= data[m1] <= data[m2] -} diff --git a/internal/compress/flate/inflate.go b/internal/compress/flate/inflate.go deleted file mode 100644 index f12f1e77..00000000 --- a/internal/compress/flate/inflate.go +++ /dev/null @@ -1,867 +0,0 @@ -// Copyright 2009 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Package flate implements the DEFLATE compressed data format, described in -// RFC 1951. The gzip and zlib packages implement access to DEFLATE-based file -// formats. -package flate - -import ( - "bufio" - "compress/flate" - "fmt" - "io" - "math/bits" - "sync" -) - -const ( - maxCodeLen = 16 // max length of Huffman code - maxCodeLenMask = 15 // mask for max length of Huffman code - // The next three numbers come from the RFC section 3.2.7, with the - // additional proviso in section 3.2.5 which implies that distance codes - // 30 and 31 should never occur in compressed data. - maxNumLit = 286 - maxNumDist = 30 - numCodes = 19 // number of codes in Huffman meta-code - - debugDecode = false -) - -// Value of length - 3 and extra bits. -type lengthExtra struct { - length, extra uint8 -} - -var decCodeToLen = [32]lengthExtra{{length: 0x0, extra: 0x0}, {length: 0x1, extra: 0x0}, {length: 0x2, extra: 0x0}, {length: 0x3, extra: 0x0}, {length: 0x4, extra: 0x0}, {length: 0x5, extra: 0x0}, {length: 0x6, extra: 0x0}, {length: 0x7, extra: 0x0}, {length: 0x8, extra: 0x1}, {length: 0xa, extra: 0x1}, {length: 0xc, extra: 0x1}, {length: 0xe, extra: 0x1}, {length: 0x10, extra: 0x2}, {length: 0x14, extra: 0x2}, {length: 0x18, extra: 0x2}, {length: 0x1c, extra: 0x2}, {length: 0x20, extra: 0x3}, {length: 0x28, extra: 0x3}, {length: 0x30, extra: 0x3}, {length: 0x38, extra: 0x3}, {length: 0x40, extra: 0x4}, {length: 0x50, extra: 0x4}, {length: 0x60, extra: 0x4}, {length: 0x70, extra: 0x4}, {length: 0x80, extra: 0x5}, {length: 0xa0, extra: 0x5}, {length: 0xc0, extra: 0x5}, {length: 0xe0, extra: 0x5}, {length: 0xff, extra: 0x0}, {length: 0x0, extra: 0x0}, {length: 0x0, extra: 0x0}, {length: 0x0, extra: 0x0}} - -var bitMask32 = [32]uint32{ - 0, 1, 3, 7, 0xF, 0x1F, 0x3F, 0x7F, 0xFF, - 0x1FF, 0x3FF, 0x7FF, 0xFFF, 0x1FFF, 0x3FFF, 0x7FFF, 0xFFFF, - 0x1ffff, 0x3ffff, 0x7FFFF, 0xfFFFF, 0x1fFFFF, 0x3fFFFF, 0x7fFFFF, 0xffFFFF, - 0x1ffFFFF, 0x3ffFFFF, 0x7ffFFFF, 0xfffFFFF, 0x1fffFFFF, 0x3fffFFFF, 0x7fffFFFF, -} // up to 32 bits - -// Initialize the fixedHuffmanDecoder only once upon first use. -var ( - fixedOnce sync.Once - fixedHuffmanDecoder huffmanDecoder -) - -// A CorruptInputError reports the presence of corrupt input at a given offset. -type CorruptInputError = flate.CorruptInputError - -// An InternalError reports an error in the flate code itself. -type InternalError string - -func (e InternalError) Error() string { return "flate: internal error: " + string(e) } - -// A ReadError reports an error encountered while reading input. -// -// Deprecated: No longer returned. -type ReadError = flate.ReadError - -// A WriteError reports an error encountered while writing output. -// -// Deprecated: No longer returned. -type WriteError = flate.WriteError - -// Resetter resets a ReadCloser returned by NewReader or NewReaderDict to -// to switch to a new underlying Reader. This permits reusing a ReadCloser -// instead of allocating a new one. -type Resetter interface { - // Reset discards any buffered data and resets the Resetter as if it was - // newly initialized with the given reader. - Reset(r io.Reader, dict []byte) error -} - -// The data structure for decoding Huffman tables is based on that of -// zlib. There is a lookup table of a fixed bit width (huffmanChunkBits), -// For codes smaller than the table width, there are multiple entries -// (each combination of trailing bits has the same value). For codes -// larger than the table width, the table contains a link to an overflow -// table. The width of each entry in the link table is the maximum code -// size minus the chunk width. -// -// Note that you can do a lookup in the table even without all bits -// filled. Since the extra bits are zero, and the DEFLATE Huffman codes -// have the property that shorter codes come before longer ones, the -// bit length estimate in the result is a lower bound on the actual -// number of bits. -// -// See the following: -// http://www.gzip.org/algorithm.txt - -// chunk & 15 is number of bits -// chunk >> 4 is value, including table link - -const ( - huffmanChunkBits = 9 - huffmanNumChunks = 1 << huffmanChunkBits - huffmanCountMask = 15 - huffmanValueShift = 4 -) - -type huffmanDecoder struct { - maxRead int // the maximum number of bits we can read and not overread - chunks *[huffmanNumChunks]uint16 // chunks as described above - links [][]uint16 // overflow links - linkMask uint32 // mask the width of the link table -} - -// Initialize Huffman decoding tables from array of code lengths. -// Following this function, h is guaranteed to be initialized into a complete -// tree (i.e., neither over-subscribed nor under-subscribed). The exception is a -// degenerate case where the tree has only a single symbol with length 1. Empty -// trees are permitted. -func (h *huffmanDecoder) init(lengths []int) bool { - // Sanity enables additional runtime tests during Huffman - // table construction. It's intended to be used during - // development to supplement the currently ad-hoc unit tests. - const sanity = false - - if h.chunks == nil { - h.chunks = new([huffmanNumChunks]uint16) - } - - if h.maxRead != 0 { - *h = huffmanDecoder{chunks: h.chunks, links: h.links} - } - - // Count number of codes of each length, - // compute maxRead and max length. - var count [maxCodeLen]int - var min, max int - for _, n := range lengths { - if n == 0 { - continue - } - if min == 0 || n < min { - min = n - } - if n > max { - max = n - } - count[n&maxCodeLenMask]++ - } - - // Empty tree. The decompressor.huffSym function will fail later if the tree - // is used. Technically, an empty tree is only valid for the HDIST tree and - // not the HCLEN and HLIT tree. However, a stream with an empty HCLEN tree - // is guaranteed to fail since it will attempt to use the tree to decode the - // codes for the HLIT and HDIST trees. Similarly, an empty HLIT tree is - // guaranteed to fail later since the compressed data section must be - // composed of at least one symbol (the end-of-block marker). - if max == 0 { - return true - } - - code := 0 - var nextcode [maxCodeLen]int - for i := min; i <= max; i++ { - code <<= 1 - nextcode[i&maxCodeLenMask] = code - code += count[i&maxCodeLenMask] - } - - // Check that the coding is complete (i.e., that we've - // assigned all 2-to-the-max possible bit sequences). - // Exception: To be compatible with zlib, we also need to - // accept degenerate single-code codings. See also - // TestDegenerateHuffmanCoding. - if code != 1< huffmanChunkBits { - numLinks := 1 << (uint(max) - huffmanChunkBits) - h.linkMask = uint32(numLinks - 1) - - // create link tables - link := nextcode[huffmanChunkBits+1] >> 1 - if cap(h.links) < huffmanNumChunks-link { - h.links = make([][]uint16, huffmanNumChunks-link) - } else { - h.links = h.links[:huffmanNumChunks-link] - } - for j := uint(link); j < huffmanNumChunks; j++ { - reverse := int(bits.Reverse16(uint16(j))) - reverse >>= uint(16 - huffmanChunkBits) - off := j - uint(link) - if sanity && h.chunks[reverse] != 0 { - panic("impossible: overwriting existing chunk") - } - h.chunks[reverse] = uint16(off<>= uint(16 - n) - if n <= huffmanChunkBits { - for off := reverse; off < len(h.chunks); off += 1 << uint(n) { - // We should never need to overwrite - // an existing chunk. Also, 0 is - // never a valid chunk, because the - // lower 4 "count" bits should be - // between 1 and 15. - if sanity && h.chunks[off] != 0 { - panic("impossible: overwriting existing chunk") - } - h.chunks[off] = chunk - } - } else { - j := reverse & (huffmanNumChunks - 1) - if sanity && h.chunks[j]&huffmanCountMask != huffmanChunkBits+1 { - // Longer codes should have been - // associated with a link table above. - panic("impossible: not an indirect chunk") - } - value := h.chunks[j] >> huffmanValueShift - linktab := h.links[value] - reverse >>= huffmanChunkBits - for off := reverse; off < len(linktab); off += 1 << uint(n-huffmanChunkBits) { - if sanity && linktab[off] != 0 { - panic("impossible: overwriting existing chunk") - } - linktab[off] = chunk - } - } - } - - if sanity { - // Above we've sanity checked that we never overwrote - // an existing entry. Here we additionally check that - // we filled the tables completely. - for i, chunk := range h.chunks { - if chunk == 0 { - // As an exception, in the degenerate - // single-code case, we allow odd - // chunks to be missing. - if code == 1 && i%2 == 1 { - continue - } - panic("impossible: missing chunk") - } - } - for _, linktab := range h.links { - for _, chunk := range linktab { - if chunk == 0 { - panic("impossible: missing chunk") - } - } - } - } - - return true -} - -// Reader is the actual read interface needed by NewReader. -// If the passed in io.Reader does not also have ReadByte, -// the NewReader will introduce its own buffering. -type Reader interface { - io.Reader - io.ByteReader -} - -type step uint8 - -const ( - copyData step = iota + 1 - nextBlock - huffmanBytesBuffer - huffmanBytesReader - huffmanBufioReader - huffmanStringsReader - huffmanGenericReader -) - -// flushMode tells decompressor when to return data -type flushMode uint8 - -const ( - syncFlush flushMode = iota // return data after sync flush block - partialFlush // return data after each block -) - -// Decompress state. -type decompressor struct { - // Input source. - r Reader - roffset int64 - - // Huffman decoders for literal/length, distance. - h1, h2 huffmanDecoder - - // Length arrays used to define Huffman codes. - bits *[maxNumLit + maxNumDist]int - codebits *[numCodes]int - - // Output history, buffer. - dict dictDecoder - - // Next step in the decompression, - // and decompression state. - step step - stepState int - err error - toRead []byte - hl, hd *huffmanDecoder - copyLen int - copyDist int - - // Temporary buffer (avoids repeated allocation). - buf [4]byte - - // Input bits, in top of b. - b uint32 - - nb uint - final bool - - flushMode flushMode -} - -func (f *decompressor) nextBlock() { - for f.nb < 1+2 { - if f.err = f.moreBits(); f.err != nil { - return - } - } - f.final = f.b&1 == 1 - f.b >>= 1 - typ := f.b & 3 - f.b >>= 2 - f.nb -= 1 + 2 - switch typ { - case 0: - f.dataBlock() - if debugDecode { - fmt.Println("stored block") - } - case 1: - // compressed, fixed Huffman tables - f.hl = &fixedHuffmanDecoder - f.hd = nil - f.huffmanBlockDecoder() - if debugDecode { - fmt.Println("predefinied huffman block") - } - case 2: - // compressed, dynamic Huffman tables - if f.err = f.readHuffman(); f.err != nil { - break - } - f.hl = &f.h1 - f.hd = &f.h2 - f.huffmanBlockDecoder() - if debugDecode { - fmt.Println("dynamic huffman block") - } - default: - // 3 is reserved. - if debugDecode { - fmt.Println("reserved data block encountered") - } - f.err = CorruptInputError(f.roffset) - } -} - -func (f *decompressor) Read(b []byte) (int, error) { - for { - if len(f.toRead) > 0 { - n := copy(b, f.toRead) - f.toRead = f.toRead[n:] - if len(f.toRead) == 0 { - return n, f.err - } - return n, nil - } - if f.err != nil { - return 0, f.err - } - - f.doStep() - - if f.err != nil && len(f.toRead) == 0 { - f.toRead = f.dict.readFlush() // Flush what's left in case of error - } - } -} - -// WriteTo implements the io.WriteTo interface for io.Copy and friends. -func (f *decompressor) WriteTo(w io.Writer) (int64, error) { - total := int64(0) - flushed := false - for { - if len(f.toRead) > 0 { - n, err := w.Write(f.toRead) - total += int64(n) - if err != nil { - f.err = err - return total, err - } - if n != len(f.toRead) { - return total, io.ErrShortWrite - } - f.toRead = f.toRead[:0] - } - if f.err != nil && flushed { - if f.err == io.EOF { - return total, nil - } - return total, f.err - } - if f.err == nil { - f.doStep() - } - if len(f.toRead) == 0 && f.err != nil && !flushed { - f.toRead = f.dict.readFlush() // Flush what's left in case of error - flushed = true - } - } -} - -func (f *decompressor) Close() error { - if f.err == io.EOF { - return nil - } - return f.err -} - -// RFC 1951 section 3.2.7. -// Compression with dynamic Huffman codes - -var codeOrder = [...]int{16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15} - -func (f *decompressor) readHuffman() error { - // HLIT[5], HDIST[5], HCLEN[4]. - for f.nb < 5+5+4 { - if err := f.moreBits(); err != nil { - return err - } - } - nlit := int(f.b&0x1F) + 257 - if nlit > maxNumLit { - if debugDecode { - fmt.Println("nlit > maxNumLit", nlit) - } - return CorruptInputError(f.roffset) - } - f.b >>= 5 - ndist := int(f.b&0x1F) + 1 - if ndist > maxNumDist { - if debugDecode { - fmt.Println("ndist > maxNumDist", ndist) - } - return CorruptInputError(f.roffset) - } - f.b >>= 5 - nclen := int(f.b&0xF) + 4 - // numCodes is 19, so nclen is always valid. - f.b >>= 4 - f.nb -= 5 + 5 + 4 - - // (HCLEN+4)*3 bits: code lengths in the magic codeOrder order. - for i := range nclen { - for f.nb < 3 { - if err := f.moreBits(); err != nil { - return err - } - } - f.codebits[codeOrder[i]] = int(f.b & 0x7) - f.b >>= 3 - f.nb -= 3 - } - for i := nclen; i < len(codeOrder); i++ { - f.codebits[codeOrder[i]] = 0 - } - if !f.h1.init(f.codebits[0:]) { - if debugDecode { - fmt.Println("init codebits failed") - } - return CorruptInputError(f.roffset) - } - - // HLIT + 257 code lengths, HDIST + 1 code lengths, - // using the code length Huffman code. - for i, n := 0, nlit+ndist; i < n; { - x, err := f.huffSym(&f.h1) - if err != nil { - return err - } - if x < 16 { - // Actual length. - f.bits[i] = x - i++ - continue - } - // Repeat previous length or zero. - var rep int - var nb uint - var b int - switch x { - default: - return InternalError("unexpected length code") - case 16: - rep = 3 - nb = 2 - if i == 0 { - if debugDecode { - fmt.Println("i==0") - } - return CorruptInputError(f.roffset) - } - b = f.bits[i-1] - case 17: - rep = 3 - nb = 3 - b = 0 - case 18: - rep = 11 - nb = 7 - b = 0 - } - for f.nb < nb { - if err := f.moreBits(); err != nil { - if debugDecode { - fmt.Println("morebits:", err) - } - return err - } - } - rep += int(f.b & uint32(1<<(nb®SizeMaskUint32)-1)) - f.b >>= nb & regSizeMaskUint32 - f.nb -= nb - if i+rep > n { - if debugDecode { - fmt.Println("i+rep > n", i, rep, n) - } - return CorruptInputError(f.roffset) - } - for j := 0; j < rep; j++ { - f.bits[i] = b - i++ - } - } - - if !f.h1.init(f.bits[0:nlit]) || !f.h2.init(f.bits[nlit:nlit+ndist]) { - if debugDecode { - fmt.Println("init2 failed") - } - return CorruptInputError(f.roffset) - } - - // As an optimization, we can initialize the maxRead bits to read at a time - // for the HLIT tree to the length of the EOB marker since we know that - // every block must terminate with one. This preserves the property that - // we never read any extra bytes after the end of the DEFLATE stream. - if f.h1.maxRead < f.bits[endBlockMarker] { - f.h1.maxRead = f.bits[endBlockMarker] - } - if !f.final { - // If not the final block, the smallest block possible is - // a predefined table, BTYPE=01, with a single EOB marker. - // This will take up 3 + 7 bits. - f.h1.maxRead += 10 - } - - return nil -} - -// Copy a single uncompressed data block from input to output. -func (f *decompressor) dataBlock() { - // Uncompressed. - // Discard current half-byte. - left := (f.nb) & 7 - f.nb -= left - f.b >>= left - - offBytes := f.nb >> 3 - // Unfilled values will be overwritten. - f.buf[0] = uint8(f.b) - f.buf[1] = uint8(f.b >> 8) - f.buf[2] = uint8(f.b >> 16) - f.buf[3] = uint8(f.b >> 24) - - f.roffset += int64(offBytes) - f.nb, f.b = 0, 0 - - // Length then ones-complement of length. - nr, err := io.ReadFull(f.r, f.buf[offBytes:4]) - f.roffset += int64(nr) - if err != nil { - f.err = noEOF(err) - return - } - n := uint16(f.buf[0]) | uint16(f.buf[1])<<8 - nn := uint16(f.buf[2]) | uint16(f.buf[3])<<8 - if nn != ^n { - if debugDecode { - ncomp := ^n - fmt.Println("uint16(nn) != uint16(^n)", nn, ncomp) - } - f.err = CorruptInputError(f.roffset) - return - } - - if n == 0 { - if f.flushMode == syncFlush { - f.toRead = f.dict.readFlush() - } - - f.finishBlock() - return - } - - f.copyLen = int(n) - f.copyData() -} - -// copyData copies f.copyLen bytes from the underlying reader into f.hist. -// It pauses for reads when f.hist is full. -func (f *decompressor) copyData() { - buf := f.dict.writeSlice() - if len(buf) > f.copyLen { - buf = buf[:f.copyLen] - } - - cnt, err := io.ReadFull(f.r, buf) - f.roffset += int64(cnt) - f.copyLen -= cnt - f.dict.writeMark(cnt) - if err != nil { - f.err = noEOF(err) - return - } - - if f.dict.availWrite() == 0 || f.copyLen > 0 { - f.toRead = f.dict.readFlush() - f.step = copyData - return - } - f.finishBlock() -} - -func (f *decompressor) finishBlock() { - if f.final { - if f.dict.availRead() > 0 { - f.toRead = f.dict.readFlush() - } - - f.err = io.EOF - } else if f.flushMode == partialFlush && f.dict.availRead() > 0 { - f.toRead = f.dict.readFlush() - } - - f.step = nextBlock -} - -func (f *decompressor) doStep() { - switch f.step { - case copyData: - f.copyData() - case nextBlock: - f.nextBlock() - case huffmanBytesBuffer: - f.huffmanBytesBuffer() - case huffmanBytesReader: - f.huffmanBytesReader() - case huffmanBufioReader: - f.huffmanBufioReader() - case huffmanStringsReader: - f.huffmanStringsReader() - case huffmanGenericReader: - f.huffmanGenericReader() - default: - panic("BUG: unexpected step state") - } -} - -// noEOF returns err, unless err == io.EOF, in which case it returns io.ErrUnexpectedEOF. -func noEOF(e error) error { - if e == io.EOF { - return io.ErrUnexpectedEOF - } - return e -} - -func (f *decompressor) moreBits() error { - c, err := f.r.ReadByte() - if err != nil { - return noEOF(err) - } - f.roffset++ - f.b |= uint32(c) << (f.nb & regSizeMaskUint32) - f.nb += 8 - return nil -} - -// Read the next Huffman-encoded symbol from f according to h. -func (f *decompressor) huffSym(h *huffmanDecoder) (int, error) { - // Since a huffmanDecoder can be empty or be composed of a degenerate tree - // with single element, huffSym must error on these two edge cases. In both - // cases, the chunks slice will be 0 for the invalid sequence, leading it - // satisfy the n == 0 check below. - n := uint(h.maxRead) - // Optimization. Compiler isn't smart enough to keep f.b,f.nb in registers, - // but is smart enough to keep local variables in registers, so use nb and b, - // inline call to moreBits and reassign b,nb back to f on return. - nb, b := f.nb, f.b - for { - for nb < n { - c, err := f.r.ReadByte() - if err != nil { - f.b = b - f.nb = nb - return 0, noEOF(err) - } - f.roffset++ - b |= uint32(c) << (nb & regSizeMaskUint32) - nb += 8 - } - chunk := h.chunks[b&(huffmanNumChunks-1)] - n = uint(chunk & huffmanCountMask) - if n > huffmanChunkBits { - chunk = h.links[chunk>>huffmanValueShift][(b>>huffmanChunkBits)&h.linkMask] - n = uint(chunk & huffmanCountMask) - } - if n <= nb { - if n == 0 { - f.b = b - f.nb = nb - if debugDecode { - fmt.Println("huffsym: n==0") - } - f.err = CorruptInputError(f.roffset) - return 0, f.err - } - f.b = b >> (n & regSizeMaskUint32) - f.nb = nb - n - return int(chunk >> huffmanValueShift), nil - } - } -} - -func makeReader(r io.Reader) Reader { - if rr, ok := r.(Reader); ok { - return rr - } - return bufio.NewReader(r) -} - -func fixedHuffmanDecoderInit() { - fixedOnce.Do(func() { - // These come from the RFC section 3.2.6. - var bits [288]int - for i := range 144 { - bits[i] = 8 - } - for i := 144; i < 256; i++ { - bits[i] = 9 - } - for i := 256; i < 280; i++ { - bits[i] = 7 - } - for i := 280; i < 288; i++ { - bits[i] = 8 - } - fixedHuffmanDecoder.init(bits[:]) - }) -} - -func (f *decompressor) Reset(r io.Reader, dict []byte) error { - *f = decompressor{ - r: makeReader(r), - bits: f.bits, - codebits: f.codebits, - h1: f.h1, - h2: f.h2, - dict: f.dict, - step: nextBlock, - } - f.dict.init(maxMatchOffset, dict) - return nil -} - -type ReaderOpt func(*decompressor) - -// WithPartialBlock tells decompressor to return after each block, -// so it can read data written with partial flush -func WithPartialBlock() ReaderOpt { - return func(f *decompressor) { - f.flushMode = partialFlush - } -} - -// WithDict initializes the reader with a preset dictionary -func WithDict(dict []byte) ReaderOpt { - return func(f *decompressor) { - f.dict.init(maxMatchOffset, dict) - } -} - -// NewReaderOpts returns new reader with provided options -func NewReaderOpts(r io.Reader, opts ...ReaderOpt) io.ReadCloser { - fixedHuffmanDecoderInit() - - var f decompressor - f.r = makeReader(r) - f.bits = new([maxNumLit + maxNumDist]int) - f.codebits = new([numCodes]int) - f.step = nextBlock - f.dict.init(maxMatchOffset, nil) - - for _, opt := range opts { - opt(&f) - } - - return &f -} - -// NewReader returns a new ReadCloser that can be used -// to read the uncompressed version of r. -// If r does not also implement io.ByteReader, -// the decompressor may read more data than necessary from r. -// It is the caller's responsibility to call Close on the ReadCloser -// when finished reading. -// -// The ReadCloser returned by NewReader also implements Resetter. -func NewReader(r io.Reader) io.ReadCloser { - return NewReaderOpts(r) -} - -// NewReaderDict is like NewReader but initializes the reader -// with a preset dictionary. The returned Reader behaves as if -// the uncompressed data stream started with the given dictionary, -// which has already been read. NewReaderDict is typically used -// to read data compressed by NewWriterDict. -// -// The ReadCloser returned by NewReader also implements Resetter. -func NewReaderDict(r io.Reader, dict []byte) io.ReadCloser { - return NewReaderOpts(r, WithDict(dict)) -} diff --git a/internal/compress/flate/inflate_gen.go b/internal/compress/flate/inflate_gen.go deleted file mode 100644 index 2b2f993f..00000000 --- a/internal/compress/flate/inflate_gen.go +++ /dev/null @@ -1,1283 +0,0 @@ -// Code generated by go generate gen_inflate.go. DO NOT EDIT. - -package flate - -import ( - "bufio" - "bytes" - "fmt" - "math/bits" - "strings" -) - -// Decode a single Huffman block from f. -// hl and hd are the Huffman states for the lit/length values -// and the distance values, respectively. If hd == nil, using the -// fixed distance encoding associated with fixed Huffman blocks. -func (f *decompressor) huffmanBytesBuffer() { - const ( - stateInit = iota // Zero value must be stateInit - stateDict - ) - fr := f.r.(*bytes.Buffer) - - // Optimization. Compiler isn't smart enough to keep f.b,f.nb in registers, - // but is smart enough to keep local variables in registers, so use nb and b, - // inline call to moreBits and reassign b,nb back to f on return. - fnb, fb, dict := f.nb, f.b, &f.dict - - switch f.stepState { - case stateInit: - goto readLiteral - case stateDict: - goto copyHistory - } - -readLiteral: - // Read literal and/or (length, distance) according to RFC section 3.2.3. - { - var v int - { - // Inlined v, err := f.huffSym(f.hl) - // Since a huffmanDecoder can be empty or be composed of a degenerate tree - // with single element, huffSym must error on these two edge cases. In both - // cases, the chunks slice will be 0 for the invalid sequence, leading it - // satisfy the n == 0 check below. - n := uint(f.hl.maxRead) - for { - for fnb < n { - c, err := fr.ReadByte() - if err != nil { - f.b, f.nb = fb, fnb - f.err = noEOF(err) - return - } - f.roffset++ - fb |= uint32(c) << (fnb & regSizeMaskUint32) - fnb += 8 - } - chunk := f.hl.chunks[fb&(huffmanNumChunks-1)] - n = uint(chunk & huffmanCountMask) - if n > huffmanChunkBits { - chunk = f.hl.links[chunk>>huffmanValueShift][(fb>>huffmanChunkBits)&f.hl.linkMask] - n = uint(chunk & huffmanCountMask) - } - if n <= fnb { - if n == 0 { - f.b, f.nb = fb, fnb - if debugDecode { - fmt.Println("huffsym: n==0") - } - f.err = CorruptInputError(f.roffset) - return - } - fb = fb >> (n & regSizeMaskUint32) - fnb = fnb - n - v = int(chunk >> huffmanValueShift) - break - } - } - } - - var length int - switch { - case v < 256: - dict.writeByte(byte(v)) - if dict.availWrite() == 0 { - f.toRead = dict.readFlush() - f.step = huffmanBytesBuffer - f.stepState = stateInit - f.b, f.nb = fb, fnb - return - } - goto readLiteral - case v == 256: - f.b, f.nb = fb, fnb - f.finishBlock() - return - // otherwise, reference to older data - case v < 265: - length = v - (257 - 3) - case v < maxNumLit: - val := decCodeToLen[(v - 257)] - length = int(val.length) + 3 - n := uint(val.extra) - for fnb < n { - c, err := fr.ReadByte() - if err != nil { - f.b, f.nb = fb, fnb - if debugDecode { - fmt.Println("morebits n>0:", err) - } - f.err = err - return - } - f.roffset++ - fb |= uint32(c) << (fnb & regSizeMaskUint32) - fnb += 8 - } - length += int(fb & bitMask32[n]) - fb >>= n & regSizeMaskUint32 - fnb -= n - default: - if debugDecode { - fmt.Println(v, ">= maxNumLit") - } - f.err = CorruptInputError(f.roffset) - f.b, f.nb = fb, fnb - return - } - - var dist uint32 - if f.hd == nil { - for fnb < 5 { - c, err := fr.ReadByte() - if err != nil { - f.b, f.nb = fb, fnb - if debugDecode { - fmt.Println("morebits f.nb<5:", err) - } - f.err = err - return - } - f.roffset++ - fb |= uint32(c) << (fnb & regSizeMaskUint32) - fnb += 8 - } - dist = uint32(bits.Reverse8(uint8(fb & 0x1F << 3))) - fb >>= 5 - fnb -= 5 - } else { - // Since a huffmanDecoder can be empty or be composed of a degenerate tree - // with single element, huffSym must error on these two edge cases. In both - // cases, the chunks slice will be 0 for the invalid sequence, leading it - // satisfy the n == 0 check below. - n := uint(f.hd.maxRead) - // Optimization. Compiler isn't smart enough to keep f.b,f.nb in registers, - // but is smart enough to keep local variables in registers, so use nb and b, - // inline call to moreBits and reassign b,nb back to f on return. - for { - for fnb < n { - c, err := fr.ReadByte() - if err != nil { - f.b, f.nb = fb, fnb - f.err = noEOF(err) - return - } - f.roffset++ - fb |= uint32(c) << (fnb & regSizeMaskUint32) - fnb += 8 - } - chunk := f.hd.chunks[fb&(huffmanNumChunks-1)] - n = uint(chunk & huffmanCountMask) - if n > huffmanChunkBits { - chunk = f.hd.links[chunk>>huffmanValueShift][(fb>>huffmanChunkBits)&f.hd.linkMask] - n = uint(chunk & huffmanCountMask) - } - if n <= fnb { - if n == 0 { - f.b, f.nb = fb, fnb - if debugDecode { - fmt.Println("huffsym: n==0") - } - f.err = CorruptInputError(f.roffset) - return - } - fb = fb >> (n & regSizeMaskUint32) - fnb = fnb - n - dist = uint32(chunk >> huffmanValueShift) - break - } - } - } - - switch { - case dist < 4: - dist++ - case dist < maxNumDist: - nb := uint(dist-2) >> 1 - // have 1 bit in bottom of dist, need nb more. - extra := (dist & 1) << (nb & regSizeMaskUint32) - for fnb < nb { - c, err := fr.ReadByte() - if err != nil { - f.b, f.nb = fb, fnb - if debugDecode { - fmt.Println("morebits f.nb>= nb & regSizeMaskUint32 - fnb -= nb - dist = 1<<((nb+1)®SizeMaskUint32) + 1 + extra - // slower: dist = bitMask32[nb+1] + 2 + extra - default: - f.b, f.nb = fb, fnb - if debugDecode { - fmt.Println("dist too big:", dist, maxNumDist) - } - f.err = CorruptInputError(f.roffset) - return - } - - // No check on length; encoding can be prescient. - if dist > uint32(dict.histSize()) { - f.b, f.nb = fb, fnb - if debugDecode { - fmt.Println("dist > dict.histSize():", dist, dict.histSize()) - } - f.err = CorruptInputError(f.roffset) - return - } - - f.copyLen, f.copyDist = length, int(dist) - goto copyHistory - } - -copyHistory: - // Perform a backwards copy according to RFC section 3.2.3. - { - cnt := dict.tryWriteCopy(f.copyDist, f.copyLen) - if cnt == 0 { - cnt = dict.writeCopy(f.copyDist, f.copyLen) - } - f.copyLen -= cnt - - if dict.availWrite() == 0 || f.copyLen > 0 { - f.toRead = dict.readFlush() - f.step = huffmanBytesBuffer // We need to continue this work - f.stepState = stateDict - f.b, f.nb = fb, fnb - return - } - goto readLiteral - } - // Not reached -} - -// Decode a single Huffman block from f. -// hl and hd are the Huffman states for the lit/length values -// and the distance values, respectively. If hd == nil, using the -// fixed distance encoding associated with fixed Huffman blocks. -func (f *decompressor) huffmanBytesReader() { - const ( - stateInit = iota // Zero value must be stateInit - stateDict - ) - fr := f.r.(*bytes.Reader) - - // Optimization. Compiler isn't smart enough to keep f.b,f.nb in registers, - // but is smart enough to keep local variables in registers, so use nb and b, - // inline call to moreBits and reassign b,nb back to f on return. - fnb, fb, dict := f.nb, f.b, &f.dict - - switch f.stepState { - case stateInit: - goto readLiteral - case stateDict: - goto copyHistory - } - -readLiteral: - // Read literal and/or (length, distance) according to RFC section 3.2.3. - { - var v int - { - // Inlined v, err := f.huffSym(f.hl) - // Since a huffmanDecoder can be empty or be composed of a degenerate tree - // with single element, huffSym must error on these two edge cases. In both - // cases, the chunks slice will be 0 for the invalid sequence, leading it - // satisfy the n == 0 check below. - n := uint(f.hl.maxRead) - for { - for fnb < n { - c, err := fr.ReadByte() - if err != nil { - f.b, f.nb = fb, fnb - f.err = noEOF(err) - return - } - f.roffset++ - fb |= uint32(c) << (fnb & regSizeMaskUint32) - fnb += 8 - } - chunk := f.hl.chunks[fb&(huffmanNumChunks-1)] - n = uint(chunk & huffmanCountMask) - if n > huffmanChunkBits { - chunk = f.hl.links[chunk>>huffmanValueShift][(fb>>huffmanChunkBits)&f.hl.linkMask] - n = uint(chunk & huffmanCountMask) - } - if n <= fnb { - if n == 0 { - f.b, f.nb = fb, fnb - if debugDecode { - fmt.Println("huffsym: n==0") - } - f.err = CorruptInputError(f.roffset) - return - } - fb = fb >> (n & regSizeMaskUint32) - fnb = fnb - n - v = int(chunk >> huffmanValueShift) - break - } - } - } - - var length int - switch { - case v < 256: - dict.writeByte(byte(v)) - if dict.availWrite() == 0 { - f.toRead = dict.readFlush() - f.step = huffmanBytesReader - f.stepState = stateInit - f.b, f.nb = fb, fnb - return - } - goto readLiteral - case v == 256: - f.b, f.nb = fb, fnb - f.finishBlock() - return - // otherwise, reference to older data - case v < 265: - length = v - (257 - 3) - case v < maxNumLit: - val := decCodeToLen[(v - 257)] - length = int(val.length) + 3 - n := uint(val.extra) - for fnb < n { - c, err := fr.ReadByte() - if err != nil { - f.b, f.nb = fb, fnb - if debugDecode { - fmt.Println("morebits n>0:", err) - } - f.err = err - return - } - f.roffset++ - fb |= uint32(c) << (fnb & regSizeMaskUint32) - fnb += 8 - } - length += int(fb & bitMask32[n]) - fb >>= n & regSizeMaskUint32 - fnb -= n - default: - if debugDecode { - fmt.Println(v, ">= maxNumLit") - } - f.err = CorruptInputError(f.roffset) - f.b, f.nb = fb, fnb - return - } - - var dist uint32 - if f.hd == nil { - for fnb < 5 { - c, err := fr.ReadByte() - if err != nil { - f.b, f.nb = fb, fnb - if debugDecode { - fmt.Println("morebits f.nb<5:", err) - } - f.err = err - return - } - f.roffset++ - fb |= uint32(c) << (fnb & regSizeMaskUint32) - fnb += 8 - } - dist = uint32(bits.Reverse8(uint8(fb & 0x1F << 3))) - fb >>= 5 - fnb -= 5 - } else { - // Since a huffmanDecoder can be empty or be composed of a degenerate tree - // with single element, huffSym must error on these two edge cases. In both - // cases, the chunks slice will be 0 for the invalid sequence, leading it - // satisfy the n == 0 check below. - n := uint(f.hd.maxRead) - // Optimization. Compiler isn't smart enough to keep f.b,f.nb in registers, - // but is smart enough to keep local variables in registers, so use nb and b, - // inline call to moreBits and reassign b,nb back to f on return. - for { - for fnb < n { - c, err := fr.ReadByte() - if err != nil { - f.b, f.nb = fb, fnb - f.err = noEOF(err) - return - } - f.roffset++ - fb |= uint32(c) << (fnb & regSizeMaskUint32) - fnb += 8 - } - chunk := f.hd.chunks[fb&(huffmanNumChunks-1)] - n = uint(chunk & huffmanCountMask) - if n > huffmanChunkBits { - chunk = f.hd.links[chunk>>huffmanValueShift][(fb>>huffmanChunkBits)&f.hd.linkMask] - n = uint(chunk & huffmanCountMask) - } - if n <= fnb { - if n == 0 { - f.b, f.nb = fb, fnb - if debugDecode { - fmt.Println("huffsym: n==0") - } - f.err = CorruptInputError(f.roffset) - return - } - fb = fb >> (n & regSizeMaskUint32) - fnb = fnb - n - dist = uint32(chunk >> huffmanValueShift) - break - } - } - } - - switch { - case dist < 4: - dist++ - case dist < maxNumDist: - nb := uint(dist-2) >> 1 - // have 1 bit in bottom of dist, need nb more. - extra := (dist & 1) << (nb & regSizeMaskUint32) - for fnb < nb { - c, err := fr.ReadByte() - if err != nil { - f.b, f.nb = fb, fnb - if debugDecode { - fmt.Println("morebits f.nb>= nb & regSizeMaskUint32 - fnb -= nb - dist = 1<<((nb+1)®SizeMaskUint32) + 1 + extra - // slower: dist = bitMask32[nb+1] + 2 + extra - default: - f.b, f.nb = fb, fnb - if debugDecode { - fmt.Println("dist too big:", dist, maxNumDist) - } - f.err = CorruptInputError(f.roffset) - return - } - - // No check on length; encoding can be prescient. - if dist > uint32(dict.histSize()) { - f.b, f.nb = fb, fnb - if debugDecode { - fmt.Println("dist > dict.histSize():", dist, dict.histSize()) - } - f.err = CorruptInputError(f.roffset) - return - } - - f.copyLen, f.copyDist = length, int(dist) - goto copyHistory - } - -copyHistory: - // Perform a backwards copy according to RFC section 3.2.3. - { - cnt := dict.tryWriteCopy(f.copyDist, f.copyLen) - if cnt == 0 { - cnt = dict.writeCopy(f.copyDist, f.copyLen) - } - f.copyLen -= cnt - - if dict.availWrite() == 0 || f.copyLen > 0 { - f.toRead = dict.readFlush() - f.step = huffmanBytesReader // We need to continue this work - f.stepState = stateDict - f.b, f.nb = fb, fnb - return - } - goto readLiteral - } - // Not reached -} - -// Decode a single Huffman block from f. -// hl and hd are the Huffman states for the lit/length values -// and the distance values, respectively. If hd == nil, using the -// fixed distance encoding associated with fixed Huffman blocks. -func (f *decompressor) huffmanBufioReader() { - const ( - stateInit = iota // Zero value must be stateInit - stateDict - ) - fr := f.r.(*bufio.Reader) - - // Optimization. Compiler isn't smart enough to keep f.b,f.nb in registers, - // but is smart enough to keep local variables in registers, so use nb and b, - // inline call to moreBits and reassign b,nb back to f on return. - fnb, fb, dict := f.nb, f.b, &f.dict - - switch f.stepState { - case stateInit: - goto readLiteral - case stateDict: - goto copyHistory - } - -readLiteral: - // Read literal and/or (length, distance) according to RFC section 3.2.3. - { - var v int - { - // Inlined v, err := f.huffSym(f.hl) - // Since a huffmanDecoder can be empty or be composed of a degenerate tree - // with single element, huffSym must error on these two edge cases. In both - // cases, the chunks slice will be 0 for the invalid sequence, leading it - // satisfy the n == 0 check below. - n := uint(f.hl.maxRead) - for { - for fnb < n { - c, err := fr.ReadByte() - if err != nil { - f.b, f.nb = fb, fnb - f.err = noEOF(err) - return - } - f.roffset++ - fb |= uint32(c) << (fnb & regSizeMaskUint32) - fnb += 8 - } - chunk := f.hl.chunks[fb&(huffmanNumChunks-1)] - n = uint(chunk & huffmanCountMask) - if n > huffmanChunkBits { - chunk = f.hl.links[chunk>>huffmanValueShift][(fb>>huffmanChunkBits)&f.hl.linkMask] - n = uint(chunk & huffmanCountMask) - } - if n <= fnb { - if n == 0 { - f.b, f.nb = fb, fnb - if debugDecode { - fmt.Println("huffsym: n==0") - } - f.err = CorruptInputError(f.roffset) - return - } - fb = fb >> (n & regSizeMaskUint32) - fnb = fnb - n - v = int(chunk >> huffmanValueShift) - break - } - } - } - - var length int - switch { - case v < 256: - dict.writeByte(byte(v)) - if dict.availWrite() == 0 { - f.toRead = dict.readFlush() - f.step = huffmanBufioReader - f.stepState = stateInit - f.b, f.nb = fb, fnb - return - } - goto readLiteral - case v == 256: - f.b, f.nb = fb, fnb - f.finishBlock() - return - // otherwise, reference to older data - case v < 265: - length = v - (257 - 3) - case v < maxNumLit: - val := decCodeToLen[(v - 257)] - length = int(val.length) + 3 - n := uint(val.extra) - for fnb < n { - c, err := fr.ReadByte() - if err != nil { - f.b, f.nb = fb, fnb - if debugDecode { - fmt.Println("morebits n>0:", err) - } - f.err = err - return - } - f.roffset++ - fb |= uint32(c) << (fnb & regSizeMaskUint32) - fnb += 8 - } - length += int(fb & bitMask32[n]) - fb >>= n & regSizeMaskUint32 - fnb -= n - default: - if debugDecode { - fmt.Println(v, ">= maxNumLit") - } - f.err = CorruptInputError(f.roffset) - f.b, f.nb = fb, fnb - return - } - - var dist uint32 - if f.hd == nil { - for fnb < 5 { - c, err := fr.ReadByte() - if err != nil { - f.b, f.nb = fb, fnb - if debugDecode { - fmt.Println("morebits f.nb<5:", err) - } - f.err = err - return - } - f.roffset++ - fb |= uint32(c) << (fnb & regSizeMaskUint32) - fnb += 8 - } - dist = uint32(bits.Reverse8(uint8(fb & 0x1F << 3))) - fb >>= 5 - fnb -= 5 - } else { - // Since a huffmanDecoder can be empty or be composed of a degenerate tree - // with single element, huffSym must error on these two edge cases. In both - // cases, the chunks slice will be 0 for the invalid sequence, leading it - // satisfy the n == 0 check below. - n := uint(f.hd.maxRead) - // Optimization. Compiler isn't smart enough to keep f.b,f.nb in registers, - // but is smart enough to keep local variables in registers, so use nb and b, - // inline call to moreBits and reassign b,nb back to f on return. - for { - for fnb < n { - c, err := fr.ReadByte() - if err != nil { - f.b, f.nb = fb, fnb - f.err = noEOF(err) - return - } - f.roffset++ - fb |= uint32(c) << (fnb & regSizeMaskUint32) - fnb += 8 - } - chunk := f.hd.chunks[fb&(huffmanNumChunks-1)] - n = uint(chunk & huffmanCountMask) - if n > huffmanChunkBits { - chunk = f.hd.links[chunk>>huffmanValueShift][(fb>>huffmanChunkBits)&f.hd.linkMask] - n = uint(chunk & huffmanCountMask) - } - if n <= fnb { - if n == 0 { - f.b, f.nb = fb, fnb - if debugDecode { - fmt.Println("huffsym: n==0") - } - f.err = CorruptInputError(f.roffset) - return - } - fb = fb >> (n & regSizeMaskUint32) - fnb = fnb - n - dist = uint32(chunk >> huffmanValueShift) - break - } - } - } - - switch { - case dist < 4: - dist++ - case dist < maxNumDist: - nb := uint(dist-2) >> 1 - // have 1 bit in bottom of dist, need nb more. - extra := (dist & 1) << (nb & regSizeMaskUint32) - for fnb < nb { - c, err := fr.ReadByte() - if err != nil { - f.b, f.nb = fb, fnb - if debugDecode { - fmt.Println("morebits f.nb>= nb & regSizeMaskUint32 - fnb -= nb - dist = 1<<((nb+1)®SizeMaskUint32) + 1 + extra - // slower: dist = bitMask32[nb+1] + 2 + extra - default: - f.b, f.nb = fb, fnb - if debugDecode { - fmt.Println("dist too big:", dist, maxNumDist) - } - f.err = CorruptInputError(f.roffset) - return - } - - // No check on length; encoding can be prescient. - if dist > uint32(dict.histSize()) { - f.b, f.nb = fb, fnb - if debugDecode { - fmt.Println("dist > dict.histSize():", dist, dict.histSize()) - } - f.err = CorruptInputError(f.roffset) - return - } - - f.copyLen, f.copyDist = length, int(dist) - goto copyHistory - } - -copyHistory: - // Perform a backwards copy according to RFC section 3.2.3. - { - cnt := dict.tryWriteCopy(f.copyDist, f.copyLen) - if cnt == 0 { - cnt = dict.writeCopy(f.copyDist, f.copyLen) - } - f.copyLen -= cnt - - if dict.availWrite() == 0 || f.copyLen > 0 { - f.toRead = dict.readFlush() - f.step = huffmanBufioReader // We need to continue this work - f.stepState = stateDict - f.b, f.nb = fb, fnb - return - } - goto readLiteral - } - // Not reached -} - -// Decode a single Huffman block from f. -// hl and hd are the Huffman states for the lit/length values -// and the distance values, respectively. If hd == nil, using the -// fixed distance encoding associated with fixed Huffman blocks. -func (f *decompressor) huffmanStringsReader() { - const ( - stateInit = iota // Zero value must be stateInit - stateDict - ) - fr := f.r.(*strings.Reader) - - // Optimization. Compiler isn't smart enough to keep f.b,f.nb in registers, - // but is smart enough to keep local variables in registers, so use nb and b, - // inline call to moreBits and reassign b,nb back to f on return. - fnb, fb, dict := f.nb, f.b, &f.dict - - switch f.stepState { - case stateInit: - goto readLiteral - case stateDict: - goto copyHistory - } - -readLiteral: - // Read literal and/or (length, distance) according to RFC section 3.2.3. - { - var v int - { - // Inlined v, err := f.huffSym(f.hl) - // Since a huffmanDecoder can be empty or be composed of a degenerate tree - // with single element, huffSym must error on these two edge cases. In both - // cases, the chunks slice will be 0 for the invalid sequence, leading it - // satisfy the n == 0 check below. - n := uint(f.hl.maxRead) - for { - for fnb < n { - c, err := fr.ReadByte() - if err != nil { - f.b, f.nb = fb, fnb - f.err = noEOF(err) - return - } - f.roffset++ - fb |= uint32(c) << (fnb & regSizeMaskUint32) - fnb += 8 - } - chunk := f.hl.chunks[fb&(huffmanNumChunks-1)] - n = uint(chunk & huffmanCountMask) - if n > huffmanChunkBits { - chunk = f.hl.links[chunk>>huffmanValueShift][(fb>>huffmanChunkBits)&f.hl.linkMask] - n = uint(chunk & huffmanCountMask) - } - if n <= fnb { - if n == 0 { - f.b, f.nb = fb, fnb - if debugDecode { - fmt.Println("huffsym: n==0") - } - f.err = CorruptInputError(f.roffset) - return - } - fb = fb >> (n & regSizeMaskUint32) - fnb = fnb - n - v = int(chunk >> huffmanValueShift) - break - } - } - } - - var length int - switch { - case v < 256: - dict.writeByte(byte(v)) - if dict.availWrite() == 0 { - f.toRead = dict.readFlush() - f.step = huffmanStringsReader - f.stepState = stateInit - f.b, f.nb = fb, fnb - return - } - goto readLiteral - case v == 256: - f.b, f.nb = fb, fnb - f.finishBlock() - return - // otherwise, reference to older data - case v < 265: - length = v - (257 - 3) - case v < maxNumLit: - val := decCodeToLen[(v - 257)] - length = int(val.length) + 3 - n := uint(val.extra) - for fnb < n { - c, err := fr.ReadByte() - if err != nil { - f.b, f.nb = fb, fnb - if debugDecode { - fmt.Println("morebits n>0:", err) - } - f.err = err - return - } - f.roffset++ - fb |= uint32(c) << (fnb & regSizeMaskUint32) - fnb += 8 - } - length += int(fb & bitMask32[n]) - fb >>= n & regSizeMaskUint32 - fnb -= n - default: - if debugDecode { - fmt.Println(v, ">= maxNumLit") - } - f.err = CorruptInputError(f.roffset) - f.b, f.nb = fb, fnb - return - } - - var dist uint32 - if f.hd == nil { - for fnb < 5 { - c, err := fr.ReadByte() - if err != nil { - f.b, f.nb = fb, fnb - if debugDecode { - fmt.Println("morebits f.nb<5:", err) - } - f.err = err - return - } - f.roffset++ - fb |= uint32(c) << (fnb & regSizeMaskUint32) - fnb += 8 - } - dist = uint32(bits.Reverse8(uint8(fb & 0x1F << 3))) - fb >>= 5 - fnb -= 5 - } else { - // Since a huffmanDecoder can be empty or be composed of a degenerate tree - // with single element, huffSym must error on these two edge cases. In both - // cases, the chunks slice will be 0 for the invalid sequence, leading it - // satisfy the n == 0 check below. - n := uint(f.hd.maxRead) - // Optimization. Compiler isn't smart enough to keep f.b,f.nb in registers, - // but is smart enough to keep local variables in registers, so use nb and b, - // inline call to moreBits and reassign b,nb back to f on return. - for { - for fnb < n { - c, err := fr.ReadByte() - if err != nil { - f.b, f.nb = fb, fnb - f.err = noEOF(err) - return - } - f.roffset++ - fb |= uint32(c) << (fnb & regSizeMaskUint32) - fnb += 8 - } - chunk := f.hd.chunks[fb&(huffmanNumChunks-1)] - n = uint(chunk & huffmanCountMask) - if n > huffmanChunkBits { - chunk = f.hd.links[chunk>>huffmanValueShift][(fb>>huffmanChunkBits)&f.hd.linkMask] - n = uint(chunk & huffmanCountMask) - } - if n <= fnb { - if n == 0 { - f.b, f.nb = fb, fnb - if debugDecode { - fmt.Println("huffsym: n==0") - } - f.err = CorruptInputError(f.roffset) - return - } - fb = fb >> (n & regSizeMaskUint32) - fnb = fnb - n - dist = uint32(chunk >> huffmanValueShift) - break - } - } - } - - switch { - case dist < 4: - dist++ - case dist < maxNumDist: - nb := uint(dist-2) >> 1 - // have 1 bit in bottom of dist, need nb more. - extra := (dist & 1) << (nb & regSizeMaskUint32) - for fnb < nb { - c, err := fr.ReadByte() - if err != nil { - f.b, f.nb = fb, fnb - if debugDecode { - fmt.Println("morebits f.nb>= nb & regSizeMaskUint32 - fnb -= nb - dist = 1<<((nb+1)®SizeMaskUint32) + 1 + extra - // slower: dist = bitMask32[nb+1] + 2 + extra - default: - f.b, f.nb = fb, fnb - if debugDecode { - fmt.Println("dist too big:", dist, maxNumDist) - } - f.err = CorruptInputError(f.roffset) - return - } - - // No check on length; encoding can be prescient. - if dist > uint32(dict.histSize()) { - f.b, f.nb = fb, fnb - if debugDecode { - fmt.Println("dist > dict.histSize():", dist, dict.histSize()) - } - f.err = CorruptInputError(f.roffset) - return - } - - f.copyLen, f.copyDist = length, int(dist) - goto copyHistory - } - -copyHistory: - // Perform a backwards copy according to RFC section 3.2.3. - { - cnt := dict.tryWriteCopy(f.copyDist, f.copyLen) - if cnt == 0 { - cnt = dict.writeCopy(f.copyDist, f.copyLen) - } - f.copyLen -= cnt - - if dict.availWrite() == 0 || f.copyLen > 0 { - f.toRead = dict.readFlush() - f.step = huffmanStringsReader // We need to continue this work - f.stepState = stateDict - f.b, f.nb = fb, fnb - return - } - goto readLiteral - } - // Not reached -} - -// Decode a single Huffman block from f. -// hl and hd are the Huffman states for the lit/length values -// and the distance values, respectively. If hd == nil, using the -// fixed distance encoding associated with fixed Huffman blocks. -func (f *decompressor) huffmanGenericReader() { - const ( - stateInit = iota // Zero value must be stateInit - stateDict - ) - fr := f.r.(Reader) - - // Optimization. Compiler isn't smart enough to keep f.b,f.nb in registers, - // but is smart enough to keep local variables in registers, so use nb and b, - // inline call to moreBits and reassign b,nb back to f on return. - fnb, fb, dict := f.nb, f.b, &f.dict - - switch f.stepState { - case stateInit: - goto readLiteral - case stateDict: - goto copyHistory - } - -readLiteral: - // Read literal and/or (length, distance) according to RFC section 3.2.3. - { - var v int - { - // Inlined v, err := f.huffSym(f.hl) - // Since a huffmanDecoder can be empty or be composed of a degenerate tree - // with single element, huffSym must error on these two edge cases. In both - // cases, the chunks slice will be 0 for the invalid sequence, leading it - // satisfy the n == 0 check below. - n := uint(f.hl.maxRead) - for { - for fnb < n { - c, err := fr.ReadByte() - if err != nil { - f.b, f.nb = fb, fnb - f.err = noEOF(err) - return - } - f.roffset++ - fb |= uint32(c) << (fnb & regSizeMaskUint32) - fnb += 8 - } - chunk := f.hl.chunks[fb&(huffmanNumChunks-1)] - n = uint(chunk & huffmanCountMask) - if n > huffmanChunkBits { - chunk = f.hl.links[chunk>>huffmanValueShift][(fb>>huffmanChunkBits)&f.hl.linkMask] - n = uint(chunk & huffmanCountMask) - } - if n <= fnb { - if n == 0 { - f.b, f.nb = fb, fnb - if debugDecode { - fmt.Println("huffsym: n==0") - } - f.err = CorruptInputError(f.roffset) - return - } - fb = fb >> (n & regSizeMaskUint32) - fnb = fnb - n - v = int(chunk >> huffmanValueShift) - break - } - } - } - - var length int - switch { - case v < 256: - dict.writeByte(byte(v)) - if dict.availWrite() == 0 { - f.toRead = dict.readFlush() - f.step = huffmanGenericReader - f.stepState = stateInit - f.b, f.nb = fb, fnb - return - } - goto readLiteral - case v == 256: - f.b, f.nb = fb, fnb - f.finishBlock() - return - // otherwise, reference to older data - case v < 265: - length = v - (257 - 3) - case v < maxNumLit: - val := decCodeToLen[(v - 257)] - length = int(val.length) + 3 - n := uint(val.extra) - for fnb < n { - c, err := fr.ReadByte() - if err != nil { - f.b, f.nb = fb, fnb - if debugDecode { - fmt.Println("morebits n>0:", err) - } - f.err = err - return - } - f.roffset++ - fb |= uint32(c) << (fnb & regSizeMaskUint32) - fnb += 8 - } - length += int(fb & bitMask32[n]) - fb >>= n & regSizeMaskUint32 - fnb -= n - default: - if debugDecode { - fmt.Println(v, ">= maxNumLit") - } - f.err = CorruptInputError(f.roffset) - f.b, f.nb = fb, fnb - return - } - - var dist uint32 - if f.hd == nil { - for fnb < 5 { - c, err := fr.ReadByte() - if err != nil { - f.b, f.nb = fb, fnb - if debugDecode { - fmt.Println("morebits f.nb<5:", err) - } - f.err = err - return - } - f.roffset++ - fb |= uint32(c) << (fnb & regSizeMaskUint32) - fnb += 8 - } - dist = uint32(bits.Reverse8(uint8(fb & 0x1F << 3))) - fb >>= 5 - fnb -= 5 - } else { - // Since a huffmanDecoder can be empty or be composed of a degenerate tree - // with single element, huffSym must error on these two edge cases. In both - // cases, the chunks slice will be 0 for the invalid sequence, leading it - // satisfy the n == 0 check below. - n := uint(f.hd.maxRead) - // Optimization. Compiler isn't smart enough to keep f.b,f.nb in registers, - // but is smart enough to keep local variables in registers, so use nb and b, - // inline call to moreBits and reassign b,nb back to f on return. - for { - for fnb < n { - c, err := fr.ReadByte() - if err != nil { - f.b, f.nb = fb, fnb - f.err = noEOF(err) - return - } - f.roffset++ - fb |= uint32(c) << (fnb & regSizeMaskUint32) - fnb += 8 - } - chunk := f.hd.chunks[fb&(huffmanNumChunks-1)] - n = uint(chunk & huffmanCountMask) - if n > huffmanChunkBits { - chunk = f.hd.links[chunk>>huffmanValueShift][(fb>>huffmanChunkBits)&f.hd.linkMask] - n = uint(chunk & huffmanCountMask) - } - if n <= fnb { - if n == 0 { - f.b, f.nb = fb, fnb - if debugDecode { - fmt.Println("huffsym: n==0") - } - f.err = CorruptInputError(f.roffset) - return - } - fb = fb >> (n & regSizeMaskUint32) - fnb = fnb - n - dist = uint32(chunk >> huffmanValueShift) - break - } - } - } - - switch { - case dist < 4: - dist++ - case dist < maxNumDist: - nb := uint(dist-2) >> 1 - // have 1 bit in bottom of dist, need nb more. - extra := (dist & 1) << (nb & regSizeMaskUint32) - for fnb < nb { - c, err := fr.ReadByte() - if err != nil { - f.b, f.nb = fb, fnb - if debugDecode { - fmt.Println("morebits f.nb>= nb & regSizeMaskUint32 - fnb -= nb - dist = 1<<((nb+1)®SizeMaskUint32) + 1 + extra - // slower: dist = bitMask32[nb+1] + 2 + extra - default: - f.b, f.nb = fb, fnb - if debugDecode { - fmt.Println("dist too big:", dist, maxNumDist) - } - f.err = CorruptInputError(f.roffset) - return - } - - // No check on length; encoding can be prescient. - if dist > uint32(dict.histSize()) { - f.b, f.nb = fb, fnb - if debugDecode { - fmt.Println("dist > dict.histSize():", dist, dict.histSize()) - } - f.err = CorruptInputError(f.roffset) - return - } - - f.copyLen, f.copyDist = length, int(dist) - goto copyHistory - } - -copyHistory: - // Perform a backwards copy according to RFC section 3.2.3. - { - cnt := dict.tryWriteCopy(f.copyDist, f.copyLen) - if cnt == 0 { - cnt = dict.writeCopy(f.copyDist, f.copyLen) - } - f.copyLen -= cnt - - if dict.availWrite() == 0 || f.copyLen > 0 { - f.toRead = dict.readFlush() - f.step = huffmanGenericReader // We need to continue this work - f.stepState = stateDict - f.b, f.nb = fb, fnb - return - } - goto readLiteral - } - // Not reached -} - -func (f *decompressor) huffmanBlockDecoder() { - switch f.r.(type) { - case *bytes.Buffer: - f.huffmanBytesBuffer() - case *bytes.Reader: - f.huffmanBytesReader() - case *bufio.Reader: - f.huffmanBufioReader() - case *strings.Reader: - f.huffmanStringsReader() - case Reader: - f.huffmanGenericReader() - default: - f.huffmanGenericReader() - } -} diff --git a/internal/compress/flate/inflate_test.go b/internal/compress/flate/inflate_test.go deleted file mode 100644 index f163695f..00000000 --- a/internal/compress/flate/inflate_test.go +++ /dev/null @@ -1,301 +0,0 @@ -// Copyright 2014 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package flate - -import ( - "bytes" - "crypto/rand" - "io" - "os" - "strconv" - "strings" - "testing" -) - -func TestReset(t *testing.T) { - ss := []string{ - "lorem ipsum izzle fo rizzle", - "the quick brown fox jumped over", - } - - deflated := make([]bytes.Buffer, 2) - for i, s := range ss { - w, _ := NewWriter(&deflated[i], 1) - w.Write([]byte(s)) - w.Close() - } - - inflated := make([]bytes.Buffer, 2) - - f := NewReader(&deflated[0]) - io.Copy(&inflated[0], f) - f.(Resetter).Reset(&deflated[1], nil) - io.Copy(&inflated[1], f) - f.Close() - - for i, s := range ss { - if s != inflated[i].String() { - t.Errorf("inflated[%d]:\ngot %q\nwant %q", i, inflated[i].String(), s) - } - } -} - -func TestReaderTruncated(t *testing.T) { - vectors := []struct{ input, output string }{ - {"\x00", ""}, - {"\x00\f", ""}, - {"\x00\f\x00", ""}, - {"\x00\f\x00\xf3\xff", ""}, - {"\x00\f\x00\xf3\xffhello", "hello"}, - {"\x00\f\x00\xf3\xffhello, world", "hello, world"}, - {"\x02", ""}, - {"\xf2H\xcd", "He"}, - {"\xf2H͙0a\u0084\t", "Hel\x90\x90\x90\x90\x90"}, - {"\xf2H͙0a\u0084\t\x00", "Hel\x90\x90\x90\x90\x90"}, - } - - for i, v := range vectors { - r := strings.NewReader(v.input) - zr := NewReader(r) - b, err := io.ReadAll(zr) - if err != io.ErrUnexpectedEOF { - t.Errorf("test %d, error mismatch: got %v, want io.ErrUnexpectedEOF", i, err) - } - if string(b) != v.output { - t.Errorf("test %d, output mismatch: got %q, want %q", i, b, v.output) - } - } -} - -func TestResetDict(t *testing.T) { - dict := []byte("the lorem fox") - ss := []string{ - "lorem ipsum izzle fo rizzle", - "the quick brown fox jumped over", - } - - deflated := make([]bytes.Buffer, len(ss)) - for i, s := range ss { - w, _ := NewWriterDict(&deflated[i], DefaultCompression, dict) - w.Write([]byte(s)) - w.Close() - } - - inflated := make([]bytes.Buffer, len(ss)) - - f := NewReader(nil) - for i := range inflated { - f.(Resetter).Reset(&deflated[i], dict) - io.Copy(&inflated[i], f) - } - f.Close() - - for i, s := range ss { - if s != inflated[i].String() { - t.Errorf("inflated[%d]:\ngot %q\nwant %q", i, inflated[i].String(), s) - } - } -} - -// Tests ported from zlib/test/infcover.c -type infTest struct { - hex string - id string - n int -} - -var infTests = []infTest{ - {"0 0 0 0 0", "invalid stored block lengths", 1}, - {"3 0", "fixed", 0}, - {"6", "invalid block type", 1}, - {"1 1 0 fe ff 0", "stored", 0}, - {"fc 0 0", "too many length or distance symbols", 1}, - {"4 0 fe ff", "invalid code lengths set", 1}, - {"4 0 24 49 0", "invalid bit length repeat", 1}, - {"4 0 24 e9 ff ff", "invalid bit length repeat", 1}, - {"4 0 24 e9 ff 6d", "invalid code -- missing end-of-block", 1}, - {"4 80 49 92 24 49 92 24 71 ff ff 93 11 0", "invalid literal/lengths set", 1}, - {"4 80 49 92 24 49 92 24 f b4 ff ff c3 84", "invalid distances set", 1}, - {"4 c0 81 8 0 0 0 0 20 7f eb b 0 0", "invalid literal/length code", 1}, - {"2 7e ff ff", "invalid distance code", 1}, - {"c c0 81 0 0 0 0 0 90 ff 6b 4 0", "invalid distance too far back", 1}, - - // also trailer mismatch just in inflate() - {"1f 8b 8 0 0 0 0 0 0 0 3 0 0 0 0 1", "incorrect data check", -1}, - {"1f 8b 8 0 0 0 0 0 0 0 3 0 0 0 0 0 0 0 0 1", "incorrect length check", -1}, - {"5 c0 21 d 0 0 0 80 b0 fe 6d 2f 91 6c", "pull 17", 0}, - {"5 e0 81 91 24 cb b2 2c 49 e2 f 2e 8b 9a 47 56 9f fb fe ec d2 ff 1f", "long code", 0}, - {"ed c0 1 1 0 0 0 40 20 ff 57 1b 42 2c 4f", "length extra", 0}, - {"ed cf c1 b1 2c 47 10 c4 30 fa 6f 35 1d 1 82 59 3d fb be 2e 2a fc f c", "long distance and extra", 0}, - {"ed c0 81 0 0 0 0 80 a0 fd a9 17 a9 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 6", "window end", 0}, -} - -func TestInflate(t *testing.T) { - for _, test := range infTests { - hex := strings.Split(test.hex, " ") - data := make([]byte, len(hex)) - for i, h := range hex { - b, _ := strconv.ParseInt(h, 16, 32) - data[i] = byte(b) - } - buf := bytes.NewReader(data) - r := NewReader(buf) - - _, err := io.Copy(io.Discard, r) - if (test.n == 0 && err == nil) || (test.n != 0 && err != nil) { - t.Logf("%q: OK:", test.id) - t.Logf(" - got %v", err) - continue - } - - if test.n == 0 && err != nil { - t.Errorf("%q: Expected no error, but got %v", test.id, err) - continue - } - - if test.n != 0 && err == nil { - t.Errorf("%q:Expected an error, but got none", test.id) - continue - } - t.Fatal(test.n, err) - } - - for _, test := range infOutTests { - hex := strings.Split(test.hex, " ") - data := make([]byte, len(hex)) - for i, h := range hex { - b, _ := strconv.ParseInt(h, 16, 32) - data[i] = byte(b) - } - buf := bytes.NewReader(data) - r := NewReader(buf) - - _, err := io.Copy(io.Discard, r) - if test.err == (err != nil) { - t.Logf("%q: OK:", test.id) - t.Logf(" - got %v", err) - continue - } - - if test.err == false && err != nil { - t.Errorf("%q: Expected no error, but got %v", test.id, err) - continue - } - - if test.err && err == nil { - t.Errorf("%q: Expected an error, but got none", test.id) - continue - } - t.Fatal(test.err, err) - } -} - -// Tests ported from zlib/test/infcover.c -// Since zlib inflate is push (writer) instead of pull (reader) -// some of the window size tests have been removed, since they -// are irrelevant. -type infOutTest struct { - hex string - id string - step int - win int - length int - err bool -} - -var infOutTests = []infOutTest{ - {"2 8 20 80 0 3 0", "inflate_fast TYPE return", 0, -15, 258, false}, - {"63 18 5 40 c 0", "window wrap", 3, -8, 300, false}, - {"e5 e0 81 ad 6d cb b2 2c c9 01 1e 59 63 ae 7d ee fb 4d fd b5 35 41 68 ff 7f 0f 0 0 0", "fast length extra bits", 0, -8, 258, true}, - {"25 fd 81 b5 6d 59 b6 6a 49 ea af 35 6 34 eb 8c b9 f6 b9 1e ef 67 49 50 fe ff ff 3f 0 0", "fast distance extra bits", 0, -8, 258, true}, - {"3 7e 0 0 0 0 0", "fast invalid distance code", 0, -8, 258, true}, - {"1b 7 0 0 0 0 0", "fast invalid literal/length code", 0, -8, 258, true}, - {"d c7 1 ae eb 38 c 4 41 a0 87 72 de df fb 1f b8 36 b1 38 5d ff ff 0", "fast 2nd level codes and too far back", 0, -8, 258, true}, - {"63 18 5 8c 10 8 0 0 0 0", "very common case", 0, -8, 259, false}, - {"63 60 60 18 c9 0 8 18 18 18 26 c0 28 0 29 0 0 0", "contiguous and wrap around window", 6, -8, 259, false}, - {"63 0 3 0 0 0 0 0", "copy direct from output", 0, -8, 259, false}, - {"1f 8b 0 0", "bad gzip method", 0, 31, 0, true}, - {"1f 8b 8 80", "bad gzip flags", 0, 31, 0, true}, - {"77 85", "bad zlib method", 0, 15, 0, true}, - {"78 9c", "bad zlib window size", 0, 8, 0, true}, - {"1f 8b 8 1e 0 0 0 0 0 0 1 0 0 0 0 0 0", "bad header crc", 0, 47, 1, true}, - {"1f 8b 8 2 0 0 0 0 0 0 1d 26 3 0 0 0 0 0 0 0 0 0", "check gzip length", 0, 47, 0, true}, - {"78 90", "bad zlib header check", 0, 47, 0, true}, - {"8 b8 0 0 0 1", "need dictionary", 0, 8, 0, true}, - {"63 18 68 30 d0 0 0", "force split window update", 4, -8, 259, false}, - {"3 0", "use fixed blocks", 0, -15, 1, false}, - {"", "bad window size", 0, 1, 0, true}, -} - -func TestWriteTo(t *testing.T) { - input := make([]byte, 100000) - n, err := rand.Read(input) - if err != nil { - t.Fatal(err) - } - if n != len(input) { - t.Fatal("did not fill buffer") - } - compressed := &bytes.Buffer{} - w, err := NewWriter(compressed, -2) - if err != nil { - t.Fatal(err) - } - n, err = w.Write(input) - if err != nil { - t.Fatal(err) - } - if n != len(input) { - t.Fatal("did not fill buffer") - } - w.Close() - buf := compressed.Bytes() - - dec := NewReader(bytes.NewBuffer(buf)) - // ReadAll does not use WriteTo, but we wrap it in a NopCloser to be sure. - readall, err := io.ReadAll(io.NopCloser(dec)) - if err != nil { - t.Fatal(err) - } - if len(readall) != len(input) { - t.Fatal("did not decompress everything") - } - - dec = NewReader(bytes.NewBuffer(buf)) - wtbuf := &bytes.Buffer{} - written, err := dec.(io.WriterTo).WriteTo(wtbuf) - if err != nil { - t.Fatal(err) - } - if written != int64(len(input)) { - t.Error("Returned length did not match, expected", len(input), "got", written) - } - if wtbuf.Len() != len(input) { - t.Error("Actual Length did not match, expected", len(input), "got", wtbuf.Len()) - } - if !bytes.Equal(wtbuf.Bytes(), input) { - t.Fatal("output did not match input") - } -} - -func TestReaderPartialBlock(t *testing.T) { - data, err := os.ReadFile("testdata/partial-block") - if err != nil { - t.Error(err) - } - - r := NewReaderOpts(bytes.NewReader(data), WithPartialBlock()) - rb := make([]byte, 32) - n, err := r.Read(rb) - if err != nil { - t.Fatalf("Read: %v", err) - } - - expected := "hello, world" - actual := string(rb[:n]) - if expected != actual { - t.Fatalf("expected: %v, got: %v", expected, actual) - } -} diff --git a/internal/compress/flate/level1.go b/internal/compress/flate/level1.go deleted file mode 100644 index 41c312e8..00000000 --- a/internal/compress/flate/level1.go +++ /dev/null @@ -1,215 +0,0 @@ -package flate - -import ( - "fmt" - - "codeberg.org/lindenii/furgit/internal/compress/internal/le" -) - -// fastGen maintains the table for matches, -// and the previous byte block for level 2. -// This is the generic implementation. -type fastEncL1 struct { - fastGen - table [tableSize]tableEntry -} - -// EncodeL1 uses a similar algorithm to level 1 -func (e *fastEncL1) Encode(dst *tokens, src []byte) { - const ( - inputMargin = 12 - 1 - minNonLiteralBlockSize = 1 + 1 + inputMargin - hashBytes = 5 - ) - if debugDeflate && e.cur < 0 { - panic(fmt.Sprint("e.cur < 0: ", e.cur)) - } - - // Protect against e.cur wraparound. - for e.cur >= bufferReset { - if len(e.hist) == 0 { - for i := range e.table[:] { - e.table[i] = tableEntry{} - } - e.cur = maxMatchOffset - break - } - // Shift down everything in the table that isn't already too far away. - minOff := e.cur + int32(len(e.hist)) - maxMatchOffset - for i := range e.table[:] { - v := e.table[i].offset - if v <= minOff { - v = 0 - } else { - v = v - e.cur + maxMatchOffset - } - e.table[i].offset = v - } - e.cur = maxMatchOffset - } - - s := e.addBlock(src) - - // This check isn't in the Snappy implementation, but there, the caller - // instead of the callee handles this case. - if len(src) < minNonLiteralBlockSize { - // We do not fill the token table. - // This will be picked up by caller. - dst.n = uint16(len(src)) - return - } - - // Override src - src = e.hist - nextEmit := s - - // sLimit is when to stop looking for offset/length copies. The inputMargin - // lets us use a fast path for emitLiteral in the main loop, while we are - // looking for copies. - sLimit := int32(len(src) - inputMargin) - - // nextEmit is where in src the next emitLiteral should start from. - cv := load6432(src, s) - - for { - const skipLog = 5 - const doEvery = 2 - - nextS := s - var candidate tableEntry - var t int32 - for { - nextHash := hashLen(cv, tableBits, hashBytes) - candidate = e.table[nextHash] - nextS = s + doEvery + (s-nextEmit)>>skipLog - if nextS > sLimit { - goto emitRemainder - } - - now := load6432(src, nextS) - e.table[nextHash] = tableEntry{offset: s + e.cur} - nextHash = hashLen(now, tableBits, hashBytes) - t = candidate.offset - e.cur - if s-t < maxMatchOffset && uint32(cv) == load3232(src, t) { - e.table[nextHash] = tableEntry{offset: nextS + e.cur} - break - } - - // Do one right away... - cv = now - s = nextS - nextS++ - candidate = e.table[nextHash] - now >>= 8 - e.table[nextHash] = tableEntry{offset: s + e.cur} - - t = candidate.offset - e.cur - if s-t < maxMatchOffset && uint32(cv) == load3232(src, t) { - e.table[nextHash] = tableEntry{offset: nextS + e.cur} - break - } - cv = now - s = nextS - } - - // A 4-byte match has been found. We'll later see if more than 4 bytes - // match. But, prior to the match, src[nextEmit:s] are unmatched. Emit - // them as literal bytes. - for { - // Invariant: we have a 4-byte match at s, and no need to emit any - // literal bytes prior to s. - - // Extend the 4-byte match as long as possible. - l := e.matchlenLong(int(s+4), int(t+4), src) + 4 - - // Extend backwards - for t > 0 && s > nextEmit && le.Load8(src, t-1) == le.Load8(src, s-1) { - s-- - t-- - l++ - } - if nextEmit < s { - if false { - emitLiteral(dst, src[nextEmit:s]) - } else { - for _, v := range src[nextEmit:s] { - dst.tokens[dst.n] = token(v) - dst.litHist[v]++ - dst.n++ - } - } - } - - // Save the match found - if false { - dst.AddMatchLong(l, uint32(s-t-baseMatchOffset)) - } else { - // Inlined... - xoffset := uint32(s - t - baseMatchOffset) - xlength := l - oc := offsetCode(xoffset) - xoffset |= oc << 16 - for xlength > 0 { - xl := xlength - if xl > 258 { - if xl > 258+baseMatchLength { - xl = 258 - } else { - xl = 258 - baseMatchLength - } - } - xlength -= xl - xl -= baseMatchLength - dst.extraHist[lengthCodes1[uint8(xl)]]++ - dst.offHist[oc]++ - dst.tokens[dst.n] = token(matchType | uint32(xl)<= s { - s = nextS + 1 - } - if s >= sLimit { - // Index first pair after match end. - if int(s+l+8) < len(src) { - cv := load6432(src, s) - e.table[hashLen(cv, tableBits, hashBytes)] = tableEntry{offset: s + e.cur} - } - goto emitRemainder - } - - // We could immediately start working at s now, but to improve - // compression we first update the hash table at s-2 and at s. If - // another emitCopy is not our next move, also calculate nextHash - // at s+1. At least on GOARCH=amd64, these three hash calculations - // are faster as one load64 call (with some shifts) instead of - // three load32 calls. - x := load6432(src, s-2) - o := e.cur + s - 2 - prevHash := hashLen(x, tableBits, hashBytes) - e.table[prevHash] = tableEntry{offset: o} - x >>= 16 - currHash := hashLen(x, tableBits, hashBytes) - candidate = e.table[currHash] - e.table[currHash] = tableEntry{offset: o + 2} - - t = candidate.offset - e.cur - if s-t > maxMatchOffset || uint32(x) != load3232(src, t) { - cv = x >> 8 - s++ - break - } - } - } - -emitRemainder: - if int(nextEmit) < len(src) { - // If nothing was added, don't encode literals. - if dst.n == 0 { - return - } - emitLiteral(dst, src[nextEmit:]) - } -} diff --git a/internal/compress/flate/level2.go b/internal/compress/flate/level2.go deleted file mode 100644 index c8d047f2..00000000 --- a/internal/compress/flate/level2.go +++ /dev/null @@ -1,214 +0,0 @@ -package flate - -import "fmt" - -// fastGen maintains the table for matches, -// and the previous byte block for level 2. -// This is the generic implementation. -type fastEncL2 struct { - fastGen - table [bTableSize]tableEntry -} - -// EncodeL2 uses a similar algorithm to level 1, but is capable -// of matching across blocks giving better compression at a small slowdown. -func (e *fastEncL2) Encode(dst *tokens, src []byte) { - const ( - inputMargin = 12 - 1 - minNonLiteralBlockSize = 1 + 1 + inputMargin - hashBytes = 5 - ) - - if debugDeflate && e.cur < 0 { - panic(fmt.Sprint("e.cur < 0: ", e.cur)) - } - - // Protect against e.cur wraparound. - for e.cur >= bufferReset { - if len(e.hist) == 0 { - for i := range e.table[:] { - e.table[i] = tableEntry{} - } - e.cur = maxMatchOffset - break - } - // Shift down everything in the table that isn't already too far away. - minOff := e.cur + int32(len(e.hist)) - maxMatchOffset - for i := range e.table[:] { - v := e.table[i].offset - if v <= minOff { - v = 0 - } else { - v = v - e.cur + maxMatchOffset - } - e.table[i].offset = v - } - e.cur = maxMatchOffset - } - - s := e.addBlock(src) - - // This check isn't in the Snappy implementation, but there, the caller - // instead of the callee handles this case. - if len(src) < minNonLiteralBlockSize { - // We do not fill the token table. - // This will be picked up by caller. - dst.n = uint16(len(src)) - return - } - - // Override src - src = e.hist - nextEmit := s - - // sLimit is when to stop looking for offset/length copies. The inputMargin - // lets us use a fast path for emitLiteral in the main loop, while we are - // looking for copies. - sLimit := int32(len(src) - inputMargin) - - // nextEmit is where in src the next emitLiteral should start from. - cv := load6432(src, s) - for { - // When should we start skipping if we haven't found matches in a long while. - const skipLog = 5 - const doEvery = 2 - - nextS := s - var candidate tableEntry - for { - nextHash := hashLen(cv, bTableBits, hashBytes) - s = nextS - nextS = s + doEvery + (s-nextEmit)>>skipLog - if nextS > sLimit { - goto emitRemainder - } - candidate = e.table[nextHash] - now := load6432(src, nextS) - e.table[nextHash] = tableEntry{offset: s + e.cur} - nextHash = hashLen(now, bTableBits, hashBytes) - - offset := s - (candidate.offset - e.cur) - if offset < maxMatchOffset && uint32(cv) == load3232(src, candidate.offset-e.cur) { - e.table[nextHash] = tableEntry{offset: nextS + e.cur} - break - } - - // Do one right away... - cv = now - s = nextS - nextS++ - candidate = e.table[nextHash] - now >>= 8 - e.table[nextHash] = tableEntry{offset: s + e.cur} - - offset = s - (candidate.offset - e.cur) - if offset < maxMatchOffset && uint32(cv) == load3232(src, candidate.offset-e.cur) { - break - } - cv = now - } - - // A 4-byte match has been found. We'll later see if more than 4 bytes - // match. But, prior to the match, src[nextEmit:s] are unmatched. Emit - // them as literal bytes. - - // Call emitCopy, and then see if another emitCopy could be our next - // move. Repeat until we find no match for the input immediately after - // what was consumed by the last emitCopy call. - // - // If we exit this loop normally then we need to call emitLiteral next, - // though we don't yet know how big the literal will be. We handle that - // by proceeding to the next iteration of the main loop. We also can - // exit this loop via goto if we get close to exhausting the input. - for { - // Invariant: we have a 4-byte match at s, and no need to emit any - // literal bytes prior to s. - - // Extend the 4-byte match as long as possible. - t := candidate.offset - e.cur - l := e.matchlenLong(int(s+4), int(t+4), src) + 4 - - // Extend backwards - for t > 0 && s > nextEmit && src[t-1] == src[s-1] { - s-- - t-- - l++ - } - if nextEmit < s { - if false { - emitLiteral(dst, src[nextEmit:s]) - } else { - for _, v := range src[nextEmit:s] { - dst.tokens[dst.n] = token(v) - dst.litHist[v]++ - dst.n++ - } - } - } - - dst.AddMatchLong(l, uint32(s-t-baseMatchOffset)) - s += l - nextEmit = s - if nextS >= s { - s = nextS + 1 - } - - if s >= sLimit { - // Index first pair after match end. - if int(s+l+8) < len(src) { - cv := load6432(src, s) - e.table[hashLen(cv, bTableBits, hashBytes)] = tableEntry{offset: s + e.cur} - } - goto emitRemainder - } - - // Store every second hash in-between, but offset by 1. - for i := s - l + 2; i < s-5; i += 7 { - x := load6432(src, i) - nextHash := hashLen(x, bTableBits, hashBytes) - e.table[nextHash] = tableEntry{offset: e.cur + i} - // Skip one - x >>= 16 - nextHash = hashLen(x, bTableBits, hashBytes) - e.table[nextHash] = tableEntry{offset: e.cur + i + 2} - // Skip one - x >>= 16 - nextHash = hashLen(x, bTableBits, hashBytes) - e.table[nextHash] = tableEntry{offset: e.cur + i + 4} - } - - // We could immediately start working at s now, but to improve - // compression we first update the hash table at s-2 to s. If - // another emitCopy is not our next move, also calculate nextHash - // at s+1. At least on GOARCH=amd64, these three hash calculations - // are faster as one load64 call (with some shifts) instead of - // three load32 calls. - x := load6432(src, s-2) - o := e.cur + s - 2 - prevHash := hashLen(x, bTableBits, hashBytes) - prevHash2 := hashLen(x>>8, bTableBits, hashBytes) - e.table[prevHash] = tableEntry{offset: o} - e.table[prevHash2] = tableEntry{offset: o + 1} - currHash := hashLen(x>>16, bTableBits, hashBytes) - candidate = e.table[currHash] - e.table[currHash] = tableEntry{offset: o + 2} - - offset := s - (candidate.offset - e.cur) - if offset > maxMatchOffset || uint32(x>>16) != load3232(src, candidate.offset-e.cur) { - cv = x >> 24 - s++ - break - } - } - } - -emitRemainder: - if int(nextEmit) < len(src) { - // If nothing was added, don't encode literals. - if dst.n == 0 { - return - } - - emitLiteral(dst, src[nextEmit:]) - } -} diff --git a/internal/compress/flate/level3.go b/internal/compress/flate/level3.go deleted file mode 100644 index 2cef0290..00000000 --- a/internal/compress/flate/level3.go +++ /dev/null @@ -1,242 +0,0 @@ -package flate - -import "fmt" - -// fastEncL3 -type fastEncL3 struct { - fastGen - table [1 << 16]tableEntryPrev -} - -// Encode uses a similar algorithm to level 2, will check up to two candidates. -func (e *fastEncL3) Encode(dst *tokens, src []byte) { - const ( - inputMargin = 12 - 1 - minNonLiteralBlockSize = 1 + 1 + inputMargin - tableBits = 16 - tableSize = 1 << tableBits - hashBytes = 5 - ) - - if debugDeflate && e.cur < 0 { - panic(fmt.Sprint("e.cur < 0: ", e.cur)) - } - - // Protect against e.cur wraparound. - for e.cur >= bufferReset { - if len(e.hist) == 0 { - for i := range e.table[:] { - e.table[i] = tableEntryPrev{} - } - e.cur = maxMatchOffset - break - } - // Shift down everything in the table that isn't already too far away. - minOff := e.cur + int32(len(e.hist)) - maxMatchOffset - for i := range e.table[:] { - v := e.table[i] - if v.Cur.offset <= minOff { - v.Cur.offset = 0 - } else { - v.Cur.offset = v.Cur.offset - e.cur + maxMatchOffset - } - if v.Prev.offset <= minOff { - v.Prev.offset = 0 - } else { - v.Prev.offset = v.Prev.offset - e.cur + maxMatchOffset - } - e.table[i] = v - } - e.cur = maxMatchOffset - } - - s := e.addBlock(src) - - // Skip if too small. - if len(src) < minNonLiteralBlockSize { - // We do not fill the token table. - // This will be picked up by caller. - dst.n = uint16(len(src)) - return - } - - // Override src - src = e.hist - nextEmit := s - - // sLimit is when to stop looking for offset/length copies. The inputMargin - // lets us use a fast path for emitLiteral in the main loop, while we are - // looking for copies. - sLimit := int32(len(src) - inputMargin) - - // nextEmit is where in src the next emitLiteral should start from. - cv := load6432(src, s) - for { - const skipLog = 7 - nextS := s - var candidate tableEntry - for { - nextHash := hashLen(cv, tableBits, hashBytes) - s = nextS - nextS = s + 1 + (s-nextEmit)>>skipLog - if nextS > sLimit { - goto emitRemainder - } - candidates := e.table[nextHash] - now := load6432(src, nextS) - - // Safe offset distance until s + 4... - minOffset := e.cur + s - (maxMatchOffset - 4) - e.table[nextHash] = tableEntryPrev{Prev: candidates.Cur, Cur: tableEntry{offset: s + e.cur}} - - // Check both candidates - candidate = candidates.Cur - if candidate.offset < minOffset { - cv = now - // Previous will also be invalid, we have nothing. - continue - } - - if uint32(cv) == load3232(src, candidate.offset-e.cur) { - if candidates.Prev.offset < minOffset || uint32(cv) != load3232(src, candidates.Prev.offset-e.cur) { - break - } - // Both match and are valid, pick longest. - offset := s - (candidate.offset - e.cur) - o2 := s - (candidates.Prev.offset - e.cur) - l1, l2 := matchLen(src[s+4:], src[s-offset+4:]), matchLen(src[s+4:], src[s-o2+4:]) - if l2 > l1 { - candidate = candidates.Prev - } - break - } else { - // We only check if value mismatches. - // Offset will always be invalid in other cases. - candidate = candidates.Prev - if candidate.offset > minOffset && uint32(cv) == load3232(src, candidate.offset-e.cur) { - break - } - } - cv = now - } - - // Call emitCopy, and then see if another emitCopy could be our next - // move. Repeat until we find no match for the input immediately after - // what was consumed by the last emitCopy call. - // - // If we exit this loop normally then we need to call emitLiteral next, - // though we don't yet know how big the literal will be. We handle that - // by proceeding to the next iteration of the main loop. We also can - // exit this loop via goto if we get close to exhausting the input. - for { - // Invariant: we have a 4-byte match at s, and no need to emit any - // literal bytes prior to s. - - // Extend the 4-byte match as long as possible. - // - t := candidate.offset - e.cur - l := e.matchlenLong(int(s+4), int(t+4), src) + 4 - - // Extend backwards - for t > 0 && s > nextEmit && src[t-1] == src[s-1] { - s-- - t-- - l++ - } - if nextEmit < s { - if false { - emitLiteral(dst, src[nextEmit:s]) - } else { - for _, v := range src[nextEmit:s] { - dst.tokens[dst.n] = token(v) - dst.litHist[v]++ - dst.n++ - } - } - } - - dst.AddMatchLong(l, uint32(s-t-baseMatchOffset)) - s += l - nextEmit = s - if nextS >= s { - s = nextS + 1 - } - - if s >= sLimit { - t += l - // Index first pair after match end. - if int(t+8) < len(src) && t > 0 { - cv = load6432(src, t) - nextHash := hashLen(cv, tableBits, hashBytes) - e.table[nextHash] = tableEntryPrev{ - Prev: e.table[nextHash].Cur, - Cur: tableEntry{offset: e.cur + t}, - } - } - goto emitRemainder - } - - // Store every 5th hash in-between. - for i := s - l + 2; i < s-5; i += 6 { - nextHash := hashLen(load6432(src, i), tableBits, hashBytes) - e.table[nextHash] = tableEntryPrev{ - Prev: e.table[nextHash].Cur, - Cur: tableEntry{offset: e.cur + i}, - } - } - // We could immediately start working at s now, but to improve - // compression we first update the hash table at s-2 to s. - x := load6432(src, s-2) - prevHash := hashLen(x, tableBits, hashBytes) - - e.table[prevHash] = tableEntryPrev{ - Prev: e.table[prevHash].Cur, - Cur: tableEntry{offset: e.cur + s - 2}, - } - x >>= 8 - prevHash = hashLen(x, tableBits, hashBytes) - - e.table[prevHash] = tableEntryPrev{ - Prev: e.table[prevHash].Cur, - Cur: tableEntry{offset: e.cur + s - 1}, - } - x >>= 8 - currHash := hashLen(x, tableBits, hashBytes) - candidates := e.table[currHash] - cv = x - e.table[currHash] = tableEntryPrev{ - Prev: candidates.Cur, - Cur: tableEntry{offset: s + e.cur}, - } - - // Check both candidates - candidate = candidates.Cur - minOffset := e.cur + s - (maxMatchOffset - 4) - - if candidate.offset > minOffset { - if uint32(cv) == load3232(src, candidate.offset-e.cur) { - // Found a match... - continue - } - candidate = candidates.Prev - if candidate.offset > minOffset && uint32(cv) == load3232(src, candidate.offset-e.cur) { - // Match at prev... - continue - } - } - cv = x >> 8 - s++ - break - } - } - -emitRemainder: - if int(nextEmit) < len(src) { - // If nothing was added, don't encode literals. - if dst.n == 0 { - return - } - - emitLiteral(dst, src[nextEmit:]) - } -} diff --git a/internal/compress/flate/level4.go b/internal/compress/flate/level4.go deleted file mode 100644 index 88509e19..00000000 --- a/internal/compress/flate/level4.go +++ /dev/null @@ -1,221 +0,0 @@ -package flate - -import "fmt" - -type fastEncL4 struct { - fastGen - table [tableSize]tableEntry - bTable [tableSize]tableEntry -} - -func (e *fastEncL4) Encode(dst *tokens, src []byte) { - const ( - inputMargin = 12 - 1 - minNonLiteralBlockSize = 1 + 1 + inputMargin - hashShortBytes = 4 - ) - if debugDeflate && e.cur < 0 { - panic(fmt.Sprint("e.cur < 0: ", e.cur)) - } - // Protect against e.cur wraparound. - for e.cur >= bufferReset { - if len(e.hist) == 0 { - for i := range e.table[:] { - e.table[i] = tableEntry{} - } - for i := range e.bTable[:] { - e.bTable[i] = tableEntry{} - } - e.cur = maxMatchOffset - break - } - // Shift down everything in the table that isn't already too far away. - minOff := e.cur + int32(len(e.hist)) - maxMatchOffset - for i := range e.table[:] { - v := e.table[i].offset - if v <= minOff { - v = 0 - } else { - v = v - e.cur + maxMatchOffset - } - e.table[i].offset = v - } - for i := range e.bTable[:] { - v := e.bTable[i].offset - if v <= minOff { - v = 0 - } else { - v = v - e.cur + maxMatchOffset - } - e.bTable[i].offset = v - } - e.cur = maxMatchOffset - } - - s := e.addBlock(src) - - // This check isn't in the Snappy implementation, but there, the caller - // instead of the callee handles this case. - if len(src) < minNonLiteralBlockSize { - // We do not fill the token table. - // This will be picked up by caller. - dst.n = uint16(len(src)) - return - } - - // Override src - src = e.hist - nextEmit := s - - // sLimit is when to stop looking for offset/length copies. The inputMargin - // lets us use a fast path for emitLiteral in the main loop, while we are - // looking for copies. - sLimit := int32(len(src) - inputMargin) - - // nextEmit is where in src the next emitLiteral should start from. - cv := load6432(src, s) - for { - const skipLog = 6 - const doEvery = 1 - - nextS := s - var t int32 - for { - nextHashS := hashLen(cv, tableBits, hashShortBytes) - nextHashL := hash7(cv, tableBits) - - s = nextS - nextS = s + doEvery + (s-nextEmit)>>skipLog - if nextS > sLimit { - goto emitRemainder - } - // Fetch a short+long candidate - sCandidate := e.table[nextHashS] - lCandidate := e.bTable[nextHashL] - next := load6432(src, nextS) - entry := tableEntry{offset: s + e.cur} - e.table[nextHashS] = entry - e.bTable[nextHashL] = entry - - t = lCandidate.offset - e.cur - if s-t < maxMatchOffset && uint32(cv) == load3232(src, t) { - // We got a long match. Use that. - break - } - - t = sCandidate.offset - e.cur - if s-t < maxMatchOffset && uint32(cv) == load3232(src, t) { - // Found a 4 match... - lCandidate = e.bTable[hash7(next, tableBits)] - - // If the next long is a candidate, check if we should use that instead... - lOff := lCandidate.offset - e.cur - if nextS-lOff < maxMatchOffset && load3232(src, lOff) == uint32(next) { - l1, l2 := matchLen(src[s+4:], src[t+4:]), matchLen(src[nextS+4:], src[nextS-lOff+4:]) - if l2 > l1 { - s = nextS - t = lCandidate.offset - e.cur - } - } - break - } - cv = next - } - - // A 4-byte match has been found. We'll later see if more than 4 bytes - // match. But, prior to the match, src[nextEmit:s] are unmatched. Emit - // them as literal bytes. - - // Extend the 4-byte match as long as possible. - l := e.matchlenLong(int(s+4), int(t+4), src) + 4 - - // Extend backwards - for t > 0 && s > nextEmit && src[t-1] == src[s-1] { - s-- - t-- - l++ - } - if nextEmit < s { - if false { - emitLiteral(dst, src[nextEmit:s]) - } else { - for _, v := range src[nextEmit:s] { - dst.tokens[dst.n] = token(v) - dst.litHist[v]++ - dst.n++ - } - } - } - if debugDeflate { - if t >= s { - panic("s-t") - } - if (s - t) > maxMatchOffset { - panic(fmt.Sprintln("mmo", t)) - } - if l < baseMatchLength { - panic("bml") - } - } - - dst.AddMatchLong(l, uint32(s-t-baseMatchOffset)) - s += l - nextEmit = s - if nextS >= s { - s = nextS + 1 - } - - if s >= sLimit { - // Index first pair after match end. - if int(s+8) < len(src) { - cv := load6432(src, s) - e.table[hashLen(cv, tableBits, hashShortBytes)] = tableEntry{offset: s + e.cur} - e.bTable[hash7(cv, tableBits)] = tableEntry{offset: s + e.cur} - } - goto emitRemainder - } - - // Store every 3rd hash in-between - if true { - i := nextS - if i < s-1 { - cv := load6432(src, i) - t := tableEntry{offset: i + e.cur} - t2 := tableEntry{offset: t.offset + 1} - e.bTable[hash7(cv, tableBits)] = t - e.bTable[hash7(cv>>8, tableBits)] = t2 - e.table[hashLen(cv>>8, tableBits, hashShortBytes)] = t2 - - i += 3 - for ; i < s-1; i += 3 { - cv := load6432(src, i) - t := tableEntry{offset: i + e.cur} - t2 := tableEntry{offset: t.offset + 1} - e.bTable[hash7(cv, tableBits)] = t - e.bTable[hash7(cv>>8, tableBits)] = t2 - e.table[hashLen(cv>>8, tableBits, hashShortBytes)] = t2 - } - } - } - - // We could immediately start working at s now, but to improve - // compression we first update the hash table at s-1 and at s. - x := load6432(src, s-1) - o := e.cur + s - 1 - prevHashS := hashLen(x, tableBits, hashShortBytes) - prevHashL := hash7(x, tableBits) - e.table[prevHashS] = tableEntry{offset: o} - e.bTable[prevHashL] = tableEntry{offset: o} - cv = x >> 8 - } - -emitRemainder: - if int(nextEmit) < len(src) { - // If nothing was added, don't encode literals. - if dst.n == 0 { - return - } - - emitLiteral(dst, src[nextEmit:]) - } -} diff --git a/internal/compress/flate/level5.go b/internal/compress/flate/level5.go deleted file mode 100644 index a22ad7d1..00000000 --- a/internal/compress/flate/level5.go +++ /dev/null @@ -1,705 +0,0 @@ -package flate - -import "fmt" - -type fastEncL5 struct { - fastGen - table [tableSize]tableEntry - bTable [tableSize]tableEntryPrev -} - -func (e *fastEncL5) Encode(dst *tokens, src []byte) { - const ( - inputMargin = 12 - 1 - minNonLiteralBlockSize = 1 + 1 + inputMargin - hashShortBytes = 4 - ) - if debugDeflate && e.cur < 0 { - panic(fmt.Sprint("e.cur < 0: ", e.cur)) - } - - // Protect against e.cur wraparound. - for e.cur >= bufferReset { - if len(e.hist) == 0 { - for i := range e.table[:] { - e.table[i] = tableEntry{} - } - for i := range e.bTable[:] { - e.bTable[i] = tableEntryPrev{} - } - e.cur = maxMatchOffset - break - } - // Shift down everything in the table that isn't already too far away. - minOff := e.cur + int32(len(e.hist)) - maxMatchOffset - for i := range e.table[:] { - v := e.table[i].offset - if v <= minOff { - v = 0 - } else { - v = v - e.cur + maxMatchOffset - } - e.table[i].offset = v - } - for i := range e.bTable[:] { - v := e.bTable[i] - if v.Cur.offset <= minOff { - v.Cur.offset = 0 - v.Prev.offset = 0 - } else { - v.Cur.offset = v.Cur.offset - e.cur + maxMatchOffset - if v.Prev.offset <= minOff { - v.Prev.offset = 0 - } else { - v.Prev.offset = v.Prev.offset - e.cur + maxMatchOffset - } - } - e.bTable[i] = v - } - e.cur = maxMatchOffset - } - - s := e.addBlock(src) - - // This check isn't in the Snappy implementation, but there, the caller - // instead of the callee handles this case. - if len(src) < minNonLiteralBlockSize { - // We do not fill the token table. - // This will be picked up by caller. - dst.n = uint16(len(src)) - return - } - - // Override src - src = e.hist - nextEmit := s - - // sLimit is when to stop looking for offset/length copies. The inputMargin - // lets us use a fast path for emitLiteral in the main loop, while we are - // looking for copies. - sLimit := int32(len(src) - inputMargin) - - // nextEmit is where in src the next emitLiteral should start from. - cv := load6432(src, s) - for { - const skipLog = 6 - const doEvery = 1 - - nextS := s - var l int32 - var t int32 - for { - nextHashS := hashLen(cv, tableBits, hashShortBytes) - nextHashL := hash7(cv, tableBits) - - s = nextS - nextS = s + doEvery + (s-nextEmit)>>skipLog - if nextS > sLimit { - goto emitRemainder - } - // Fetch a short+long candidate - sCandidate := e.table[nextHashS] - lCandidate := e.bTable[nextHashL] - next := load6432(src, nextS) - entry := tableEntry{offset: s + e.cur} - e.table[nextHashS] = entry - eLong := &e.bTable[nextHashL] - eLong.Cur, eLong.Prev = entry, eLong.Cur - - nextHashS = hashLen(next, tableBits, hashShortBytes) - nextHashL = hash7(next, tableBits) - - t = lCandidate.Cur.offset - e.cur - if s-t < maxMatchOffset { - if uint32(cv) == load3232(src, t) { - // Store the next match - e.table[nextHashS] = tableEntry{offset: nextS + e.cur} - eLong := &e.bTable[nextHashL] - eLong.Cur, eLong.Prev = tableEntry{offset: nextS + e.cur}, eLong.Cur - - t2 := lCandidate.Prev.offset - e.cur - if s-t2 < maxMatchOffset && uint32(cv) == load3232(src, t2) { - l = e.matchlen(int(s+4), int(t+4), src) + 4 - ml1 := e.matchlen(int(s+4), int(t2+4), src) + 4 - if ml1 > l { - t = t2 - l = ml1 - break - } - } - break - } - t = lCandidate.Prev.offset - e.cur - if s-t < maxMatchOffset && uint32(cv) == load3232(src, t) { - // Store the next match - e.table[nextHashS] = tableEntry{offset: nextS + e.cur} - eLong := &e.bTable[nextHashL] - eLong.Cur, eLong.Prev = tableEntry{offset: nextS + e.cur}, eLong.Cur - break - } - } - - t = sCandidate.offset - e.cur - if s-t < maxMatchOffset && uint32(cv) == load3232(src, t) { - // Found a 4 match... - l = e.matchlen(int(s+4), int(t+4), src) + 4 - lCandidate = e.bTable[nextHashL] - // Store the next match - - e.table[nextHashS] = tableEntry{offset: nextS + e.cur} - eLong := &e.bTable[nextHashL] - eLong.Cur, eLong.Prev = tableEntry{offset: nextS + e.cur}, eLong.Cur - - // If the next long is a candidate, use that... - t2 := lCandidate.Cur.offset - e.cur - if nextS-t2 < maxMatchOffset { - if load3232(src, t2) == uint32(next) { - ml := e.matchlen(int(nextS+4), int(t2+4), src) + 4 - if ml > l { - t = t2 - s = nextS - l = ml - break - } - } - // If the previous long is a candidate, use that... - t2 = lCandidate.Prev.offset - e.cur - if nextS-t2 < maxMatchOffset && load3232(src, t2) == uint32(next) { - ml := e.matchlen(int(nextS+4), int(t2+4), src) + 4 - if ml > l { - t = t2 - s = nextS - l = ml - break - } - } - } - break - } - cv = next - } - - // A 4-byte match has been found. We'll later see if more than 4 bytes - // match. But, prior to the match, src[nextEmit:s] are unmatched. Emit - // them as literal bytes. - - if l == 0 { - // Extend the 4-byte match as long as possible. - l = e.matchlenLong(int(s+4), int(t+4), src) + 4 - } else if l == maxMatchLength { - l += e.matchlenLong(int(s+l), int(t+l), src) - } - - // Try to locate a better match by checking the end of best match... - if sAt := s + l; l < 30 && sAt < sLimit { - // Allow some bytes at the beginning to mismatch. - // Sweet spot is 2/3 bytes depending on input. - // 3 is only a little better when it is but sometimes a lot worse. - // The skipped bytes are tested in Extend backwards, - // and still picked up as part of the match if they do. - const skipBeginning = 2 - eLong := e.bTable[hash7(load6432(src, sAt), tableBits)].Cur.offset - t2 := eLong - e.cur - l + skipBeginning - s2 := s + skipBeginning - off := s2 - t2 - if t2 >= 0 && off < maxMatchOffset && off > 0 { - if l2 := e.matchlenLong(int(s2), int(t2), src); l2 > l { - t = t2 - l = l2 - s = s2 - } - } - } - - // Extend backwards - for t > 0 && s > nextEmit && src[t-1] == src[s-1] { - s-- - t-- - l++ - } - if nextEmit < s { - if false { - emitLiteral(dst, src[nextEmit:s]) - } else { - for _, v := range src[nextEmit:s] { - dst.tokens[dst.n] = token(v) - dst.litHist[v]++ - dst.n++ - } - } - } - if debugDeflate { - if t >= s { - panic(fmt.Sprintln("s-t", s, t)) - } - if (s - t) > maxMatchOffset { - panic(fmt.Sprintln("mmo", s-t)) - } - if l < baseMatchLength { - panic("bml") - } - } - - dst.AddMatchLong(l, uint32(s-t-baseMatchOffset)) - s += l - nextEmit = s - if nextS >= s { - s = nextS + 1 - } - - if s >= sLimit { - goto emitRemainder - } - - // Store every 3rd hash in-between. - if true { - const hashEvery = 3 - i := s - l + 1 - if i < s-1 { - cv := load6432(src, i) - t := tableEntry{offset: i + e.cur} - e.table[hashLen(cv, tableBits, hashShortBytes)] = t - eLong := &e.bTable[hash7(cv, tableBits)] - eLong.Cur, eLong.Prev = t, eLong.Cur - - // Do an long at i+1 - cv >>= 8 - t = tableEntry{offset: t.offset + 1} - eLong = &e.bTable[hash7(cv, tableBits)] - eLong.Cur, eLong.Prev = t, eLong.Cur - - // We only have enough bits for a short entry at i+2 - cv >>= 8 - t = tableEntry{offset: t.offset + 1} - e.table[hashLen(cv, tableBits, hashShortBytes)] = t - - // Skip one - otherwise we risk hitting 's' - i += 4 - for ; i < s-1; i += hashEvery { - cv := load6432(src, i) - t := tableEntry{offset: i + e.cur} - t2 := tableEntry{offset: t.offset + 1} - eLong := &e.bTable[hash7(cv, tableBits)] - eLong.Cur, eLong.Prev = t, eLong.Cur - e.table[hashLen(cv>>8, tableBits, hashShortBytes)] = t2 - } - } - } - - // We could immediately start working at s now, but to improve - // compression we first update the hash table at s-1 and at s. - x := load6432(src, s-1) - o := e.cur + s - 1 - prevHashS := hashLen(x, tableBits, hashShortBytes) - prevHashL := hash7(x, tableBits) - e.table[prevHashS] = tableEntry{offset: o} - eLong := &e.bTable[prevHashL] - eLong.Cur, eLong.Prev = tableEntry{offset: o}, eLong.Cur - cv = x >> 8 - } - -emitRemainder: - if int(nextEmit) < len(src) { - // If nothing was added, don't encode literals. - if dst.n == 0 { - return - } - - emitLiteral(dst, src[nextEmit:]) - } -} - -// fastEncL5Window is a level 5 encoder, -// but with a custom window size. -type fastEncL5Window struct { - hist []byte - cur int32 - maxOffset int32 - table [tableSize]tableEntry - bTable [tableSize]tableEntryPrev -} - -func (e *fastEncL5Window) Encode(dst *tokens, src []byte) { - const ( - inputMargin = 12 - 1 - minNonLiteralBlockSize = 1 + 1 + inputMargin - hashShortBytes = 4 - ) - maxMatchOffset := e.maxOffset - if debugDeflate && e.cur < 0 { - panic(fmt.Sprint("e.cur < 0: ", e.cur)) - } - - // Protect against e.cur wraparound. - for e.cur >= bufferReset { - if len(e.hist) == 0 { - for i := range e.table[:] { - e.table[i] = tableEntry{} - } - for i := range e.bTable[:] { - e.bTable[i] = tableEntryPrev{} - } - e.cur = maxMatchOffset - break - } - // Shift down everything in the table that isn't already too far away. - minOff := e.cur + int32(len(e.hist)) - maxMatchOffset - for i := range e.table[:] { - v := e.table[i].offset - if v <= minOff { - v = 0 - } else { - v = v - e.cur + maxMatchOffset - } - e.table[i].offset = v - } - for i := range e.bTable[:] { - v := e.bTable[i] - if v.Cur.offset <= minOff { - v.Cur.offset = 0 - v.Prev.offset = 0 - } else { - v.Cur.offset = v.Cur.offset - e.cur + maxMatchOffset - if v.Prev.offset <= minOff { - v.Prev.offset = 0 - } else { - v.Prev.offset = v.Prev.offset - e.cur + maxMatchOffset - } - } - e.bTable[i] = v - } - e.cur = maxMatchOffset - } - - s := e.addBlock(src) - - // This check isn't in the Snappy implementation, but there, the caller - // instead of the callee handles this case. - if len(src) < minNonLiteralBlockSize { - // We do not fill the token table. - // This will be picked up by caller. - dst.n = uint16(len(src)) - return - } - - // Override src - src = e.hist - nextEmit := s - - // sLimit is when to stop looking for offset/length copies. The inputMargin - // lets us use a fast path for emitLiteral in the main loop, while we are - // looking for copies. - sLimit := int32(len(src) - inputMargin) - - // nextEmit is where in src the next emitLiteral should start from. - cv := load6432(src, s) - for { - const skipLog = 6 - const doEvery = 1 - - nextS := s - var l int32 - var t int32 - for { - nextHashS := hashLen(cv, tableBits, hashShortBytes) - nextHashL := hash7(cv, tableBits) - - s = nextS - nextS = s + doEvery + (s-nextEmit)>>skipLog - if nextS > sLimit { - goto emitRemainder - } - // Fetch a short+long candidate - sCandidate := e.table[nextHashS] - lCandidate := e.bTable[nextHashL] - next := load6432(src, nextS) - entry := tableEntry{offset: s + e.cur} - e.table[nextHashS] = entry - eLong := &e.bTable[nextHashL] - eLong.Cur, eLong.Prev = entry, eLong.Cur - - nextHashS = hashLen(next, tableBits, hashShortBytes) - nextHashL = hash7(next, tableBits) - - t = lCandidate.Cur.offset - e.cur - if s-t < maxMatchOffset { - if uint32(cv) == load3232(src, t) { - // Store the next match - e.table[nextHashS] = tableEntry{offset: nextS + e.cur} - eLong := &e.bTable[nextHashL] - eLong.Cur, eLong.Prev = tableEntry{offset: nextS + e.cur}, eLong.Cur - - t2 := lCandidate.Prev.offset - e.cur - if s-t2 < maxMatchOffset && uint32(cv) == load3232(src, t2) { - l = e.matchlen(s+4, t+4, src) + 4 - ml1 := e.matchlen(s+4, t2+4, src) + 4 - if ml1 > l { - t = t2 - l = ml1 - break - } - } - break - } - t = lCandidate.Prev.offset - e.cur - if s-t < maxMatchOffset && uint32(cv) == load3232(src, t) { - // Store the next match - e.table[nextHashS] = tableEntry{offset: nextS + e.cur} - eLong := &e.bTable[nextHashL] - eLong.Cur, eLong.Prev = tableEntry{offset: nextS + e.cur}, eLong.Cur - break - } - } - - t = sCandidate.offset - e.cur - if s-t < maxMatchOffset && uint32(cv) == load3232(src, t) { - // Found a 4 match... - l = e.matchlen(s+4, t+4, src) + 4 - lCandidate = e.bTable[nextHashL] - // Store the next match - - e.table[nextHashS] = tableEntry{offset: nextS + e.cur} - eLong := &e.bTable[nextHashL] - eLong.Cur, eLong.Prev = tableEntry{offset: nextS + e.cur}, eLong.Cur - - // If the next long is a candidate, use that... - t2 := lCandidate.Cur.offset - e.cur - if nextS-t2 < maxMatchOffset { - if load3232(src, t2) == uint32(next) { - ml := e.matchlen(nextS+4, t2+4, src) + 4 - if ml > l { - t = t2 - s = nextS - l = ml - break - } - } - // If the previous long is a candidate, use that... - t2 = lCandidate.Prev.offset - e.cur - if nextS-t2 < maxMatchOffset && load3232(src, t2) == uint32(next) { - ml := e.matchlen(nextS+4, t2+4, src) + 4 - if ml > l { - t = t2 - s = nextS - l = ml - break - } - } - } - break - } - cv = next - } - - // A 4-byte match has been found. We'll later see if more than 4 bytes - // match. But, prior to the match, src[nextEmit:s] are unmatched. Emit - // them as literal bytes. - - if l == 0 { - // Extend the 4-byte match as long as possible. - l = e.matchlenLong(s+4, t+4, src) + 4 - } else if l == maxMatchLength { - l += e.matchlenLong(s+l, t+l, src) - } - - // Try to locate a better match by checking the end of best match... - if sAt := s + l; l < 30 && sAt < sLimit { - // Allow some bytes at the beginning to mismatch. - // Sweet spot is 2/3 bytes depending on input. - // 3 is only a little better when it is but sometimes a lot worse. - // The skipped bytes are tested in Extend backwards, - // and still picked up as part of the match if they do. - const skipBeginning = 2 - eLong := e.bTable[hash7(load6432(src, sAt), tableBits)].Cur.offset - t2 := eLong - e.cur - l + skipBeginning - s2 := s + skipBeginning - off := s2 - t2 - if t2 >= 0 && off < maxMatchOffset && off > 0 { - if l2 := e.matchlenLong(s2, t2, src); l2 > l { - t = t2 - l = l2 - s = s2 - } - } - } - - // Extend backwards - for t > 0 && s > nextEmit && src[t-1] == src[s-1] { - s-- - t-- - l++ - } - if nextEmit < s { - if false { - emitLiteral(dst, src[nextEmit:s]) - } else { - for _, v := range src[nextEmit:s] { - dst.tokens[dst.n] = token(v) - dst.litHist[v]++ - dst.n++ - } - } - } - if debugDeflate { - if t >= s { - panic(fmt.Sprintln("s-t", s, t)) - } - if (s - t) > maxMatchOffset { - panic(fmt.Sprintln("mmo", s-t)) - } - if l < baseMatchLength { - panic("bml") - } - } - - dst.AddMatchLong(l, uint32(s-t-baseMatchOffset)) - s += l - nextEmit = s - if nextS >= s { - s = nextS + 1 - } - - if s >= sLimit { - goto emitRemainder - } - - // Store every 3rd hash in-between. - if true { - const hashEvery = 3 - i := s - l + 1 - if i < s-1 { - cv := load6432(src, i) - t := tableEntry{offset: i + e.cur} - e.table[hashLen(cv, tableBits, hashShortBytes)] = t - eLong := &e.bTable[hash7(cv, tableBits)] - eLong.Cur, eLong.Prev = t, eLong.Cur - - // Do an long at i+1 - cv >>= 8 - t = tableEntry{offset: t.offset + 1} - eLong = &e.bTable[hash7(cv, tableBits)] - eLong.Cur, eLong.Prev = t, eLong.Cur - - // We only have enough bits for a short entry at i+2 - cv >>= 8 - t = tableEntry{offset: t.offset + 1} - e.table[hashLen(cv, tableBits, hashShortBytes)] = t - - // Skip one - otherwise we risk hitting 's' - i += 4 - for ; i < s-1; i += hashEvery { - cv := load6432(src, i) - t := tableEntry{offset: i + e.cur} - t2 := tableEntry{offset: t.offset + 1} - eLong := &e.bTable[hash7(cv, tableBits)] - eLong.Cur, eLong.Prev = t, eLong.Cur - e.table[hashLen(cv>>8, tableBits, hashShortBytes)] = t2 - } - } - } - - // We could immediately start working at s now, but to improve - // compression we first update the hash table at s-1 and at s. - x := load6432(src, s-1) - o := e.cur + s - 1 - prevHashS := hashLen(x, tableBits, hashShortBytes) - prevHashL := hash7(x, tableBits) - e.table[prevHashS] = tableEntry{offset: o} - eLong := &e.bTable[prevHashL] - eLong.Cur, eLong.Prev = tableEntry{offset: o}, eLong.Cur - cv = x >> 8 - } - -emitRemainder: - if int(nextEmit) < len(src) { - // If nothing was added, don't encode literals. - if dst.n == 0 { - return - } - - emitLiteral(dst, src[nextEmit:]) - } -} - -// Reset the encoding table. -func (e *fastEncL5Window) Reset() { - // We keep the same allocs, since we are compressing the same block sizes. - if cap(e.hist) < allocHistory { - e.hist = make([]byte, 0, allocHistory) - } - - // We offset current position so everything will be out of reach. - // If we are above the buffer reset it will be cleared anyway since len(hist) == 0. - if e.cur <= int32(bufferReset) { - e.cur += e.maxOffset + int32(len(e.hist)) - } - e.hist = e.hist[:0] -} - -func (e *fastEncL5Window) addBlock(src []byte) int32 { - // check if we have space already - maxMatchOffset := e.maxOffset - - if len(e.hist)+len(src) > cap(e.hist) { - if cap(e.hist) == 0 { - e.hist = make([]byte, 0, allocHistory) - } else { - if cap(e.hist) < int(maxMatchOffset*2) { - panic("unexpected buffer size") - } - // Move down - offset := int32(len(e.hist)) - maxMatchOffset - copy(e.hist[0:maxMatchOffset], e.hist[offset:]) - e.cur += offset - e.hist = e.hist[:maxMatchOffset] - } - } - s := int32(len(e.hist)) - e.hist = append(e.hist, src...) - return s -} - -// matchlen will return the match length between offsets and t in src. -// The maximum length returned is maxMatchLength - 4. -// It is assumed that s > t, that t >=0 and s < len(src). -func (e *fastEncL5Window) matchlen(s, t int32, src []byte) int32 { - if debugDecode { - if t >= s { - panic(fmt.Sprint("t >=s:", t, s)) - } - if int(s) >= len(src) { - panic(fmt.Sprint("s >= len(src):", s, len(src))) - } - if t < 0 { - panic(fmt.Sprint("t < 0:", t)) - } - if s-t > e.maxOffset { - panic(fmt.Sprint(s, "-", t, "(", s-t, ") > maxMatchLength (", maxMatchOffset, ")")) - } - } - s1 := min(int(s)+maxMatchLength-4, len(src)) - - // Extend the match to be as long as possible. - return int32(matchLen(src[s:s1], src[t:])) -} - -// matchlenLong will return the match length between offsets and t in src. -// It is assumed that s > t, that t >=0 and s < len(src). -func (e *fastEncL5Window) matchlenLong(s, t int32, src []byte) int32 { - if debugDeflate { - if t >= s { - panic(fmt.Sprint("t >=s:", t, s)) - } - if int(s) >= len(src) { - panic(fmt.Sprint("s >= len(src):", s, len(src))) - } - if t < 0 { - panic(fmt.Sprint("t < 0:", t)) - } - if s-t > e.maxOffset { - panic(fmt.Sprint(s, "-", t, "(", s-t, ") > maxMatchLength (", maxMatchOffset, ")")) - } - } - // Extend the match to be as long as possible. - return int32(matchLen(src[s:], src[t:])) -} diff --git a/internal/compress/flate/level6.go b/internal/compress/flate/level6.go deleted file mode 100644 index 96f5bb43..00000000 --- a/internal/compress/flate/level6.go +++ /dev/null @@ -1,325 +0,0 @@ -package flate - -import "fmt" - -type fastEncL6 struct { - fastGen - table [tableSize]tableEntry - bTable [tableSize]tableEntryPrev -} - -func (e *fastEncL6) Encode(dst *tokens, src []byte) { - const ( - inputMargin = 12 - 1 - minNonLiteralBlockSize = 1 + 1 + inputMargin - hashShortBytes = 4 - ) - if debugDeflate && e.cur < 0 { - panic(fmt.Sprint("e.cur < 0: ", e.cur)) - } - - // Protect against e.cur wraparound. - for e.cur >= bufferReset { - if len(e.hist) == 0 { - for i := range e.table[:] { - e.table[i] = tableEntry{} - } - for i := range e.bTable[:] { - e.bTable[i] = tableEntryPrev{} - } - e.cur = maxMatchOffset - break - } - // Shift down everything in the table that isn't already too far away. - minOff := e.cur + int32(len(e.hist)) - maxMatchOffset - for i := range e.table[:] { - v := e.table[i].offset - if v <= minOff { - v = 0 - } else { - v = v - e.cur + maxMatchOffset - } - e.table[i].offset = v - } - for i := range e.bTable[:] { - v := e.bTable[i] - if v.Cur.offset <= minOff { - v.Cur.offset = 0 - v.Prev.offset = 0 - } else { - v.Cur.offset = v.Cur.offset - e.cur + maxMatchOffset - if v.Prev.offset <= minOff { - v.Prev.offset = 0 - } else { - v.Prev.offset = v.Prev.offset - e.cur + maxMatchOffset - } - } - e.bTable[i] = v - } - e.cur = maxMatchOffset - } - - s := e.addBlock(src) - - // This check isn't in the Snappy implementation, but there, the caller - // instead of the callee handles this case. - if len(src) < minNonLiteralBlockSize { - // We do not fill the token table. - // This will be picked up by caller. - dst.n = uint16(len(src)) - return - } - - // Override src - src = e.hist - nextEmit := s - - // sLimit is when to stop looking for offset/length copies. The inputMargin - // lets us use a fast path for emitLiteral in the main loop, while we are - // looking for copies. - sLimit := int32(len(src) - inputMargin) - - // nextEmit is where in src the next emitLiteral should start from. - cv := load6432(src, s) - // Repeat MUST be > 1 and within range - repeat := int32(1) - for { - const skipLog = 7 - const doEvery = 1 - - nextS := s - var l int32 - var t int32 - for { - nextHashS := hashLen(cv, tableBits, hashShortBytes) - nextHashL := hash7(cv, tableBits) - s = nextS - nextS = s + doEvery + (s-nextEmit)>>skipLog - if nextS > sLimit { - goto emitRemainder - } - // Fetch a short+long candidate - sCandidate := e.table[nextHashS] - lCandidate := e.bTable[nextHashL] - next := load6432(src, nextS) - entry := tableEntry{offset: s + e.cur} - e.table[nextHashS] = entry - eLong := &e.bTable[nextHashL] - eLong.Cur, eLong.Prev = entry, eLong.Cur - - // Calculate hashes of 'next' - nextHashS = hashLen(next, tableBits, hashShortBytes) - nextHashL = hash7(next, tableBits) - - t = lCandidate.Cur.offset - e.cur - if s-t < maxMatchOffset { - if uint32(cv) == load3232(src, t) { - // Long candidate matches at least 4 bytes. - - // Store the next match - e.table[nextHashS] = tableEntry{offset: nextS + e.cur} - eLong := &e.bTable[nextHashL] - eLong.Cur, eLong.Prev = tableEntry{offset: nextS + e.cur}, eLong.Cur - - // Check the previous long candidate as well. - t2 := lCandidate.Prev.offset - e.cur - if s-t2 < maxMatchOffset && uint32(cv) == load3232(src, t2) { - l = e.matchlen(int(s+4), int(t+4), src) + 4 - ml1 := e.matchlen(int(s+4), int(t2+4), src) + 4 - if ml1 > l { - t = t2 - l = ml1 - break - } - } - break - } - // Current value did not match, but check if previous long value does. - t = lCandidate.Prev.offset - e.cur - if s-t < maxMatchOffset && uint32(cv) == load3232(src, t) { - // Store the next match - e.table[nextHashS] = tableEntry{offset: nextS + e.cur} - eLong := &e.bTable[nextHashL] - eLong.Cur, eLong.Prev = tableEntry{offset: nextS + e.cur}, eLong.Cur - break - } - } - - t = sCandidate.offset - e.cur - if s-t < maxMatchOffset && uint32(cv) == load3232(src, t) { - // Found a 4 match... - l = e.matchlen(int(s+4), int(t+4), src) + 4 - - // Look up next long candidate (at nextS) - lCandidate = e.bTable[nextHashL] - - // Store the next match - e.table[nextHashS] = tableEntry{offset: nextS + e.cur} - eLong := &e.bTable[nextHashL] - eLong.Cur, eLong.Prev = tableEntry{offset: nextS + e.cur}, eLong.Cur - - // Check repeat at s + repOff - const repOff = 1 - t2 := s - repeat + repOff - if load3232(src, t2) == uint32(cv>>(8*repOff)) { - ml := e.matchlen(int(s+4+repOff), int(t2+4), src) + 4 - if ml > l { - t = t2 - l = ml - s += repOff - // Not worth checking more. - break - } - } - - // If the next long is a candidate, use that... - t2 = lCandidate.Cur.offset - e.cur - if nextS-t2 < maxMatchOffset { - if load3232(src, t2) == uint32(next) { - ml := e.matchlen(int(nextS+4), int(t2+4), src) + 4 - if ml > l { - t = t2 - s = nextS - l = ml - // This is ok, but check previous as well. - } - } - // If the previous long is a candidate, use that... - t2 = lCandidate.Prev.offset - e.cur - if nextS-t2 < maxMatchOffset && load3232(src, t2) == uint32(next) { - ml := e.matchlen(int(nextS+4), int(t2+4), src) + 4 - if ml > l { - t = t2 - s = nextS - l = ml - break - } - } - } - break - } - cv = next - } - - // A 4-byte match has been found. We'll later see if more than 4 bytes - // match. But, prior to the match, src[nextEmit:s] are unmatched. Emit - // them as literal bytes. - - // Extend the 4-byte match as long as possible. - if l == 0 { - l = e.matchlenLong(int(s+4), int(t+4), src) + 4 - } else if l == maxMatchLength { - l += e.matchlenLong(int(s+l), int(t+l), src) - } - - // Try to locate a better match by checking the end-of-match... - if sAt := s + l; sAt < sLimit { - // Allow some bytes at the beginning to mismatch. - // Sweet spot is 2/3 bytes depending on input. - // 3 is only a little better when it is but sometimes a lot worse. - // The skipped bytes are tested in Extend backwards, - // and still picked up as part of the match if they do. - const skipBeginning = 2 - eLong := &e.bTable[hash7(load6432(src, sAt), tableBits)] - // Test current - t2 := eLong.Cur.offset - e.cur - l + skipBeginning - s2 := s + skipBeginning - off := s2 - t2 - if off < maxMatchOffset { - if off > 0 && t2 >= 0 { - if l2 := e.matchlenLong(int(s2), int(t2), src); l2 > l { - t = t2 - l = l2 - s = s2 - } - } - // Test next: - t2 = eLong.Prev.offset - e.cur - l + skipBeginning - off := s2 - t2 - if off > 0 && off < maxMatchOffset && t2 >= 0 { - if l2 := e.matchlenLong(int(s2), int(t2), src); l2 > l { - t = t2 - l = l2 - s = s2 - } - } - } - } - - // Extend backwards - for t > 0 && s > nextEmit && src[t-1] == src[s-1] { - s-- - t-- - l++ - } - if nextEmit < s { - if false { - emitLiteral(dst, src[nextEmit:s]) - } else { - for _, v := range src[nextEmit:s] { - dst.tokens[dst.n] = token(v) - dst.litHist[v]++ - dst.n++ - } - } - } - if false { - if t >= s { - panic(fmt.Sprintln("s-t", s, t)) - } - if (s - t) > maxMatchOffset { - panic(fmt.Sprintln("mmo", s-t)) - } - if l < baseMatchLength { - panic("bml") - } - } - - dst.AddMatchLong(l, uint32(s-t-baseMatchOffset)) - repeat = s - t - s += l - nextEmit = s - if nextS >= s { - s = nextS + 1 - } - - if s >= sLimit { - // Index after match end. - for i := nextS + 1; i < int32(len(src))-8; i += 2 { - cv := load6432(src, i) - e.table[hashLen(cv, tableBits, hashShortBytes)] = tableEntry{offset: i + e.cur} - eLong := &e.bTable[hash7(cv, tableBits)] - eLong.Cur, eLong.Prev = tableEntry{offset: i + e.cur}, eLong.Cur - } - goto emitRemainder - } - - // Store every long hash in-between and every second short. - if true { - for i := nextS + 1; i < s-1; i += 2 { - cv := load6432(src, i) - t := tableEntry{offset: i + e.cur} - t2 := tableEntry{offset: t.offset + 1} - eLong := &e.bTable[hash7(cv, tableBits)] - eLong2 := &e.bTable[hash7(cv>>8, tableBits)] - e.table[hashLen(cv, tableBits, hashShortBytes)] = t - eLong.Cur, eLong.Prev = t, eLong.Cur - eLong2.Cur, eLong2.Prev = t2, eLong2.Cur - } - } - - // We could immediately start working at s now, but to improve - // compression we first update the hash table at s-1 and at s. - cv = load6432(src, s) - } - -emitRemainder: - if int(nextEmit) < len(src) { - // If nothing was added, don't encode literals. - if dst.n == 0 { - return - } - - emitLiteral(dst, src[nextEmit:]) - } -} diff --git a/internal/compress/flate/matchlen_generic.go b/internal/compress/flate/matchlen_generic.go deleted file mode 100644 index 63c0637d..00000000 --- a/internal/compress/flate/matchlen_generic.go +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2019+ Klaus Post. All rights reserved. -// License information can be found in the LICENSE file. - -package flate - -import ( - "math/bits" - - "codeberg.org/lindenii/furgit/internal/compress/internal/le" -) - -// matchLen returns the maximum common prefix length of a and b. -// a must be the shortest of the two. -func matchLen(a, b []byte) (n int) { - left := len(a) - for left >= 8 { - diff := le.Load64(a, n) ^ le.Load64(b, n) - if diff != 0 { - return n + bits.TrailingZeros64(diff)>>3 - } - n += 8 - left -= 8 - } - - a = a[n:] - b = b[n:] - for i := range a { - if a[i] != b[i] { - break - } - n++ - } - return n -} diff --git a/internal/compress/flate/reader_test.go b/internal/compress/flate/reader_test.go deleted file mode 100644 index 6eedfb9b..00000000 --- a/internal/compress/flate/reader_test.go +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright 2012 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package flate - -import ( - "bytes" - "io" - "os" - "runtime" - "strings" - "testing" -) - -func TestNlitOutOfRange(t *testing.T) { - // Trying to decode this bogus flate data, which has a Huffman table - // with nlit=288, should not panic. - io.Copy(io.Discard, NewReader(strings.NewReader( - "\xfc\xfe\x36\xe7\x5e\x1c\xef\xb3\x55\x58\x77\xb6\x56\xb5\x43\xf4"+ - "\x6f\xf2\xd2\xe6\x3d\x99\xa0\x85\x8c\x48\xeb\xf8\xda\x83\x04\x2a"+ - "\x75\xc4\xf8\x0f\x12\x11\xb9\xb4\x4b\x09\xa0\xbe\x8b\x91\x4c"))) -} - -const ( - digits = iota - twain - random -) - -var testfiles = []string{ - // Digits is the digits of the irrational number e. Its decimal representation - // does not repeat, but there are only 10 possible digits, so it should be - // reasonably compressible. - digits: "../testdata/e.txt", - // Twain is Project Gutenberg's edition of Mark Twain's classic English novel. - twain: "../testdata/Mark.Twain-Tom.Sawyer.txt", - // Random bytes - random: "../testdata/sharnd.out", -} - -func benchmarkDecode(b *testing.B, testfile, level, n int) { - b.ReportAllocs() - b.StopTimer() - b.SetBytes(int64(n)) - buf0, err := os.ReadFile(testfiles[testfile]) - if err != nil { - b.Fatal(err) - } - if len(buf0) == 0 { - b.Fatalf("test file %q has no data", testfiles[testfile]) - } - compressed := new(bytes.Buffer) - w, err := NewWriter(compressed, level) - if err != nil { - b.Fatal(err) - } - for i := 0; i < n; i += len(buf0) { - if len(buf0) > n-i { - buf0 = buf0[:n-i] - } - io.Copy(w, bytes.NewReader(buf0)) - } - w.Close() - buf1 := compressed.Bytes() - buf0, compressed, w = nil, nil, nil - r := NewReader(bytes.NewReader(buf1)) - res := r.(Resetter) - runtime.GC() - b.StartTimer() - - for i := 0; i < b.N; i++ { - _ = res.Reset(bytes.NewReader(buf1), nil) - _, _ = io.Copy(io.Discard, r) - } -} - -// These short names are so that gofmt doesn't break the BenchmarkXxx function -// bodies below over multiple lines. -const ( - constant = ConstantCompression - speed = BestSpeed - default_ = DefaultCompression - compress = BestCompression - oneK = -1024 -) - -func BenchmarkDecodeDigitsSpeed1e4(b *testing.B) { benchmarkDecode(b, digits, speed, 1e4) } -func BenchmarkDecodeDigitsSpeed1e5(b *testing.B) { benchmarkDecode(b, digits, speed, 1e5) } -func BenchmarkDecodeDigitsSpeed1e6(b *testing.B) { benchmarkDecode(b, digits, speed, 1e6) } -func BenchmarkDecodeDigitsDefault1e4(b *testing.B) { benchmarkDecode(b, digits, default_, 1e4) } -func BenchmarkDecodeDigitsDefault1e5(b *testing.B) { benchmarkDecode(b, digits, default_, 1e5) } -func BenchmarkDecodeDigitsDefault1e6(b *testing.B) { benchmarkDecode(b, digits, default_, 1e6) } -func BenchmarkDecodeDigitsCompress1e4(b *testing.B) { benchmarkDecode(b, digits, compress, 1e4) } -func BenchmarkDecodeDigitsCompress1e5(b *testing.B) { benchmarkDecode(b, digits, compress, 1e5) } -func BenchmarkDecodeDigitsCompress1e6(b *testing.B) { benchmarkDecode(b, digits, compress, 1e6) } -func BenchmarkDecodeTwainSpeed1e4(b *testing.B) { benchmarkDecode(b, twain, speed, 1e4) } -func BenchmarkDecodeTwainSpeed1e5(b *testing.B) { benchmarkDecode(b, twain, speed, 1e5) } -func BenchmarkDecodeTwainSpeed1e6(b *testing.B) { benchmarkDecode(b, twain, speed, 1e6) } -func BenchmarkDecodeTwainDefault1e4(b *testing.B) { benchmarkDecode(b, twain, default_, 1e4) } -func BenchmarkDecodeTwainDefault1e5(b *testing.B) { benchmarkDecode(b, twain, default_, 1e5) } -func BenchmarkDecodeTwainDefault1e6(b *testing.B) { benchmarkDecode(b, twain, default_, 1e6) } -func BenchmarkDecodeTwainCompress1e4(b *testing.B) { benchmarkDecode(b, twain, compress, 1e4) } -func BenchmarkDecodeTwainCompress1e5(b *testing.B) { benchmarkDecode(b, twain, compress, 1e5) } -func BenchmarkDecodeTwainCompress1e6(b *testing.B) { benchmarkDecode(b, twain, compress, 1e6) } -func BenchmarkDecodeRandomSpeed1e4(b *testing.B) { benchmarkDecode(b, random, speed, 1e4) } -func BenchmarkDecodeRandomSpeed1e5(b *testing.B) { benchmarkDecode(b, random, speed, 1e5) } -func BenchmarkDecodeRandomSpeed1e6(b *testing.B) { benchmarkDecode(b, random, speed, 1e6) } diff --git a/internal/compress/flate/regmask_amd64.go b/internal/compress/flate/regmask_amd64.go deleted file mode 100644 index 6ed28061..00000000 --- a/internal/compress/flate/regmask_amd64.go +++ /dev/null @@ -1,37 +0,0 @@ -package flate - -const ( - // Masks for shifts with register sizes of the shift value. - // This can be used to work around the x86 design of shifting by mod register size. - // It can be used when a variable shift is always smaller than the register size. - - // reg8SizeMaskX - shift value is 8 bits, shifted is X - reg8SizeMask8 = 7 - reg8SizeMask16 = 15 - reg8SizeMask32 = 31 - reg8SizeMask64 = 63 - - // reg16SizeMaskX - shift value is 16 bits, shifted is X - reg16SizeMask8 = reg8SizeMask8 - reg16SizeMask16 = reg8SizeMask16 - reg16SizeMask32 = reg8SizeMask32 - reg16SizeMask64 = reg8SizeMask64 - - // reg32SizeMaskX - shift value is 32 bits, shifted is X - reg32SizeMask8 = reg8SizeMask8 - reg32SizeMask16 = reg8SizeMask16 - reg32SizeMask32 = reg8SizeMask32 - reg32SizeMask64 = reg8SizeMask64 - - // reg64SizeMaskX - shift value is 64 bits, shifted is X - reg64SizeMask8 = reg8SizeMask8 - reg64SizeMask16 = reg8SizeMask16 - reg64SizeMask32 = reg8SizeMask32 - reg64SizeMask64 = reg8SizeMask64 - - // regSizeMaskUintX - shift value is uint, shifted is X - regSizeMaskUint8 = reg8SizeMask8 - regSizeMaskUint16 = reg8SizeMask16 - regSizeMaskUint32 = reg8SizeMask32 - regSizeMaskUint64 = reg8SizeMask64 -) diff --git a/internal/compress/flate/regmask_other.go b/internal/compress/flate/regmask_other.go deleted file mode 100644 index e62caf71..00000000 --- a/internal/compress/flate/regmask_other.go +++ /dev/null @@ -1,39 +0,0 @@ -//go:build !amd64 - -package flate - -const ( - // Masks for shifts with register sizes of the shift value. - // This can be used to work around the x86 design of shifting by mod register size. - // It can be used when a variable shift is always smaller than the register size. - - // reg8SizeMaskX - shift value is 8 bits, shifted is X - reg8SizeMask8 = 0xff - reg8SizeMask16 = 0xff - reg8SizeMask32 = 0xff - reg8SizeMask64 = 0xff - - // reg16SizeMaskX - shift value is 16 bits, shifted is X - reg16SizeMask8 = 0xffff - reg16SizeMask16 = 0xffff - reg16SizeMask32 = 0xffff - reg16SizeMask64 = 0xffff - - // reg32SizeMaskX - shift value is 32 bits, shifted is X - reg32SizeMask8 = 0xffffffff - reg32SizeMask16 = 0xffffffff - reg32SizeMask32 = 0xffffffff - reg32SizeMask64 = 0xffffffff - - // reg64SizeMaskX - shift value is 64 bits, shifted is X - reg64SizeMask8 = 0xffffffffffffffff - reg64SizeMask16 = 0xffffffffffffffff - reg64SizeMask32 = 0xffffffffffffffff - reg64SizeMask64 = 0xffffffffffffffff - - // regSizeMaskUintX - shift value is uint, shifted is X - regSizeMaskUint8 = ^uint(0) - regSizeMaskUint16 = ^uint(0) - regSizeMaskUint32 = ^uint(0) - regSizeMaskUint64 = ^uint(0) -) diff --git a/internal/compress/flate/stateless.go b/internal/compress/flate/stateless.go deleted file mode 100644 index 7e944bfb..00000000 --- a/internal/compress/flate/stateless.go +++ /dev/null @@ -1,325 +0,0 @@ -package flate - -import ( - "io" - "math" - "sync" - - "codeberg.org/lindenii/furgit/internal/compress/internal/le" -) - -const ( - maxStatelessBlock = math.MaxInt16 - // dictionary will be taken from maxStatelessBlock, so limit it. - maxStatelessDict = 8 << 10 - - slTableBits = 13 - slTableSize = 1 << slTableBits - slTableShift = 32 - slTableBits -) - -type statelessWriter struct { - dst io.Writer - closed bool -} - -func (s *statelessWriter) Close() error { - if s.closed { - return nil - } - s.closed = true - // Emit EOF block - return StatelessDeflate(s.dst, nil, true, nil) -} - -func (s *statelessWriter) Write(p []byte) (n int, err error) { - err = StatelessDeflate(s.dst, p, false, nil) - if err != nil { - return 0, err - } - return len(p), nil -} - -func (s *statelessWriter) Reset(w io.Writer) { - s.dst = w - s.closed = false -} - -// NewStatelessWriter will do compression but without maintaining any state -// between Write calls. -// There will be no memory kept between Write calls, -// but compression and speed will be suboptimal. -// Because of this, the size of actual Write calls will affect output size. -func NewStatelessWriter(dst io.Writer) io.WriteCloser { - return &statelessWriter{dst: dst} -} - -// bitWriterPool contains bit writers that can be reused. -var bitWriterPool = sync.Pool{ - New: func() any { - return newHuffmanBitWriter(nil) - }, -} - -// tokensPool contains tokens struct objects that can be reused -var tokensPool = sync.Pool{ - New: func() any { - return &tokens{} - }, -} - -// StatelessDeflate allows compressing directly to a Writer without retaining state. -// When returning everything will be flushed. -// Up to 8KB of an optional dictionary can be given which is presumed to precede the block. -// Longer dictionaries will be truncated and will still produce valid output. -// Sending nil dictionary is perfectly fine. -func StatelessDeflate(out io.Writer, in []byte, eof bool, dict []byte) error { - bw := bitWriterPool.Get().(*huffmanBitWriter) - bw.reset(out) - defer func() { - // don't keep a reference to our output - bw.reset(nil) - bitWriterPool.Put(bw) - }() - if eof && len(in) == 0 { - // Just write an EOF block. - // Could be faster... - bw.writeStoredHeader(0, true) - bw.flush() - return bw.err - } - - // Truncate dict - if len(dict) > maxStatelessDict { - dict = dict[len(dict)-maxStatelessDict:] - } - - // For subsequent loops, keep shallow dict reference to avoid alloc+copy. - var inDict []byte - - dst := tokensPool.Get().(*tokens) - dst.Reset() - defer func() { - tokensPool.Put(dst) - }() - - for len(in) > 0 { - todo := in - if len(inDict) > 0 { - if len(todo) > maxStatelessBlock-maxStatelessDict { - todo = todo[:maxStatelessBlock-maxStatelessDict] - } - } else if len(todo) > maxStatelessBlock-len(dict) { - todo = todo[:maxStatelessBlock-len(dict)] - } - inOrg := in - in = in[len(todo):] - uncompressed := todo - if len(dict) > 0 { - // combine dict and source - bufLen := len(todo) + len(dict) - combined := make([]byte, bufLen) - copy(combined, dict) - copy(combined[len(dict):], todo) - todo = combined - } - // Compress - if len(inDict) == 0 { - statelessEnc(dst, todo, int16(len(dict))) - } else { - statelessEnc(dst, inDict[:maxStatelessDict+len(todo)], maxStatelessDict) - } - isEof := eof && len(in) == 0 - - if dst.n == 0 { - bw.writeStoredHeader(len(uncompressed), isEof) - if bw.err != nil { - return bw.err - } - bw.writeBytes(uncompressed) - } else if int(dst.n) > len(uncompressed)-len(uncompressed)>>4 { - // If we removed less than 1/16th, huffman compress the block. - bw.writeBlockHuff(isEof, uncompressed, len(in) == 0) - } else { - bw.writeBlockDynamic(dst, isEof, uncompressed, len(in) == 0) - } - if len(in) > 0 { - // Retain a dict if we have more - inDict = inOrg[len(uncompressed)-maxStatelessDict:] - dict = nil - dst.Reset() - } - if bw.err != nil { - return bw.err - } - } - if !eof { - // Align, only a stored block can do that. - bw.writeStoredHeader(0, false) - } - bw.flush() - return bw.err -} - -func hashSL(u uint32) uint32 { - return (u * 0x1e35a7bd) >> slTableShift -} - -func load3216(b []byte, i int16) uint32 { - return le.Load32(b, i) -} - -func load6416(b []byte, i int16) uint64 { - return le.Load64(b, i) -} - -func statelessEnc(dst *tokens, src []byte, startAt int16) { - const ( - inputMargin = 12 - 1 - minNonLiteralBlockSize = 1 + 1 + inputMargin - ) - - type tableEntry struct { - offset int16 - } - - var table [slTableSize]tableEntry - - // This check isn't in the Snappy implementation, but there, the caller - // instead of the callee handles this case. - if len(src)-int(startAt) < minNonLiteralBlockSize { - // We do not fill the token table. - // This will be picked up by caller. - dst.n = 0 - return - } - // Index until startAt - if startAt > 0 { - cv := load3232(src, 0) - for i := range startAt { - table[hashSL(cv)] = tableEntry{offset: i} - cv = (cv >> 8) | (uint32(src[i+4]) << 24) - } - } - - s := startAt + 1 - nextEmit := startAt - // sLimit is when to stop looking for offset/length copies. The inputMargin - // lets us use a fast path for emitLiteral in the main loop, while we are - // looking for copies. - sLimit := int16(len(src) - inputMargin) - - // nextEmit is where in src the next emitLiteral should start from. - cv := load3216(src, s) - - for { - const skipLog = 5 - const doEvery = 2 - - nextS := s - var candidate tableEntry - for { - nextHash := hashSL(cv) - candidate = table[nextHash] - nextS = s + doEvery + (s-nextEmit)>>skipLog - if nextS > sLimit || nextS <= 0 { - goto emitRemainder - } - - now := load6416(src, nextS) - table[nextHash] = tableEntry{offset: s} - nextHash = hashSL(uint32(now)) - - if cv == load3216(src, candidate.offset) { - table[nextHash] = tableEntry{offset: nextS} - break - } - - // Do one right away... - cv = uint32(now) - s = nextS - nextS++ - candidate = table[nextHash] - now >>= 8 - table[nextHash] = tableEntry{offset: s} - - if cv == load3216(src, candidate.offset) { - table[nextHash] = tableEntry{offset: nextS} - break - } - cv = uint32(now) - s = nextS - } - - // A 4-byte match has been found. We'll later see if more than 4 bytes - // match. But, prior to the match, src[nextEmit:s] are unmatched. Emit - // them as literal bytes. - for { - // Invariant: we have a 4-byte match at s, and no need to emit any - // literal bytes prior to s. - - // Extend the 4-byte match as long as possible. - t := candidate.offset - l := int16(matchLen(src[s+4:], src[t+4:]) + 4) - - // Extend backwards - for t > 0 && s > nextEmit && src[t-1] == src[s-1] { - s-- - t-- - l++ - } - if nextEmit < s { - if false { - emitLiteral(dst, src[nextEmit:s]) - } else { - for _, v := range src[nextEmit:s] { - dst.tokens[dst.n] = token(v) - dst.litHist[v]++ - dst.n++ - } - } - } - - // Save the match found - dst.AddMatchLong(int32(l), uint32(s-t-baseMatchOffset)) - s += l - nextEmit = s - if nextS >= s { - s = nextS + 1 - } - if s >= sLimit { - goto emitRemainder - } - - // We could immediately start working at s now, but to improve - // compression we first update the hash table at s-2 and at s. If - // another emitCopy is not our next move, also calculate nextHash - // at s+1. At least on GOARCH=amd64, these three hash calculations - // are faster as one load64 call (with some shifts) instead of - // three load32 calls. - x := load6416(src, s-2) - o := s - 2 - prevHash := hashSL(uint32(x)) - table[prevHash] = tableEntry{offset: o} - x >>= 16 - currHash := hashSL(uint32(x)) - candidate = table[currHash] - table[currHash] = tableEntry{offset: o + 2} - - if uint32(x) != load3216(src, candidate.offset) { - cv = uint32(x >> 8) - s++ - break - } - } - } - -emitRemainder: - if int(nextEmit) < len(src) { - // If nothing was added, don't encode literals. - if dst.n == 0 { - return - } - emitLiteral(dst, src[nextEmit:]) - } -} diff --git a/internal/compress/flate/testdata/fuzz/FuzzEncoding.zip b/internal/compress/flate/testdata/fuzz/FuzzEncoding.zip deleted file mode 100644 index feae35f1..00000000 Binary files a/internal/compress/flate/testdata/fuzz/FuzzEncoding.zip and /dev/null differ diff --git a/internal/compress/flate/testdata/fuzz/encode-raw-corpus.zip b/internal/compress/flate/testdata/fuzz/encode-raw-corpus.zip deleted file mode 100644 index 7b33f54f..00000000 Binary files a/internal/compress/flate/testdata/fuzz/encode-raw-corpus.zip and /dev/null differ diff --git a/internal/compress/flate/testdata/huffman-null-max.dyn.expect b/internal/compress/flate/testdata/huffman-null-max.dyn.expect deleted file mode 100644 index c0816514..00000000 Binary files a/internal/compress/flate/testdata/huffman-null-max.dyn.expect and /dev/null differ diff --git a/internal/compress/flate/testdata/huffman-null-max.dyn.expect-noinput b/internal/compress/flate/testdata/huffman-null-max.dyn.expect-noinput deleted file mode 100644 index c0816514..00000000 Binary files a/internal/compress/flate/testdata/huffman-null-max.dyn.expect-noinput and /dev/null differ diff --git a/internal/compress/flate/testdata/huffman-null-max.golden b/internal/compress/flate/testdata/huffman-null-max.golden deleted file mode 100644 index db422ca3..00000000 Binary files a/internal/compress/flate/testdata/huffman-null-max.golden and /dev/null differ diff --git a/internal/compress/flate/testdata/huffman-null-max.in b/internal/compress/flate/testdata/huffman-null-max.in deleted file mode 100644 index 5dfddf07..00000000 Binary files a/internal/compress/flate/testdata/huffman-null-max.in and /dev/null differ diff --git a/internal/compress/flate/testdata/huffman-null-max.sync.expect b/internal/compress/flate/testdata/huffman-null-max.sync.expect deleted file mode 100644 index c0816514..00000000 Binary files a/internal/compress/flate/testdata/huffman-null-max.sync.expect and /dev/null differ diff --git a/internal/compress/flate/testdata/huffman-null-max.sync.expect-noinput b/internal/compress/flate/testdata/huffman-null-max.sync.expect-noinput deleted file mode 100644 index c0816514..00000000 Binary files a/internal/compress/flate/testdata/huffman-null-max.sync.expect-noinput and /dev/null differ diff --git a/internal/compress/flate/testdata/huffman-null-max.wb.expect b/internal/compress/flate/testdata/huffman-null-max.wb.expect deleted file mode 100644 index c0816514..00000000 Binary files a/internal/compress/flate/testdata/huffman-null-max.wb.expect and /dev/null differ diff --git a/internal/compress/flate/testdata/huffman-null-max.wb.expect-noinput b/internal/compress/flate/testdata/huffman-null-max.wb.expect-noinput deleted file mode 100644 index c0816514..00000000 Binary files a/internal/compress/flate/testdata/huffman-null-max.wb.expect-noinput and /dev/null differ diff --git a/internal/compress/flate/testdata/huffman-pi.dyn.expect b/internal/compress/flate/testdata/huffman-pi.dyn.expect deleted file mode 100644 index e4396ac6..00000000 Binary files a/internal/compress/flate/testdata/huffman-pi.dyn.expect and /dev/null differ diff --git a/internal/compress/flate/testdata/huffman-pi.dyn.expect-noinput b/internal/compress/flate/testdata/huffman-pi.dyn.expect-noinput deleted file mode 100644 index e4396ac6..00000000 Binary files a/internal/compress/flate/testdata/huffman-pi.dyn.expect-noinput and /dev/null differ diff --git a/internal/compress/flate/testdata/huffman-pi.golden b/internal/compress/flate/testdata/huffman-pi.golden deleted file mode 100644 index 23d8f7f9..00000000 Binary files a/internal/compress/flate/testdata/huffman-pi.golden and /dev/null differ diff --git a/internal/compress/flate/testdata/huffman-pi.in b/internal/compress/flate/testdata/huffman-pi.in deleted file mode 100644 index efaed434..00000000 --- a/internal/compress/flate/testdata/huffman-pi.in +++ /dev/null @@ -1 +0,0 @@ -3.141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117067982148086513282306647093844609550582231725359408128481117450284102701938521105559644622948954930381964428810975665933446128475648233786783165271201909145648566923460348610454326648213393607260249141273724587006606315588174881520920962829254091715364367892590360011330530548820466521384146951941511609433057270365759591953092186117381932611793105118548074462379962749567351885752724891227938183011949129833673362440656643086021394946395224737190702179860943702770539217176293176752384674818467669405132000568127145263560827785771342757789609173637178721468440901224953430146549585371050792279689258923542019956112129021960864034418159813629774771309960518707211349999998372978049951059731732816096318595024459455346908302642522308253344685035261931188171010003137838752886587533208381420617177669147303598253490428755468731159562863882353787593751957781857780532171226806613001927876611195909216420198938095257201065485863278865936153381827968230301952035301852968995773622599413891249721775283479131515574857242454150695950829533116861727855889075098381754637464939319255060400927701671139009848824012858361603563707660104710181942955596198946767837449448255379774726847104047534646208046684259069491293313677028989152104752162056966024058038150193511253382430035587640247496473263914199272604269922796782354781636009341721641219924586315030286182974555706749838505494588586926995690927210797509302955321165344987202755960236480665499119881834797753566369807426542527862551818417574672890977772793800081647060016145249192173217214772350141441973568548161361157352552133475741849468438523323907394143334547762416862518983569485562099219222184272550254256887671790494601653466804988627232791786085784383827967976681454100953883786360950680064225125205117392984896084128488626945604241965285022210661186306744278622039194945047123713786960956364371917287467764657573962413890865832645995813390478027590099465764078951269468398352595709825822620522489407726719478268482601476990902640136394437455305068203496252451749399651431429809190659250937221696461515709858387410597885959772975498930161753928468138268683868942774155991855925245953959431049972524680845987273644695848653836736222626099124608051243884390451244136549762780797715691435997700129616089441694868555848406353422072225828488648158456028506016842739452267467678895252138522549954666727823986456596116354886230577456498035593634568174324112515076069479451096596094025228879710893145669136867228748940560101503308617928680920874760917824938589009714909675985261365549781893129784821682998948722658804857564014270477555132379641451523746234364542858444795265867821051141354735739523113427166102135969536231442952484937187110145765403590279934403742007310578539062198387447808478489683321445713868751943506430218453191048481005370614680674919278191197939952061419663428754440643745123718192179998391015919561814675142691239748940907186494231961567945208095146550225231603881930142093762137855956638937787083039069792077346722182562599661501421503068038447734549202605414665925201497442850732518666002132434088190710486331734649651453905796268561005508106658796998163574736384052571459102897064140110971206280439039759515677157700420337869936007230558763176359421873125147120532928191826186125867321579198414848829164470609575270695722091756711672291098169091528017350671274858322287183520935396572512108357915136988209144421006751033467110314126711136990865851639831501970165151168517143765761835155650884909989859982387345528331635507647918535893226185489632132933089857064204675259070915481416549859461637180 \ No newline at end of file diff --git a/internal/compress/flate/testdata/huffman-pi.sync.expect b/internal/compress/flate/testdata/huffman-pi.sync.expect deleted file mode 100644 index e4396ac6..00000000 Binary files a/internal/compress/flate/testdata/huffman-pi.sync.expect and /dev/null differ diff --git a/internal/compress/flate/testdata/huffman-pi.sync.expect-noinput b/internal/compress/flate/testdata/huffman-pi.sync.expect-noinput deleted file mode 100644 index e4396ac6..00000000 Binary files a/internal/compress/flate/testdata/huffman-pi.sync.expect-noinput and /dev/null differ diff --git a/internal/compress/flate/testdata/huffman-pi.wb.expect b/internal/compress/flate/testdata/huffman-pi.wb.expect deleted file mode 100644 index e4396ac6..00000000 Binary files a/internal/compress/flate/testdata/huffman-pi.wb.expect and /dev/null differ diff --git a/internal/compress/flate/testdata/huffman-pi.wb.expect-noinput b/internal/compress/flate/testdata/huffman-pi.wb.expect-noinput deleted file mode 100644 index e4396ac6..00000000 Binary files a/internal/compress/flate/testdata/huffman-pi.wb.expect-noinput and /dev/null differ diff --git a/internal/compress/flate/testdata/huffman-rand-1k.dyn.expect b/internal/compress/flate/testdata/huffman-rand-1k.dyn.expect deleted file mode 100644 index 09dc798e..00000000 Binary files a/internal/compress/flate/testdata/huffman-rand-1k.dyn.expect and /dev/null differ diff --git a/internal/compress/flate/testdata/huffman-rand-1k.dyn.expect-noinput b/internal/compress/flate/testdata/huffman-rand-1k.dyn.expect-noinput deleted file mode 100644 index 0c24742f..00000000 Binary files a/internal/compress/flate/testdata/huffman-rand-1k.dyn.expect-noinput and /dev/null differ diff --git a/internal/compress/flate/testdata/huffman-rand-1k.golden b/internal/compress/flate/testdata/huffman-rand-1k.golden deleted file mode 100644 index 09dc798e..00000000 Binary files a/internal/compress/flate/testdata/huffman-rand-1k.golden and /dev/null differ diff --git a/internal/compress/flate/testdata/huffman-rand-1k.in b/internal/compress/flate/testdata/huffman-rand-1k.in deleted file mode 100644 index ce038ebb..00000000 Binary files a/internal/compress/flate/testdata/huffman-rand-1k.in and /dev/null differ diff --git a/internal/compress/flate/testdata/huffman-rand-1k.sync.expect b/internal/compress/flate/testdata/huffman-rand-1k.sync.expect deleted file mode 100644 index 09dc798e..00000000 Binary files a/internal/compress/flate/testdata/huffman-rand-1k.sync.expect and /dev/null differ diff --git a/internal/compress/flate/testdata/huffman-rand-1k.sync.expect-noinput b/internal/compress/flate/testdata/huffman-rand-1k.sync.expect-noinput deleted file mode 100644 index 0c24742f..00000000 Binary files a/internal/compress/flate/testdata/huffman-rand-1k.sync.expect-noinput and /dev/null differ diff --git a/internal/compress/flate/testdata/huffman-rand-1k.wb.expect b/internal/compress/flate/testdata/huffman-rand-1k.wb.expect deleted file mode 100644 index 09dc798e..00000000 Binary files a/internal/compress/flate/testdata/huffman-rand-1k.wb.expect and /dev/null differ diff --git a/internal/compress/flate/testdata/huffman-rand-1k.wb.expect-noinput b/internal/compress/flate/testdata/huffman-rand-1k.wb.expect-noinput deleted file mode 100644 index 0c24742f..00000000 Binary files a/internal/compress/flate/testdata/huffman-rand-1k.wb.expect-noinput and /dev/null differ diff --git a/internal/compress/flate/testdata/huffman-rand-limit.dyn.expect b/internal/compress/flate/testdata/huffman-rand-limit.dyn.expect deleted file mode 100644 index 881e59c9..00000000 Binary files a/internal/compress/flate/testdata/huffman-rand-limit.dyn.expect and /dev/null differ diff --git a/internal/compress/flate/testdata/huffman-rand-limit.dyn.expect-noinput b/internal/compress/flate/testdata/huffman-rand-limit.dyn.expect-noinput deleted file mode 100644 index 881e59c9..00000000 Binary files a/internal/compress/flate/testdata/huffman-rand-limit.dyn.expect-noinput and /dev/null differ diff --git a/internal/compress/flate/testdata/huffman-rand-limit.golden b/internal/compress/flate/testdata/huffman-rand-limit.golden deleted file mode 100644 index 9ca0eb1c..00000000 Binary files a/internal/compress/flate/testdata/huffman-rand-limit.golden and /dev/null differ diff --git a/internal/compress/flate/testdata/huffman-rand-limit.in b/internal/compress/flate/testdata/huffman-rand-limit.in deleted file mode 100644 index fb5b1be6..00000000 --- a/internal/compress/flate/testdata/huffman-rand-limit.in +++ /dev/null @@ -1,4 +0,0 @@ -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -vH -% ɷ}>lsmIGH1Y4[ 0ˆ[|]o# --#ulpfٱnYԀYwC8ɯ02 F=gnrN!O{k*w(b kQC9/lu>5C.u diff --git a/internal/compress/flate/testdata/huffman-rand-limit.sync.expect b/internal/compress/flate/testdata/huffman-rand-limit.sync.expect deleted file mode 100644 index 881e59c9..00000000 Binary files a/internal/compress/flate/testdata/huffman-rand-limit.sync.expect and /dev/null differ diff --git a/internal/compress/flate/testdata/huffman-rand-limit.sync.expect-noinput b/internal/compress/flate/testdata/huffman-rand-limit.sync.expect-noinput deleted file mode 100644 index 881e59c9..00000000 Binary files a/internal/compress/flate/testdata/huffman-rand-limit.sync.expect-noinput and /dev/null differ diff --git a/internal/compress/flate/testdata/huffman-rand-limit.wb.expect b/internal/compress/flate/testdata/huffman-rand-limit.wb.expect deleted file mode 100644 index 881e59c9..00000000 Binary files a/internal/compress/flate/testdata/huffman-rand-limit.wb.expect and /dev/null differ diff --git a/internal/compress/flate/testdata/huffman-rand-limit.wb.expect-noinput b/internal/compress/flate/testdata/huffman-rand-limit.wb.expect-noinput deleted file mode 100644 index 881e59c9..00000000 Binary files a/internal/compress/flate/testdata/huffman-rand-limit.wb.expect-noinput and /dev/null differ diff --git a/internal/compress/flate/testdata/huffman-rand-max.golden b/internal/compress/flate/testdata/huffman-rand-max.golden deleted file mode 100644 index 47d53c89..00000000 Binary files a/internal/compress/flate/testdata/huffman-rand-max.golden and /dev/null differ diff --git a/internal/compress/flate/testdata/huffman-rand-max.in b/internal/compress/flate/testdata/huffman-rand-max.in deleted file mode 100644 index 8418633d..00000000 Binary files a/internal/compress/flate/testdata/huffman-rand-max.in and /dev/null differ diff --git a/internal/compress/flate/testdata/huffman-shifts.dyn.expect b/internal/compress/flate/testdata/huffman-shifts.dyn.expect deleted file mode 100644 index 7812c1c6..00000000 Binary files a/internal/compress/flate/testdata/huffman-shifts.dyn.expect and /dev/null differ diff --git a/internal/compress/flate/testdata/huffman-shifts.dyn.expect-noinput b/internal/compress/flate/testdata/huffman-shifts.dyn.expect-noinput deleted file mode 100644 index 7812c1c6..00000000 Binary files a/internal/compress/flate/testdata/huffman-shifts.dyn.expect-noinput and /dev/null differ diff --git a/internal/compress/flate/testdata/huffman-shifts.golden b/internal/compress/flate/testdata/huffman-shifts.golden deleted file mode 100644 index f5133778..00000000 Binary files a/internal/compress/flate/testdata/huffman-shifts.golden and /dev/null differ diff --git a/internal/compress/flate/testdata/huffman-shifts.in b/internal/compress/flate/testdata/huffman-shifts.in deleted file mode 100644 index 7c7a50d1..00000000 --- a/internal/compress/flate/testdata/huffman-shifts.in +++ /dev/null @@ -1,2 +0,0 @@ -101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 -232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323 \ No newline at end of file diff --git a/internal/compress/flate/testdata/huffman-shifts.sync.expect b/internal/compress/flate/testdata/huffman-shifts.sync.expect deleted file mode 100644 index 7812c1c6..00000000 Binary files a/internal/compress/flate/testdata/huffman-shifts.sync.expect and /dev/null differ diff --git a/internal/compress/flate/testdata/huffman-shifts.sync.expect-noinput b/internal/compress/flate/testdata/huffman-shifts.sync.expect-noinput deleted file mode 100644 index 7812c1c6..00000000 Binary files a/internal/compress/flate/testdata/huffman-shifts.sync.expect-noinput and /dev/null differ diff --git a/internal/compress/flate/testdata/huffman-shifts.wb.expect b/internal/compress/flate/testdata/huffman-shifts.wb.expect deleted file mode 100644 index 7812c1c6..00000000 Binary files a/internal/compress/flate/testdata/huffman-shifts.wb.expect and /dev/null differ diff --git a/internal/compress/flate/testdata/huffman-shifts.wb.expect-noinput b/internal/compress/flate/testdata/huffman-shifts.wb.expect-noinput deleted file mode 100644 index 7812c1c6..00000000 Binary files a/internal/compress/flate/testdata/huffman-shifts.wb.expect-noinput and /dev/null differ diff --git a/internal/compress/flate/testdata/huffman-text-shift.dyn.expect b/internal/compress/flate/testdata/huffman-text-shift.dyn.expect deleted file mode 100644 index 71ce3aeb..00000000 Binary files a/internal/compress/flate/testdata/huffman-text-shift.dyn.expect and /dev/null differ diff --git a/internal/compress/flate/testdata/huffman-text-shift.dyn.expect-noinput b/internal/compress/flate/testdata/huffman-text-shift.dyn.expect-noinput deleted file mode 100644 index 71ce3aeb..00000000 Binary files a/internal/compress/flate/testdata/huffman-text-shift.dyn.expect-noinput and /dev/null differ diff --git a/internal/compress/flate/testdata/huffman-text-shift.golden b/internal/compress/flate/testdata/huffman-text-shift.golden deleted file mode 100644 index ff023114..00000000 Binary files a/internal/compress/flate/testdata/huffman-text-shift.golden and /dev/null differ diff --git a/internal/compress/flate/testdata/huffman-text-shift.in b/internal/compress/flate/testdata/huffman-text-shift.in deleted file mode 100644 index cc5c3ad6..00000000 --- a/internal/compress/flate/testdata/huffman-text-shift.in +++ /dev/null @@ -1,14 +0,0 @@ -//Copyright2009ThGoAuthor.Allrightrrvd. -//UofthiourccodigovrndbyBSD-tyl -//licnthtcnbfoundinthLICENSEfil. - -pckgmin - -import"o" - -funcmin(){ - vrb=mk([]byt,65535) - f,_:=o.Crt("huffmn-null-mx.in") - f.Writ(b) -} -ABCDEFGHIJKLMNOPQRSTUVXxyz!"#¤%&/?" \ No newline at end of file diff --git a/internal/compress/flate/testdata/huffman-text-shift.sync.expect b/internal/compress/flate/testdata/huffman-text-shift.sync.expect deleted file mode 100644 index 71ce3aeb..00000000 Binary files a/internal/compress/flate/testdata/huffman-text-shift.sync.expect and /dev/null differ diff --git a/internal/compress/flate/testdata/huffman-text-shift.sync.expect-noinput b/internal/compress/flate/testdata/huffman-text-shift.sync.expect-noinput deleted file mode 100644 index 71ce3aeb..00000000 Binary files a/internal/compress/flate/testdata/huffman-text-shift.sync.expect-noinput and /dev/null differ diff --git a/internal/compress/flate/testdata/huffman-text-shift.wb.expect b/internal/compress/flate/testdata/huffman-text-shift.wb.expect deleted file mode 100644 index 71ce3aeb..00000000 Binary files a/internal/compress/flate/testdata/huffman-text-shift.wb.expect and /dev/null differ diff --git a/internal/compress/flate/testdata/huffman-text-shift.wb.expect-noinput b/internal/compress/flate/testdata/huffman-text-shift.wb.expect-noinput deleted file mode 100644 index 71ce3aeb..00000000 Binary files a/internal/compress/flate/testdata/huffman-text-shift.wb.expect-noinput and /dev/null differ diff --git a/internal/compress/flate/testdata/huffman-text.dyn.expect b/internal/compress/flate/testdata/huffman-text.dyn.expect deleted file mode 100644 index d448727c..00000000 --- a/internal/compress/flate/testdata/huffman-text.dyn.expect +++ /dev/null @@ -1 +0,0 @@ -_K0`K0Aasě)^HIɟb߻_>4 a=-^ 1`_ 1 ő:Y-F66!A`aC;ANyr4ߜU!GKС#r:B[G3.L׶bFRuM]^⇳(#Z ivBBH2S]u/ֽWTGnr \ No newline at end of file diff --git a/internal/compress/flate/testdata/huffman-text.dyn.expect-noinput b/internal/compress/flate/testdata/huffman-text.dyn.expect-noinput deleted file mode 100644 index d448727c..00000000 --- a/internal/compress/flate/testdata/huffman-text.dyn.expect-noinput +++ /dev/null @@ -1 +0,0 @@ -_K0`K0Aasě)^HIɟb߻_>4 a=-^ 1`_ 1 ő:Y-F66!A`aC;ANyr4ߜU!GKС#r:B[G3.L׶bFRuM]^⇳(#Z ivBBH2S]u/ֽWTGnr \ No newline at end of file diff --git a/internal/compress/flate/testdata/huffman-text.golden b/internal/compress/flate/testdata/huffman-text.golden deleted file mode 100644 index 6d34c61f..00000000 --- a/internal/compress/flate/testdata/huffman-text.golden +++ /dev/null @@ -1,3 +0,0 @@ -AK0xßZLPa!xADI&#IEp]LƿFp 188h$5S- F66!)v.0Y& SN|d2: -t|둍xz9骺Ɏ3 -&&=ôUD=Fu]qUL+>FQYLZofTߵEŴ{Yʶbe \ No newline at end of file diff --git a/internal/compress/flate/testdata/huffman-text.in b/internal/compress/flate/testdata/huffman-text.in deleted file mode 100644 index 73398b98..00000000 --- a/internal/compress/flate/testdata/huffman-text.in +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2009 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package main - -import "os" - -func main() { - var b = make([]byte, 65535) - f, _ := os.Create("huffman-null-max.in") - f.Write(b) -} diff --git a/internal/compress/flate/testdata/huffman-text.sync.expect b/internal/compress/flate/testdata/huffman-text.sync.expect deleted file mode 100644 index d448727c..00000000 --- a/internal/compress/flate/testdata/huffman-text.sync.expect +++ /dev/null @@ -1 +0,0 @@ -_K0`K0Aasě)^HIɟb߻_>4 a=-^ 1`_ 1 ő:Y-F66!A`aC;ANyr4ߜU!GKС#r:B[G3.L׶bFRuM]^⇳(#Z ivBBH2S]u/ֽWTGnr \ No newline at end of file diff --git a/internal/compress/flate/testdata/huffman-text.sync.expect-noinput b/internal/compress/flate/testdata/huffman-text.sync.expect-noinput deleted file mode 100644 index d448727c..00000000 --- a/internal/compress/flate/testdata/huffman-text.sync.expect-noinput +++ /dev/null @@ -1 +0,0 @@ -_K0`K0Aasě)^HIɟb߻_>4 a=-^ 1`_ 1 ő:Y-F66!A`aC;ANyr4ߜU!GKС#r:B[G3.L׶bFRuM]^⇳(#Z ivBBH2S]u/ֽWTGnr \ No newline at end of file diff --git a/internal/compress/flate/testdata/huffman-text.wb.expect b/internal/compress/flate/testdata/huffman-text.wb.expect deleted file mode 100644 index d448727c..00000000 --- a/internal/compress/flate/testdata/huffman-text.wb.expect +++ /dev/null @@ -1 +0,0 @@ -_K0`K0Aasě)^HIɟb߻_>4 a=-^ 1`_ 1 ő:Y-F66!A`aC;ANyr4ߜU!GKС#r:B[G3.L׶bFRuM]^⇳(#Z ivBBH2S]u/ֽWTGnr \ No newline at end of file diff --git a/internal/compress/flate/testdata/huffman-text.wb.expect-noinput b/internal/compress/flate/testdata/huffman-text.wb.expect-noinput deleted file mode 100644 index d448727c..00000000 --- a/internal/compress/flate/testdata/huffman-text.wb.expect-noinput +++ /dev/null @@ -1 +0,0 @@ -_K0`K0Aasě)^HIɟb߻_>4 a=-^ 1`_ 1 ő:Y-F66!A`aC;ANyr4ߜU!GKС#r:B[G3.L׶bFRuM]^⇳(#Z ivBBH2S]u/ֽWTGnr \ No newline at end of file diff --git a/internal/compress/flate/testdata/huffman-zero.dyn.expect b/internal/compress/flate/testdata/huffman-zero.dyn.expect deleted file mode 100644 index dbe401c5..00000000 Binary files a/internal/compress/flate/testdata/huffman-zero.dyn.expect and /dev/null differ diff --git a/internal/compress/flate/testdata/huffman-zero.dyn.expect-noinput b/internal/compress/flate/testdata/huffman-zero.dyn.expect-noinput deleted file mode 100644 index dbe401c5..00000000 Binary files a/internal/compress/flate/testdata/huffman-zero.dyn.expect-noinput and /dev/null differ diff --git a/internal/compress/flate/testdata/huffman-zero.golden b/internal/compress/flate/testdata/huffman-zero.golden deleted file mode 100644 index 5abdbaff..00000000 Binary files a/internal/compress/flate/testdata/huffman-zero.golden and /dev/null differ diff --git a/internal/compress/flate/testdata/huffman-zero.in b/internal/compress/flate/testdata/huffman-zero.in deleted file mode 100644 index 349be0e6..00000000 --- a/internal/compress/flate/testdata/huffman-zero.in +++ /dev/null @@ -1 +0,0 @@ -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 \ No newline at end of file diff --git a/internal/compress/flate/testdata/huffman-zero.sync.expect b/internal/compress/flate/testdata/huffman-zero.sync.expect deleted file mode 100644 index dbe401c5..00000000 Binary files a/internal/compress/flate/testdata/huffman-zero.sync.expect and /dev/null differ diff --git a/internal/compress/flate/testdata/huffman-zero.sync.expect-noinput b/internal/compress/flate/testdata/huffman-zero.sync.expect-noinput deleted file mode 100644 index dbe401c5..00000000 Binary files a/internal/compress/flate/testdata/huffman-zero.sync.expect-noinput and /dev/null differ diff --git a/internal/compress/flate/testdata/huffman-zero.wb.expect b/internal/compress/flate/testdata/huffman-zero.wb.expect deleted file mode 100644 index dbe401c5..00000000 Binary files a/internal/compress/flate/testdata/huffman-zero.wb.expect and /dev/null differ diff --git a/internal/compress/flate/testdata/huffman-zero.wb.expect-noinput b/internal/compress/flate/testdata/huffman-zero.wb.expect-noinput deleted file mode 100644 index dbe401c5..00000000 Binary files a/internal/compress/flate/testdata/huffman-zero.wb.expect-noinput and /dev/null differ diff --git a/internal/compress/flate/testdata/null-long-match.dyn.expect-noinput b/internal/compress/flate/testdata/null-long-match.dyn.expect-noinput deleted file mode 100644 index 8b92d9fc..00000000 Binary files a/internal/compress/flate/testdata/null-long-match.dyn.expect-noinput and /dev/null differ diff --git a/internal/compress/flate/testdata/null-long-match.sync.expect-noinput b/internal/compress/flate/testdata/null-long-match.sync.expect-noinput deleted file mode 100644 index 8b92d9fc..00000000 Binary files a/internal/compress/flate/testdata/null-long-match.sync.expect-noinput and /dev/null differ diff --git a/internal/compress/flate/testdata/null-long-match.wb.expect-noinput b/internal/compress/flate/testdata/null-long-match.wb.expect-noinput deleted file mode 100644 index 8b92d9fc..00000000 Binary files a/internal/compress/flate/testdata/null-long-match.wb.expect-noinput and /dev/null differ diff --git a/internal/compress/flate/testdata/partial-block b/internal/compress/flate/testdata/partial-block deleted file mode 100644 index b14e816a..00000000 --- a/internal/compress/flate/testdata/partial-block +++ /dev/null @@ -1 +0,0 @@ -HQ(/I \ No newline at end of file diff --git a/internal/compress/flate/testdata/regression.zip b/internal/compress/flate/testdata/regression.zip deleted file mode 100644 index 73cf8403..00000000 Binary files a/internal/compress/flate/testdata/regression.zip and /dev/null differ diff --git a/internal/compress/flate/testdata/tokens.bin b/internal/compress/flate/testdata/tokens.bin deleted file mode 100644 index b93c6968..00000000 --- a/internal/compress/flate/testdata/tokens.bin +++ /dev/null @@ -1,63 +0,0 @@ - - - name>Wikip끀en./Main_PageMediaWiki 1.6alphaSpecial0" /ɀ1">Talkŀ2">User3 t؀4">܂݀5 6">Image7ڀ89 10">Template 1Ӄ 2">Helpހ3ڀ4">Category5 00">Port101Às҈AaA12005-12-27T18:46:47Z ؀ 614213쁀Ӏ#REDIRECT [[AAA]]adding cur_id=5: {{R from CamelCase}}ҀԂa]]ԀmericanSamoaɂ69ԃˁ4:1to6 ۂ݂ ނppliedEthics858989432-02-25T15:43:11ip>Con script - <Afghaany132002-08-27T03:07:44ZMagnusskewhoops׀<Þ xml:space="rve">#REDIRECT [[҂GeoȀ쁀92-25T15:43:11ip>Con꽁criptmaxnumlit - offHist [32]uint16 // offset codes - litHist [256]uint16 // codes 0->255 - nFilled int - n uint16 // Must be able to contain maxStoreBlockSize - tokens [maxStoreBlockSize + 1]token -} - -func (t *tokens) Reset() { - if t.n == 0 { - return - } - t.n = 0 - t.nFilled = 0 - for i := range t.litHist[:] { - t.litHist[i] = 0 - } - for i := range t.extraHist[:] { - t.extraHist[i] = 0 - } - for i := range t.offHist[:] { - t.offHist[i] = 0 - } -} - -func (t *tokens) Fill() { - if t.n == 0 { - return - } - for i, v := range t.litHist[:] { - if v == 0 { - t.litHist[i] = 1 - t.nFilled++ - } - } - for i, v := range t.extraHist[:literalCount-256] { - if v == 0 { - t.nFilled++ - t.extraHist[i] = 1 - } - } - for i, v := range t.offHist[:offsetCodeCount] { - if v == 0 { - t.offHist[i] = 1 - } - } -} - -func indexTokens(in []token) tokens { - var t tokens - t.indexTokens(in) - return t -} - -func (t *tokens) indexTokens(in []token) { - t.Reset() - for _, tok := range in { - if tok < matchType { - t.AddLiteral(tok.literal()) - continue - } - t.AddMatch(uint32(tok.length()), tok.offset()&matchOffsetOnlyMask) - } -} - -// emitLiteral writes a literal chunk and returns the number of bytes written. -func emitLiteral(dst *tokens, lit []byte) { - for _, v := range lit { - dst.tokens[dst.n] = token(v) - dst.litHist[v]++ - dst.n++ - } -} - -func (t *tokens) AddLiteral(lit byte) { - t.tokens[t.n] = token(lit) - t.litHist[lit]++ - t.n++ -} - -// from https://stackoverflow.com/a/28730362 -func mFastLog2(val float32) float32 { - ux := int32(math.Float32bits(val)) - log2 := (float32)(((ux >> 23) & 255) - 128) - ux &= -0x7f800001 - ux += 127 << 23 - uval := math.Float32frombits(uint32(ux)) - log2 += ((-0.34484843)*uval+2.02466578)*uval - 0.67487759 - return log2 -} - -// EstimatedBits will return an minimum size estimated by an *optimal* -// compression of the block. -// The size of the block -func (t *tokens) EstimatedBits() int { - shannon := float32(0) - bits := int(0) - nMatches := 0 - total := int(t.n) + t.nFilled - if total > 0 { - invTotal := 1.0 / float32(total) - for _, v := range t.litHist[:] { - if v > 0 { - n := float32(v) - shannon += atLeastOne(-mFastLog2(n*invTotal)) * n - } - } - // Just add 15 for EOB - shannon += 15 - for i, v := range t.extraHist[1 : literalCount-256] { - if v > 0 { - n := float32(v) - shannon += atLeastOne(-mFastLog2(n*invTotal)) * n - bits += int(lengthExtraBits[i&31]) * int(v) - nMatches += int(v) - } - } - } - if nMatches > 0 { - invTotal := 1.0 / float32(nMatches) - for i, v := range t.offHist[:offsetCodeCount] { - if v > 0 { - n := float32(v) - shannon += atLeastOne(-mFastLog2(n*invTotal)) * n - bits += int(offsetExtraBits[i&31]) * int(v) - } - } - } - return int(shannon) + bits -} - -// AddMatch adds a match to the tokens. -// This function is very sensitive to inlining and right on the border. -func (t *tokens) AddMatch(xlength uint32, xoffset uint32) { - if debugDeflate { - if xlength >= maxMatchLength+baseMatchLength { - panic(fmt.Errorf("invalid length: %v", xlength)) - } - if xoffset >= maxMatchOffset+baseMatchOffset { - panic(fmt.Errorf("invalid offset: %v", xoffset)) - } - } - oCode := offsetCode(xoffset) - xoffset |= oCode << 16 - - t.extraHist[lengthCodes1[uint8(xlength)]]++ - t.offHist[oCode&31]++ - t.tokens[t.n] = token(matchType | xlength<= maxMatchOffset+baseMatchOffset { - panic(fmt.Errorf("invalid offset: %v", xoffset)) - } - } - oc := offsetCode(xoffset) - xoffset |= oc << 16 - for xlength > 0 { - xl := xlength - if xl > 258 { - // We need to have at least baseMatchLength left over for next loop. - if xl > 258+baseMatchLength { - xl = 258 - } else { - xl = 258 - baseMatchLength - } - } - xlength -= xl - xl -= baseMatchLength - t.extraHist[lengthCodes1[uint8(xl)]]++ - t.offHist[oc&31]++ - t.tokens[t.n] = token(matchType | uint32(xl)<> lengthShift) } - -// Convert length to code. -func lengthCode(len uint8) uint8 { return lengthCodes[len] } - -// Returns the offset code corresponding to a specific offset -func offsetCode(off uint32) uint32 { - if false { - if off < uint32(len(offsetCodes)) { - return offsetCodes[off&255] - } else if off>>7 < uint32(len(offsetCodes)) { - return offsetCodes[(off>>7)&255] + 14 - } else { - return offsetCodes[(off>>14)&255] + 28 - } - } - if off < uint32(len(offsetCodes)) { - return offsetCodes[uint8(off)] - } - return offsetCodes14[uint8(off>>7)] -} diff --git a/internal/compress/flate/token_test.go b/internal/compress/flate/token_test.go deleted file mode 100644 index 9070c341..00000000 --- a/internal/compress/flate/token_test.go +++ /dev/null @@ -1,54 +0,0 @@ -package flate - -import ( - "bytes" - "os" - "testing" -) - -type testFatal interface { - Fatal(args ...any) -} - -// loadTestTokens will load test tokens. -// First block from enwik9, varint encoded. -func loadTestTokens(t testFatal) *tokens { - b, err := os.ReadFile("testdata/tokens.bin") - if err != nil { - t.Fatal(err) - } - var tokens tokens - err = tokens.FromVarInt(b) - if err != nil { - t.Fatal(err) - } - return &tokens -} - -func Test_tokens_EstimatedBits(t *testing.T) { - tok := loadTestTokens(t) - // The estimated size, update if method changes. - const expect = 221057 - n := tok.EstimatedBits() - var buf bytes.Buffer - wr := newHuffmanBitWriter(&buf) - wr.writeBlockDynamic(tok, true, nil, true) - if wr.err != nil { - t.Fatal(wr.err) - } - wr.flush() - t.Log("got:", n, "actual:", buf.Len()*8, "(header not part of estimate)") - if n != expect { - t.Error("want:", expect, "bits, got:", n) - } -} - -func Benchmark_tokens_EstimatedBits(b *testing.B) { - tok := loadTestTokens(b) - b.ResetTimer() - // One "byte", one token iteration. - b.SetBytes(1) - for i := 0; i < b.N; i++ { - _ = tok.EstimatedBits() - } -} diff --git a/internal/compress/flate/writer_test.go b/internal/compress/flate/writer_test.go deleted file mode 100644 index 01893e50..00000000 --- a/internal/compress/flate/writer_test.go +++ /dev/null @@ -1,544 +0,0 @@ -// Copyright 2012 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package flate - -import ( - "archive/zip" - "bytes" - "compress/flate" - "fmt" - "io" - "math" - "math/rand" - "os" - "runtime" - "strconv" - "strings" - "testing" -) - -func TestWriterMemUsage(t *testing.T) { - testMem := func(t *testing.T, fn func()) { - var before, after runtime.MemStats - runtime.GC() - runtime.ReadMemStats(&before) - fn() - runtime.GC() - runtime.ReadMemStats(&after) - t.Logf("%s: Memory Used: %dKB, %d allocs", t.Name(), (after.HeapInuse-before.HeapInuse)/1024, after.HeapObjects-before.HeapObjects) - } - data := make([]byte, 100000) - t.Run("stateless", func(t *testing.T) { - testMem(t, func() { - StatelessDeflate(io.Discard, data, false, nil) - }) - }) - for level := HuffmanOnly; level <= BestCompression; level++ { - t.Run(fmt.Sprint("level-", level), func(t *testing.T) { - var zr *Writer - var err error - testMem(t, func() { - zr, err = NewWriter(io.Discard, level) - if err != nil { - t.Fatal(err) - } - zr.Write(data) - }) - zr.Close() - }) - } - for level := HuffmanOnly; level <= BestCompression; level++ { - t.Run(fmt.Sprint("stdlib-", level), func(t *testing.T) { - var zr *flate.Writer - var err error - testMem(t, func() { - zr, err = flate.NewWriter(io.Discard, level) - if err != nil { - t.Fatal(err) - } - zr.Write(data) - }) - zr.Close() - }) - } -} - -func TestWriterRegression(t *testing.T) { - data, err := os.ReadFile("testdata/regression.zip") - if err != nil { - t.Fatal(err) - } - for level := HuffmanOnly; level <= BestCompression; level++ { - t.Run(fmt.Sprint("level_", level), func(t *testing.T) { - zr, err := zip.NewReader(bytes.NewReader(data), int64(len(data))) - if err != nil { - t.Fatal(err) - } - - for _, tt := range zr.File { - if !strings.HasSuffix(t.Name(), "") { - continue - } - - t.Run(tt.Name, func(t *testing.T) { - if testing.Short() && tt.FileInfo().Size() > 10000 { - t.SkipNow() - } - r, err := tt.Open() - if err != nil { - t.Error(err) - return - } - in, err := io.ReadAll(r) - if err != nil { - t.Error(err) - } - msg := "level " + strconv.Itoa(level) + ":" - buf := new(bytes.Buffer) - fw, err := NewWriter(buf, level) - if err != nil { - t.Fatal(msg + err.Error()) - } - n, err := fw.Write(in) - if n != len(in) { - t.Fatal(msg + "short write") - } - if err != nil { - t.Fatal(msg + err.Error()) - } - err = fw.Close() - if err != nil { - t.Fatal(msg + err.Error()) - } - fr1 := NewReader(buf) - data2, err := io.ReadAll(fr1) - if err != nil { - t.Fatal(msg + err.Error()) - } - if !bytes.Equal(in, data2) { - t.Fatal(msg + "not equal") - } - // Do it again... - msg = "level " + strconv.Itoa(level) + " (reset):" - buf.Reset() - fw.Reset(buf) - n, err = fw.Write(in) - if n != len(in) { - t.Fatal(msg + "short write") - } - if err != nil { - t.Fatal(msg + err.Error()) - } - err = fw.Close() - if err != nil { - t.Fatal(msg + err.Error()) - } - fr1 = NewReader(buf) - data2, err = io.ReadAll(fr1) - if err != nil { - t.Fatal(msg + err.Error()) - } - if !bytes.Equal(in, data2) { - t.Fatal(msg + "not equal") - } - }) - } - }) - } -} - -func benchmarkEncoder(b *testing.B, testfile, level, n int) { - b.SetBytes(int64(n)) - buf0, err := os.ReadFile(testfiles[testfile]) - if err != nil { - b.Fatal(err) - } - if len(buf0) == 0 { - b.Fatalf("test file %q has no data", testfiles[testfile]) - } - buf1 := make([]byte, n) - for i := 0; i < n; i += len(buf0) { - if len(buf0) > n-i { - buf0 = buf0[:n-i] - } - copy(buf1[i:], buf0) - } - buf0 = nil - runtime.GC() - w, err := NewWriter(io.Discard, level) - if err != nil { - b.Fatal(err) - } - b.ResetTimer() - b.ReportAllocs() - for i := 0; i < b.N; i++ { - w.Reset(io.Discard) - _, err = w.Write(buf1) - if err != nil { - b.Fatal(err) - } - err = w.Close() - if err != nil { - b.Fatal(err) - } - } -} - -func BenchmarkEncodeDigitsConstant1e4(b *testing.B) { benchmarkEncoder(b, digits, constant, 1e4) } -func BenchmarkEncodeDigitsConstant1e5(b *testing.B) { benchmarkEncoder(b, digits, constant, 1e5) } -func BenchmarkEncodeDigitsConstant1e6(b *testing.B) { benchmarkEncoder(b, digits, constant, 1e6) } -func BenchmarkEncodeDigitsSpeed1e4(b *testing.B) { benchmarkEncoder(b, digits, speed, 1e4) } -func BenchmarkEncodeDigitsSpeed1e5(b *testing.B) { benchmarkEncoder(b, digits, speed, 1e5) } -func BenchmarkEncodeDigitsSpeed1e6(b *testing.B) { benchmarkEncoder(b, digits, speed, 1e6) } -func BenchmarkEncodeDigitsDefault1e4(b *testing.B) { benchmarkEncoder(b, digits, default_, 1e4) } -func BenchmarkEncodeDigitsDefault1e5(b *testing.B) { benchmarkEncoder(b, digits, default_, 1e5) } -func BenchmarkEncodeDigitsDefault1e6(b *testing.B) { benchmarkEncoder(b, digits, default_, 1e6) } -func BenchmarkEncodeDigitsCompress1e4(b *testing.B) { benchmarkEncoder(b, digits, compress, 1e4) } -func BenchmarkEncodeDigitsCompress1e5(b *testing.B) { benchmarkEncoder(b, digits, compress, 1e5) } -func BenchmarkEncodeDigitsCompress1e6(b *testing.B) { benchmarkEncoder(b, digits, compress, 1e6) } -func BenchmarkEncodeDigitsSL1e4(b *testing.B) { benchmarkStatelessEncoder(b, digits, 1e4) } -func BenchmarkEncodeDigitsSL1e5(b *testing.B) { benchmarkStatelessEncoder(b, digits, 1e5) } -func BenchmarkEncodeDigitsSL1e6(b *testing.B) { benchmarkStatelessEncoder(b, digits, 1e6) } -func BenchmarkEncodeTwainConstant1e4(b *testing.B) { benchmarkEncoder(b, twain, constant, 1e4) } -func BenchmarkEncodeTwainConstant1e5(b *testing.B) { benchmarkEncoder(b, twain, constant, 1e5) } -func BenchmarkEncodeTwainConstant1e6(b *testing.B) { benchmarkEncoder(b, twain, constant, 1e6) } -func BenchmarkEncodeTwainSpeed1e4(b *testing.B) { benchmarkEncoder(b, twain, speed, 1e4) } -func BenchmarkEncodeTwainSpeed1e5(b *testing.B) { benchmarkEncoder(b, twain, speed, 1e5) } -func BenchmarkEncodeTwainSpeed1e6(b *testing.B) { benchmarkEncoder(b, twain, speed, 1e6) } -func BenchmarkEncodeTwainDefault1e4(b *testing.B) { benchmarkEncoder(b, twain, default_, 1e4) } -func BenchmarkEncodeTwainDefault1e5(b *testing.B) { benchmarkEncoder(b, twain, default_, 1e5) } -func BenchmarkEncodeTwainDefault1e6(b *testing.B) { benchmarkEncoder(b, twain, default_, 1e6) } -func BenchmarkEncodeTwainCompress1e4(b *testing.B) { benchmarkEncoder(b, twain, compress, 1e4) } -func BenchmarkEncodeTwainCompress1e5(b *testing.B) { benchmarkEncoder(b, twain, compress, 1e5) } -func BenchmarkEncodeTwainCompress1e6(b *testing.B) { benchmarkEncoder(b, twain, compress, 1e6) } -func BenchmarkEncodeTwainSL1e4(b *testing.B) { benchmarkStatelessEncoder(b, twain, 1e4) } -func BenchmarkEncodeTwainSL1e5(b *testing.B) { benchmarkStatelessEncoder(b, twain, 1e5) } -func BenchmarkEncodeTwainSL1e6(b *testing.B) { benchmarkStatelessEncoder(b, twain, 1e6) } - -func BenchmarkEncodeTwain1024Win1e4(b *testing.B) { benchmarkEncoder(b, twain, oneK, 1e4) } -func BenchmarkEncodeTwain1024Win1e5(b *testing.B) { benchmarkEncoder(b, twain, oneK, 1e5) } -func BenchmarkEncodeTwain1024Win1e6(b *testing.B) { benchmarkEncoder(b, twain, oneK, 1e6) } - -func benchmarkStatelessEncoder(b *testing.B, testfile, n int) { - b.SetBytes(int64(n)) - buf0, err := os.ReadFile(testfiles[testfile]) - if err != nil { - b.Fatal(err) - } - if len(buf0) == 0 { - b.Fatalf("test file %q has no data", testfiles[testfile]) - } - buf1 := make([]byte, n) - for i := 0; i < n; i += len(buf0) { - if len(buf0) > n-i { - buf0 = buf0[:n-i] - } - copy(buf1[i:], buf0) - } - buf0 = nil - runtime.GC() - b.ResetTimer() - b.ReportAllocs() - for i := 0; i < b.N; i++ { - w := NewStatelessWriter(io.Discard) - _, err = w.Write(buf1) - if err != nil { - b.Fatal(err) - } - err = w.Close() - if err != nil { - b.Fatal(err) - } - } -} - -// A writer that fails after N writes. -type errorWriter struct { - N int -} - -func (e *errorWriter) Write(b []byte) (int, error) { - if e.N <= 0 { - return 0, io.ErrClosedPipe - } - e.N-- - return len(b), nil -} - -// Test if errors from the underlying writer is passed upwards. -func TestWriteError(t *testing.T) { - buf := new(bytes.Buffer) - n := 65536 - if !testing.Short() { - n *= 4 - } - for i := 0; i < n; i++ { - fmt.Fprintf(buf, "asdasfasf%d%dfghfgujyut%dyutyu\n", i, i, i) - } - in := buf.Bytes() - // We create our own buffer to control number of writes. - copyBuf := make([]byte, 128) - for l := range 10 { - for fail := 1; fail <= 256; fail *= 2 { - // Fail after 'fail' writes - ew := &errorWriter{N: fail} - w, err := NewWriter(ew, l) - if err != nil { - t.Fatalf("NewWriter: level %d: %v", l, err) - } - n, err := copyBuffer(w, bytes.NewBuffer(in), copyBuf) - if err == nil { - t.Fatalf("Level %d: Expected an error, writer was %#v", l, ew) - } - n2, err := w.Write([]byte{1, 2, 2, 3, 4, 5}) - if n2 != 0 { - t.Fatal("Level", l, "Expected 0 length write, got", n) - } - if err == nil { - t.Fatal("Level", l, "Expected an error") - } - err = w.Flush() - if err == nil { - t.Fatal("Level", l, "Expected an error on flush") - } - err = w.Close() - if err == nil { - t.Fatal("Level", l, "Expected an error on close") - } - - w.Reset(io.Discard) - n2, err = w.Write([]byte{1, 2, 3, 4, 5, 6}) - if err != nil { - t.Fatal("Level", l, "Got unexpected error after reset:", err) - } - if n2 == 0 { - t.Fatal("Level", l, "Got 0 length write, expected > 0") - } - if testing.Short() { - return - } - } - } -} - -// Test if errors from the underlying writer is passed upwards. -func TestWriter_Reset(t *testing.T) { - buf := new(bytes.Buffer) - n := 65536 - if !testing.Short() { - n *= 4 - } - for i := 0; i < n; i++ { - fmt.Fprintf(buf, "asdasfasf%d%dfghfgujyut%dyutyu\n", i, i, i) - } - in := buf.Bytes() - for l := range 10 { - if testing.Short() && l > 1 { - continue - } - t.Run(fmt.Sprintf("level-%d", l), func(t *testing.T) { - t.Parallel() - offset := 1 - if testing.Short() { - offset = 256 - } - for ; offset <= 256; offset *= 2 { - // Fail after 'fail' writes - w, err := NewWriter(io.Discard, l) - if err != nil { - t.Fatalf("NewWriter: level %d: %v", l, err) - } - if w.d.fast == nil { - t.Skip("Not Fast...") - return - } - for i := 0; i < (bufferReset-len(in)-offset-maxMatchOffset)/maxMatchOffset; i++ { - // skip ahead to where we are close to wrap around... - w.d.fast.Reset() - } - w.d.fast.Reset() - _, err = w.Write(in) - if err != nil { - t.Fatal(err) - } - for range 50 { - // skip ahead again... This should wrap around... - w.d.fast.Reset() - } - w.d.fast.Reset() - - _, err = w.Write(in) - if err != nil { - t.Fatal(err) - } - for range (math.MaxUint32 - bufferReset) / maxMatchOffset { - // skip ahead to where we are close to wrap around... - w.d.fast.Reset() - } - - _, err = w.Write(in) - if err != nil { - t.Fatal(err) - } - err = w.Close() - if err != nil { - t.Fatal(err) - } - } - }) - } -} - -func TestDeterministicL1(t *testing.T) { testDeterministic(1, t) } -func TestDeterministicL2(t *testing.T) { testDeterministic(2, t) } -func TestDeterministicL3(t *testing.T) { testDeterministic(3, t) } -func TestDeterministicL4(t *testing.T) { testDeterministic(4, t) } -func TestDeterministicL5(t *testing.T) { testDeterministic(5, t) } -func TestDeterministicL6(t *testing.T) { testDeterministic(6, t) } -func TestDeterministicL7(t *testing.T) { testDeterministic(7, t) } -func TestDeterministicL8(t *testing.T) { testDeterministic(8, t) } -func TestDeterministicL9(t *testing.T) { testDeterministic(9, t) } -func TestDeterministicL0(t *testing.T) { testDeterministic(0, t) } -func TestDeterministicLM2(t *testing.T) { testDeterministic(-2, t) } - -func testDeterministic(i int, t *testing.T) { - // Test so much we cross a good number of block boundaries. - length := maxStoreBlockSize*30 + 500 - if testing.Short() { - length /= 10 - } - - // Create a random, but compressible stream. - rng := rand.New(rand.NewSource(1)) - t1 := make([]byte, length) - for i := range t1 { - t1[i] = byte(rng.Int63() & 7) - } - - // Do our first encode. - var b1 bytes.Buffer - br := bytes.NewBuffer(t1) - w, err := NewWriter(&b1, i) - if err != nil { - t.Fatal(err) - } - // Use a very small prime sized buffer. - cbuf := make([]byte, 787) - _, err = copyBuffer(w, br, cbuf) - if err != nil { - t.Fatal(err) - } - w.Close() - - // We choose a different buffer size, - // bigger than a maximum block, and also a prime. - var b2 bytes.Buffer - cbuf = make([]byte, 81761) - br2 := bytes.NewBuffer(t1) - w2, err := NewWriter(&b2, i) - if err != nil { - t.Fatal(err) - } - _, err = copyBuffer(w2, br2, cbuf) - if err != nil { - t.Fatal(err) - } - w2.Close() - - b1b := b1.Bytes() - b2b := b2.Bytes() - - if !bytes.Equal(b1b, b2b) { - t.Errorf("level %d did not produce deterministic result, result mismatch, len(a) = %d, len(b) = %d", i, len(b1b), len(b2b)) - } - - // Test using io.WriterTo interface. - var b3 bytes.Buffer - br = bytes.NewBuffer(t1) - w, err = NewWriter(&b3, i) - if err != nil { - t.Fatal(err) - } - _, err = br.WriteTo(w) - if err != nil { - t.Fatal(err) - } - w.Close() - - b3b := b3.Bytes() - if !bytes.Equal(b1b, b3b) { - t.Errorf("level %d (io.WriterTo) did not produce deterministic result, result mismatch, len(a) = %d, len(b) = %d", i, len(b1b), len(b3b)) - } -} - -// copyBuffer is a copy of io.CopyBuffer, since we want to support older go versions. -// This is modified to never use io.WriterTo or io.ReaderFrom interfaces. -func copyBuffer(dst io.Writer, src io.Reader, buf []byte) (written int64, err error) { - if buf == nil { - buf = make([]byte, 32*1024) - } - for { - nr, er := src.Read(buf) - if nr > 0 { - nw, ew := dst.Write(buf[0:nr]) - if nw > 0 { - written += int64(nw) - } - if ew != nil { - err = ew - break - } - if nr != nw { - err = io.ErrShortWrite - break - } - } - if er == io.EOF { - break - } - if er != nil { - err = er - break - } - } - return written, err -} - -func BenchmarkCompressAllocations(b *testing.B) { - payload := []byte(strings.Repeat("Tiny payload", 20)) - for j := -2; j <= 9; j++ { - b.Run("level("+strconv.Itoa(j)+")", func(b *testing.B) { - b.Run("flate", func(b *testing.B) { - b.ReportAllocs() - - for i := 0; i < b.N; i++ { - w, err := NewWriter(io.Discard, j) - if err != nil { - b.Fatal(err) - } - w.Write(payload) - w.Close() - } - }) - }) - } -} - -func BenchmarkCompressAllocationsSingle(b *testing.B) { - payload := []byte(strings.Repeat("Tiny payload", 20)) - const level = 2 - b.Run("flate", func(b *testing.B) { - b.ReportAllocs() - - for i := 0; i < b.N; i++ { - w, err := NewWriter(io.Discard, level) - if err != nil { - b.Fatal(err) - } - w.Write(payload) - w.Close() - } - }) -} diff --git a/internal/compress/internal/doc.go b/internal/compress/internal/doc.go deleted file mode 100644 index b28bad09..00000000 --- a/internal/compress/internal/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package internal provides utilities internal to the compression library. -package internal diff --git a/internal/compress/internal/fuzz/helpers.go b/internal/compress/internal/fuzz/helpers.go deleted file mode 100644 index 71332ac6..00000000 --- a/internal/compress/internal/fuzz/helpers.go +++ /dev/null @@ -1,218 +0,0 @@ -// Copyright (c) 2024+ Klaus Post. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Package fuzz provides a way to add test cases to a testing.F instance from a zip file. -package fuzz - -import ( - "archive/zip" - "bytes" - "encoding/binary" - "fmt" - "go/ast" - "go/parser" - "go/token" - "io" - "os" - "strconv" - "testing" -) - -type InputType uint8 - -const ( - // TypeRaw indicates that files are raw bytes. - TypeRaw InputType = iota - // TypeGoFuzz indicates files are from Go Fuzzer. - TypeGoFuzz - // TypeOSSFuzz indicates that files are from OSS fuzzer with size before data. - TypeOSSFuzz -) - -// AddFromZip will read the supplied zip and add all as corpus for f. -// Byte slices only. -func AddFromZip(f *testing.F, filename string, t InputType, short bool) { - file, err := os.Open(filename) - if err != nil { - f.Fatal(err) - } - fi, err := file.Stat() - if fi == nil { - return - } - - if err != nil { - f.Fatal(err) - } - zr, err := zip.NewReader(file, fi.Size()) - if err != nil { - f.Fatal(err) - } - for i, file := range zr.File { - if short && i%10 != 0 { - continue - } - rc, err := file.Open() - if err != nil { - f.Fatal(err) - } - - b, err := io.ReadAll(rc) - if err != nil { - f.Fatal(err) - } - rc.Close() - t := t - if t == TypeOSSFuzz { - t = TypeRaw // Fallback - if len(b) >= 4 { - sz := binary.BigEndian.Uint32(b) - if sz <= uint32(len(b))-4 { - f.Add(b[4 : 4+sz]) - continue - } - } - } - - if bytes.HasPrefix(b, []byte("go test fuzz")) { - t = TypeGoFuzz - } else { - t = TypeRaw - } - - if t == TypeRaw { - f.Add(b) - continue - } - vals, err := unmarshalCorpusFile(b) - if err != nil { - f.Fatal(err) - } - for _, v := range vals { - f.Add(v) - } - } -} - -// ReturnFromZip will read the supplied zip and add all as corpus for f. -// Byte slices only. -func ReturnFromZip(tb testing.TB, filename string, t InputType, fn func([]byte)) { - file, err := os.Open(filename) - if err != nil { - tb.Fatal(err) - } - fi, err := file.Stat() - if fi == nil { - return - } - if err != nil { - tb.Fatal(err) - } - zr, err := zip.NewReader(file, fi.Size()) - if err != nil { - tb.Fatal(err) - } - for _, file := range zr.File { - rc, err := file.Open() - if err != nil { - tb.Fatal(err) - } - - b, err := io.ReadAll(rc) - if err != nil { - tb.Fatal(err) - } - rc.Close() - t := t - if t == TypeOSSFuzz { - t = TypeRaw // Fallback - if len(b) >= 4 { - sz := binary.BigEndian.Uint32(b) - if sz <= uint32(len(b))-4 { - fn(b[4 : 4+sz]) - continue - } - } - } - - if bytes.HasPrefix(b, []byte("go test fuzz")) { - t = TypeGoFuzz - } else { - t = TypeRaw - } - - if t == TypeRaw { - fn(b) - continue - } - vals, err := unmarshalCorpusFile(b) - if err != nil { - tb.Fatal(err) - } - for _, v := range vals { - fn(v) - } - } -} - -// unmarshalCorpusFile decodes corpus bytes into their respective values. -func unmarshalCorpusFile(b []byte) ([][]byte, error) { - if len(b) == 0 { - return nil, fmt.Errorf("cannot unmarshal empty string") - } - lines := bytes.Split(b, []byte("\n")) - if len(lines) < 2 { - return nil, fmt.Errorf("must include version and at least one value") - } - vals := make([][]byte, 0, len(lines)-1) - for _, line := range lines[1:] { - line = bytes.TrimSpace(line) - if len(line) == 0 { - continue - } - v, err := parseCorpusValue(line) - if err != nil { - return nil, fmt.Errorf("malformed line %q: %v", line, err) - } - vals = append(vals, v) - } - return vals, nil -} - -// parseCorpusValue -func parseCorpusValue(line []byte) ([]byte, error) { - fs := token.NewFileSet() - expr, err := parser.ParseExprFrom(fs, "(test)", line, 0) - if err != nil { - return nil, err - } - call, ok := expr.(*ast.CallExpr) - if !ok { - return nil, fmt.Errorf("expected call expression") - } - if len(call.Args) != 1 { - return nil, fmt.Errorf("expected call expression with 1 argument; got %d", len(call.Args)) - } - arg := call.Args[0] - - if arrayType, ok := call.Fun.(*ast.ArrayType); ok { - if arrayType.Len != nil { - return nil, fmt.Errorf("expected []byte or primitive type") - } - elt, ok := arrayType.Elt.(*ast.Ident) - if !ok || elt.Name != "byte" { - return nil, fmt.Errorf("expected []byte") - } - lit, ok := arg.(*ast.BasicLit) - if !ok || lit.Kind != token.STRING { - return nil, fmt.Errorf("string literal required for type []byte") - } - s, err := strconv.Unquote(lit.Value) - if err != nil { - return nil, err - } - return []byte(s), nil - } - return nil, fmt.Errorf("expected []byte") -} diff --git a/internal/compress/internal/le/le.go b/internal/compress/internal/le/le.go deleted file mode 100644 index 890ba873..00000000 --- a/internal/compress/internal/le/le.go +++ /dev/null @@ -1,6 +0,0 @@ -// Package le provides fast little endian integer routines. -package le - -type Indexer interface { - int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 -} diff --git a/internal/compress/internal/le/unsafe_disabled.go b/internal/compress/internal/le/unsafe_disabled.go deleted file mode 100644 index 4f2a0d8c..00000000 --- a/internal/compress/internal/le/unsafe_disabled.go +++ /dev/null @@ -1,42 +0,0 @@ -//go:build !(amd64 || arm64 || ppc64le || riscv64) || nounsafe || purego || appengine - -package le - -import ( - "encoding/binary" -) - -// Load8 will load from b at index i. -func Load8[I Indexer](b []byte, i I) byte { - return b[i] -} - -// Load16 will load from b at index i. -func Load16[I Indexer](b []byte, i I) uint16 { - return binary.LittleEndian.Uint16(b[i:]) -} - -// Load32 will load from b at index i. -func Load32[I Indexer](b []byte, i I) uint32 { - return binary.LittleEndian.Uint32(b[i:]) -} - -// Load64 will load from b at index i. -func Load64[I Indexer](b []byte, i I) uint64 { - return binary.LittleEndian.Uint64(b[i:]) -} - -// Store16 will store v at b. -func Store16(b []byte, v uint16) { - binary.LittleEndian.PutUint16(b, v) -} - -// Store32 will store v at b. -func Store32(b []byte, v uint32) { - binary.LittleEndian.PutUint32(b, v) -} - -// Store64 will store v at b. -func Store64[I Indexer](b []byte, i I, v uint64) { - binary.LittleEndian.PutUint64(b[i:], v) -} diff --git a/internal/compress/internal/le/unsafe_enabled.go b/internal/compress/internal/le/unsafe_enabled.go deleted file mode 100644 index b47fd0db..00000000 --- a/internal/compress/internal/le/unsafe_enabled.go +++ /dev/null @@ -1,52 +0,0 @@ -// We enable 64 bit LE platforms: - -//go:build (amd64 || arm64 || ppc64le || riscv64) && !nounsafe && !purego && !appengine - -package le - -import ( - "unsafe" -) - -// Load8 will load from b at index i. -func Load8[I Indexer](b []byte, i I) byte { - // return binary.LittleEndian.Uint16(b[i:]) - // return *(*uint16)(unsafe.Pointer(&b[i])) - return *(*byte)(unsafe.Add(unsafe.Pointer(unsafe.SliceData(b)), i)) -} - -// Load16 will load from b at index i. -func Load16[I Indexer](b []byte, i I) uint16 { - // return binary.LittleEndian.Uint16(b[i:]) - // return *(*uint16)(unsafe.Pointer(&b[i])) - return *(*uint16)(unsafe.Add(unsafe.Pointer(unsafe.SliceData(b)), i)) -} - -// Load32 will load from b at index i. -func Load32[I Indexer](b []byte, i I) uint32 { - // return binary.LittleEndian.Uint32(b[i:]) - // return *(*uint32)(unsafe.Pointer(&b[i])) - return *(*uint32)(unsafe.Add(unsafe.Pointer(unsafe.SliceData(b)), i)) -} - -// Load64 will load from b at index i. -func Load64[I Indexer](b []byte, i I) uint64 { - // return binary.LittleEndian.Uint64(b[i:]) - // return *(*uint64)(unsafe.Pointer(&b[i])) - return *(*uint64)(unsafe.Add(unsafe.Pointer(unsafe.SliceData(b)), i)) -} - -// Store16 will store v at b. -func Store16(b []byte, v uint16) { - *(*uint16)(unsafe.Pointer(unsafe.SliceData(b))) = v -} - -// Store32 will store v at b. -func Store32(b []byte, v uint32) { - *(*uint32)(unsafe.Pointer(unsafe.SliceData(b))) = v -} - -// Store64 will store v at b[i:]. -func Store64[I Indexer](b []byte, i I, v uint64) { - *(*uint64)(unsafe.Add(unsafe.Pointer(unsafe.SliceData(b)), i)) = v -} diff --git a/internal/compress/testdata/Mark.Twain-Tom.Sawyer.txt b/internal/compress/testdata/Mark.Twain-Tom.Sawyer.txt deleted file mode 100644 index 565627a9..00000000 --- a/internal/compress/testdata/Mark.Twain-Tom.Sawyer.txt +++ /dev/null @@ -1,8472 +0,0 @@ -Produced by David Widger. The previous edition was updated by Jose -Menendez. - - - - - - THE ADVENTURES OF TOM SAWYER - BY - MARK TWAIN - (Samuel Langhorne Clemens) - - - - - P R E F A C E - -MOST of the adventures recorded in this book really occurred; one or -two were experiences of my own, the rest those of boys who were -schoolmates of mine. Huck Finn is drawn from life; Tom Sawyer also, but -not from an individual--he is a combination of the characteristics of -three boys whom I knew, and therefore belongs to the composite order of -architecture. - -The odd superstitions touched upon were all prevalent among children -and slaves in the West at the period of this story--that is to say, -thirty or forty years ago. - -Although my book is intended mainly for the entertainment of boys and -girls, I hope it will not be shunned by men and women on that account, -for part of my plan has been to try to pleasantly remind adults of what -they once were themselves, and of how they felt and thought and talked, -and what queer enterprises they sometimes engaged in. - - THE AUTHOR. - -HARTFORD, 1876. - - - - T O M S A W Y E R - - - -CHAPTER I - -"TOM!" - -No answer. - -"TOM!" - -No answer. - -"What's gone with that boy, I wonder? You TOM!" - -No answer. - -The old lady pulled her spectacles down and looked over them about the -room; then she put them up and looked out under them. She seldom or -never looked THROUGH them for so small a thing as a boy; they were her -state pair, the pride of her heart, and were built for "style," not -service--she could have seen through a pair of stove-lids just as well. -She looked perplexed for a moment, and then said, not fiercely, but -still loud enough for the furniture to hear: - -"Well, I lay if I get hold of you I'll--" - -She did not finish, for by this time she was bending down and punching -under the bed with the broom, and so she needed breath to punctuate the -punches with. She resurrected nothing but the cat. - -"I never did see the beat of that boy!" - -She went to the open door and stood in it and looked out among the -tomato vines and "jimpson" weeds that constituted the garden. No Tom. -So she lifted up her voice at an angle calculated for distance and -shouted: - -"Y-o-u-u TOM!" - -There was a slight noise behind her and she turned just in time to -seize a small boy by the slack of his roundabout and arrest his flight. - -"There! I might 'a' thought of that closet. What you been doing in -there?" - -"Nothing." - -"Nothing! Look at your hands. And look at your mouth. What IS that -truck?" - -"I don't know, aunt." - -"Well, I know. It's jam--that's what it is. Forty times I've said if -you didn't let that jam alone I'd skin you. Hand me that switch." - -The switch hovered in the air--the peril was desperate-- - -"My! Look behind you, aunt!" - -The old lady whirled round, and snatched her skirts out of danger. The -lad fled on the instant, scrambled up the high board-fence, and -disappeared over it. - -His aunt Polly stood surprised a moment, and then broke into a gentle -laugh. - -"Hang the boy, can't I never learn anything? Ain't he played me tricks -enough like that for me to be looking out for him by this time? But old -fools is the biggest fools there is. Can't learn an old dog new tricks, -as the saying is. But my goodness, he never plays them alike, two days, -and how is a body to know what's coming? He 'pears to know just how -long he can torment me before I get my dander up, and he knows if he -can make out to put me off for a minute or make me laugh, it's all down -again and I can't hit him a lick. I ain't doing my duty by that boy, -and that's the Lord's truth, goodness knows. Spare the rod and spile -the child, as the Good Book says. I'm a laying up sin and suffering for -us both, I know. He's full of the Old Scratch, but laws-a-me! he's my -own dead sister's boy, poor thing, and I ain't got the heart to lash -him, somehow. Every time I let him off, my conscience does hurt me so, -and every time I hit him my old heart most breaks. Well-a-well, man -that is born of woman is of few days and full of trouble, as the -Scripture says, and I reckon it's so. He'll play hookey this evening, * -and [* Southwestern for "afternoon"] I'll just be obleeged to make him -work, to-morrow, to punish him. It's mighty hard to make him work -Saturdays, when all the boys is having holiday, but he hates work more -than he hates anything else, and I've GOT to do some of my duty by him, -or I'll be the ruination of the child." - -Tom did play hookey, and he had a very good time. He got back home -barely in season to help Jim, the small colored boy, saw next-day's -wood and split the kindlings before supper--at least he was there in -time to tell his adventures to Jim while Jim did three-fourths of the -work. Tom's younger brother (or rather half-brother) Sid was already -through with his part of the work (picking up chips), for he was a -quiet boy, and had no adventurous, troublesome ways. - -While Tom was eating his supper, and stealing sugar as opportunity -offered, Aunt Polly asked him questions that were full of guile, and -very deep--for she wanted to trap him into damaging revealments. Like -many other simple-hearted souls, it was her pet vanity to believe she -was endowed with a talent for dark and mysterious diplomacy, and she -loved to contemplate her most transparent devices as marvels of low -cunning. Said she: - -"Tom, it was middling warm in school, warn't it?" - -"Yes'm." - -"Powerful warm, warn't it?" - -"Yes'm." - -"Didn't you want to go in a-swimming, Tom?" - -A bit of a scare shot through Tom--a touch of uncomfortable suspicion. -He searched Aunt Polly's face, but it told him nothing. So he said: - -"No'm--well, not very much." - -The old lady reached out her hand and felt Tom's shirt, and said: - -"But you ain't too warm now, though." And it flattered her to reflect -that she had discovered that the shirt was dry without anybody knowing -that that was what she had in her mind. But in spite of her, Tom knew -where the wind lay, now. So he forestalled what might be the next move: - -"Some of us pumped on our heads--mine's damp yet. See?" - -Aunt Polly was vexed to think she had overlooked that bit of -circumstantial evidence, and missed a trick. Then she had a new -inspiration: - -"Tom, you didn't have to undo your shirt collar where I sewed it, to -pump on your head, did you? Unbutton your jacket!" - -The trouble vanished out of Tom's face. He opened his jacket. His -shirt collar was securely sewed. - -"Bother! Well, go 'long with you. I'd made sure you'd played hookey -and been a-swimming. But I forgive ye, Tom. I reckon you're a kind of a -singed cat, as the saying is--better'n you look. THIS time." - -She was half sorry her sagacity had miscarried, and half glad that Tom -had stumbled into obedient conduct for once. - -But Sidney said: - -"Well, now, if I didn't think you sewed his collar with white thread, -but it's black." - -"Why, I did sew it with white! Tom!" - -But Tom did not wait for the rest. As he went out at the door he said: - -"Siddy, I'll lick you for that." - -In a safe place Tom examined two large needles which were thrust into -the lapels of his jacket, and had thread bound about them--one needle -carried white thread and the other black. He said: - -"She'd never noticed if it hadn't been for Sid. Confound it! sometimes -she sews it with white, and sometimes she sews it with black. I wish to -geeminy she'd stick to one or t'other--I can't keep the run of 'em. But -I bet you I'll lam Sid for that. I'll learn him!" - -He was not the Model Boy of the village. He knew the model boy very -well though--and loathed him. - -Within two minutes, or even less, he had forgotten all his troubles. -Not because his troubles were one whit less heavy and bitter to him -than a man's are to a man, but because a new and powerful interest bore -them down and drove them out of his mind for the time--just as men's -misfortunes are forgotten in the excitement of new enterprises. This -new interest was a valued novelty in whistling, which he had just -acquired from a negro, and he was suffering to practise it undisturbed. -It consisted in a peculiar bird-like turn, a sort of liquid warble, -produced by touching the tongue to the roof of the mouth at short -intervals in the midst of the music--the reader probably remembers how -to do it, if he has ever been a boy. Diligence and attention soon gave -him the knack of it, and he strode down the street with his mouth full -of harmony and his soul full of gratitude. He felt much as an -astronomer feels who has discovered a new planet--no doubt, as far as -strong, deep, unalloyed pleasure is concerned, the advantage was with -the boy, not the astronomer. - -The summer evenings were long. It was not dark, yet. Presently Tom -checked his whistle. A stranger was before him--a boy a shade larger -than himself. A new-comer of any age or either sex was an impressive -curiosity in the poor little shabby village of St. Petersburg. This boy -was well dressed, too--well dressed on a week-day. This was simply -astounding. His cap was a dainty thing, his close-buttoned blue cloth -roundabout was new and natty, and so were his pantaloons. He had shoes -on--and it was only Friday. He even wore a necktie, a bright bit of -ribbon. He had a citified air about him that ate into Tom's vitals. The -more Tom stared at the splendid marvel, the higher he turned up his -nose at his finery and the shabbier and shabbier his own outfit seemed -to him to grow. Neither boy spoke. If one moved, the other moved--but -only sidewise, in a circle; they kept face to face and eye to eye all -the time. Finally Tom said: - -"I can lick you!" - -"I'd like to see you try it." - -"Well, I can do it." - -"No you can't, either." - -"Yes I can." - -"No you can't." - -"I can." - -"You can't." - -"Can!" - -"Can't!" - -An uncomfortable pause. Then Tom said: - -"What's your name?" - -"'Tisn't any of your business, maybe." - -"Well I 'low I'll MAKE it my business." - -"Well why don't you?" - -"If you say much, I will." - -"Much--much--MUCH. There now." - -"Oh, you think you're mighty smart, DON'T you? I could lick you with -one hand tied behind me, if I wanted to." - -"Well why don't you DO it? You SAY you can do it." - -"Well I WILL, if you fool with me." - -"Oh yes--I've seen whole families in the same fix." - -"Smarty! You think you're SOME, now, DON'T you? Oh, what a hat!" - -"You can lump that hat if you don't like it. I dare you to knock it -off--and anybody that'll take a dare will suck eggs." - -"You're a liar!" - -"You're another." - -"You're a fighting liar and dasn't take it up." - -"Aw--take a walk!" - -"Say--if you give me much more of your sass I'll take and bounce a -rock off'n your head." - -"Oh, of COURSE you will." - -"Well I WILL." - -"Well why don't you DO it then? What do you keep SAYING you will for? -Why don't you DO it? It's because you're afraid." - -"I AIN'T afraid." - -"You are." - -"I ain't." - -"You are." - -Another pause, and more eying and sidling around each other. Presently -they were shoulder to shoulder. Tom said: - -"Get away from here!" - -"Go away yourself!" - -"I won't." - -"I won't either." - -So they stood, each with a foot placed at an angle as a brace, and -both shoving with might and main, and glowering at each other with -hate. But neither could get an advantage. After struggling till both -were hot and flushed, each relaxed his strain with watchful caution, -and Tom said: - -"You're a coward and a pup. I'll tell my big brother on you, and he -can thrash you with his little finger, and I'll make him do it, too." - -"What do I care for your big brother? I've got a brother that's bigger -than he is--and what's more, he can throw him over that fence, too." -[Both brothers were imaginary.] - -"That's a lie." - -"YOUR saying so don't make it so." - -Tom drew a line in the dust with his big toe, and said: - -"I dare you to step over that, and I'll lick you till you can't stand -up. Anybody that'll take a dare will steal sheep." - -The new boy stepped over promptly, and said: - -"Now you said you'd do it, now let's see you do it." - -"Don't you crowd me now; you better look out." - -"Well, you SAID you'd do it--why don't you do it?" - -"By jingo! for two cents I WILL do it." - -The new boy took two broad coppers out of his pocket and held them out -with derision. Tom struck them to the ground. In an instant both boys -were rolling and tumbling in the dirt, gripped together like cats; and -for the space of a minute they tugged and tore at each other's hair and -clothes, punched and scratched each other's nose, and covered -themselves with dust and glory. Presently the confusion took form, and -through the fog of battle Tom appeared, seated astride the new boy, and -pounding him with his fists. "Holler 'nuff!" said he. - -The boy only struggled to free himself. He was crying--mainly from rage. - -"Holler 'nuff!"--and the pounding went on. - -At last the stranger got out a smothered "'Nuff!" and Tom let him up -and said: - -"Now that'll learn you. Better look out who you're fooling with next -time." - -The new boy went off brushing the dust from his clothes, sobbing, -snuffling, and occasionally looking back and shaking his head and -threatening what he would do to Tom the "next time he caught him out." -To which Tom responded with jeers, and started off in high feather, and -as soon as his back was turned the new boy snatched up a stone, threw -it and hit him between the shoulders and then turned tail and ran like -an antelope. Tom chased the traitor home, and thus found out where he -lived. He then held a position at the gate for some time, daring the -enemy to come outside, but the enemy only made faces at him through the -window and declined. At last the enemy's mother appeared, and called -Tom a bad, vicious, vulgar child, and ordered him away. So he went -away; but he said he "'lowed" to "lay" for that boy. - -He got home pretty late that night, and when he climbed cautiously in -at the window, he uncovered an ambuscade, in the person of his aunt; -and when she saw the state his clothes were in her resolution to turn -his Saturday holiday into captivity at hard labor became adamantine in -its firmness. - - - -CHAPTER II - -SATURDAY morning was come, and all the summer world was bright and -fresh, and brimming with life. There was a song in every heart; and if -the heart was young the music issued at the lips. There was cheer in -every face and a spring in every step. The locust-trees were in bloom -and the fragrance of the blossoms filled the air. Cardiff Hill, beyond -the village and above it, was green with vegetation and it lay just far -enough away to seem a Delectable Land, dreamy, reposeful, and inviting. - -Tom appeared on the sidewalk with a bucket of whitewash and a -long-handled brush. He surveyed the fence, and all gladness left him and -a deep melancholy settled down upon his spirit. Thirty yards of board -fence nine feet high. Life to him seemed hollow, and existence but a -burden. Sighing, he dipped his brush and passed it along the topmost -plank; repeated the operation; did it again; compared the insignificant -whitewashed streak with the far-reaching continent of unwhitewashed -fence, and sat down on a tree-box discouraged. Jim came skipping out at -the gate with a tin pail, and singing Buffalo Gals. Bringing water from -the town pump had always been hateful work in Tom's eyes, before, but -now it did not strike him so. He remembered that there was company at -the pump. White, mulatto, and negro boys and girls were always there -waiting their turns, resting, trading playthings, quarrelling, -fighting, skylarking. And he remembered that although the pump was only -a hundred and fifty yards off, Jim never got back with a bucket of -water under an hour--and even then somebody generally had to go after -him. Tom said: - -"Say, Jim, I'll fetch the water if you'll whitewash some." - -Jim shook his head and said: - -"Can't, Mars Tom. Ole missis, she tole me I got to go an' git dis -water an' not stop foolin' roun' wid anybody. She say she spec' Mars -Tom gwine to ax me to whitewash, an' so she tole me go 'long an' 'tend -to my own business--she 'lowed SHE'D 'tend to de whitewashin'." - -"Oh, never you mind what she said, Jim. That's the way she always -talks. Gimme the bucket--I won't be gone only a a minute. SHE won't -ever know." - -"Oh, I dasn't, Mars Tom. Ole missis she'd take an' tar de head off'n -me. 'Deed she would." - -"SHE! She never licks anybody--whacks 'em over the head with her -thimble--and who cares for that, I'd like to know. She talks awful, but -talk don't hurt--anyways it don't if she don't cry. Jim, I'll give you -a marvel. I'll give you a white alley!" - -Jim began to waver. - -"White alley, Jim! And it's a bully taw." - -"My! Dat's a mighty gay marvel, I tell you! But Mars Tom I's powerful -'fraid ole missis--" - -"And besides, if you will I'll show you my sore toe." - -Jim was only human--this attraction was too much for him. He put down -his pail, took the white alley, and bent over the toe with absorbing -interest while the bandage was being unwound. In another moment he was -flying down the street with his pail and a tingling rear, Tom was -whitewashing with vigor, and Aunt Polly was retiring from the field -with a slipper in her hand and triumph in her eye. - -But Tom's energy did not last. He began to think of the fun he had -planned for this day, and his sorrows multiplied. Soon the free boys -would come tripping along on all sorts of delicious expeditions, and -they would make a world of fun of him for having to work--the very -thought of it burnt him like fire. He got out his worldly wealth and -examined it--bits of toys, marbles, and trash; enough to buy an -exchange of WORK, maybe, but not half enough to buy so much as half an -hour of pure freedom. So he returned his straitened means to his -pocket, and gave up the idea of trying to buy the boys. At this dark -and hopeless moment an inspiration burst upon him! Nothing less than a -great, magnificent inspiration. - -He took up his brush and went tranquilly to work. Ben Rogers hove in -sight presently--the very boy, of all boys, whose ridicule he had been -dreading. Ben's gait was the hop-skip-and-jump--proof enough that his -heart was light and his anticipations high. He was eating an apple, and -giving a long, melodious whoop, at intervals, followed by a deep-toned -ding-dong-dong, ding-dong-dong, for he was personating a steamboat. As -he drew near, he slackened speed, took the middle of the street, leaned -far over to starboard and rounded to ponderously and with laborious -pomp and circumstance--for he was personating the Big Missouri, and -considered himself to be drawing nine feet of water. He was boat and -captain and engine-bells combined, so he had to imagine himself -standing on his own hurricane-deck giving the orders and executing them: - -"Stop her, sir! Ting-a-ling-ling!" The headway ran almost out, and he -drew up slowly toward the sidewalk. - -"Ship up to back! Ting-a-ling-ling!" His arms straightened and -stiffened down his sides. - -"Set her back on the stabboard! Ting-a-ling-ling! Chow! ch-chow-wow! -Chow!" His right hand, meantime, describing stately circles--for it was -representing a forty-foot wheel. - -"Let her go back on the labboard! Ting-a-lingling! Chow-ch-chow-chow!" -The left hand began to describe circles. - -"Stop the stabboard! Ting-a-ling-ling! Stop the labboard! Come ahead -on the stabboard! Stop her! Let your outside turn over slow! -Ting-a-ling-ling! Chow-ow-ow! Get out that head-line! LIVELY now! -Come--out with your spring-line--what're you about there! Take a turn -round that stump with the bight of it! Stand by that stage, now--let her -go! Done with the engines, sir! Ting-a-ling-ling! SH'T! S'H'T! SH'T!" -(trying the gauge-cocks). - -Tom went on whitewashing--paid no attention to the steamboat. Ben -stared a moment and then said: "Hi-YI! YOU'RE up a stump, ain't you!" - -No answer. Tom surveyed his last touch with the eye of an artist, then -he gave his brush another gentle sweep and surveyed the result, as -before. Ben ranged up alongside of him. Tom's mouth watered for the -apple, but he stuck to his work. Ben said: - -"Hello, old chap, you got to work, hey?" - -Tom wheeled suddenly and said: - -"Why, it's you, Ben! I warn't noticing." - -"Say--I'm going in a-swimming, I am. Don't you wish you could? But of -course you'd druther WORK--wouldn't you? Course you would!" - -Tom contemplated the boy a bit, and said: - -"What do you call work?" - -"Why, ain't THAT work?" - -Tom resumed his whitewashing, and answered carelessly: - -"Well, maybe it is, and maybe it ain't. All I know, is, it suits Tom -Sawyer." - -"Oh come, now, you don't mean to let on that you LIKE it?" - -The brush continued to move. - -"Like it? Well, I don't see why I oughtn't to like it. Does a boy get -a chance to whitewash a fence every day?" - -That put the thing in a new light. Ben stopped nibbling his apple. Tom -swept his brush daintily back and forth--stepped back to note the -effect--added a touch here and there--criticised the effect again--Ben -watching every move and getting more and more interested, more and more -absorbed. Presently he said: - -"Say, Tom, let ME whitewash a little." - -Tom considered, was about to consent; but he altered his mind: - -"No--no--I reckon it wouldn't hardly do, Ben. You see, Aunt Polly's -awful particular about this fence--right here on the street, you know ---but if it was the back fence I wouldn't mind and SHE wouldn't. Yes, -she's awful particular about this fence; it's got to be done very -careful; I reckon there ain't one boy in a thousand, maybe two -thousand, that can do it the way it's got to be done." - -"No--is that so? Oh come, now--lemme just try. Only just a little--I'd -let YOU, if you was me, Tom." - -"Ben, I'd like to, honest injun; but Aunt Polly--well, Jim wanted to -do it, but she wouldn't let him; Sid wanted to do it, and she wouldn't -let Sid. Now don't you see how I'm fixed? If you was to tackle this -fence and anything was to happen to it--" - -"Oh, shucks, I'll be just as careful. Now lemme try. Say--I'll give -you the core of my apple." - -"Well, here--No, Ben, now don't. I'm afeard--" - -"I'll give you ALL of it!" - -Tom gave up the brush with reluctance in his face, but alacrity in his -heart. And while the late steamer Big Missouri worked and sweated in -the sun, the retired artist sat on a barrel in the shade close by, -dangled his legs, munched his apple, and planned the slaughter of more -innocents. There was no lack of material; boys happened along every -little while; they came to jeer, but remained to whitewash. By the time -Ben was fagged out, Tom had traded the next chance to Billy Fisher for -a kite, in good repair; and when he played out, Johnny Miller bought in -for a dead rat and a string to swing it with--and so on, and so on, -hour after hour. And when the middle of the afternoon came, from being -a poor poverty-stricken boy in the morning, Tom was literally rolling -in wealth. He had besides the things before mentioned, twelve marbles, -part of a jews-harp, a piece of blue bottle-glass to look through, a -spool cannon, a key that wouldn't unlock anything, a fragment of chalk, -a glass stopper of a decanter, a tin soldier, a couple of tadpoles, six -fire-crackers, a kitten with only one eye, a brass doorknob, a -dog-collar--but no dog--the handle of a knife, four pieces of -orange-peel, and a dilapidated old window sash. - -He had had a nice, good, idle time all the while--plenty of company ---and the fence had three coats of whitewash on it! If he hadn't run out -of whitewash he would have bankrupted every boy in the village. - -Tom said to himself that it was not such a hollow world, after all. He -had discovered a great law of human action, without knowing it--namely, -that in order to make a man or a boy covet a thing, it is only -necessary to make the thing difficult to attain. If he had been a great -and wise philosopher, like the writer of this book, he would now have -comprehended that Work consists of whatever a body is OBLIGED to do, -and that Play consists of whatever a body is not obliged to do. And -this would help him to understand why constructing artificial flowers -or performing on a tread-mill is work, while rolling ten-pins or -climbing Mont Blanc is only amusement. There are wealthy gentlemen in -England who drive four-horse passenger-coaches twenty or thirty miles -on a daily line, in the summer, because the privilege costs them -considerable money; but if they were offered wages for the service, -that would turn it into work and then they would resign. - -The boy mused awhile over the substantial change which had taken place -in his worldly circumstances, and then wended toward headquarters to -report. - - - -CHAPTER III - -TOM presented himself before Aunt Polly, who was sitting by an open -window in a pleasant rearward apartment, which was bedroom, -breakfast-room, dining-room, and library, combined. The balmy summer -air, the restful quiet, the odor of the flowers, and the drowsing murmur -of the bees had had their effect, and she was nodding over her knitting ---for she had no company but the cat, and it was asleep in her lap. Her -spectacles were propped up on her gray head for safety. She had thought -that of course Tom had deserted long ago, and she wondered at seeing him -place himself in her power again in this intrepid way. He said: "Mayn't -I go and play now, aunt?" - -"What, a'ready? How much have you done?" - -"It's all done, aunt." - -"Tom, don't lie to me--I can't bear it." - -"I ain't, aunt; it IS all done." - -Aunt Polly placed small trust in such evidence. She went out to see -for herself; and she would have been content to find twenty per cent. -of Tom's statement true. When she found the entire fence whitewashed, -and not only whitewashed but elaborately coated and recoated, and even -a streak added to the ground, her astonishment was almost unspeakable. -She said: - -"Well, I never! There's no getting round it, you can work when you're -a mind to, Tom." And then she diluted the compliment by adding, "But -it's powerful seldom you're a mind to, I'm bound to say. Well, go 'long -and play; but mind you get back some time in a week, or I'll tan you." - -She was so overcome by the splendor of his achievement that she took -him into the closet and selected a choice apple and delivered it to -him, along with an improving lecture upon the added value and flavor a -treat took to itself when it came without sin through virtuous effort. -And while she closed with a happy Scriptural flourish, he "hooked" a -doughnut. - -Then he skipped out, and saw Sid just starting up the outside stairway -that led to the back rooms on the second floor. Clods were handy and -the air was full of them in a twinkling. They raged around Sid like a -hail-storm; and before Aunt Polly could collect her surprised faculties -and sally to the rescue, six or seven clods had taken personal effect, -and Tom was over the fence and gone. There was a gate, but as a general -thing he was too crowded for time to make use of it. His soul was at -peace, now that he had settled with Sid for calling attention to his -black thread and getting him into trouble. - -Tom skirted the block, and came round into a muddy alley that led by -the back of his aunt's cow-stable. He presently got safely beyond the -reach of capture and punishment, and hastened toward the public square -of the village, where two "military" companies of boys had met for -conflict, according to previous appointment. Tom was General of one of -these armies, Joe Harper (a bosom friend) General of the other. These -two great commanders did not condescend to fight in person--that being -better suited to the still smaller fry--but sat together on an eminence -and conducted the field operations by orders delivered through -aides-de-camp. Tom's army won a great victory, after a long and -hard-fought battle. Then the dead were counted, prisoners exchanged, -the terms of the next disagreement agreed upon, and the day for the -necessary battle appointed; after which the armies fell into line and -marched away, and Tom turned homeward alone. - -As he was passing by the house where Jeff Thatcher lived, he saw a new -girl in the garden--a lovely little blue-eyed creature with yellow hair -plaited into two long-tails, white summer frock and embroidered -pantalettes. The fresh-crowned hero fell without firing a shot. A -certain Amy Lawrence vanished out of his heart and left not even a -memory of herself behind. He had thought he loved her to distraction; -he had regarded his passion as adoration; and behold it was only a poor -little evanescent partiality. He had been months winning her; she had -confessed hardly a week ago; he had been the happiest and the proudest -boy in the world only seven short days, and here in one instant of time -she had gone out of his heart like a casual stranger whose visit is -done. - -He worshipped this new angel with furtive eye, till he saw that she -had discovered him; then he pretended he did not know she was present, -and began to "show off" in all sorts of absurd boyish ways, in order to -win her admiration. He kept up this grotesque foolishness for some -time; but by-and-by, while he was in the midst of some dangerous -gymnastic performances, he glanced aside and saw that the little girl -was wending her way toward the house. Tom came up to the fence and -leaned on it, grieving, and hoping she would tarry yet awhile longer. -She halted a moment on the steps and then moved toward the door. Tom -heaved a great sigh as she put her foot on the threshold. But his face -lit up, right away, for she tossed a pansy over the fence a moment -before she disappeared. - -The boy ran around and stopped within a foot or two of the flower, and -then shaded his eyes with his hand and began to look down street as if -he had discovered something of interest going on in that direction. -Presently he picked up a straw and began trying to balance it on his -nose, with his head tilted far back; and as he moved from side to side, -in his efforts, he edged nearer and nearer toward the pansy; finally -his bare foot rested upon it, his pliant toes closed upon it, and he -hopped away with the treasure and disappeared round the corner. But -only for a minute--only while he could button the flower inside his -jacket, next his heart--or next his stomach, possibly, for he was not -much posted in anatomy, and not hypercritical, anyway. - -He returned, now, and hung about the fence till nightfall, "showing -off," as before; but the girl never exhibited herself again, though Tom -comforted himself a little with the hope that she had been near some -window, meantime, and been aware of his attentions. Finally he strode -home reluctantly, with his poor head full of visions. - -All through supper his spirits were so high that his aunt wondered -"what had got into the child." He took a good scolding about clodding -Sid, and did not seem to mind it in the least. He tried to steal sugar -under his aunt's very nose, and got his knuckles rapped for it. He said: - -"Aunt, you don't whack Sid when he takes it." - -"Well, Sid don't torment a body the way you do. You'd be always into -that sugar if I warn't watching you." - -Presently she stepped into the kitchen, and Sid, happy in his -immunity, reached for the sugar-bowl--a sort of glorying over Tom which -was wellnigh unbearable. But Sid's fingers slipped and the bowl dropped -and broke. Tom was in ecstasies. In such ecstasies that he even -controlled his tongue and was silent. He said to himself that he would -not speak a word, even when his aunt came in, but would sit perfectly -still till she asked who did the mischief; and then he would tell, and -there would be nothing so good in the world as to see that pet model -"catch it." He was so brimful of exultation that he could hardly hold -himself when the old lady came back and stood above the wreck -discharging lightnings of wrath from over her spectacles. He said to -himself, "Now it's coming!" And the next instant he was sprawling on -the floor! The potent palm was uplifted to strike again when Tom cried -out: - -"Hold on, now, what 'er you belting ME for?--Sid broke it!" - -Aunt Polly paused, perplexed, and Tom looked for healing pity. But -when she got her tongue again, she only said: - -"Umf! Well, you didn't get a lick amiss, I reckon. You been into some -other audacious mischief when I wasn't around, like enough." - -Then her conscience reproached her, and she yearned to say something -kind and loving; but she judged that this would be construed into a -confession that she had been in the wrong, and discipline forbade that. -So she kept silence, and went about her affairs with a troubled heart. -Tom sulked in a corner and exalted his woes. He knew that in her heart -his aunt was on her knees to him, and he was morosely gratified by the -consciousness of it. He would hang out no signals, he would take notice -of none. He knew that a yearning glance fell upon him, now and then, -through a film of tears, but he refused recognition of it. He pictured -himself lying sick unto death and his aunt bending over him beseeching -one little forgiving word, but he would turn his face to the wall, and -die with that word unsaid. Ah, how would she feel then? And he pictured -himself brought home from the river, dead, with his curls all wet, and -his sore heart at rest. How she would throw herself upon him, and how -her tears would fall like rain, and her lips pray God to give her back -her boy and she would never, never abuse him any more! But he would lie -there cold and white and make no sign--a poor little sufferer, whose -griefs were at an end. He so worked upon his feelings with the pathos -of these dreams, that he had to keep swallowing, he was so like to -choke; and his eyes swam in a blur of water, which overflowed when he -winked, and ran down and trickled from the end of his nose. And such a -luxury to him was this petting of his sorrows, that he could not bear -to have any worldly cheeriness or any grating delight intrude upon it; -it was too sacred for such contact; and so, presently, when his cousin -Mary danced in, all alive with the joy of seeing home again after an -age-long visit of one week to the country, he got up and moved in -clouds and darkness out at one door as she brought song and sunshine in -at the other. - -He wandered far from the accustomed haunts of boys, and sought -desolate places that were in harmony with his spirit. A log raft in the -river invited him, and he seated himself on its outer edge and -contemplated the dreary vastness of the stream, wishing, the while, -that he could only be drowned, all at once and unconsciously, without -undergoing the uncomfortable routine devised by nature. Then he thought -of his flower. He got it out, rumpled and wilted, and it mightily -increased his dismal felicity. He wondered if she would pity him if she -knew? Would she cry, and wish that she had a right to put her arms -around his neck and comfort him? Or would she turn coldly away like all -the hollow world? This picture brought such an agony of pleasurable -suffering that he worked it over and over again in his mind and set it -up in new and varied lights, till he wore it threadbare. At last he -rose up sighing and departed in the darkness. - -About half-past nine or ten o'clock he came along the deserted street -to where the Adored Unknown lived; he paused a moment; no sound fell -upon his listening ear; a candle was casting a dull glow upon the -curtain of a second-story window. Was the sacred presence there? He -climbed the fence, threaded his stealthy way through the plants, till -he stood under that window; he looked up at it long, and with emotion; -then he laid him down on the ground under it, disposing himself upon -his back, with his hands clasped upon his breast and holding his poor -wilted flower. And thus he would die--out in the cold world, with no -shelter over his homeless head, no friendly hand to wipe the -death-damps from his brow, no loving face to bend pityingly over him -when the great agony came. And thus SHE would see him when she looked -out upon the glad morning, and oh! would she drop one little tear upon -his poor, lifeless form, would she heave one little sigh to see a bright -young life so rudely blighted, so untimely cut down? - -The window went up, a maid-servant's discordant voice profaned the -holy calm, and a deluge of water drenched the prone martyr's remains! - -The strangling hero sprang up with a relieving snort. There was a whiz -as of a missile in the air, mingled with the murmur of a curse, a sound -as of shivering glass followed, and a small, vague form went over the -fence and shot away in the gloom. - -Not long after, as Tom, all undressed for bed, was surveying his -drenched garments by the light of a tallow dip, Sid woke up; but if he -had any dim idea of making any "references to allusions," he thought -better of it and held his peace, for there was danger in Tom's eye. - -Tom turned in without the added vexation of prayers, and Sid made -mental note of the omission. - - - -CHAPTER IV - -THE sun rose upon a tranquil world, and beamed down upon the peaceful -village like a benediction. Breakfast over, Aunt Polly had family -worship: it began with a prayer built from the ground up of solid -courses of Scriptural quotations, welded together with a thin mortar of -originality; and from the summit of this she delivered a grim chapter -of the Mosaic Law, as from Sinai. - -Then Tom girded up his loins, so to speak, and went to work to "get -his verses." Sid had learned his lesson days before. Tom bent all his -energies to the memorizing of five verses, and he chose part of the -Sermon on the Mount, because he could find no verses that were shorter. -At the end of half an hour Tom had a vague general idea of his lesson, -but no more, for his mind was traversing the whole field of human -thought, and his hands were busy with distracting recreations. Mary -took his book to hear him recite, and he tried to find his way through -the fog: - -"Blessed are the--a--a--" - -"Poor"-- - -"Yes--poor; blessed are the poor--a--a--" - -"In spirit--" - -"In spirit; blessed are the poor in spirit, for they--they--" - -"THEIRS--" - -"For THEIRS. Blessed are the poor in spirit, for theirs is the kingdom -of heaven. Blessed are they that mourn, for they--they--" - -"Sh--" - -"For they--a--" - -"S, H, A--" - -"For they S, H--Oh, I don't know what it is!" - -"SHALL!" - -"Oh, SHALL! for they shall--for they shall--a--a--shall mourn--a--a-- -blessed are they that shall--they that--a--they that shall mourn, for -they shall--a--shall WHAT? Why don't you tell me, Mary?--what do you -want to be so mean for?" - -"Oh, Tom, you poor thick-headed thing, I'm not teasing you. I wouldn't -do that. You must go and learn it again. Don't you be discouraged, Tom, -you'll manage it--and if you do, I'll give you something ever so nice. -There, now, that's a good boy." - -"All right! What is it, Mary, tell me what it is." - -"Never you mind, Tom. You know if I say it's nice, it is nice." - -"You bet you that's so, Mary. All right, I'll tackle it again." - -And he did "tackle it again"--and under the double pressure of -curiosity and prospective gain he did it with such spirit that he -accomplished a shining success. Mary gave him a brand-new "Barlow" -knife worth twelve and a half cents; and the convulsion of delight that -swept his system shook him to his foundations. True, the knife would -not cut anything, but it was a "sure-enough" Barlow, and there was -inconceivable grandeur in that--though where the Western boys ever got -the idea that such a weapon could possibly be counterfeited to its -injury is an imposing mystery and will always remain so, perhaps. Tom -contrived to scarify the cupboard with it, and was arranging to begin -on the bureau, when he was called off to dress for Sunday-school. - -Mary gave him a tin basin of water and a piece of soap, and he went -outside the door and set the basin on a little bench there; then he -dipped the soap in the water and laid it down; turned up his sleeves; -poured out the water on the ground, gently, and then entered the -kitchen and began to wipe his face diligently on the towel behind the -door. But Mary removed the towel and said: - -"Now ain't you ashamed, Tom. You mustn't be so bad. Water won't hurt -you." - -Tom was a trifle disconcerted. The basin was refilled, and this time -he stood over it a little while, gathering resolution; took in a big -breath and began. When he entered the kitchen presently, with both eyes -shut and groping for the towel with his hands, an honorable testimony -of suds and water was dripping from his face. But when he emerged from -the towel, he was not yet satisfactory, for the clean territory stopped -short at his chin and his jaws, like a mask; below and beyond this line -there was a dark expanse of unirrigated soil that spread downward in -front and backward around his neck. Mary took him in hand, and when she -was done with him he was a man and a brother, without distinction of -color, and his saturated hair was neatly brushed, and its short curls -wrought into a dainty and symmetrical general effect. [He privately -smoothed out the curls, with labor and difficulty, and plastered his -hair close down to his head; for he held curls to be effeminate, and -his own filled his life with bitterness.] Then Mary got out a suit of -his clothing that had been used only on Sundays during two years--they -were simply called his "other clothes"--and so by that we know the -size of his wardrobe. The girl "put him to rights" after he had dressed -himself; she buttoned his neat roundabout up to his chin, turned his -vast shirt collar down over his shoulders, brushed him off and crowned -him with his speckled straw hat. He now looked exceedingly improved and -uncomfortable. He was fully as uncomfortable as he looked; for there -was a restraint about whole clothes and cleanliness that galled him. He -hoped that Mary would forget his shoes, but the hope was blighted; she -coated them thoroughly with tallow, as was the custom, and brought them -out. He lost his temper and said he was always being made to do -everything he didn't want to do. But Mary said, persuasively: - -"Please, Tom--that's a good boy." - -So he got into the shoes snarling. Mary was soon ready, and the three -children set out for Sunday-school--a place that Tom hated with his -whole heart; but Sid and Mary were fond of it. - -Sabbath-school hours were from nine to half-past ten; and then church -service. Two of the children always remained for the sermon -voluntarily, and the other always remained too--for stronger reasons. -The church's high-backed, uncushioned pews would seat about three -hundred persons; the edifice was but a small, plain affair, with a sort -of pine board tree-box on top of it for a steeple. At the door Tom -dropped back a step and accosted a Sunday-dressed comrade: - -"Say, Billy, got a yaller ticket?" - -"Yes." - -"What'll you take for her?" - -"What'll you give?" - -"Piece of lickrish and a fish-hook." - -"Less see 'em." - -Tom exhibited. They were satisfactory, and the property changed hands. -Then Tom traded a couple of white alleys for three red tickets, and -some small trifle or other for a couple of blue ones. He waylaid other -boys as they came, and went on buying tickets of various colors ten or -fifteen minutes longer. He entered the church, now, with a swarm of -clean and noisy boys and girls, proceeded to his seat and started a -quarrel with the first boy that came handy. The teacher, a grave, -elderly man, interfered; then turned his back a moment and Tom pulled a -boy's hair in the next bench, and was absorbed in his book when the boy -turned around; stuck a pin in another boy, presently, in order to hear -him say "Ouch!" and got a new reprimand from his teacher. Tom's whole -class were of a pattern--restless, noisy, and troublesome. When they -came to recite their lessons, not one of them knew his verses -perfectly, but had to be prompted all along. However, they worried -through, and each got his reward--in small blue tickets, each with a -passage of Scripture on it; each blue ticket was pay for two verses of -the recitation. Ten blue tickets equalled a red one, and could be -exchanged for it; ten red tickets equalled a yellow one; for ten yellow -tickets the superintendent gave a very plainly bound Bible (worth forty -cents in those easy times) to the pupil. How many of my readers would -have the industry and application to memorize two thousand verses, even -for a Dore Bible? And yet Mary had acquired two Bibles in this way--it -was the patient work of two years--and a boy of German parentage had -won four or five. He once recited three thousand verses without -stopping; but the strain upon his mental faculties was too great, and -he was little better than an idiot from that day forth--a grievous -misfortune for the school, for on great occasions, before company, the -superintendent (as Tom expressed it) had always made this boy come out -and "spread himself." Only the older pupils managed to keep their -tickets and stick to their tedious work long enough to get a Bible, and -so the delivery of one of these prizes was a rare and noteworthy -circumstance; the successful pupil was so great and conspicuous for -that day that on the spot every scholar's heart was fired with a fresh -ambition that often lasted a couple of weeks. It is possible that Tom's -mental stomach had never really hungered for one of those prizes, but -unquestionably his entire being had for many a day longed for the glory -and the eclat that came with it. - -In due course the superintendent stood up in front of the pulpit, with -a closed hymn-book in his hand and his forefinger inserted between its -leaves, and commanded attention. When a Sunday-school superintendent -makes his customary little speech, a hymn-book in the hand is as -necessary as is the inevitable sheet of music in the hand of a singer -who stands forward on the platform and sings a solo at a concert ---though why, is a mystery: for neither the hymn-book nor the sheet of -music is ever referred to by the sufferer. This superintendent was a -slim creature of thirty-five, with a sandy goatee and short sandy hair; -he wore a stiff standing-collar whose upper edge almost reached his -ears and whose sharp points curved forward abreast the corners of his -mouth--a fence that compelled a straight lookout ahead, and a turning -of the whole body when a side view was required; his chin was propped -on a spreading cravat which was as broad and as long as a bank-note, -and had fringed ends; his boot toes were turned sharply up, in the -fashion of the day, like sleigh-runners--an effect patiently and -laboriously produced by the young men by sitting with their toes -pressed against a wall for hours together. Mr. Walters was very earnest -of mien, and very sincere and honest at heart; and he held sacred -things and places in such reverence, and so separated them from worldly -matters, that unconsciously to himself his Sunday-school voice had -acquired a peculiar intonation which was wholly absent on week-days. He -began after this fashion: - -"Now, children, I want you all to sit up just as straight and pretty -as you can and give me all your attention for a minute or two. There ---that is it. That is the way good little boys and girls should do. I see -one little girl who is looking out of the window--I am afraid she -thinks I am out there somewhere--perhaps up in one of the trees making -a speech to the little birds. [Applausive titter.] I want to tell you -how good it makes me feel to see so many bright, clean little faces -assembled in a place like this, learning to do right and be good." And -so forth and so on. It is not necessary to set down the rest of the -oration. It was of a pattern which does not vary, and so it is familiar -to us all. - -The latter third of the speech was marred by the resumption of fights -and other recreations among certain of the bad boys, and by fidgetings -and whisperings that extended far and wide, washing even to the bases -of isolated and incorruptible rocks like Sid and Mary. But now every -sound ceased suddenly, with the subsidence of Mr. Walters' voice, and -the conclusion of the speech was received with a burst of silent -gratitude. - -A good part of the whispering had been occasioned by an event which -was more or less rare--the entrance of visitors: lawyer Thatcher, -accompanied by a very feeble and aged man; a fine, portly, middle-aged -gentleman with iron-gray hair; and a dignified lady who was doubtless -the latter's wife. The lady was leading a child. Tom had been restless -and full of chafings and repinings; conscience-smitten, too--he could -not meet Amy Lawrence's eye, he could not brook her loving gaze. But -when he saw this small new-comer his soul was all ablaze with bliss in -a moment. The next moment he was "showing off" with all his might ---cuffing boys, pulling hair, making faces--in a word, using every art -that seemed likely to fascinate a girl and win her applause. His -exaltation had but one alloy--the memory of his humiliation in this -angel's garden--and that record in sand was fast washing out, under -the waves of happiness that were sweeping over it now. - -The visitors were given the highest seat of honor, and as soon as Mr. -Walters' speech was finished, he introduced them to the school. The -middle-aged man turned out to be a prodigious personage--no less a one -than the county judge--altogether the most august creation these -children had ever looked upon--and they wondered what kind of material -he was made of--and they half wanted to hear him roar, and were half -afraid he might, too. He was from Constantinople, twelve miles away--so -he had travelled, and seen the world--these very eyes had looked upon -the county court-house--which was said to have a tin roof. The awe -which these reflections inspired was attested by the impressive silence -and the ranks of staring eyes. This was the great Judge Thatcher, -brother of their own lawyer. Jeff Thatcher immediately went forward, to -be familiar with the great man and be envied by the school. It would -have been music to his soul to hear the whisperings: - -"Look at him, Jim! He's a going up there. Say--look! he's a going to -shake hands with him--he IS shaking hands with him! By jings, don't you -wish you was Jeff?" - -Mr. Walters fell to "showing off," with all sorts of official -bustlings and activities, giving orders, delivering judgments, -discharging directions here, there, everywhere that he could find a -target. The librarian "showed off"--running hither and thither with his -arms full of books and making a deal of the splutter and fuss that -insect authority delights in. The young lady teachers "showed off" ---bending sweetly over pupils that were lately being boxed, lifting -pretty warning fingers at bad little boys and patting good ones -lovingly. The young gentlemen teachers "showed off" with small -scoldings and other little displays of authority and fine attention to -discipline--and most of the teachers, of both sexes, found business up -at the library, by the pulpit; and it was business that frequently had -to be done over again two or three times (with much seeming vexation). -The little girls "showed off" in various ways, and the little boys -"showed off" with such diligence that the air was thick with paper wads -and the murmur of scufflings. And above it all the great man sat and -beamed a majestic judicial smile upon all the house, and warmed himself -in the sun of his own grandeur--for he was "showing off," too. - -There was only one thing wanting to make Mr. Walters' ecstasy -complete, and that was a chance to deliver a Bible-prize and exhibit a -prodigy. Several pupils had a few yellow tickets, but none had enough ---he had been around among the star pupils inquiring. He would have given -worlds, now, to have that German lad back again with a sound mind. - -And now at this moment, when hope was dead, Tom Sawyer came forward -with nine yellow tickets, nine red tickets, and ten blue ones, and -demanded a Bible. This was a thunderbolt out of a clear sky. Walters -was not expecting an application from this source for the next ten -years. But there was no getting around it--here were the certified -checks, and they were good for their face. Tom was therefore elevated -to a place with the Judge and the other elect, and the great news was -announced from headquarters. It was the most stunning surprise of the -decade, and so profound was the sensation that it lifted the new hero -up to the judicial one's altitude, and the school had two marvels to -gaze upon in place of one. The boys were all eaten up with envy--but -those that suffered the bitterest pangs were those who perceived too -late that they themselves had contributed to this hated splendor by -trading tickets to Tom for the wealth he had amassed in selling -whitewashing privileges. These despised themselves, as being the dupes -of a wily fraud, a guileful snake in the grass. - -The prize was delivered to Tom with as much effusion as the -superintendent could pump up under the circumstances; but it lacked -somewhat of the true gush, for the poor fellow's instinct taught him -that there was a mystery here that could not well bear the light, -perhaps; it was simply preposterous that this boy had warehoused two -thousand sheaves of Scriptural wisdom on his premises--a dozen would -strain his capacity, without a doubt. - -Amy Lawrence was proud and glad, and she tried to make Tom see it in -her face--but he wouldn't look. She wondered; then she was just a grain -troubled; next a dim suspicion came and went--came again; she watched; -a furtive glance told her worlds--and then her heart broke, and she was -jealous, and angry, and the tears came and she hated everybody. Tom -most of all (she thought). - -Tom was introduced to the Judge; but his tongue was tied, his breath -would hardly come, his heart quaked--partly because of the awful -greatness of the man, but mainly because he was her parent. He would -have liked to fall down and worship him, if it were in the dark. The -Judge put his hand on Tom's head and called him a fine little man, and -asked him what his name was. The boy stammered, gasped, and got it out: - -"Tom." - -"Oh, no, not Tom--it is--" - -"Thomas." - -"Ah, that's it. I thought there was more to it, maybe. That's very -well. But you've another one I daresay, and you'll tell it to me, won't -you?" - -"Tell the gentleman your other name, Thomas," said Walters, "and say -sir. You mustn't forget your manners." - -"Thomas Sawyer--sir." - -"That's it! That's a good boy. Fine boy. Fine, manly little fellow. -Two thousand verses is a great many--very, very great many. And you -never can be sorry for the trouble you took to learn them; for -knowledge is worth more than anything there is in the world; it's what -makes great men and good men; you'll be a great man and a good man -yourself, some day, Thomas, and then you'll look back and say, It's all -owing to the precious Sunday-school privileges of my boyhood--it's all -owing to my dear teachers that taught me to learn--it's all owing to -the good superintendent, who encouraged me, and watched over me, and -gave me a beautiful Bible--a splendid elegant Bible--to keep and have -it all for my own, always--it's all owing to right bringing up! That is -what you will say, Thomas--and you wouldn't take any money for those -two thousand verses--no indeed you wouldn't. And now you wouldn't mind -telling me and this lady some of the things you've learned--no, I know -you wouldn't--for we are proud of little boys that learn. Now, no -doubt you know the names of all the twelve disciples. Won't you tell us -the names of the first two that were appointed?" - -Tom was tugging at a button-hole and looking sheepish. He blushed, -now, and his eyes fell. Mr. Walters' heart sank within him. He said to -himself, it is not possible that the boy can answer the simplest -question--why DID the Judge ask him? Yet he felt obliged to speak up -and say: - -"Answer the gentleman, Thomas--don't be afraid." - -Tom still hung fire. - -"Now I know you'll tell me," said the lady. "The names of the first -two disciples were--" - -"DAVID AND GOLIAH!" - -Let us draw the curtain of charity over the rest of the scene. - - - -CHAPTER V - -ABOUT half-past ten the cracked bell of the small church began to -ring, and presently the people began to gather for the morning sermon. -The Sunday-school children distributed themselves about the house and -occupied pews with their parents, so as to be under supervision. Aunt -Polly came, and Tom and Sid and Mary sat with her--Tom being placed -next the aisle, in order that he might be as far away from the open -window and the seductive outside summer scenes as possible. The crowd -filed up the aisles: the aged and needy postmaster, who had seen better -days; the mayor and his wife--for they had a mayor there, among other -unnecessaries; the justice of the peace; the widow Douglass, fair, -smart, and forty, a generous, good-hearted soul and well-to-do, her -hill mansion the only palace in the town, and the most hospitable and -much the most lavish in the matter of festivities that St. Petersburg -could boast; the bent and venerable Major and Mrs. Ward; lawyer -Riverson, the new notable from a distance; next the belle of the -village, followed by a troop of lawn-clad and ribbon-decked young -heart-breakers; then all the young clerks in town in a body--for they -had stood in the vestibule sucking their cane-heads, a circling wall of -oiled and simpering admirers, till the last girl had run their gantlet; -and last of all came the Model Boy, Willie Mufferson, taking as heedful -care of his mother as if she were cut glass. He always brought his -mother to church, and was the pride of all the matrons. The boys all -hated him, he was so good. And besides, he had been "thrown up to them" -so much. His white handkerchief was hanging out of his pocket behind, as -usual on Sundays--accidentally. Tom had no handkerchief, and he looked -upon boys who had as snobs. - -The congregation being fully assembled, now, the bell rang once more, -to warn laggards and stragglers, and then a solemn hush fell upon the -church which was only broken by the tittering and whispering of the -choir in the gallery. The choir always tittered and whispered all -through service. There was once a church choir that was not ill-bred, -but I have forgotten where it was, now. It was a great many years ago, -and I can scarcely remember anything about it, but I think it was in -some foreign country. - -The minister gave out the hymn, and read it through with a relish, in -a peculiar style which was much admired in that part of the country. -His voice began on a medium key and climbed steadily up till it reached -a certain point, where it bore with strong emphasis upon the topmost -word and then plunged down as if from a spring-board: - - Shall I be car-ri-ed toe the skies, on flow'ry BEDS of ease, - - Whilst others fight to win the prize, and sail thro' BLOODY seas? - -He was regarded as a wonderful reader. At church "sociables" he was -always called upon to read poetry; and when he was through, the ladies -would lift up their hands and let them fall helplessly in their laps, -and "wall" their eyes, and shake their heads, as much as to say, "Words -cannot express it; it is too beautiful, TOO beautiful for this mortal -earth." - -After the hymn had been sung, the Rev. Mr. Sprague turned himself into -a bulletin-board, and read off "notices" of meetings and societies and -things till it seemed that the list would stretch out to the crack of -doom--a queer custom which is still kept up in America, even in cities, -away here in this age of abundant newspapers. Often, the less there is -to justify a traditional custom, the harder it is to get rid of it. - -And now the minister prayed. A good, generous prayer it was, and went -into details: it pleaded for the church, and the little children of the -church; for the other churches of the village; for the village itself; -for the county; for the State; for the State officers; for the United -States; for the churches of the United States; for Congress; for the -President; for the officers of the Government; for poor sailors, tossed -by stormy seas; for the oppressed millions groaning under the heel of -European monarchies and Oriental despotisms; for such as have the light -and the good tidings, and yet have not eyes to see nor ears to hear -withal; for the heathen in the far islands of the sea; and closed with -a supplication that the words he was about to speak might find grace -and favor, and be as seed sown in fertile ground, yielding in time a -grateful harvest of good. Amen. - -There was a rustling of dresses, and the standing congregation sat -down. The boy whose history this book relates did not enjoy the prayer, -he only endured it--if he even did that much. He was restive all -through it; he kept tally of the details of the prayer, unconsciously ---for he was not listening, but he knew the ground of old, and the -clergyman's regular route over it--and when a little trifle of new -matter was interlarded, his ear detected it and his whole nature -resented it; he considered additions unfair, and scoundrelly. In the -midst of the prayer a fly had lit on the back of the pew in front of -him and tortured his spirit by calmly rubbing its hands together, -embracing its head with its arms, and polishing it so vigorously that -it seemed to almost part company with the body, and the slender thread -of a neck was exposed to view; scraping its wings with its hind legs -and smoothing them to its body as if they had been coat-tails; going -through its whole toilet as tranquilly as if it knew it was perfectly -safe. As indeed it was; for as sorely as Tom's hands itched to grab for -it they did not dare--he believed his soul would be instantly destroyed -if he did such a thing while the prayer was going on. But with the -closing sentence his hand began to curve and steal forward; and the -instant the "Amen" was out the fly was a prisoner of war. His aunt -detected the act and made him let it go. - -The minister gave out his text and droned along monotonously through -an argument that was so prosy that many a head by and by began to nod ---and yet it was an argument that dealt in limitless fire and brimstone -and thinned the predestined elect down to a company so small as to be -hardly worth the saving. Tom counted the pages of the sermon; after -church he always knew how many pages there had been, but he seldom knew -anything else about the discourse. However, this time he was really -interested for a little while. The minister made a grand and moving -picture of the assembling together of the world's hosts at the -millennium when the lion and the lamb should lie down together and a -little child should lead them. But the pathos, the lesson, the moral of -the great spectacle were lost upon the boy; he only thought of the -conspicuousness of the principal character before the on-looking -nations; his face lit with the thought, and he said to himself that he -wished he could be that child, if it was a tame lion. - -Now he lapsed into suffering again, as the dry argument was resumed. -Presently he bethought him of a treasure he had and got it out. It was -a large black beetle with formidable jaws--a "pinchbug," he called it. -It was in a percussion-cap box. The first thing the beetle did was to -take him by the finger. A natural fillip followed, the beetle went -floundering into the aisle and lit on its back, and the hurt finger -went into the boy's mouth. The beetle lay there working its helpless -legs, unable to turn over. Tom eyed it, and longed for it; but it was -safe out of his reach. Other people uninterested in the sermon found -relief in the beetle, and they eyed it too. Presently a vagrant poodle -dog came idling along, sad at heart, lazy with the summer softness and -the quiet, weary of captivity, sighing for change. He spied the beetle; -the drooping tail lifted and wagged. He surveyed the prize; walked -around it; smelt at it from a safe distance; walked around it again; -grew bolder, and took a closer smell; then lifted his lip and made a -gingerly snatch at it, just missing it; made another, and another; -began to enjoy the diversion; subsided to his stomach with the beetle -between his paws, and continued his experiments; grew weary at last, -and then indifferent and absent-minded. His head nodded, and little by -little his chin descended and touched the enemy, who seized it. There -was a sharp yelp, a flirt of the poodle's head, and the beetle fell a -couple of yards away, and lit on its back once more. The neighboring -spectators shook with a gentle inward joy, several faces went behind -fans and handkerchiefs, and Tom was entirely happy. The dog looked -foolish, and probably felt so; but there was resentment in his heart, -too, and a craving for revenge. So he went to the beetle and began a -wary attack on it again; jumping at it from every point of a circle, -lighting with his fore-paws within an inch of the creature, making even -closer snatches at it with his teeth, and jerking his head till his -ears flapped again. But he grew tired once more, after a while; tried -to amuse himself with a fly but found no relief; followed an ant -around, with his nose close to the floor, and quickly wearied of that; -yawned, sighed, forgot the beetle entirely, and sat down on it. Then -there was a wild yelp of agony and the poodle went sailing up the -aisle; the yelps continued, and so did the dog; he crossed the house in -front of the altar; he flew down the other aisle; he crossed before the -doors; he clamored up the home-stretch; his anguish grew with his -progress, till presently he was but a woolly comet moving in its orbit -with the gleam and the speed of light. At last the frantic sufferer -sheered from its course, and sprang into its master's lap; he flung it -out of the window, and the voice of distress quickly thinned away and -died in the distance. - -By this time the whole church was red-faced and suffocating with -suppressed laughter, and the sermon had come to a dead standstill. The -discourse was resumed presently, but it went lame and halting, all -possibility of impressiveness being at an end; for even the gravest -sentiments were constantly being received with a smothered burst of -unholy mirth, under cover of some remote pew-back, as if the poor -parson had said a rarely facetious thing. It was a genuine relief to -the whole congregation when the ordeal was over and the benediction -pronounced. - -Tom Sawyer went home quite cheerful, thinking to himself that there -was some satisfaction about divine service when there was a bit of -variety in it. He had but one marring thought; he was willing that the -dog should play with his pinchbug, but he did not think it was upright -in him to carry it off. - - - -CHAPTER VI - -MONDAY morning found Tom Sawyer miserable. Monday morning always found -him so--because it began another week's slow suffering in school. He -generally began that day with wishing he had had no intervening -holiday, it made the going into captivity and fetters again so much -more odious. - -Tom lay thinking. Presently it occurred to him that he wished he was -sick; then he could stay home from school. Here was a vague -possibility. He canvassed his system. No ailment was found, and he -investigated again. This time he thought he could detect colicky -symptoms, and he began to encourage them with considerable hope. But -they soon grew feeble, and presently died wholly away. He reflected -further. Suddenly he discovered something. One of his upper front teeth -was loose. This was lucky; he was about to begin to groan, as a -"starter," as he called it, when it occurred to him that if he came -into court with that argument, his aunt would pull it out, and that -would hurt. So he thought he would hold the tooth in reserve for the -present, and seek further. Nothing offered for some little time, and -then he remembered hearing the doctor tell about a certain thing that -laid up a patient for two or three weeks and threatened to make him -lose a finger. So the boy eagerly drew his sore toe from under the -sheet and held it up for inspection. But now he did not know the -necessary symptoms. However, it seemed well worth while to chance it, -so he fell to groaning with considerable spirit. - -But Sid slept on unconscious. - -Tom groaned louder, and fancied that he began to feel pain in the toe. - -No result from Sid. - -Tom was panting with his exertions by this time. He took a rest and -then swelled himself up and fetched a succession of admirable groans. - -Sid snored on. - -Tom was aggravated. He said, "Sid, Sid!" and shook him. This course -worked well, and Tom began to groan again. Sid yawned, stretched, then -brought himself up on his elbow with a snort, and began to stare at -Tom. Tom went on groaning. Sid said: - -"Tom! Say, Tom!" [No response.] "Here, Tom! TOM! What is the matter, -Tom?" And he shook him and looked in his face anxiously. - -Tom moaned out: - -"Oh, don't, Sid. Don't joggle me." - -"Why, what's the matter, Tom? I must call auntie." - -"No--never mind. It'll be over by and by, maybe. Don't call anybody." - -"But I must! DON'T groan so, Tom, it's awful. How long you been this -way?" - -"Hours. Ouch! Oh, don't stir so, Sid, you'll kill me." - -"Tom, why didn't you wake me sooner? Oh, Tom, DON'T! It makes my -flesh crawl to hear you. Tom, what is the matter?" - -"I forgive you everything, Sid. [Groan.] Everything you've ever done -to me. When I'm gone--" - -"Oh, Tom, you ain't dying, are you? Don't, Tom--oh, don't. Maybe--" - -"I forgive everybody, Sid. [Groan.] Tell 'em so, Sid. And Sid, you -give my window-sash and my cat with one eye to that new girl that's -come to town, and tell her--" - -But Sid had snatched his clothes and gone. Tom was suffering in -reality, now, so handsomely was his imagination working, and so his -groans had gathered quite a genuine tone. - -Sid flew down-stairs and said: - -"Oh, Aunt Polly, come! Tom's dying!" - -"Dying!" - -"Yes'm. Don't wait--come quick!" - -"Rubbage! I don't believe it!" - -But she fled up-stairs, nevertheless, with Sid and Mary at her heels. -And her face grew white, too, and her lip trembled. When she reached -the bedside she gasped out: - -"You, Tom! Tom, what's the matter with you?" - -"Oh, auntie, I'm--" - -"What's the matter with you--what is the matter with you, child?" - -"Oh, auntie, my sore toe's mortified!" - -The old lady sank down into a chair and laughed a little, then cried a -little, then did both together. This restored her and she said: - -"Tom, what a turn you did give me. Now you shut up that nonsense and -climb out of this." - -The groans ceased and the pain vanished from the toe. The boy felt a -little foolish, and he said: - -"Aunt Polly, it SEEMED mortified, and it hurt so I never minded my -tooth at all." - -"Your tooth, indeed! What's the matter with your tooth?" - -"One of them's loose, and it aches perfectly awful." - -"There, there, now, don't begin that groaning again. Open your mouth. -Well--your tooth IS loose, but you're not going to die about that. -Mary, get me a silk thread, and a chunk of fire out of the kitchen." - -Tom said: - -"Oh, please, auntie, don't pull it out. It don't hurt any more. I wish -I may never stir if it does. Please don't, auntie. I don't want to stay -home from school." - -"Oh, you don't, don't you? So all this row was because you thought -you'd get to stay home from school and go a-fishing? Tom, Tom, I love -you so, and you seem to try every way you can to break my old heart -with your outrageousness." By this time the dental instruments were -ready. The old lady made one end of the silk thread fast to Tom's tooth -with a loop and tied the other to the bedpost. Then she seized the -chunk of fire and suddenly thrust it almost into the boy's face. The -tooth hung dangling by the bedpost, now. - -But all trials bring their compensations. As Tom wended to school -after breakfast, he was the envy of every boy he met because the gap in -his upper row of teeth enabled him to expectorate in a new and -admirable way. He gathered quite a following of lads interested in the -exhibition; and one that had cut his finger and had been a centre of -fascination and homage up to this time, now found himself suddenly -without an adherent, and shorn of his glory. His heart was heavy, and -he said with a disdain which he did not feel that it wasn't anything to -spit like Tom Sawyer; but another boy said, "Sour grapes!" and he -wandered away a dismantled hero. - -Shortly Tom came upon the juvenile pariah of the village, Huckleberry -Finn, son of the town drunkard. Huckleberry was cordially hated and -dreaded by all the mothers of the town, because he was idle and lawless -and vulgar and bad--and because all their children admired him so, and -delighted in his forbidden society, and wished they dared to be like -him. Tom was like the rest of the respectable boys, in that he envied -Huckleberry his gaudy outcast condition, and was under strict orders -not to play with him. So he played with him every time he got a chance. -Huckleberry was always dressed in the cast-off clothes of full-grown -men, and they were in perennial bloom and fluttering with rags. His hat -was a vast ruin with a wide crescent lopped out of its brim; his coat, -when he wore one, hung nearly to his heels and had the rearward buttons -far down the back; but one suspender supported his trousers; the seat -of the trousers bagged low and contained nothing, the fringed legs -dragged in the dirt when not rolled up. - -Huckleberry came and went, at his own free will. He slept on doorsteps -in fine weather and in empty hogsheads in wet; he did not have to go to -school or to church, or call any being master or obey anybody; he could -go fishing or swimming when and where he chose, and stay as long as it -suited him; nobody forbade him to fight; he could sit up as late as he -pleased; he was always the first boy that went barefoot in the spring -and the last to resume leather in the fall; he never had to wash, nor -put on clean clothes; he could swear wonderfully. In a word, everything -that goes to make life precious that boy had. So thought every -harassed, hampered, respectable boy in St. Petersburg. - -Tom hailed the romantic outcast: - -"Hello, Huckleberry!" - -"Hello yourself, and see how you like it." - -"What's that you got?" - -"Dead cat." - -"Lemme see him, Huck. My, he's pretty stiff. Where'd you get him?" - -"Bought him off'n a boy." - -"What did you give?" - -"I give a blue ticket and a bladder that I got at the slaughter-house." - -"Where'd you get the blue ticket?" - -"Bought it off'n Ben Rogers two weeks ago for a hoop-stick." - -"Say--what is dead cats good for, Huck?" - -"Good for? Cure warts with." - -"No! Is that so? I know something that's better." - -"I bet you don't. What is it?" - -"Why, spunk-water." - -"Spunk-water! I wouldn't give a dern for spunk-water." - -"You wouldn't, wouldn't you? D'you ever try it?" - -"No, I hain't. But Bob Tanner did." - -"Who told you so!" - -"Why, he told Jeff Thatcher, and Jeff told Johnny Baker, and Johnny -told Jim Hollis, and Jim told Ben Rogers, and Ben told a nigger, and -the nigger told me. There now!" - -"Well, what of it? They'll all lie. Leastways all but the nigger. I -don't know HIM. But I never see a nigger that WOULDN'T lie. Shucks! Now -you tell me how Bob Tanner done it, Huck." - -"Why, he took and dipped his hand in a rotten stump where the -rain-water was." - -"In the daytime?" - -"Certainly." - -"With his face to the stump?" - -"Yes. Least I reckon so." - -"Did he say anything?" - -"I don't reckon he did. I don't know." - -"Aha! Talk about trying to cure warts with spunk-water such a blame -fool way as that! Why, that ain't a-going to do any good. You got to go -all by yourself, to the middle of the woods, where you know there's a -spunk-water stump, and just as it's midnight you back up against the -stump and jam your hand in and say: - - 'Barley-corn, barley-corn, injun-meal shorts, - Spunk-water, spunk-water, swaller these warts,' - -and then walk away quick, eleven steps, with your eyes shut, and then -turn around three times and walk home without speaking to anybody. -Because if you speak the charm's busted." - -"Well, that sounds like a good way; but that ain't the way Bob Tanner -done." - -"No, sir, you can bet he didn't, becuz he's the wartiest boy in this -town; and he wouldn't have a wart on him if he'd knowed how to work -spunk-water. I've took off thousands of warts off of my hands that way, -Huck. I play with frogs so much that I've always got considerable many -warts. Sometimes I take 'em off with a bean." - -"Yes, bean's good. I've done that." - -"Have you? What's your way?" - -"You take and split the bean, and cut the wart so as to get some -blood, and then you put the blood on one piece of the bean and take and -dig a hole and bury it 'bout midnight at the crossroads in the dark of -the moon, and then you burn up the rest of the bean. You see that piece -that's got the blood on it will keep drawing and drawing, trying to -fetch the other piece to it, and so that helps the blood to draw the -wart, and pretty soon off she comes." - -"Yes, that's it, Huck--that's it; though when you're burying it if you -say 'Down bean; off wart; come no more to bother me!' it's better. -That's the way Joe Harper does, and he's been nearly to Coonville and -most everywheres. But say--how do you cure 'em with dead cats?" - -"Why, you take your cat and go and get in the graveyard 'long about -midnight when somebody that was wicked has been buried; and when it's -midnight a devil will come, or maybe two or three, but you can't see -'em, you can only hear something like the wind, or maybe hear 'em talk; -and when they're taking that feller away, you heave your cat after 'em -and say, 'Devil follow corpse, cat follow devil, warts follow cat, I'm -done with ye!' That'll fetch ANY wart." - -"Sounds right. D'you ever try it, Huck?" - -"No, but old Mother Hopkins told me." - -"Well, I reckon it's so, then. Becuz they say she's a witch." - -"Say! Why, Tom, I KNOW she is. She witched pap. Pap says so his own -self. He come along one day, and he see she was a-witching him, so he -took up a rock, and if she hadn't dodged, he'd a got her. Well, that -very night he rolled off'n a shed wher' he was a layin drunk, and broke -his arm." - -"Why, that's awful. How did he know she was a-witching him?" - -"Lord, pap can tell, easy. Pap says when they keep looking at you -right stiddy, they're a-witching you. Specially if they mumble. Becuz -when they mumble they're saying the Lord's Prayer backards." - -"Say, Hucky, when you going to try the cat?" - -"To-night. I reckon they'll come after old Hoss Williams to-night." - -"But they buried him Saturday. Didn't they get him Saturday night?" - -"Why, how you talk! How could their charms work till midnight?--and -THEN it's Sunday. Devils don't slosh around much of a Sunday, I don't -reckon." - -"I never thought of that. That's so. Lemme go with you?" - -"Of course--if you ain't afeard." - -"Afeard! 'Tain't likely. Will you meow?" - -"Yes--and you meow back, if you get a chance. Last time, you kep' me -a-meowing around till old Hays went to throwing rocks at me and says -'Dern that cat!' and so I hove a brick through his window--but don't -you tell." - -"I won't. I couldn't meow that night, becuz auntie was watching me, -but I'll meow this time. Say--what's that?" - -"Nothing but a tick." - -"Where'd you get him?" - -"Out in the woods." - -"What'll you take for him?" - -"I don't know. I don't want to sell him." - -"All right. It's a mighty small tick, anyway." - -"Oh, anybody can run a tick down that don't belong to them. I'm -satisfied with it. It's a good enough tick for me." - -"Sho, there's ticks a plenty. I could have a thousand of 'em if I -wanted to." - -"Well, why don't you? Becuz you know mighty well you can't. This is a -pretty early tick, I reckon. It's the first one I've seen this year." - -"Say, Huck--I'll give you my tooth for him." - -"Less see it." - -Tom got out a bit of paper and carefully unrolled it. Huckleberry -viewed it wistfully. The temptation was very strong. At last he said: - -"Is it genuwyne?" - -Tom lifted his lip and showed the vacancy. - -"Well, all right," said Huckleberry, "it's a trade." - -Tom enclosed the tick in the percussion-cap box that had lately been -the pinchbug's prison, and the boys separated, each feeling wealthier -than before. - -When Tom reached the little isolated frame schoolhouse, he strode in -briskly, with the manner of one who had come with all honest speed. -He hung his hat on a peg and flung himself into his seat with -business-like alacrity. The master, throned on high in his great -splint-bottom arm-chair, was dozing, lulled by the drowsy hum of study. -The interruption roused him. - -"Thomas Sawyer!" - -Tom knew that when his name was pronounced in full, it meant trouble. - -"Sir!" - -"Come up here. Now, sir, why are you late again, as usual?" - -Tom was about to take refuge in a lie, when he saw two long tails of -yellow hair hanging down a back that he recognized by the electric -sympathy of love; and by that form was THE ONLY VACANT PLACE on the -girls' side of the schoolhouse. He instantly said: - -"I STOPPED TO TALK WITH HUCKLEBERRY FINN!" - -The master's pulse stood still, and he stared helplessly. The buzz of -study ceased. The pupils wondered if this foolhardy boy had lost his -mind. The master said: - -"You--you did what?" - -"Stopped to talk with Huckleberry Finn." - -There was no mistaking the words. - -"Thomas Sawyer, this is the most astounding confession I have ever -listened to. No mere ferule will answer for this offence. Take off your -jacket." - -The master's arm performed until it was tired and the stock of -switches notably diminished. Then the order followed: - -"Now, sir, go and sit with the girls! And let this be a warning to you." - -The titter that rippled around the room appeared to abash the boy, but -in reality that result was caused rather more by his worshipful awe of -his unknown idol and the dread pleasure that lay in his high good -fortune. He sat down upon the end of the pine bench and the girl -hitched herself away from him with a toss of her head. Nudges and winks -and whispers traversed the room, but Tom sat still, with his arms upon -the long, low desk before him, and seemed to study his book. - -By and by attention ceased from him, and the accustomed school murmur -rose upon the dull air once more. Presently the boy began to steal -furtive glances at the girl. She observed it, "made a mouth" at him and -gave him the back of her head for the space of a minute. When she -cautiously faced around again, a peach lay before her. She thrust it -away. Tom gently put it back. She thrust it away again, but with less -animosity. Tom patiently returned it to its place. Then she let it -remain. Tom scrawled on his slate, "Please take it--I got more." The -girl glanced at the words, but made no sign. Now the boy began to draw -something on the slate, hiding his work with his left hand. For a time -the girl refused to notice; but her human curiosity presently began to -manifest itself by hardly perceptible signs. The boy worked on, -apparently unconscious. The girl made a sort of noncommittal attempt to -see, but the boy did not betray that he was aware of it. At last she -gave in and hesitatingly whispered: - -"Let me see it." - -Tom partly uncovered a dismal caricature of a house with two gable -ends to it and a corkscrew of smoke issuing from the chimney. Then the -girl's interest began to fasten itself upon the work and she forgot -everything else. When it was finished, she gazed a moment, then -whispered: - -"It's nice--make a man." - -The artist erected a man in the front yard, that resembled a derrick. -He could have stepped over the house; but the girl was not -hypercritical; she was satisfied with the monster, and whispered: - -"It's a beautiful man--now make me coming along." - -Tom drew an hour-glass with a full moon and straw limbs to it and -armed the spreading fingers with a portentous fan. The girl said: - -"It's ever so nice--I wish I could draw." - -"It's easy," whispered Tom, "I'll learn you." - -"Oh, will you? When?" - -"At noon. Do you go home to dinner?" - -"I'll stay if you will." - -"Good--that's a whack. What's your name?" - -"Becky Thatcher. What's yours? Oh, I know. It's Thomas Sawyer." - -"That's the name they lick me by. I'm Tom when I'm good. You call me -Tom, will you?" - -"Yes." - -Now Tom began to scrawl something on the slate, hiding the words from -the girl. But she was not backward this time. She begged to see. Tom -said: - -"Oh, it ain't anything." - -"Yes it is." - -"No it ain't. You don't want to see." - -"Yes I do, indeed I do. Please let me." - -"You'll tell." - -"No I won't--deed and deed and double deed won't." - -"You won't tell anybody at all? Ever, as long as you live?" - -"No, I won't ever tell ANYbody. Now let me." - -"Oh, YOU don't want to see!" - -"Now that you treat me so, I WILL see." And she put her small hand -upon his and a little scuffle ensued, Tom pretending to resist in -earnest but letting his hand slip by degrees till these words were -revealed: "I LOVE YOU." - -"Oh, you bad thing!" And she hit his hand a smart rap, but reddened -and looked pleased, nevertheless. - -Just at this juncture the boy felt a slow, fateful grip closing on his -ear, and a steady lifting impulse. In that wise he was borne across the -house and deposited in his own seat, under a peppering fire of giggles -from the whole school. Then the master stood over him during a few -awful moments, and finally moved away to his throne without saying a -word. But although Tom's ear tingled, his heart was jubilant. - -As the school quieted down Tom made an honest effort to study, but the -turmoil within him was too great. In turn he took his place in the -reading class and made a botch of it; then in the geography class and -turned lakes into mountains, mountains into rivers, and rivers into -continents, till chaos was come again; then in the spelling class, and -got "turned down," by a succession of mere baby words, till he brought -up at the foot and yielded up the pewter medal which he had worn with -ostentation for months. - - - -CHAPTER VII - -THE harder Tom tried to fasten his mind on his book, the more his -ideas wandered. So at last, with a sigh and a yawn, he gave it up. It -seemed to him that the noon recess would never come. The air was -utterly dead. There was not a breath stirring. It was the sleepiest of -sleepy days. The drowsing murmur of the five and twenty studying -scholars soothed the soul like the spell that is in the murmur of bees. -Away off in the flaming sunshine, Cardiff Hill lifted its soft green -sides through a shimmering veil of heat, tinted with the purple of -distance; a few birds floated on lazy wing high in the air; no other -living thing was visible but some cows, and they were asleep. Tom's -heart ached to be free, or else to have something of interest to do to -pass the dreary time. His hand wandered into his pocket and his face -lit up with a glow of gratitude that was prayer, though he did not know -it. Then furtively the percussion-cap box came out. He released the -tick and put him on the long flat desk. The creature probably glowed -with a gratitude that amounted to prayer, too, at this moment, but it -was premature: for when he started thankfully to travel off, Tom turned -him aside with a pin and made him take a new direction. - -Tom's bosom friend sat next him, suffering just as Tom had been, and -now he was deeply and gratefully interested in this entertainment in an -instant. This bosom friend was Joe Harper. The two boys were sworn -friends all the week, and embattled enemies on Saturdays. Joe took a -pin out of his lapel and began to assist in exercising the prisoner. -The sport grew in interest momently. Soon Tom said that they were -interfering with each other, and neither getting the fullest benefit of -the tick. So he put Joe's slate on the desk and drew a line down the -middle of it from top to bottom. - -"Now," said he, "as long as he is on your side you can stir him up and -I'll let him alone; but if you let him get away and get on my side, -you're to leave him alone as long as I can keep him from crossing over." - -"All right, go ahead; start him up." - -The tick escaped from Tom, presently, and crossed the equator. Joe -harassed him awhile, and then he got away and crossed back again. This -change of base occurred often. While one boy was worrying the tick with -absorbing interest, the other would look on with interest as strong, -the two heads bowed together over the slate, and the two souls dead to -all things else. At last luck seemed to settle and abide with Joe. The -tick tried this, that, and the other course, and got as excited and as -anxious as the boys themselves, but time and again just as he would -have victory in his very grasp, so to speak, and Tom's fingers would be -twitching to begin, Joe's pin would deftly head him off, and keep -possession. At last Tom could stand it no longer. The temptation was -too strong. So he reached out and lent a hand with his pin. Joe was -angry in a moment. Said he: - -"Tom, you let him alone." - -"I only just want to stir him up a little, Joe." - -"No, sir, it ain't fair; you just let him alone." - -"Blame it, I ain't going to stir him much." - -"Let him alone, I tell you." - -"I won't!" - -"You shall--he's on my side of the line." - -"Look here, Joe Harper, whose is that tick?" - -"I don't care whose tick he is--he's on my side of the line, and you -sha'n't touch him." - -"Well, I'll just bet I will, though. He's my tick and I'll do what I -blame please with him, or die!" - -A tremendous whack came down on Tom's shoulders, and its duplicate on -Joe's; and for the space of two minutes the dust continued to fly from -the two jackets and the whole school to enjoy it. The boys had been too -absorbed to notice the hush that had stolen upon the school awhile -before when the master came tiptoeing down the room and stood over -them. He had contemplated a good part of the performance before he -contributed his bit of variety to it. - -When school broke up at noon, Tom flew to Becky Thatcher, and -whispered in her ear: - -"Put on your bonnet and let on you're going home; and when you get to -the corner, give the rest of 'em the slip, and turn down through the -lane and come back. I'll go the other way and come it over 'em the same -way." - -So the one went off with one group of scholars, and the other with -another. In a little while the two met at the bottom of the lane, and -when they reached the school they had it all to themselves. Then they -sat together, with a slate before them, and Tom gave Becky the pencil -and held her hand in his, guiding it, and so created another surprising -house. When the interest in art began to wane, the two fell to talking. -Tom was swimming in bliss. He said: - -"Do you love rats?" - -"No! I hate them!" - -"Well, I do, too--LIVE ones. But I mean dead ones, to swing round your -head with a string." - -"No, I don't care for rats much, anyway. What I like is chewing-gum." - -"Oh, I should say so! I wish I had some now." - -"Do you? I've got some. I'll let you chew it awhile, but you must give -it back to me." - -That was agreeable, so they chewed it turn about, and dangled their -legs against the bench in excess of contentment. - -"Was you ever at a circus?" said Tom. - -"Yes, and my pa's going to take me again some time, if I'm good." - -"I been to the circus three or four times--lots of times. Church ain't -shucks to a circus. There's things going on at a circus all the time. -I'm going to be a clown in a circus when I grow up." - -"Oh, are you! That will be nice. They're so lovely, all spotted up." - -"Yes, that's so. And they get slathers of money--most a dollar a day, -Ben Rogers says. Say, Becky, was you ever engaged?" - -"What's that?" - -"Why, engaged to be married." - -"No." - -"Would you like to?" - -"I reckon so. I don't know. What is it like?" - -"Like? Why it ain't like anything. You only just tell a boy you won't -ever have anybody but him, ever ever ever, and then you kiss and that's -all. Anybody can do it." - -"Kiss? What do you kiss for?" - -"Why, that, you know, is to--well, they always do that." - -"Everybody?" - -"Why, yes, everybody that's in love with each other. Do you remember -what I wrote on the slate?" - -"Ye--yes." - -"What was it?" - -"I sha'n't tell you." - -"Shall I tell YOU?" - -"Ye--yes--but some other time." - -"No, now." - -"No, not now--to-morrow." - -"Oh, no, NOW. Please, Becky--I'll whisper it, I'll whisper it ever so -easy." - -Becky hesitating, Tom took silence for consent, and passed his arm -about her waist and whispered the tale ever so softly, with his mouth -close to her ear. And then he added: - -"Now you whisper it to me--just the same." - -She resisted, for a while, and then said: - -"You turn your face away so you can't see, and then I will. But you -mustn't ever tell anybody--WILL you, Tom? Now you won't, WILL you?" - -"No, indeed, indeed I won't. Now, Becky." - -He turned his face away. She bent timidly around till her breath -stirred his curls and whispered, "I--love--you!" - -Then she sprang away and ran around and around the desks and benches, -with Tom after her, and took refuge in a corner at last, with her -little white apron to her face. Tom clasped her about her neck and -pleaded: - -"Now, Becky, it's all done--all over but the kiss. Don't you be afraid -of that--it ain't anything at all. Please, Becky." And he tugged at her -apron and the hands. - -By and by she gave up, and let her hands drop; her face, all glowing -with the struggle, came up and submitted. Tom kissed the red lips and -said: - -"Now it's all done, Becky. And always after this, you know, you ain't -ever to love anybody but me, and you ain't ever to marry anybody but -me, ever never and forever. Will you?" - -"No, I'll never love anybody but you, Tom, and I'll never marry -anybody but you--and you ain't to ever marry anybody but me, either." - -"Certainly. Of course. That's PART of it. And always coming to school -or when we're going home, you're to walk with me, when there ain't -anybody looking--and you choose me and I choose you at parties, because -that's the way you do when you're engaged." - -"It's so nice. I never heard of it before." - -"Oh, it's ever so gay! Why, me and Amy Lawrence--" - -The big eyes told Tom his blunder and he stopped, confused. - -"Oh, Tom! Then I ain't the first you've ever been engaged to!" - -The child began to cry. Tom said: - -"Oh, don't cry, Becky, I don't care for her any more." - -"Yes, you do, Tom--you know you do." - -Tom tried to put his arm about her neck, but she pushed him away and -turned her face to the wall, and went on crying. Tom tried again, with -soothing words in his mouth, and was repulsed again. Then his pride was -up, and he strode away and went outside. He stood about, restless and -uneasy, for a while, glancing at the door, every now and then, hoping -she would repent and come to find him. But she did not. Then he began -to feel badly and fear that he was in the wrong. It was a hard struggle -with him to make new advances, now, but he nerved himself to it and -entered. She was still standing back there in the corner, sobbing, with -her face to the wall. Tom's heart smote him. He went to her and stood a -moment, not knowing exactly how to proceed. Then he said hesitatingly: - -"Becky, I--I don't care for anybody but you." - -No reply--but sobs. - -"Becky"--pleadingly. "Becky, won't you say something?" - -More sobs. - -Tom got out his chiefest jewel, a brass knob from the top of an -andiron, and passed it around her so that she could see it, and said: - -"Please, Becky, won't you take it?" - -She struck it to the floor. Then Tom marched out of the house and over -the hills and far away, to return to school no more that day. Presently -Becky began to suspect. She ran to the door; he was not in sight; she -flew around to the play-yard; he was not there. Then she called: - -"Tom! Come back, Tom!" - -She listened intently, but there was no answer. She had no companions -but silence and loneliness. So she sat down to cry again and upbraid -herself; and by this time the scholars began to gather again, and she -had to hide her griefs and still her broken heart and take up the cross -of a long, dreary, aching afternoon, with none among the strangers -about her to exchange sorrows with. - - - -CHAPTER VIII - -TOM dodged hither and thither through lanes until he was well out of -the track of returning scholars, and then fell into a moody jog. He -crossed a small "branch" two or three times, because of a prevailing -juvenile superstition that to cross water baffled pursuit. Half an hour -later he was disappearing behind the Douglas mansion on the summit of -Cardiff Hill, and the schoolhouse was hardly distinguishable away off -in the valley behind him. He entered a dense wood, picked his pathless -way to the centre of it, and sat down on a mossy spot under a spreading -oak. There was not even a zephyr stirring; the dead noonday heat had -even stilled the songs of the birds; nature lay in a trance that was -broken by no sound but the occasional far-off hammering of a -woodpecker, and this seemed to render the pervading silence and sense -of loneliness the more profound. The boy's soul was steeped in -melancholy; his feelings were in happy accord with his surroundings. He -sat long with his elbows on his knees and his chin in his hands, -meditating. It seemed to him that life was but a trouble, at best, and -he more than half envied Jimmy Hodges, so lately released; it must be -very peaceful, he thought, to lie and slumber and dream forever and -ever, with the wind whispering through the trees and caressing the -grass and the flowers over the grave, and nothing to bother and grieve -about, ever any more. If he only had a clean Sunday-school record he -could be willing to go, and be done with it all. Now as to this girl. -What had he done? Nothing. He had meant the best in the world, and been -treated like a dog--like a very dog. She would be sorry some day--maybe -when it was too late. Ah, if he could only die TEMPORARILY! - -But the elastic heart of youth cannot be compressed into one -constrained shape long at a time. Tom presently began to drift -insensibly back into the concerns of this life again. What if he turned -his back, now, and disappeared mysteriously? What if he went away--ever -so far away, into unknown countries beyond the seas--and never came -back any more! How would she feel then! The idea of being a clown -recurred to him now, only to fill him with disgust. For frivolity and -jokes and spotted tights were an offense, when they intruded themselves -upon a spirit that was exalted into the vague august realm of the -romantic. No, he would be a soldier, and return after long years, all -war-worn and illustrious. No--better still, he would join the Indians, -and hunt buffaloes and go on the warpath in the mountain ranges and the -trackless great plains of the Far West, and away in the future come -back a great chief, bristling with feathers, hideous with paint, and -prance into Sunday-school, some drowsy summer morning, with a -bloodcurdling war-whoop, and sear the eyeballs of all his companions -with unappeasable envy. But no, there was something gaudier even than -this. He would be a pirate! That was it! NOW his future lay plain -before him, and glowing with unimaginable splendor. How his name would -fill the world, and make people shudder! How gloriously he would go -plowing the dancing seas, in his long, low, black-hulled racer, the -Spirit of the Storm, with his grisly flag flying at the fore! And at -the zenith of his fame, how he would suddenly appear at the old village -and stalk into church, brown and weather-beaten, in his black velvet -doublet and trunks, his great jack-boots, his crimson sash, his belt -bristling with horse-pistols, his crime-rusted cutlass at his side, his -slouch hat with waving plumes, his black flag unfurled, with the skull -and crossbones on it, and hear with swelling ecstasy the whisperings, -"It's Tom Sawyer the Pirate!--the Black Avenger of the Spanish Main!" - -Yes, it was settled; his career was determined. He would run away from -home and enter upon it. He would start the very next morning. Therefore -he must now begin to get ready. He would collect his resources -together. He went to a rotten log near at hand and began to dig under -one end of it with his Barlow knife. He soon struck wood that sounded -hollow. He put his hand there and uttered this incantation impressively: - -"What hasn't come here, come! What's here, stay here!" - -Then he scraped away the dirt, and exposed a pine shingle. He took it -up and disclosed a shapely little treasure-house whose bottom and sides -were of shingles. In it lay a marble. Tom's astonishment was boundless! -He scratched his head with a perplexed air, and said: - -"Well, that beats anything!" - -Then he tossed the marble away pettishly, and stood cogitating. The -truth was, that a superstition of his had failed, here, which he and -all his comrades had always looked upon as infallible. If you buried a -marble with certain necessary incantations, and left it alone a -fortnight, and then opened the place with the incantation he had just -used, you would find that all the marbles you had ever lost had -gathered themselves together there, meantime, no matter how widely they -had been separated. But now, this thing had actually and unquestionably -failed. Tom's whole structure of faith was shaken to its foundations. -He had many a time heard of this thing succeeding but never of its -failing before. It did not occur to him that he had tried it several -times before, himself, but could never find the hiding-places -afterward. He puzzled over the matter some time, and finally decided -that some witch had interfered and broken the charm. He thought he -would satisfy himself on that point; so he searched around till he -found a small sandy spot with a little funnel-shaped depression in it. -He laid himself down and put his mouth close to this depression and -called-- - -"Doodle-bug, doodle-bug, tell me what I want to know! Doodle-bug, -doodle-bug, tell me what I want to know!" - -The sand began to work, and presently a small black bug appeared for a -second and then darted under again in a fright. - -"He dasn't tell! So it WAS a witch that done it. I just knowed it." - -He well knew the futility of trying to contend against witches, so he -gave up discouraged. But it occurred to him that he might as well have -the marble he had just thrown away, and therefore he went and made a -patient search for it. But he could not find it. Now he went back to -his treasure-house and carefully placed himself just as he had been -standing when he tossed the marble away; then he took another marble -from his pocket and tossed it in the same way, saying: - -"Brother, go find your brother!" - -He watched where it stopped, and went there and looked. But it must -have fallen short or gone too far; so he tried twice more. The last -repetition was successful. The two marbles lay within a foot of each -other. - -Just here the blast of a toy tin trumpet came faintly down the green -aisles of the forest. Tom flung off his jacket and trousers, turned a -suspender into a belt, raked away some brush behind the rotten log, -disclosing a rude bow and arrow, a lath sword and a tin trumpet, and in -a moment had seized these things and bounded away, barelegged, with -fluttering shirt. He presently halted under a great elm, blew an -answering blast, and then began to tiptoe and look warily out, this way -and that. He said cautiously--to an imaginary company: - -"Hold, my merry men! Keep hid till I blow." - -Now appeared Joe Harper, as airily clad and elaborately armed as Tom. -Tom called: - -"Hold! Who comes here into Sherwood Forest without my pass?" - -"Guy of Guisborne wants no man's pass. Who art thou that--that--" - -"Dares to hold such language," said Tom, prompting--for they talked -"by the book," from memory. - -"Who art thou that dares to hold such language?" - -"I, indeed! I am Robin Hood, as thy caitiff carcase soon shall know." - -"Then art thou indeed that famous outlaw? Right gladly will I dispute -with thee the passes of the merry wood. Have at thee!" - -They took their lath swords, dumped their other traps on the ground, -struck a fencing attitude, foot to foot, and began a grave, careful -combat, "two up and two down." Presently Tom said: - -"Now, if you've got the hang, go it lively!" - -So they "went it lively," panting and perspiring with the work. By and -by Tom shouted: - -"Fall! fall! Why don't you fall?" - -"I sha'n't! Why don't you fall yourself? You're getting the worst of -it." - -"Why, that ain't anything. I can't fall; that ain't the way it is in -the book. The book says, 'Then with one back-handed stroke he slew poor -Guy of Guisborne.' You're to turn around and let me hit you in the -back." - -There was no getting around the authorities, so Joe turned, received -the whack and fell. - -"Now," said Joe, getting up, "you got to let me kill YOU. That's fair." - -"Why, I can't do that, it ain't in the book." - -"Well, it's blamed mean--that's all." - -"Well, say, Joe, you can be Friar Tuck or Much the miller's son, and -lam me with a quarter-staff; or I'll be the Sheriff of Nottingham and -you be Robin Hood a little while and kill me." - -This was satisfactory, and so these adventures were carried out. Then -Tom became Robin Hood again, and was allowed by the treacherous nun to -bleed his strength away through his neglected wound. And at last Joe, -representing a whole tribe of weeping outlaws, dragged him sadly forth, -gave his bow into his feeble hands, and Tom said, "Where this arrow -falls, there bury poor Robin Hood under the greenwood tree." Then he -shot the arrow and fell back and would have died, but he lit on a -nettle and sprang up too gaily for a corpse. - -The boys dressed themselves, hid their accoutrements, and went off -grieving that there were no outlaws any more, and wondering what modern -civilization could claim to have done to compensate for their loss. -They said they would rather be outlaws a year in Sherwood Forest than -President of the United States forever. - - - -CHAPTER IX - -AT half-past nine, that night, Tom and Sid were sent to bed, as usual. -They said their prayers, and Sid was soon asleep. Tom lay awake and -waited, in restless impatience. When it seemed to him that it must be -nearly daylight, he heard the clock strike ten! This was despair. He -would have tossed and fidgeted, as his nerves demanded, but he was -afraid he might wake Sid. So he lay still, and stared up into the dark. -Everything was dismally still. By and by, out of the stillness, little, -scarcely perceptible noises began to emphasize themselves. The ticking -of the clock began to bring itself into notice. Old beams began to -crack mysteriously. The stairs creaked faintly. Evidently spirits were -abroad. A measured, muffled snore issued from Aunt Polly's chamber. And -now the tiresome chirping of a cricket that no human ingenuity could -locate, began. Next the ghastly ticking of a deathwatch in the wall at -the bed's head made Tom shudder--it meant that somebody's days were -numbered. Then the howl of a far-off dog rose on the night air, and was -answered by a fainter howl from a remoter distance. Tom was in an -agony. At last he was satisfied that time had ceased and eternity -begun; he began to doze, in spite of himself; the clock chimed eleven, -but he did not hear it. And then there came, mingling with his -half-formed dreams, a most melancholy caterwauling. The raising of a -neighboring window disturbed him. A cry of "Scat! you devil!" and the -crash of an empty bottle against the back of his aunt's woodshed -brought him wide awake, and a single minute later he was dressed and -out of the window and creeping along the roof of the "ell" on all -fours. He "meow'd" with caution once or twice, as he went; then jumped -to the roof of the woodshed and thence to the ground. Huckleberry Finn -was there, with his dead cat. The boys moved off and disappeared in the -gloom. At the end of half an hour they were wading through the tall -grass of the graveyard. - -It was a graveyard of the old-fashioned Western kind. It was on a -hill, about a mile and a half from the village. It had a crazy board -fence around it, which leaned inward in places, and outward the rest of -the time, but stood upright nowhere. Grass and weeds grew rank over the -whole cemetery. All the old graves were sunken in, there was not a -tombstone on the place; round-topped, worm-eaten boards staggered over -the graves, leaning for support and finding none. "Sacred to the memory -of" So-and-So had been painted on them once, but it could no longer -have been read, on the most of them, now, even if there had been light. - -A faint wind moaned through the trees, and Tom feared it might be the -spirits of the dead, complaining at being disturbed. The boys talked -little, and only under their breath, for the time and the place and the -pervading solemnity and silence oppressed their spirits. They found the -sharp new heap they were seeking, and ensconced themselves within the -protection of three great elms that grew in a bunch within a few feet -of the grave. - -Then they waited in silence for what seemed a long time. The hooting -of a distant owl was all the sound that troubled the dead stillness. -Tom's reflections grew oppressive. He must force some talk. So he said -in a whisper: - -"Hucky, do you believe the dead people like it for us to be here?" - -Huckleberry whispered: - -"I wisht I knowed. It's awful solemn like, AIN'T it?" - -"I bet it is." - -There was a considerable pause, while the boys canvassed this matter -inwardly. Then Tom whispered: - -"Say, Hucky--do you reckon Hoss Williams hears us talking?" - -"O' course he does. Least his sperrit does." - -Tom, after a pause: - -"I wish I'd said Mister Williams. But I never meant any harm. -Everybody calls him Hoss." - -"A body can't be too partic'lar how they talk 'bout these-yer dead -people, Tom." - -This was a damper, and conversation died again. - -Presently Tom seized his comrade's arm and said: - -"Sh!" - -"What is it, Tom?" And the two clung together with beating hearts. - -"Sh! There 'tis again! Didn't you hear it?" - -"I--" - -"There! Now you hear it." - -"Lord, Tom, they're coming! They're coming, sure. What'll we do?" - -"I dono. Think they'll see us?" - -"Oh, Tom, they can see in the dark, same as cats. I wisht I hadn't -come." - -"Oh, don't be afeard. I don't believe they'll bother us. We ain't -doing any harm. If we keep perfectly still, maybe they won't notice us -at all." - -"I'll try to, Tom, but, Lord, I'm all of a shiver." - -"Listen!" - -The boys bent their heads together and scarcely breathed. A muffled -sound of voices floated up from the far end of the graveyard. - -"Look! See there!" whispered Tom. "What is it?" - -"It's devil-fire. Oh, Tom, this is awful." - -Some vague figures approached through the gloom, swinging an -old-fashioned tin lantern that freckled the ground with innumerable -little spangles of light. Presently Huckleberry whispered with a -shudder: - -"It's the devils sure enough. Three of 'em! Lordy, Tom, we're goners! -Can you pray?" - -"I'll try, but don't you be afeard. They ain't going to hurt us. 'Now -I lay me down to sleep, I--'" - -"Sh!" - -"What is it, Huck?" - -"They're HUMANS! One of 'em is, anyway. One of 'em's old Muff Potter's -voice." - -"No--'tain't so, is it?" - -"I bet I know it. Don't you stir nor budge. He ain't sharp enough to -notice us. Drunk, the same as usual, likely--blamed old rip!" - -"All right, I'll keep still. Now they're stuck. Can't find it. Here -they come again. Now they're hot. Cold again. Hot again. Red hot! -They're p'inted right, this time. Say, Huck, I know another o' them -voices; it's Injun Joe." - -"That's so--that murderin' half-breed! I'd druther they was devils a -dern sight. What kin they be up to?" - -The whisper died wholly out, now, for the three men had reached the -grave and stood within a few feet of the boys' hiding-place. - -"Here it is," said the third voice; and the owner of it held the -lantern up and revealed the face of young Doctor Robinson. - -Potter and Injun Joe were carrying a handbarrow with a rope and a -couple of shovels on it. They cast down their load and began to open -the grave. The doctor put the lantern at the head of the grave and came -and sat down with his back against one of the elm trees. He was so -close the boys could have touched him. - -"Hurry, men!" he said, in a low voice; "the moon might come out at any -moment." - -They growled a response and went on digging. For some time there was -no noise but the grating sound of the spades discharging their freight -of mould and gravel. It was very monotonous. Finally a spade struck -upon the coffin with a dull woody accent, and within another minute or -two the men had hoisted it out on the ground. They pried off the lid -with their shovels, got out the body and dumped it rudely on the -ground. The moon drifted from behind the clouds and exposed the pallid -face. The barrow was got ready and the corpse placed on it, covered -with a blanket, and bound to its place with the rope. Potter took out a -large spring-knife and cut off the dangling end of the rope and then -said: - -"Now the cussed thing's ready, Sawbones, and you'll just out with -another five, or here she stays." - -"That's the talk!" said Injun Joe. - -"Look here, what does this mean?" said the doctor. "You required your -pay in advance, and I've paid you." - -"Yes, and you done more than that," said Injun Joe, approaching the -doctor, who was now standing. "Five years ago you drove me away from -your father's kitchen one night, when I come to ask for something to -eat, and you said I warn't there for any good; and when I swore I'd get -even with you if it took a hundred years, your father had me jailed for -a vagrant. Did you think I'd forget? The Injun blood ain't in me for -nothing. And now I've GOT you, and you got to SETTLE, you know!" - -He was threatening the doctor, with his fist in his face, by this -time. The doctor struck out suddenly and stretched the ruffian on the -ground. Potter dropped his knife, and exclaimed: - -"Here, now, don't you hit my pard!" and the next moment he had -grappled with the doctor and the two were struggling with might and -main, trampling the grass and tearing the ground with their heels. -Injun Joe sprang to his feet, his eyes flaming with passion, snatched -up Potter's knife, and went creeping, catlike and stooping, round and -round about the combatants, seeking an opportunity. All at once the -doctor flung himself free, seized the heavy headboard of Williams' -grave and felled Potter to the earth with it--and in the same instant -the half-breed saw his chance and drove the knife to the hilt in the -young man's breast. He reeled and fell partly upon Potter, flooding him -with his blood, and in the same moment the clouds blotted out the -dreadful spectacle and the two frightened boys went speeding away in -the dark. - -Presently, when the moon emerged again, Injun Joe was standing over -the two forms, contemplating them. The doctor murmured inarticulately, -gave a long gasp or two and was still. The half-breed muttered: - -"THAT score is settled--damn you." - -Then he robbed the body. After which he put the fatal knife in -Potter's open right hand, and sat down on the dismantled coffin. Three ---four--five minutes passed, and then Potter began to stir and moan. His -hand closed upon the knife; he raised it, glanced at it, and let it -fall, with a shudder. Then he sat up, pushing the body from him, and -gazed at it, and then around him, confusedly. His eyes met Joe's. - -"Lord, how is this, Joe?" he said. - -"It's a dirty business," said Joe, without moving. - -"What did you do it for?" - -"I! I never done it!" - -"Look here! That kind of talk won't wash." - -Potter trembled and grew white. - -"I thought I'd got sober. I'd no business to drink to-night. But it's -in my head yet--worse'n when we started here. I'm all in a muddle; -can't recollect anything of it, hardly. Tell me, Joe--HONEST, now, old -feller--did I do it? Joe, I never meant to--'pon my soul and honor, I -never meant to, Joe. Tell me how it was, Joe. Oh, it's awful--and him -so young and promising." - -"Why, you two was scuffling, and he fetched you one with the headboard -and you fell flat; and then up you come, all reeling and staggering -like, and snatched the knife and jammed it into him, just as he fetched -you another awful clip--and here you've laid, as dead as a wedge til -now." - -"Oh, I didn't know what I was a-doing. I wish I may die this minute if -I did. It was all on account of the whiskey and the excitement, I -reckon. I never used a weepon in my life before, Joe. I've fought, but -never with weepons. They'll all say that. Joe, don't tell! Say you -won't tell, Joe--that's a good feller. I always liked you, Joe, and -stood up for you, too. Don't you remember? You WON'T tell, WILL you, -Joe?" And the poor creature dropped on his knees before the stolid -murderer, and clasped his appealing hands. - -"No, you've always been fair and square with me, Muff Potter, and I -won't go back on you. There, now, that's as fair as a man can say." - -"Oh, Joe, you're an angel. I'll bless you for this the longest day I -live." And Potter began to cry. - -"Come, now, that's enough of that. This ain't any time for blubbering. -You be off yonder way and I'll go this. Move, now, and don't leave any -tracks behind you." - -Potter started on a trot that quickly increased to a run. The -half-breed stood looking after him. He muttered: - -"If he's as much stunned with the lick and fuddled with the rum as he -had the look of being, he won't think of the knife till he's gone so -far he'll be afraid to come back after it to such a place by himself ---chicken-heart!" - -Two or three minutes later the murdered man, the blanketed corpse, the -lidless coffin, and the open grave were under no inspection but the -moon's. The stillness was complete again, too. - - - -CHAPTER X - -THE two boys flew on and on, toward the village, speechless with -horror. They glanced backward over their shoulders from time to time, -apprehensively, as if they feared they might be followed. Every stump -that started up in their path seemed a man and an enemy, and made them -catch their breath; and as they sped by some outlying cottages that lay -near the village, the barking of the aroused watch-dogs seemed to give -wings to their feet. - -"If we can only get to the old tannery before we break down!" -whispered Tom, in short catches between breaths. "I can't stand it much -longer." - -Huckleberry's hard pantings were his only reply, and the boys fixed -their eyes on the goal of their hopes and bent to their work to win it. -They gained steadily on it, and at last, breast to breast, they burst -through the open door and fell grateful and exhausted in the sheltering -shadows beyond. By and by their pulses slowed down, and Tom whispered: - -"Huckleberry, what do you reckon'll come of this?" - -"If Doctor Robinson dies, I reckon hanging'll come of it." - -"Do you though?" - -"Why, I KNOW it, Tom." - -Tom thought a while, then he said: - -"Who'll tell? We?" - -"What are you talking about? S'pose something happened and Injun Joe -DIDN'T hang? Why, he'd kill us some time or other, just as dead sure as -we're a laying here." - -"That's just what I was thinking to myself, Huck." - -"If anybody tells, let Muff Potter do it, if he's fool enough. He's -generally drunk enough." - -Tom said nothing--went on thinking. Presently he whispered: - -"Huck, Muff Potter don't know it. How can he tell?" - -"What's the reason he don't know it?" - -"Because he'd just got that whack when Injun Joe done it. D'you reckon -he could see anything? D'you reckon he knowed anything?" - -"By hokey, that's so, Tom!" - -"And besides, look-a-here--maybe that whack done for HIM!" - -"No, 'taint likely, Tom. He had liquor in him; I could see that; and -besides, he always has. Well, when pap's full, you might take and belt -him over the head with a church and you couldn't phase him. He says so, -his own self. So it's the same with Muff Potter, of course. But if a -man was dead sober, I reckon maybe that whack might fetch him; I dono." - -After another reflective silence, Tom said: - -"Hucky, you sure you can keep mum?" - -"Tom, we GOT to keep mum. You know that. That Injun devil wouldn't -make any more of drownding us than a couple of cats, if we was to -squeak 'bout this and they didn't hang him. Now, look-a-here, Tom, less -take and swear to one another--that's what we got to do--swear to keep -mum." - -"I'm agreed. It's the best thing. Would you just hold hands and swear -that we--" - -"Oh no, that wouldn't do for this. That's good enough for little -rubbishy common things--specially with gals, cuz THEY go back on you -anyway, and blab if they get in a huff--but there orter be writing -'bout a big thing like this. And blood." - -Tom's whole being applauded this idea. It was deep, and dark, and -awful; the hour, the circumstances, the surroundings, were in keeping -with it. He picked up a clean pine shingle that lay in the moonlight, -took a little fragment of "red keel" out of his pocket, got the moon on -his work, and painfully scrawled these lines, emphasizing each slow -down-stroke by clamping his tongue between his teeth, and letting up -the pressure on the up-strokes. [See next page.] - - "Huck Finn and - Tom Sawyer swears - they will keep mum - about This and They - wish They may Drop - down dead in Their - Tracks if They ever - Tell and Rot." - -Huckleberry was filled with admiration of Tom's facility in writing, -and the sublimity of his language. He at once took a pin from his lapel -and was going to prick his flesh, but Tom said: - -"Hold on! Don't do that. A pin's brass. It might have verdigrease on -it." - -"What's verdigrease?" - -"It's p'ison. That's what it is. You just swaller some of it once ---you'll see." - -So Tom unwound the thread from one of his needles, and each boy -pricked the ball of his thumb and squeezed out a drop of blood. In -time, after many squeezes, Tom managed to sign his initials, using the -ball of his little finger for a pen. Then he showed Huckleberry how to -make an H and an F, and the oath was complete. They buried the shingle -close to the wall, with some dismal ceremonies and incantations, and -the fetters that bound their tongues were considered to be locked and -the key thrown away. - -A figure crept stealthily through a break in the other end of the -ruined building, now, but they did not notice it. - -"Tom," whispered Huckleberry, "does this keep us from EVER telling ---ALWAYS?" - -"Of course it does. It don't make any difference WHAT happens, we got -to keep mum. We'd drop down dead--don't YOU know that?" - -"Yes, I reckon that's so." - -They continued to whisper for some little time. Presently a dog set up -a long, lugubrious howl just outside--within ten feet of them. The boys -clasped each other suddenly, in an agony of fright. - -"Which of us does he mean?" gasped Huckleberry. - -"I dono--peep through the crack. Quick!" - -"No, YOU, Tom!" - -"I can't--I can't DO it, Huck!" - -"Please, Tom. There 'tis again!" - -"Oh, lordy, I'm thankful!" whispered Tom. "I know his voice. It's Bull -Harbison." * - -[* If Mr. Harbison owned a slave named Bull, Tom would have spoken of -him as "Harbison's Bull," but a son or a dog of that name was "Bull -Harbison."] - -"Oh, that's good--I tell you, Tom, I was most scared to death; I'd a -bet anything it was a STRAY dog." - -The dog howled again. The boys' hearts sank once more. - -"Oh, my! that ain't no Bull Harbison!" whispered Huckleberry. "DO, Tom!" - -Tom, quaking with fear, yielded, and put his eye to the crack. His -whisper was hardly audible when he said: - -"Oh, Huck, IT S A STRAY DOG!" - -"Quick, Tom, quick! Who does he mean?" - -"Huck, he must mean us both--we're right together." - -"Oh, Tom, I reckon we're goners. I reckon there ain't no mistake 'bout -where I'LL go to. I been so wicked." - -"Dad fetch it! This comes of playing hookey and doing everything a -feller's told NOT to do. I might a been good, like Sid, if I'd a tried ---but no, I wouldn't, of course. But if ever I get off this time, I lay -I'll just WALLER in Sunday-schools!" And Tom began to snuffle a little. - -"YOU bad!" and Huckleberry began to snuffle too. "Consound it, Tom -Sawyer, you're just old pie, 'longside o' what I am. Oh, LORDY, lordy, -lordy, I wisht I only had half your chance." - -Tom choked off and whispered: - -"Look, Hucky, look! He's got his BACK to us!" - -Hucky looked, with joy in his heart. - -"Well, he has, by jingoes! Did he before?" - -"Yes, he did. But I, like a fool, never thought. Oh, this is bully, -you know. NOW who can he mean?" - -The howling stopped. Tom pricked up his ears. - -"Sh! What's that?" he whispered. - -"Sounds like--like hogs grunting. No--it's somebody snoring, Tom." - -"That IS it! Where 'bouts is it, Huck?" - -"I bleeve it's down at 'tother end. Sounds so, anyway. Pap used to -sleep there, sometimes, 'long with the hogs, but laws bless you, he -just lifts things when HE snores. Besides, I reckon he ain't ever -coming back to this town any more." - -The spirit of adventure rose in the boys' souls once more. - -"Hucky, do you das't to go if I lead?" - -"I don't like to, much. Tom, s'pose it's Injun Joe!" - -Tom quailed. But presently the temptation rose up strong again and the -boys agreed to try, with the understanding that they would take to -their heels if the snoring stopped. So they went tiptoeing stealthily -down, the one behind the other. When they had got to within five steps -of the snorer, Tom stepped on a stick, and it broke with a sharp snap. -The man moaned, writhed a little, and his face came into the moonlight. -It was Muff Potter. The boys' hearts had stood still, and their hopes -too, when the man moved, but their fears passed away now. They tiptoed -out, through the broken weather-boarding, and stopped at a little -distance to exchange a parting word. That long, lugubrious howl rose on -the night air again! They turned and saw the strange dog standing -within a few feet of where Potter was lying, and FACING Potter, with -his nose pointing heavenward. - -"Oh, geeminy, it's HIM!" exclaimed both boys, in a breath. - -"Say, Tom--they say a stray dog come howling around Johnny Miller's -house, 'bout midnight, as much as two weeks ago; and a whippoorwill -come in and lit on the banisters and sung, the very same evening; and -there ain't anybody dead there yet." - -"Well, I know that. And suppose there ain't. Didn't Gracie Miller fall -in the kitchen fire and burn herself terrible the very next Saturday?" - -"Yes, but she ain't DEAD. And what's more, she's getting better, too." - -"All right, you wait and see. She's a goner, just as dead sure as Muff -Potter's a goner. That's what the niggers say, and they know all about -these kind of things, Huck." - -Then they separated, cogitating. When Tom crept in at his bedroom -window the night was almost spent. He undressed with excessive caution, -and fell asleep congratulating himself that nobody knew of his -escapade. He was not aware that the gently-snoring Sid was awake, and -had been so for an hour. - -When Tom awoke, Sid was dressed and gone. There was a late look in the -light, a late sense in the atmosphere. He was startled. Why had he not -been called--persecuted till he was up, as usual? The thought filled -him with bodings. Within five minutes he was dressed and down-stairs, -feeling sore and drowsy. The family were still at table, but they had -finished breakfast. There was no voice of rebuke; but there were -averted eyes; there was a silence and an air of solemnity that struck a -chill to the culprit's heart. He sat down and tried to seem gay, but it -was up-hill work; it roused no smile, no response, and he lapsed into -silence and let his heart sink down to the depths. - -After breakfast his aunt took him aside, and Tom almost brightened in -the hope that he was going to be flogged; but it was not so. His aunt -wept over him and asked him how he could go and break her old heart so; -and finally told him to go on, and ruin himself and bring her gray -hairs with sorrow to the grave, for it was no use for her to try any -more. This was worse than a thousand whippings, and Tom's heart was -sorer now than his body. He cried, he pleaded for forgiveness, promised -to reform over and over again, and then received his dismissal, feeling -that he had won but an imperfect forgiveness and established but a -feeble confidence. - -He left the presence too miserable to even feel revengeful toward Sid; -and so the latter's prompt retreat through the back gate was -unnecessary. He moped to school gloomy and sad, and took his flogging, -along with Joe Harper, for playing hookey the day before, with the air -of one whose heart was busy with heavier woes and wholly dead to -trifles. Then he betook himself to his seat, rested his elbows on his -desk and his jaws in his hands, and stared at the wall with the stony -stare of suffering that has reached the limit and can no further go. -His elbow was pressing against some hard substance. After a long time -he slowly and sadly changed his position, and took up this object with -a sigh. It was in a paper. He unrolled it. A long, lingering, colossal -sigh followed, and his heart broke. It was his brass andiron knob! - -This final feather broke the camel's back. - - - -CHAPTER XI - -CLOSE upon the hour of noon the whole village was suddenly electrified -with the ghastly news. No need of the as yet undreamed-of telegraph; -the tale flew from man to man, from group to group, from house to -house, with little less than telegraphic speed. Of course the -schoolmaster gave holiday for that afternoon; the town would have -thought strangely of him if he had not. - -A gory knife had been found close to the murdered man, and it had been -recognized by somebody as belonging to Muff Potter--so the story ran. -And it was said that a belated citizen had come upon Potter washing -himself in the "branch" about one or two o'clock in the morning, and -that Potter had at once sneaked off--suspicious circumstances, -especially the washing which was not a habit with Potter. It was also -said that the town had been ransacked for this "murderer" (the public -are not slow in the matter of sifting evidence and arriving at a -verdict), but that he could not be found. Horsemen had departed down -all the roads in every direction, and the Sheriff "was confident" that -he would be captured before night. - -All the town was drifting toward the graveyard. Tom's heartbreak -vanished and he joined the procession, not because he would not a -thousand times rather go anywhere else, but because an awful, -unaccountable fascination drew him on. Arrived at the dreadful place, -he wormed his small body through the crowd and saw the dismal -spectacle. It seemed to him an age since he was there before. Somebody -pinched his arm. He turned, and his eyes met Huckleberry's. Then both -looked elsewhere at once, and wondered if anybody had noticed anything -in their mutual glance. But everybody was talking, and intent upon the -grisly spectacle before them. - -"Poor fellow!" "Poor young fellow!" "This ought to be a lesson to -grave robbers!" "Muff Potter'll hang for this if they catch him!" This -was the drift of remark; and the minister said, "It was a judgment; His -hand is here." - -Now Tom shivered from head to heel; for his eye fell upon the stolid -face of Injun Joe. At this moment the crowd began to sway and struggle, -and voices shouted, "It's him! it's him! he's coming himself!" - -"Who? Who?" from twenty voices. - -"Muff Potter!" - -"Hallo, he's stopped!--Look out, he's turning! Don't let him get away!" - -People in the branches of the trees over Tom's head said he wasn't -trying to get away--he only looked doubtful and perplexed. - -"Infernal impudence!" said a bystander; "wanted to come and take a -quiet look at his work, I reckon--didn't expect any company." - -The crowd fell apart, now, and the Sheriff came through, -ostentatiously leading Potter by the arm. The poor fellow's face was -haggard, and his eyes showed the fear that was upon him. When he stood -before the murdered man, he shook as with a palsy, and he put his face -in his hands and burst into tears. - -"I didn't do it, friends," he sobbed; "'pon my word and honor I never -done it." - -"Who's accused you?" shouted a voice. - -This shot seemed to carry home. Potter lifted his face and looked -around him with a pathetic hopelessness in his eyes. He saw Injun Joe, -and exclaimed: - -"Oh, Injun Joe, you promised me you'd never--" - -"Is that your knife?" and it was thrust before him by the Sheriff. - -Potter would have fallen if they had not caught him and eased him to -the ground. Then he said: - -"Something told me 't if I didn't come back and get--" He shuddered; -then waved his nerveless hand with a vanquished gesture and said, "Tell -'em, Joe, tell 'em--it ain't any use any more." - -Then Huckleberry and Tom stood dumb and staring, and heard the -stony-hearted liar reel off his serene statement, they expecting every -moment that the clear sky would deliver God's lightnings upon his head, -and wondering to see how long the stroke was delayed. And when he had -finished and still stood alive and whole, their wavering impulse to -break their oath and save the poor betrayed prisoner's life faded and -vanished away, for plainly this miscreant had sold himself to Satan and -it would be fatal to meddle with the property of such a power as that. - -"Why didn't you leave? What did you want to come here for?" somebody -said. - -"I couldn't help it--I couldn't help it," Potter moaned. "I wanted to -run away, but I couldn't seem to come anywhere but here." And he fell -to sobbing again. - -Injun Joe repeated his statement, just as calmly, a few minutes -afterward on the inquest, under oath; and the boys, seeing that the -lightnings were still withheld, were confirmed in their belief that Joe -had sold himself to the devil. He was now become, to them, the most -balefully interesting object they had ever looked upon, and they could -not take their fascinated eyes from his face. - -They inwardly resolved to watch him nights, when opportunity should -offer, in the hope of getting a glimpse of his dread master. - -Injun Joe helped to raise the body of the murdered man and put it in a -wagon for removal; and it was whispered through the shuddering crowd -that the wound bled a little! The boys thought that this happy -circumstance would turn suspicion in the right direction; but they were -disappointed, for more than one villager remarked: - -"It was within three feet of Muff Potter when it done it." - -Tom's fearful secret and gnawing conscience disturbed his sleep for as -much as a week after this; and at breakfast one morning Sid said: - -"Tom, you pitch around and talk in your sleep so much that you keep me -awake half the time." - -Tom blanched and dropped his eyes. - -"It's a bad sign," said Aunt Polly, gravely. "What you got on your -mind, Tom?" - -"Nothing. Nothing 't I know of." But the boy's hand shook so that he -spilled his coffee. - -"And you do talk such stuff," Sid said. "Last night you said, 'It's -blood, it's blood, that's what it is!' You said that over and over. And -you said, 'Don't torment me so--I'll tell!' Tell WHAT? What is it -you'll tell?" - -Everything was swimming before Tom. There is no telling what might -have happened, now, but luckily the concern passed out of Aunt Polly's -face and she came to Tom's relief without knowing it. She said: - -"Sho! It's that dreadful murder. I dream about it most every night -myself. Sometimes I dream it's me that done it." - -Mary said she had been affected much the same way. Sid seemed -satisfied. Tom got out of the presence as quick as he plausibly could, -and after that he complained of toothache for a week, and tied up his -jaws every night. He never knew that Sid lay nightly watching, and -frequently slipped the bandage free and then leaned on his elbow -listening a good while at a time, and afterward slipped the bandage -back to its place again. Tom's distress of mind wore off gradually and -the toothache grew irksome and was discarded. If Sid really managed to -make anything out of Tom's disjointed mutterings, he kept it to himself. - -It seemed to Tom that his schoolmates never would get done holding -inquests on dead cats, and thus keeping his trouble present to his -mind. Sid noticed that Tom never was coroner at one of these inquiries, -though it had been his habit to take the lead in all new enterprises; -he noticed, too, that Tom never acted as a witness--and that was -strange; and Sid did not overlook the fact that Tom even showed a -marked aversion to these inquests, and always avoided them when he -could. Sid marvelled, but said nothing. However, even inquests went out -of vogue at last, and ceased to torture Tom's conscience. - -Every day or two, during this time of sorrow, Tom watched his -opportunity and went to the little grated jail-window and smuggled such -small comforts through to the "murderer" as he could get hold of. The -jail was a trifling little brick den that stood in a marsh at the edge -of the village, and no guards were afforded for it; indeed, it was -seldom occupied. These offerings greatly helped to ease Tom's -conscience. - -The villagers had a strong desire to tar-and-feather Injun Joe and -ride him on a rail, for body-snatching, but so formidable was his -character that nobody could be found who was willing to take the lead -in the matter, so it was dropped. He had been careful to begin both of -his inquest-statements with the fight, without confessing the -grave-robbery that preceded it; therefore it was deemed wisest not -to try the case in the courts at present. - - - -CHAPTER XII - -ONE of the reasons why Tom's mind had drifted away from its secret -troubles was, that it had found a new and weighty matter to interest -itself about. Becky Thatcher had stopped coming to school. Tom had -struggled with his pride a few days, and tried to "whistle her down the -wind," but failed. He began to find himself hanging around her father's -house, nights, and feeling very miserable. She was ill. What if she -should die! There was distraction in the thought. He no longer took an -interest in war, nor even in piracy. The charm of life was gone; there -was nothing but dreariness left. He put his hoop away, and his bat; -there was no joy in them any more. His aunt was concerned. She began to -try all manner of remedies on him. She was one of those people who are -infatuated with patent medicines and all new-fangled methods of -producing health or mending it. She was an inveterate experimenter in -these things. When something fresh in this line came out she was in a -fever, right away, to try it; not on herself, for she was never ailing, -but on anybody else that came handy. She was a subscriber for all the -"Health" periodicals and phrenological frauds; and the solemn ignorance -they were inflated with was breath to her nostrils. All the "rot" they -contained about ventilation, and how to go to bed, and how to get up, -and what to eat, and what to drink, and how much exercise to take, and -what frame of mind to keep one's self in, and what sort of clothing to -wear, was all gospel to her, and she never observed that her -health-journals of the current month customarily upset everything they -had recommended the month before. She was as simple-hearted and honest -as the day was long, and so she was an easy victim. She gathered -together her quack periodicals and her quack medicines, and thus armed -with death, went about on her pale horse, metaphorically speaking, with -"hell following after." But she never suspected that she was not an -angel of healing and the balm of Gilead in disguise, to the suffering -neighbors. - -The water treatment was new, now, and Tom's low condition was a -windfall to her. She had him out at daylight every morning, stood him -up in the woodshed and drowned him with a deluge of cold water; then -she scrubbed him down with a towel like a file, and so brought him to; -then she rolled him up in a wet sheet and put him away under blankets -till she sweated his soul clean and "the yellow stains of it came -through his pores"--as Tom said. - -Yet notwithstanding all this, the boy grew more and more melancholy -and pale and dejected. She added hot baths, sitz baths, shower baths, -and plunges. The boy remained as dismal as a hearse. She began to -assist the water with a slim oatmeal diet and blister-plasters. She -calculated his capacity as she would a jug's, and filled him up every -day with quack cure-alls. - -Tom had become indifferent to persecution by this time. This phase -filled the old lady's heart with consternation. This indifference must -be broken up at any cost. Now she heard of Pain-killer for the first -time. She ordered a lot at once. She tasted it and was filled with -gratitude. It was simply fire in a liquid form. She dropped the water -treatment and everything else, and pinned her faith to Pain-killer. She -gave Tom a teaspoonful and watched with the deepest anxiety for the -result. Her troubles were instantly at rest, her soul at peace again; -for the "indifference" was broken up. The boy could not have shown a -wilder, heartier interest, if she had built a fire under him. - -Tom felt that it was time to wake up; this sort of life might be -romantic enough, in his blighted condition, but it was getting to have -too little sentiment and too much distracting variety about it. So he -thought over various plans for relief, and finally hit pon that of -professing to be fond of Pain-killer. He asked for it so often that he -became a nuisance, and his aunt ended by telling him to help himself -and quit bothering her. If it had been Sid, she would have had no -misgivings to alloy her delight; but since it was Tom, she watched the -bottle clandestinely. She found that the medicine did really diminish, -but it did not occur to her that the boy was mending the health of a -crack in the sitting-room floor with it. - -One day Tom was in the act of dosing the crack when his aunt's yellow -cat came along, purring, eying the teaspoon avariciously, and begging -for a taste. Tom said: - -"Don't ask for it unless you want it, Peter." - -But Peter signified that he did want it. - -"You better make sure." - -Peter was sure. - -"Now you've asked for it, and I'll give it to you, because there ain't -anything mean about me; but if you find you don't like it, you mustn't -blame anybody but your own self." - -Peter was agreeable. So Tom pried his mouth open and poured down the -Pain-killer. Peter sprang a couple of yards in the air, and then -delivered a war-whoop and set off round and round the room, banging -against furniture, upsetting flower-pots, and making general havoc. -Next he rose on his hind feet and pranced around, in a frenzy of -enjoyment, with his head over his shoulder and his voice proclaiming -his unappeasable happiness. Then he went tearing around the house again -spreading chaos and destruction in his path. Aunt Polly entered in time -to see him throw a few double summersets, deliver a final mighty -hurrah, and sail through the open window, carrying the rest of the -flower-pots with him. The old lady stood petrified with astonishment, -peering over her glasses; Tom lay on the floor expiring with laughter. - -"Tom, what on earth ails that cat?" - -"I don't know, aunt," gasped the boy. - -"Why, I never see anything like it. What did make him act so?" - -"Deed I don't know, Aunt Polly; cats always act so when they're having -a good time." - -"They do, do they?" There was something in the tone that made Tom -apprehensive. - -"Yes'm. That is, I believe they do." - -"You DO?" - -"Yes'm." - -The old lady was bending down, Tom watching, with interest emphasized -by anxiety. Too late he divined her "drift." The handle of the telltale -teaspoon was visible under the bed-valance. Aunt Polly took it, held it -up. Tom winced, and dropped his eyes. Aunt Polly raised him by the -usual handle--his ear--and cracked his head soundly with her thimble. - -"Now, sir, what did you want to treat that poor dumb beast so, for?" - -"I done it out of pity for him--because he hadn't any aunt." - -"Hadn't any aunt!--you numskull. What has that got to do with it?" - -"Heaps. Because if he'd had one she'd a burnt him out herself! She'd a -roasted his bowels out of him 'thout any more feeling than if he was a -human!" - -Aunt Polly felt a sudden pang of remorse. This was putting the thing -in a new light; what was cruelty to a cat MIGHT be cruelty to a boy, -too. She began to soften; she felt sorry. Her eyes watered a little, -and she put her hand on Tom's head and said gently: - -"I was meaning for the best, Tom. And, Tom, it DID do you good." - -Tom looked up in her face with just a perceptible twinkle peeping -through his gravity. - -"I know you was meaning for the best, aunty, and so was I with Peter. -It done HIM good, too. I never see him get around so since--" - -"Oh, go 'long with you, Tom, before you aggravate me again. And you -try and see if you can't be a good boy, for once, and you needn't take -any more medicine." - -Tom reached school ahead of time. It was noticed that this strange -thing had been occurring every day latterly. And now, as usual of late, -he hung about the gate of the schoolyard instead of playing with his -comrades. He was sick, he said, and he looked it. He tried to seem to -be looking everywhere but whither he really was looking--down the road. -Presently Jeff Thatcher hove in sight, and Tom's face lighted; he gazed -a moment, and then turned sorrowfully away. When Jeff arrived, Tom -accosted him; and "led up" warily to opportunities for remark about -Becky, but the giddy lad never could see the bait. Tom watched and -watched, hoping whenever a frisking frock came in sight, and hating the -owner of it as soon as he saw she was not the right one. At last frocks -ceased to appear, and he dropped hopelessly into the dumps; he entered -the empty schoolhouse and sat down to suffer. Then one more frock -passed in at the gate, and Tom's heart gave a great bound. The next -instant he was out, and "going on" like an Indian; yelling, laughing, -chasing boys, jumping over the fence at risk of life and limb, throwing -handsprings, standing on his head--doing all the heroic things he could -conceive of, and keeping a furtive eye out, all the while, to see if -Becky Thatcher was noticing. But she seemed to be unconscious of it -all; she never looked. Could it be possible that she was not aware that -he was there? He carried his exploits to her immediate vicinity; came -war-whooping around, snatched a boy's cap, hurled it to the roof of the -schoolhouse, broke through a group of boys, tumbling them in every -direction, and fell sprawling, himself, under Becky's nose, almost -upsetting her--and she turned, with her nose in the air, and he heard -her say: "Mf! some people think they're mighty smart--always showing -off!" - -Tom's cheeks burned. He gathered himself up and sneaked off, crushed -and crestfallen. - - - -CHAPTER XIII - -TOM'S mind was made up now. He was gloomy and desperate. He was a -forsaken, friendless boy, he said; nobody loved him; when they found -out what they had driven him to, perhaps they would be sorry; he had -tried to do right and get along, but they would not let him; since -nothing would do them but to be rid of him, let it be so; and let them -blame HIM for the consequences--why shouldn't they? What right had the -friendless to complain? Yes, they had forced him to it at last: he -would lead a life of crime. There was no choice. - -By this time he was far down Meadow Lane, and the bell for school to -"take up" tinkled faintly upon his ear. He sobbed, now, to think he -should never, never hear that old familiar sound any more--it was very -hard, but it was forced on him; since he was driven out into the cold -world, he must submit--but he forgave them. Then the sobs came thick -and fast. - -Just at this point he met his soul's sworn comrade, Joe Harper ---hard-eyed, and with evidently a great and dismal purpose in his heart. -Plainly here were "two souls with but a single thought." Tom, wiping -his eyes with his sleeve, began to blubber out something about a -resolution to escape from hard usage and lack of sympathy at home by -roaming abroad into the great world never to return; and ended by -hoping that Joe would not forget him. - -But it transpired that this was a request which Joe had just been -going to make of Tom, and had come to hunt him up for that purpose. His -mother had whipped him for drinking some cream which he had never -tasted and knew nothing about; it was plain that she was tired of him -and wished him to go; if she felt that way, there was nothing for him -to do but succumb; he hoped she would be happy, and never regret having -driven her poor boy out into the unfeeling world to suffer and die. - -As the two boys walked sorrowing along, they made a new compact to -stand by each other and be brothers and never separate till death -relieved them of their troubles. Then they began to lay their plans. -Joe was for being a hermit, and living on crusts in a remote cave, and -dying, some time, of cold and want and grief; but after listening to -Tom, he conceded that there were some conspicuous advantages about a -life of crime, and so he consented to be a pirate. - -Three miles below St. Petersburg, at a point where the Mississippi -River was a trifle over a mile wide, there was a long, narrow, wooded -island, with a shallow bar at the head of it, and this offered well as -a rendezvous. It was not inhabited; it lay far over toward the further -shore, abreast a dense and almost wholly unpeopled forest. So Jackson's -Island was chosen. Who were to be the subjects of their piracies was a -matter that did not occur to them. Then they hunted up Huckleberry -Finn, and he joined them promptly, for all careers were one to him; he -was indifferent. They presently separated to meet at a lonely spot on -the river-bank two miles above the village at the favorite hour--which -was midnight. There was a small log raft there which they meant to -capture. Each would bring hooks and lines, and such provision as he -could steal in the most dark and mysterious way--as became outlaws. And -before the afternoon was done, they had all managed to enjoy the sweet -glory of spreading the fact that pretty soon the town would "hear -something." All who got this vague hint were cautioned to "be mum and -wait." - -About midnight Tom arrived with a boiled ham and a few trifles, -and stopped in a dense undergrowth on a small bluff overlooking the -meeting-place. It was starlight, and very still. The mighty river lay -like an ocean at rest. Tom listened a moment, but no sound disturbed the -quiet. Then he gave a low, distinct whistle. It was answered from under -the bluff. Tom whistled twice more; these signals were answered in the -same way. Then a guarded voice said: - -"Who goes there?" - -"Tom Sawyer, the Black Avenger of the Spanish Main. Name your names." - -"Huck Finn the Red-Handed, and Joe Harper the Terror of the Seas." Tom -had furnished these titles, from his favorite literature. - -"'Tis well. Give the countersign." - -Two hoarse whispers delivered the same awful word simultaneously to -the brooding night: - -"BLOOD!" - -Then Tom tumbled his ham over the bluff and let himself down after it, -tearing both skin and clothes to some extent in the effort. There was -an easy, comfortable path along the shore under the bluff, but it -lacked the advantages of difficulty and danger so valued by a pirate. - -The Terror of the Seas had brought a side of bacon, and had about worn -himself out with getting it there. Finn the Red-Handed had stolen a -skillet and a quantity of half-cured leaf tobacco, and had also brought -a few corn-cobs to make pipes with. But none of the pirates smoked or -"chewed" but himself. The Black Avenger of the Spanish Main said it -would never do to start without some fire. That was a wise thought; -matches were hardly known there in that day. They saw a fire -smouldering upon a great raft a hundred yards above, and they went -stealthily thither and helped themselves to a chunk. They made an -imposing adventure of it, saying, "Hist!" every now and then, and -suddenly halting with finger on lip; moving with hands on imaginary -dagger-hilts; and giving orders in dismal whispers that if "the foe" -stirred, to "let him have it to the hilt," because "dead men tell no -tales." They knew well enough that the raftsmen were all down at the -village laying in stores or having a spree, but still that was no -excuse for their conducting this thing in an unpiratical way. - -They shoved off, presently, Tom in command, Huck at the after oar and -Joe at the forward. Tom stood amidships, gloomy-browed, and with folded -arms, and gave his orders in a low, stern whisper: - -"Luff, and bring her to the wind!" - -"Aye-aye, sir!" - -"Steady, steady-y-y-y!" - -"Steady it is, sir!" - -"Let her go off a point!" - -"Point it is, sir!" - -As the boys steadily and monotonously drove the raft toward mid-stream -it was no doubt understood that these orders were given only for -"style," and were not intended to mean anything in particular. - -"What sail's she carrying?" - -"Courses, tops'ls, and flying-jib, sir." - -"Send the r'yals up! Lay out aloft, there, half a dozen of ye ---foretopmaststuns'l! Lively, now!" - -"Aye-aye, sir!" - -"Shake out that maintogalans'l! Sheets and braces! NOW my hearties!" - -"Aye-aye, sir!" - -"Hellum-a-lee--hard a port! Stand by to meet her when she comes! Port, -port! NOW, men! With a will! Stead-y-y-y!" - -"Steady it is, sir!" - -The raft drew beyond the middle of the river; the boys pointed her -head right, and then lay on their oars. The river was not high, so -there was not more than a two or three mile current. Hardly a word was -said during the next three-quarters of an hour. Now the raft was -passing before the distant town. Two or three glimmering lights showed -where it lay, peacefully sleeping, beyond the vague vast sweep of -star-gemmed water, unconscious of the tremendous event that was happening. -The Black Avenger stood still with folded arms, "looking his last" upon -the scene of his former joys and his later sufferings, and wishing -"she" could see him now, abroad on the wild sea, facing peril and death -with dauntless heart, going to his doom with a grim smile on his lips. -It was but a small strain on his imagination to remove Jackson's Island -beyond eyeshot of the village, and so he "looked his last" with a -broken and satisfied heart. The other pirates were looking their last, -too; and they all looked so long that they came near letting the -current drift them out of the range of the island. But they discovered -the danger in time, and made shift to avert it. About two o'clock in -the morning the raft grounded on the bar two hundred yards above the -head of the island, and they waded back and forth until they had landed -their freight. Part of the little raft's belongings consisted of an old -sail, and this they spread over a nook in the bushes for a tent to -shelter their provisions; but they themselves would sleep in the open -air in good weather, as became outlaws. - -They built a fire against the side of a great log twenty or thirty -steps within the sombre depths of the forest, and then cooked some -bacon in the frying-pan for supper, and used up half of the corn "pone" -stock they had brought. It seemed glorious sport to be feasting in that -wild, free way in the virgin forest of an unexplored and uninhabited -island, far from the haunts of men, and they said they never would -return to civilization. The climbing fire lit up their faces and threw -its ruddy glare upon the pillared tree-trunks of their forest temple, -and upon the varnished foliage and festooning vines. - -When the last crisp slice of bacon was gone, and the last allowance of -corn pone devoured, the boys stretched themselves out on the grass, -filled with contentment. They could have found a cooler place, but they -would not deny themselves such a romantic feature as the roasting -camp-fire. - -"AIN'T it gay?" said Joe. - -"It's NUTS!" said Tom. "What would the boys say if they could see us?" - -"Say? Well, they'd just die to be here--hey, Hucky!" - -"I reckon so," said Huckleberry; "anyways, I'm suited. I don't want -nothing better'n this. I don't ever get enough to eat, gen'ally--and -here they can't come and pick at a feller and bullyrag him so." - -"It's just the life for me," said Tom. "You don't have to get up, -mornings, and you don't have to go to school, and wash, and all that -blame foolishness. You see a pirate don't have to do ANYTHING, Joe, -when he's ashore, but a hermit HE has to be praying considerable, and -then he don't have any fun, anyway, all by himself that way." - -"Oh yes, that's so," said Joe, "but I hadn't thought much about it, -you know. I'd a good deal rather be a pirate, now that I've tried it." - -"You see," said Tom, "people don't go much on hermits, nowadays, like -they used to in old times, but a pirate's always respected. And a -hermit's got to sleep on the hardest place he can find, and put -sackcloth and ashes on his head, and stand out in the rain, and--" - -"What does he put sackcloth and ashes on his head for?" inquired Huck. - -"I dono. But they've GOT to do it. Hermits always do. You'd have to do -that if you was a hermit." - -"Dern'd if I would," said Huck. - -"Well, what would you do?" - -"I dono. But I wouldn't do that." - -"Why, Huck, you'd HAVE to. How'd you get around it?" - -"Why, I just wouldn't stand it. I'd run away." - -"Run away! Well, you WOULD be a nice old slouch of a hermit. You'd be -a disgrace." - -The Red-Handed made no response, being better employed. He had -finished gouging out a cob, and now he fitted a weed stem to it, loaded -it with tobacco, and was pressing a coal to the charge and blowing a -cloud of fragrant smoke--he was in the full bloom of luxurious -contentment. The other pirates envied him this majestic vice, and -secretly resolved to acquire it shortly. Presently Huck said: - -"What does pirates have to do?" - -Tom said: - -"Oh, they have just a bully time--take ships and burn them, and get -the money and bury it in awful places in their island where there's -ghosts and things to watch it, and kill everybody in the ships--make -'em walk a plank." - -"And they carry the women to the island," said Joe; "they don't kill -the women." - -"No," assented Tom, "they don't kill the women--they're too noble. And -the women's always beautiful, too. - -"And don't they wear the bulliest clothes! Oh no! All gold and silver -and di'monds," said Joe, with enthusiasm. - -"Who?" said Huck. - -"Why, the pirates." - -Huck scanned his own clothing forlornly. - -"I reckon I ain't dressed fitten for a pirate," said he, with a -regretful pathos in his voice; "but I ain't got none but these." - -But the other boys told him the fine clothes would come fast enough, -after they should have begun their adventures. They made him understand -that his poor rags would do to begin with, though it was customary for -wealthy pirates to start with a proper wardrobe. - -Gradually their talk died out and drowsiness began to steal upon the -eyelids of the little waifs. The pipe dropped from the fingers of the -Red-Handed, and he slept the sleep of the conscience-free and the -weary. The Terror of the Seas and the Black Avenger of the Spanish Main -had more difficulty in getting to sleep. They said their prayers -inwardly, and lying down, since there was nobody there with authority -to make them kneel and recite aloud; in truth, they had a mind not to -say them at all, but they were afraid to proceed to such lengths as -that, lest they might call down a sudden and special thunderbolt from -heaven. Then at once they reached and hovered upon the imminent verge -of sleep--but an intruder came, now, that would not "down." It was -conscience. They began to feel a vague fear that they had been doing -wrong to run away; and next they thought of the stolen meat, and then -the real torture came. They tried to argue it away by reminding -conscience that they had purloined sweetmeats and apples scores of -times; but conscience was not to be appeased by such thin -plausibilities; it seemed to them, in the end, that there was no -getting around the stubborn fact that taking sweetmeats was only -"hooking," while taking bacon and hams and such valuables was plain -simple stealing--and there was a command against that in the Bible. So -they inwardly resolved that so long as they remained in the business, -their piracies should not again be sullied with the crime of stealing. -Then conscience granted a truce, and these curiously inconsistent -pirates fell peacefully to sleep. - - - -CHAPTER XIV - -WHEN Tom awoke in the morning, he wondered where he was. He sat up and -rubbed his eyes and looked around. Then he comprehended. It was the -cool gray dawn, and there was a delicious sense of repose and peace in -the deep pervading calm and silence of the woods. Not a leaf stirred; -not a sound obtruded upon great Nature's meditation. Beaded dewdrops -stood upon the leaves and grasses. A white layer of ashes covered the -fire, and a thin blue breath of smoke rose straight into the air. Joe -and Huck still slept. - -Now, far away in the woods a bird called; another answered; presently -the hammering of a woodpecker was heard. Gradually the cool dim gray of -the morning whitened, and as gradually sounds multiplied and life -manifested itself. The marvel of Nature shaking off sleep and going to -work unfolded itself to the musing boy. A little green worm came -crawling over a dewy leaf, lifting two-thirds of his body into the air -from time to time and "sniffing around," then proceeding again--for he -was measuring, Tom said; and when the worm approached him, of its own -accord, he sat as still as a stone, with his hopes rising and falling, -by turns, as the creature still came toward him or seemed inclined to -go elsewhere; and when at last it considered a painful moment with its -curved body in the air and then came decisively down upon Tom's leg and -began a journey over him, his whole heart was glad--for that meant that -he was going to have a new suit of clothes--without the shadow of a -doubt a gaudy piratical uniform. Now a procession of ants appeared, -from nowhere in particular, and went about their labors; one struggled -manfully by with a dead spider five times as big as itself in its arms, -and lugged it straight up a tree-trunk. A brown spotted lady-bug -climbed the dizzy height of a grass blade, and Tom bent down close to -it and said, "Lady-bug, lady-bug, fly away home, your house is on fire, -your children's alone," and she took wing and went off to see about it ---which did not surprise the boy, for he knew of old that this insect was -credulous about conflagrations, and he had practised upon its -simplicity more than once. A tumblebug came next, heaving sturdily at -its ball, and Tom touched the creature, to see it shut its legs against -its body and pretend to be dead. The birds were fairly rioting by this -time. A catbird, the Northern mocker, lit in a tree over Tom's head, -and trilled out her imitations of her neighbors in a rapture of -enjoyment; then a shrill jay swept down, a flash of blue flame, and -stopped on a twig almost within the boy's reach, cocked his head to one -side and eyed the strangers with a consuming curiosity; a gray squirrel -and a big fellow of the "fox" kind came skurrying along, sitting up at -intervals to inspect and chatter at the boys, for the wild things had -probably never seen a human being before and scarcely knew whether to -be afraid or not. All Nature was wide awake and stirring, now; long -lances of sunlight pierced down through the dense foliage far and near, -and a few butterflies came fluttering upon the scene. - -Tom stirred up the other pirates and they all clattered away with a -shout, and in a minute or two were stripped and chasing after and -tumbling over each other in the shallow limpid water of the white -sandbar. They felt no longing for the little village sleeping in the -distance beyond the majestic waste of water. A vagrant current or a -slight rise in the river had carried off their raft, but this only -gratified them, since its going was something like burning the bridge -between them and civilization. - -They came back to camp wonderfully refreshed, glad-hearted, and -ravenous; and they soon had the camp-fire blazing up again. Huck found -a spring of clear cold water close by, and the boys made cups of broad -oak or hickory leaves, and felt that water, sweetened with such a -wildwood charm as that, would be a good enough substitute for coffee. -While Joe was slicing bacon for breakfast, Tom and Huck asked him to -hold on a minute; they stepped to a promising nook in the river-bank -and threw in their lines; almost immediately they had reward. Joe had -not had time to get impatient before they were back again with some -handsome bass, a couple of sun-perch and a small catfish--provisions -enough for quite a family. They fried the fish with the bacon, and were -astonished; for no fish had ever seemed so delicious before. They did -not know that the quicker a fresh-water fish is on the fire after he is -caught the better he is; and they reflected little upon what a sauce -open-air sleeping, open-air exercise, bathing, and a large ingredient -of hunger make, too. - -They lay around in the shade, after breakfast, while Huck had a smoke, -and then went off through the woods on an exploring expedition. They -tramped gayly along, over decaying logs, through tangled underbrush, -among solemn monarchs of the forest, hung from their crowns to the -ground with a drooping regalia of grape-vines. Now and then they came -upon snug nooks carpeted with grass and jeweled with flowers. - -They found plenty of things to be delighted with, but nothing to be -astonished at. They discovered that the island was about three miles -long and a quarter of a mile wide, and that the shore it lay closest to -was only separated from it by a narrow channel hardly two hundred yards -wide. They took a swim about every hour, so it was close upon the -middle of the afternoon when they got back to camp. They were too -hungry to stop to fish, but they fared sumptuously upon cold ham, and -then threw themselves down in the shade to talk. But the talk soon -began to drag, and then died. The stillness, the solemnity that brooded -in the woods, and the sense of loneliness, began to tell upon the -spirits of the boys. They fell to thinking. A sort of undefined longing -crept upon them. This took dim shape, presently--it was budding -homesickness. Even Finn the Red-Handed was dreaming of his doorsteps -and empty hogsheads. But they were all ashamed of their weakness, and -none was brave enough to speak his thought. - -For some time, now, the boys had been dully conscious of a peculiar -sound in the distance, just as one sometimes is of the ticking of a -clock which he takes no distinct note of. But now this mysterious sound -became more pronounced, and forced a recognition. The boys started, -glanced at each other, and then each assumed a listening attitude. -There was a long silence, profound and unbroken; then a deep, sullen -boom came floating down out of the distance. - -"What is it!" exclaimed Joe, under his breath. - -"I wonder," said Tom in a whisper. - -"'Tain't thunder," said Huckleberry, in an awed tone, "becuz thunder--" - -"Hark!" said Tom. "Listen--don't talk." - -They waited a time that seemed an age, and then the same muffled boom -troubled the solemn hush. - -"Let's go and see." - -They sprang to their feet and hurried to the shore toward the town. -They parted the bushes on the bank and peered out over the water. The -little steam ferryboat was about a mile below the village, drifting -with the current. Her broad deck seemed crowded with people. There were -a great many skiffs rowing about or floating with the stream in the -neighborhood of the ferryboat, but the boys could not determine what -the men in them were doing. Presently a great jet of white smoke burst -from the ferryboat's side, and as it expanded and rose in a lazy cloud, -that same dull throb of sound was borne to the listeners again. - -"I know now!" exclaimed Tom; "somebody's drownded!" - -"That's it!" said Huck; "they done that last summer, when Bill Turner -got drownded; they shoot a cannon over the water, and that makes him -come up to the top. Yes, and they take loaves of bread and put -quicksilver in 'em and set 'em afloat, and wherever there's anybody -that's drownded, they'll float right there and stop." - -"Yes, I've heard about that," said Joe. "I wonder what makes the bread -do that." - -"Oh, it ain't the bread, so much," said Tom; "I reckon it's mostly -what they SAY over it before they start it out." - -"But they don't say anything over it," said Huck. "I've seen 'em and -they don't." - -"Well, that's funny," said Tom. "But maybe they say it to themselves. -Of COURSE they do. Anybody might know that." - -The other boys agreed that there was reason in what Tom said, because -an ignorant lump of bread, uninstructed by an incantation, could not be -expected to act very intelligently when set upon an errand of such -gravity. - -"By jings, I wish I was over there, now," said Joe. - -"I do too" said Huck "I'd give heaps to know who it is." - -The boys still listened and watched. Presently a revealing thought -flashed through Tom's mind, and he exclaimed: - -"Boys, I know who's drownded--it's us!" - -They felt like heroes in an instant. Here was a gorgeous triumph; they -were missed; they were mourned; hearts were breaking on their account; -tears were being shed; accusing memories of unkindness to these poor -lost lads were rising up, and unavailing regrets and remorse were being -indulged; and best of all, the departed were the talk of the whole -town, and the envy of all the boys, as far as this dazzling notoriety -was concerned. This was fine. It was worth while to be a pirate, after -all. - -As twilight drew on, the ferryboat went back to her accustomed -business and the skiffs disappeared. The pirates returned to camp. They -were jubilant with vanity over their new grandeur and the illustrious -trouble they were making. They caught fish, cooked supper and ate it, -and then fell to guessing at what the village was thinking and saying -about them; and the pictures they drew of the public distress on their -account were gratifying to look upon--from their point of view. But -when the shadows of night closed them in, they gradually ceased to -talk, and sat gazing into the fire, with their minds evidently -wandering elsewhere. The excitement was gone, now, and Tom and Joe -could not keep back thoughts of certain persons at home who were not -enjoying this fine frolic as much as they were. Misgivings came; they -grew troubled and unhappy; a sigh or two escaped, unawares. By and by -Joe timidly ventured upon a roundabout "feeler" as to how the others -might look upon a return to civilization--not right now, but-- - -Tom withered him with derision! Huck, being uncommitted as yet, joined -in with Tom, and the waverer quickly "explained," and was glad to get -out of the scrape with as little taint of chicken-hearted homesickness -clinging to his garments as he could. Mutiny was effectually laid to -rest for the moment. - -As the night deepened, Huck began to nod, and presently to snore. Joe -followed next. Tom lay upon his elbow motionless, for some time, -watching the two intently. At last he got up cautiously, on his knees, -and went searching among the grass and the flickering reflections flung -by the camp-fire. He picked up and inspected several large -semi-cylinders of the thin white bark of a sycamore, and finally chose -two which seemed to suit him. Then he knelt by the fire and painfully -wrote something upon each of these with his "red keel"; one he rolled up -and put in his jacket pocket, and the other he put in Joe's hat and -removed it to a little distance from the owner. And he also put into the -hat certain schoolboy treasures of almost inestimable value--among them -a lump of chalk, an India-rubber ball, three fishhooks, and one of that -kind of marbles known as a "sure 'nough crystal." Then he tiptoed his -way cautiously among the trees till he felt that he was out of hearing, -and straightway broke into a keen run in the direction of the sandbar. - - - -CHAPTER XV - -A FEW minutes later Tom was in the shoal water of the bar, wading -toward the Illinois shore. Before the depth reached his middle he was -half-way over; the current would permit no more wading, now, so he -struck out confidently to swim the remaining hundred yards. He swam -quartering upstream, but still was swept downward rather faster than he -had expected. However, he reached the shore finally, and drifted along -till he found a low place and drew himself out. He put his hand on his -jacket pocket, found his piece of bark safe, and then struck through -the woods, following the shore, with streaming garments. Shortly before -ten o'clock he came out into an open place opposite the village, and -saw the ferryboat lying in the shadow of the trees and the high bank. -Everything was quiet under the blinking stars. He crept down the bank, -watching with all his eyes, slipped into the water, swam three or four -strokes and climbed into the skiff that did "yawl" duty at the boat's -stern. He laid himself down under the thwarts and waited, panting. - -Presently the cracked bell tapped and a voice gave the order to "cast -off." A minute or two later the skiff's head was standing high up, -against the boat's swell, and the voyage was begun. Tom felt happy in -his success, for he knew it was the boat's last trip for the night. At -the end of a long twelve or fifteen minutes the wheels stopped, and Tom -slipped overboard and swam ashore in the dusk, landing fifty yards -downstream, out of danger of possible stragglers. - -He flew along unfrequented alleys, and shortly found himself at his -aunt's back fence. He climbed over, approached the "ell," and looked in -at the sitting-room window, for a light was burning there. There sat -Aunt Polly, Sid, Mary, and Joe Harper's mother, grouped together, -talking. They were by the bed, and the bed was between them and the -door. Tom went to the door and began to softly lift the latch; then he -pressed gently and the door yielded a crack; he continued pushing -cautiously, and quaking every time it creaked, till he judged he might -squeeze through on his knees; so he put his head through and began, -warily. - -"What makes the candle blow so?" said Aunt Polly. Tom hurried up. -"Why, that door's open, I believe. Why, of course it is. No end of -strange things now. Go 'long and shut it, Sid." - -Tom disappeared under the bed just in time. He lay and "breathed" -himself for a time, and then crept to where he could almost touch his -aunt's foot. - -"But as I was saying," said Aunt Polly, "he warn't BAD, so to say ---only mischEEvous. Only just giddy, and harum-scarum, you know. He -warn't any more responsible than a colt. HE never meant any harm, and -he was the best-hearted boy that ever was"--and she began to cry. - -"It was just so with my Joe--always full of his devilment, and up to -every kind of mischief, but he was just as unselfish and kind as he -could be--and laws bless me, to think I went and whipped him for taking -that cream, never once recollecting that I throwed it out myself -because it was sour, and I never to see him again in this world, never, -never, never, poor abused boy!" And Mrs. Harper sobbed as if her heart -would break. - -"I hope Tom's better off where he is," said Sid, "but if he'd been -better in some ways--" - -"SID!" Tom felt the glare of the old lady's eye, though he could not -see it. "Not a word against my Tom, now that he's gone! God'll take -care of HIM--never you trouble YOURself, sir! Oh, Mrs. Harper, I don't -know how to give him up! I don't know how to give him up! He was such a -comfort to me, although he tormented my old heart out of me, 'most." - -"The Lord giveth and the Lord hath taken away--Blessed be the name of -the Lord! But it's so hard--Oh, it's so hard! Only last Saturday my -Joe busted a firecracker right under my nose and I knocked him -sprawling. Little did I know then, how soon--Oh, if it was to do over -again I'd hug him and bless him for it." - -"Yes, yes, yes, I know just how you feel, Mrs. Harper, I know just -exactly how you feel. No longer ago than yesterday noon, my Tom took -and filled the cat full of Pain-killer, and I did think the cretur -would tear the house down. And God forgive me, I cracked Tom's head -with my thimble, poor boy, poor dead boy. But he's out of all his -troubles now. And the last words I ever heard him say was to reproach--" - -But this memory was too much for the old lady, and she broke entirely -down. Tom was snuffling, now, himself--and more in pity of himself than -anybody else. He could hear Mary crying, and putting in a kindly word -for him from time to time. He began to have a nobler opinion of himself -than ever before. Still, he was sufficiently touched by his aunt's -grief to long to rush out from under the bed and overwhelm her with -joy--and the theatrical gorgeousness of the thing appealed strongly to -his nature, too, but he resisted and lay still. - -He went on listening, and gathered by odds and ends that it was -conjectured at first that the boys had got drowned while taking a swim; -then the small raft had been missed; next, certain boys said the -missing lads had promised that the village should "hear something" -soon; the wise-heads had "put this and that together" and decided that -the lads had gone off on that raft and would turn up at the next town -below, presently; but toward noon the raft had been found, lodged -against the Missouri shore some five or six miles below the village ---and then hope perished; they must be drowned, else hunger would have -driven them home by nightfall if not sooner. It was believed that the -search for the bodies had been a fruitless effort merely because the -drowning must have occurred in mid-channel, since the boys, being good -swimmers, would otherwise have escaped to shore. This was Wednesday -night. If the bodies continued missing until Sunday, all hope would be -given over, and the funerals would be preached on that morning. Tom -shuddered. - -Mrs. Harper gave a sobbing good-night and turned to go. Then with a -mutual impulse the two bereaved women flung themselves into each -other's arms and had a good, consoling cry, and then parted. Aunt Polly -was tender far beyond her wont, in her good-night to Sid and Mary. Sid -snuffled a bit and Mary went off crying with all her heart. - -Aunt Polly knelt down and prayed for Tom so touchingly, so -appealingly, and with such measureless love in her words and her old -trembling voice, that he was weltering in tears again, long before she -was through. - -He had to keep still long after she went to bed, for she kept making -broken-hearted ejaculations from time to time, tossing unrestfully, and -turning over. But at last she was still, only moaning a little in her -sleep. Now the boy stole out, rose gradually by the bedside, shaded the -candle-light with his hand, and stood regarding her. His heart was full -of pity for her. He took out his sycamore scroll and placed it by the -candle. But something occurred to him, and he lingered considering. His -face lighted with a happy solution of his thought; he put the bark -hastily in his pocket. Then he bent over and kissed the faded lips, and -straightway made his stealthy exit, latching the door behind him. - -He threaded his way back to the ferry landing, found nobody at large -there, and walked boldly on board the boat, for he knew she was -tenantless except that there was a watchman, who always turned in and -slept like a graven image. He untied the skiff at the stern, slipped -into it, and was soon rowing cautiously upstream. When he had pulled a -mile above the village, he started quartering across and bent himself -stoutly to his work. He hit the landing on the other side neatly, for -this was a familiar bit of work to him. He was moved to capture the -skiff, arguing that it might be considered a ship and therefore -legitimate prey for a pirate, but he knew a thorough search would be -made for it and that might end in revelations. So he stepped ashore and -entered the woods. - -He sat down and took a long rest, torturing himself meanwhile to keep -awake, and then started warily down the home-stretch. The night was far -spent. It was broad daylight before he found himself fairly abreast the -island bar. He rested again until the sun was well up and gilding the -great river with its splendor, and then he plunged into the stream. A -little later he paused, dripping, upon the threshold of the camp, and -heard Joe say: - -"No, Tom's true-blue, Huck, and he'll come back. He won't desert. He -knows that would be a disgrace to a pirate, and Tom's too proud for -that sort of thing. He's up to something or other. Now I wonder what?" - -"Well, the things is ours, anyway, ain't they?" - -"Pretty near, but not yet, Huck. The writing says they are if he ain't -back here to breakfast." - -"Which he is!" exclaimed Tom, with fine dramatic effect, stepping -grandly into camp. - -A sumptuous breakfast of bacon and fish was shortly provided, and as -the boys set to work upon it, Tom recounted (and adorned) his -adventures. They were a vain and boastful company of heroes when the -tale was done. Then Tom hid himself away in a shady nook to sleep till -noon, and the other pirates got ready to fish and explore. - - - -CHAPTER XVI - -AFTER dinner all the gang turned out to hunt for turtle eggs on the -bar. They went about poking sticks into the sand, and when they found a -soft place they went down on their knees and dug with their hands. -Sometimes they would take fifty or sixty eggs out of one hole. They -were perfectly round white things a trifle smaller than an English -walnut. They had a famous fried-egg feast that night, and another on -Friday morning. - -After breakfast they went whooping and prancing out on the bar, and -chased each other round and round, shedding clothes as they went, until -they were naked, and then continued the frolic far away up the shoal -water of the bar, against the stiff current, which latter tripped their -legs from under them from time to time and greatly increased the fun. -And now and then they stooped in a group and splashed water in each -other's faces with their palms, gradually approaching each other, with -averted faces to avoid the strangling sprays, and finally gripping and -struggling till the best man ducked his neighbor, and then they all -went under in a tangle of white legs and arms and came up blowing, -sputtering, laughing, and gasping for breath at one and the same time. - -When they were well exhausted, they would run out and sprawl on the -dry, hot sand, and lie there and cover themselves up with it, and by -and by break for the water again and go through the original -performance once more. Finally it occurred to them that their naked -skin represented flesh-colored "tights" very fairly; so they drew a -ring in the sand and had a circus--with three clowns in it, for none -would yield this proudest post to his neighbor. - -Next they got their marbles and played "knucks" and "ring-taw" and -"keeps" till that amusement grew stale. Then Joe and Huck had another -swim, but Tom would not venture, because he found that in kicking off -his trousers he had kicked his string of rattlesnake rattles off his -ankle, and he wondered how he had escaped cramp so long without the -protection of this mysterious charm. He did not venture again until he -had found it, and by that time the other boys were tired and ready to -rest. They gradually wandered apart, dropped into the "dumps," and fell -to gazing longingly across the wide river to where the village lay -drowsing in the sun. Tom found himself writing "BECKY" in the sand with -his big toe; he scratched it out, and was angry with himself for his -weakness. But he wrote it again, nevertheless; he could not help it. He -erased it once more and then took himself out of temptation by driving -the other boys together and joining them. - -But Joe's spirits had gone down almost beyond resurrection. He was so -homesick that he could hardly endure the misery of it. The tears lay -very near the surface. Huck was melancholy, too. Tom was downhearted, -but tried hard not to show it. He had a secret which he was not ready -to tell, yet, but if this mutinous depression was not broken up soon, -he would have to bring it out. He said, with a great show of -cheerfulness: - -"I bet there's been pirates on this island before, boys. We'll explore -it again. They've hid treasures here somewhere. How'd you feel to light -on a rotten chest full of gold and silver--hey?" - -But it roused only faint enthusiasm, which faded out, with no reply. -Tom tried one or two other seductions; but they failed, too. It was -discouraging work. Joe sat poking up the sand with a stick and looking -very gloomy. Finally he said: - -"Oh, boys, let's give it up. I want to go home. It's so lonesome." - -"Oh no, Joe, you'll feel better by and by," said Tom. "Just think of -the fishing that's here." - -"I don't care for fishing. I want to go home." - -"But, Joe, there ain't such another swimming-place anywhere." - -"Swimming's no good. I don't seem to care for it, somehow, when there -ain't anybody to say I sha'n't go in. I mean to go home." - -"Oh, shucks! Baby! You want to see your mother, I reckon." - -"Yes, I DO want to see my mother--and you would, too, if you had one. -I ain't any more baby than you are." And Joe snuffled a little. - -"Well, we'll let the cry-baby go home to his mother, won't we, Huck? -Poor thing--does it want to see its mother? And so it shall. You like -it here, don't you, Huck? We'll stay, won't we?" - -Huck said, "Y-e-s"--without any heart in it. - -"I'll never speak to you again as long as I live," said Joe, rising. -"There now!" And he moved moodily away and began to dress himself. - -"Who cares!" said Tom. "Nobody wants you to. Go 'long home and get -laughed at. Oh, you're a nice pirate. Huck and me ain't cry-babies. -We'll stay, won't we, Huck? Let him go if he wants to. I reckon we can -get along without him, per'aps." - -But Tom was uneasy, nevertheless, and was alarmed to see Joe go -sullenly on with his dressing. And then it was discomforting to see -Huck eying Joe's preparations so wistfully, and keeping up such an -ominous silence. Presently, without a parting word, Joe began to wade -off toward the Illinois shore. Tom's heart began to sink. He glanced at -Huck. Huck could not bear the look, and dropped his eyes. Then he said: - -"I want to go, too, Tom. It was getting so lonesome anyway, and now -it'll be worse. Let's us go, too, Tom." - -"I won't! You can all go, if you want to. I mean to stay." - -"Tom, I better go." - -"Well, go 'long--who's hendering you." - -Huck began to pick up his scattered clothes. He said: - -"Tom, I wisht you'd come, too. Now you think it over. We'll wait for -you when we get to shore." - -"Well, you'll wait a blame long time, that's all." - -Huck started sorrowfully away, and Tom stood looking after him, with a -strong desire tugging at his heart to yield his pride and go along too. -He hoped the boys would stop, but they still waded slowly on. It -suddenly dawned on Tom that it was become very lonely and still. He -made one final struggle with his pride, and then darted after his -comrades, yelling: - -"Wait! Wait! I want to tell you something!" - -They presently stopped and turned around. When he got to where they -were, he began unfolding his secret, and they listened moodily till at -last they saw the "point" he was driving at, and then they set up a -war-whoop of applause and said it was "splendid!" and said if he had -told them at first, they wouldn't have started away. He made a plausible -excuse; but his real reason had been the fear that not even the secret -would keep them with him any very great length of time, and so he had -meant to hold it in reserve as a last seduction. - -The lads came gayly back and went at their sports again with a will, -chattering all the time about Tom's stupendous plan and admiring the -genius of it. After a dainty egg and fish dinner, Tom said he wanted to -learn to smoke, now. Joe caught at the idea and said he would like to -try, too. So Huck made pipes and filled them. These novices had never -smoked anything before but cigars made of grape-vine, and they "bit" -the tongue, and were not considered manly anyway. - -Now they stretched themselves out on their elbows and began to puff, -charily, and with slender confidence. The smoke had an unpleasant -taste, and they gagged a little, but Tom said: - -"Why, it's just as easy! If I'd a knowed this was all, I'd a learnt -long ago." - -"So would I," said Joe. "It's just nothing." - -"Why, many a time I've looked at people smoking, and thought well I -wish I could do that; but I never thought I could," said Tom. - -"That's just the way with me, hain't it, Huck? You've heard me talk -just that way--haven't you, Huck? I'll leave it to Huck if I haven't." - -"Yes--heaps of times," said Huck. - -"Well, I have too," said Tom; "oh, hundreds of times. Once down by the -slaughter-house. Don't you remember, Huck? Bob Tanner was there, and -Johnny Miller, and Jeff Thatcher, when I said it. Don't you remember, -Huck, 'bout me saying that?" - -"Yes, that's so," said Huck. "That was the day after I lost a white -alley. No, 'twas the day before." - -"There--I told you so," said Tom. "Huck recollects it." - -"I bleeve I could smoke this pipe all day," said Joe. "I don't feel -sick." - -"Neither do I," said Tom. "I could smoke it all day. But I bet you -Jeff Thatcher couldn't." - -"Jeff Thatcher! Why, he'd keel over just with two draws. Just let him -try it once. HE'D see!" - -"I bet he would. And Johnny Miller--I wish could see Johnny Miller -tackle it once." - -"Oh, don't I!" said Joe. "Why, I bet you Johnny Miller couldn't any -more do this than nothing. Just one little snifter would fetch HIM." - -"'Deed it would, Joe. Say--I wish the boys could see us now." - -"So do I." - -"Say--boys, don't say anything about it, and some time when they're -around, I'll come up to you and say, 'Joe, got a pipe? I want a smoke.' -And you'll say, kind of careless like, as if it warn't anything, you'll -say, 'Yes, I got my OLD pipe, and another one, but my tobacker ain't -very good.' And I'll say, 'Oh, that's all right, if it's STRONG -enough.' And then you'll out with the pipes, and we'll light up just as -ca'm, and then just see 'em look!" - -"By jings, that'll be gay, Tom! I wish it was NOW!" - -"So do I! And when we tell 'em we learned when we was off pirating, -won't they wish they'd been along?" - -"Oh, I reckon not! I'll just BET they will!" - -So the talk ran on. But presently it began to flag a trifle, and grow -disjointed. The silences widened; the expectoration marvellously -increased. Every pore inside the boys' cheeks became a spouting -fountain; they could scarcely bail out the cellars under their tongues -fast enough to prevent an inundation; little overflowings down their -throats occurred in spite of all they could do, and sudden retchings -followed every time. Both boys were looking very pale and miserable, -now. Joe's pipe dropped from his nerveless fingers. Tom's followed. -Both fountains were going furiously and both pumps bailing with might -and main. Joe said feebly: - -"I've lost my knife. I reckon I better go and find it." - -Tom said, with quivering lips and halting utterance: - -"I'll help you. You go over that way and I'll hunt around by the -spring. No, you needn't come, Huck--we can find it." - -So Huck sat down again, and waited an hour. Then he found it lonesome, -and went to find his comrades. They were wide apart in the woods, both -very pale, both fast asleep. But something informed him that if they -had had any trouble they had got rid of it. - -They were not talkative at supper that night. They had a humble look, -and when Huck prepared his pipe after the meal and was going to prepare -theirs, they said no, they were not feeling very well--something they -ate at dinner had disagreed with them. - -About midnight Joe awoke, and called the boys. There was a brooding -oppressiveness in the air that seemed to bode something. The boys -huddled themselves together and sought the friendly companionship of -the fire, though the dull dead heat of the breathless atmosphere was -stifling. They sat still, intent and waiting. The solemn hush -continued. Beyond the light of the fire everything was swallowed up in -the blackness of darkness. Presently there came a quivering glow that -vaguely revealed the foliage for a moment and then vanished. By and by -another came, a little stronger. Then another. Then a faint moan came -sighing through the branches of the forest and the boys felt a fleeting -breath upon their cheeks, and shuddered with the fancy that the Spirit -of the Night had gone by. There was a pause. Now a weird flash turned -night into day and showed every little grass-blade, separate and -distinct, that grew about their feet. And it showed three white, -startled faces, too. A deep peal of thunder went rolling and tumbling -down the heavens and lost itself in sullen rumblings in the distance. A -sweep of chilly air passed by, rustling all the leaves and snowing the -flaky ashes broadcast about the fire. Another fierce glare lit up the -forest and an instant crash followed that seemed to rend the tree-tops -right over the boys' heads. They clung together in terror, in the thick -gloom that followed. A few big rain-drops fell pattering upon the -leaves. - -"Quick! boys, go for the tent!" exclaimed Tom. - -They sprang away, stumbling over roots and among vines in the dark, no -two plunging in the same direction. A furious blast roared through the -trees, making everything sing as it went. One blinding flash after -another came, and peal on peal of deafening thunder. And now a -drenching rain poured down and the rising hurricane drove it in sheets -along the ground. The boys cried out to each other, but the roaring -wind and the booming thunder-blasts drowned their voices utterly. -However, one by one they straggled in at last and took shelter under -the tent, cold, scared, and streaming with water; but to have company -in misery seemed something to be grateful for. They could not talk, the -old sail flapped so furiously, even if the other noises would have -allowed them. The tempest rose higher and higher, and presently the -sail tore loose from its fastenings and went winging away on the blast. -The boys seized each others' hands and fled, with many tumblings and -bruises, to the shelter of a great oak that stood upon the river-bank. -Now the battle was at its highest. Under the ceaseless conflagration of -lightning that flamed in the skies, everything below stood out in -clean-cut and shadowless distinctness: the bending trees, the billowy -river, white with foam, the driving spray of spume-flakes, the dim -outlines of the high bluffs on the other side, glimpsed through the -drifting cloud-rack and the slanting veil of rain. Every little while -some giant tree yielded the fight and fell crashing through the younger -growth; and the unflagging thunder-peals came now in ear-splitting -explosive bursts, keen and sharp, and unspeakably appalling. The storm -culminated in one matchless effort that seemed likely to tear the island -to pieces, burn it up, drown it to the tree-tops, blow it away, and -deafen every creature in it, all at one and the same moment. It was a -wild night for homeless young heads to be out in. - -But at last the battle was done, and the forces retired with weaker -and weaker threatenings and grumblings, and peace resumed her sway. The -boys went back to camp, a good deal awed; but they found there was -still something to be thankful for, because the great sycamore, the -shelter of their beds, was a ruin, now, blasted by the lightnings, and -they were not under it when the catastrophe happened. - -Everything in camp was drenched, the camp-fire as well; for they were -but heedless lads, like their generation, and had made no provision -against rain. Here was matter for dismay, for they were soaked through -and chilled. They were eloquent in their distress; but they presently -discovered that the fire had eaten so far up under the great log it had -been built against (where it curved upward and separated itself from -the ground), that a handbreadth or so of it had escaped wetting; so -they patiently wrought until, with shreds and bark gathered from the -under sides of sheltered logs, they coaxed the fire to burn again. Then -they piled on great dead boughs till they had a roaring furnace, and -were glad-hearted once more. They dried their boiled ham and had a -feast, and after that they sat by the fire and expanded and glorified -their midnight adventure until morning, for there was not a dry spot to -sleep on, anywhere around. - -As the sun began to steal in upon the boys, drowsiness came over them, -and they went out on the sandbar and lay down to sleep. They got -scorched out by and by, and drearily set about getting breakfast. After -the meal they felt rusty, and stiff-jointed, and a little homesick once -more. Tom saw the signs, and fell to cheering up the pirates as well as -he could. But they cared nothing for marbles, or circus, or swimming, -or anything. He reminded them of the imposing secret, and raised a ray -of cheer. While it lasted, he got them interested in a new device. This -was to knock off being pirates, for a while, and be Indians for a -change. They were attracted by this idea; so it was not long before -they were stripped, and striped from head to heel with black mud, like -so many zebras--all of them chiefs, of course--and then they went -tearing through the woods to attack an English settlement. - -By and by they separated into three hostile tribes, and darted upon -each other from ambush with dreadful war-whoops, and killed and scalped -each other by thousands. It was a gory day. Consequently it was an -extremely satisfactory one. - -They assembled in camp toward supper-time, hungry and happy; but now a -difficulty arose--hostile Indians could not break the bread of -hospitality together without first making peace, and this was a simple -impossibility without smoking a pipe of peace. There was no other -process that ever they had heard of. Two of the savages almost wished -they had remained pirates. However, there was no other way; so with -such show of cheerfulness as they could muster they called for the pipe -and took their whiff as it passed, in due form. - -And behold, they were glad they had gone into savagery, for they had -gained something; they found that they could now smoke a little without -having to go and hunt for a lost knife; they did not get sick enough to -be seriously uncomfortable. They were not likely to fool away this high -promise for lack of effort. No, they practised cautiously, after -supper, with right fair success, and so they spent a jubilant evening. -They were prouder and happier in their new acquirement than they would -have been in the scalping and skinning of the Six Nations. We will -leave them to smoke and chatter and brag, since we have no further use -for them at present. - - - -CHAPTER XVII - -BUT there was no hilarity in the little town that same tranquil -Saturday afternoon. The Harpers, and Aunt Polly's family, were being -put into mourning, with great grief and many tears. An unusual quiet -possessed the village, although it was ordinarily quiet enough, in all -conscience. The villagers conducted their concerns with an absent air, -and talked little; but they sighed often. The Saturday holiday seemed a -burden to the children. They had no heart in their sports, and -gradually gave them up. - -In the afternoon Becky Thatcher found herself moping about the -deserted schoolhouse yard, and feeling very melancholy. But she found -nothing there to comfort her. She soliloquized: - -"Oh, if I only had a brass andiron-knob again! But I haven't got -anything now to remember him by." And she choked back a little sob. - -Presently she stopped, and said to herself: - -"It was right here. Oh, if it was to do over again, I wouldn't say -that--I wouldn't say it for the whole world. But he's gone now; I'll -never, never, never see him any more." - -This thought broke her down, and she wandered away, with tears rolling -down her cheeks. Then quite a group of boys and girls--playmates of -Tom's and Joe's--came by, and stood looking over the paling fence and -talking in reverent tones of how Tom did so-and-so the last time they -saw him, and how Joe said this and that small trifle (pregnant with -awful prophecy, as they could easily see now!)--and each speaker -pointed out the exact spot where the lost lads stood at the time, and -then added something like "and I was a-standing just so--just as I am -now, and as if you was him--I was as close as that--and he smiled, just -this way--and then something seemed to go all over me, like--awful, you -know--and I never thought what it meant, of course, but I can see now!" - -Then there was a dispute about who saw the dead boys last in life, and -many claimed that dismal distinction, and offered evidences, more or -less tampered with by the witness; and when it was ultimately decided -who DID see the departed last, and exchanged the last words with them, -the lucky parties took upon themselves a sort of sacred importance, and -were gaped at and envied by all the rest. One poor chap, who had no -other grandeur to offer, said with tolerably manifest pride in the -remembrance: - -"Well, Tom Sawyer he licked me once." - -But that bid for glory was a failure. Most of the boys could say that, -and so that cheapened the distinction too much. The group loitered -away, still recalling memories of the lost heroes, in awed voices. - -When the Sunday-school hour was finished, the next morning, the bell -began to toll, instead of ringing in the usual way. It was a very still -Sabbath, and the mournful sound seemed in keeping with the musing hush -that lay upon nature. The villagers began to gather, loitering a moment -in the vestibule to converse in whispers about the sad event. But there -was no whispering in the house; only the funereal rustling of dresses -as the women gathered to their seats disturbed the silence there. None -could remember when the little church had been so full before. There -was finally a waiting pause, an expectant dumbness, and then Aunt Polly -entered, followed by Sid and Mary, and they by the Harper family, all -in deep black, and the whole congregation, the old minister as well, -rose reverently and stood until the mourners were seated in the front -pew. There was another communing silence, broken at intervals by -muffled sobs, and then the minister spread his hands abroad and prayed. -A moving hymn was sung, and the text followed: "I am the Resurrection -and the Life." - -As the service proceeded, the clergyman drew such pictures of the -graces, the winning ways, and the rare promise of the lost lads that -every soul there, thinking he recognized these pictures, felt a pang in -remembering that he had persistently blinded himself to them always -before, and had as persistently seen only faults and flaws in the poor -boys. The minister related many a touching incident in the lives of the -departed, too, which illustrated their sweet, generous natures, and the -people could easily see, now, how noble and beautiful those episodes -were, and remembered with grief that at the time they occurred they had -seemed rank rascalities, well deserving of the cowhide. The -congregation became more and more moved, as the pathetic tale went on, -till at last the whole company broke down and joined the weeping -mourners in a chorus of anguished sobs, the preacher himself giving way -to his feelings, and crying in the pulpit. - -There was a rustle in the gallery, which nobody noticed; a moment -later the church door creaked; the minister raised his streaming eyes -above his handkerchief, and stood transfixed! First one and then -another pair of eyes followed the minister's, and then almost with one -impulse the congregation rose and stared while the three dead boys came -marching up the aisle, Tom in the lead, Joe next, and Huck, a ruin of -drooping rags, sneaking sheepishly in the rear! They had been hid in -the unused gallery listening to their own funeral sermon! - -Aunt Polly, Mary, and the Harpers threw themselves upon their restored -ones, smothered them with kisses and poured out thanksgivings, while -poor Huck stood abashed and uncomfortable, not knowing exactly what to -do or where to hide from so many unwelcoming eyes. He wavered, and -started to slink away, but Tom seized him and said: - -"Aunt Polly, it ain't fair. Somebody's got to be glad to see Huck." - -"And so they shall. I'm glad to see him, poor motherless thing!" And -the loving attentions Aunt Polly lavished upon him were the one thing -capable of making him more uncomfortable than he was before. - -Suddenly the minister shouted at the top of his voice: "Praise God -from whom all blessings flow--SING!--and put your hearts in it!" - -And they did. Old Hundred swelled up with a triumphant burst, and -while it shook the rafters Tom Sawyer the Pirate looked around upon the -envying juveniles about him and confessed in his heart that this was -the proudest moment of his life. - -As the "sold" congregation trooped out they said they would almost be -willing to be made ridiculous again to hear Old Hundred sung like that -once more. - -Tom got more cuffs and kisses that day--according to Aunt Polly's -varying moods--than he had earned before in a year; and he hardly knew -which expressed the most gratefulness to God and affection for himself. - - - -CHAPTER XVIII - -THAT was Tom's great secret--the scheme to return home with his -brother pirates and attend their own funerals. They had paddled over to -the Missouri shore on a log, at dusk on Saturday, landing five or six -miles below the village; they had slept in the woods at the edge of the -town till nearly daylight, and had then crept through back lanes and -alleys and finished their sleep in the gallery of the church among a -chaos of invalided benches. - -At breakfast, Monday morning, Aunt Polly and Mary were very loving to -Tom, and very attentive to his wants. There was an unusual amount of -talk. In the course of it Aunt Polly said: - -"Well, I don't say it wasn't a fine joke, Tom, to keep everybody -suffering 'most a week so you boys had a good time, but it is a pity -you could be so hard-hearted as to let me suffer so. If you could come -over on a log to go to your funeral, you could have come over and give -me a hint some way that you warn't dead, but only run off." - -"Yes, you could have done that, Tom," said Mary; "and I believe you -would if you had thought of it." - -"Would you, Tom?" said Aunt Polly, her face lighting wistfully. "Say, -now, would you, if you'd thought of it?" - -"I--well, I don't know. 'Twould 'a' spoiled everything." - -"Tom, I hoped you loved me that much," said Aunt Polly, with a grieved -tone that discomforted the boy. "It would have been something if you'd -cared enough to THINK of it, even if you didn't DO it." - -"Now, auntie, that ain't any harm," pleaded Mary; "it's only Tom's -giddy way--he is always in such a rush that he never thinks of -anything." - -"More's the pity. Sid would have thought. And Sid would have come and -DONE it, too. Tom, you'll look back, some day, when it's too late, and -wish you'd cared a little more for me when it would have cost you so -little." - -"Now, auntie, you know I do care for you," said Tom. - -"I'd know it better if you acted more like it." - -"I wish now I'd thought," said Tom, with a repentant tone; "but I -dreamt about you, anyway. That's something, ain't it?" - -"It ain't much--a cat does that much--but it's better than nothing. -What did you dream?" - -"Why, Wednesday night I dreamt that you was sitting over there by the -bed, and Sid was sitting by the woodbox, and Mary next to him." - -"Well, so we did. So we always do. I'm glad your dreams could take -even that much trouble about us." - -"And I dreamt that Joe Harper's mother was here." - -"Why, she was here! Did you dream any more?" - -"Oh, lots. But it's so dim, now." - -"Well, try to recollect--can't you?" - -"Somehow it seems to me that the wind--the wind blowed the--the--" - -"Try harder, Tom! The wind did blow something. Come!" - -Tom pressed his fingers on his forehead an anxious minute, and then -said: - -"I've got it now! I've got it now! It blowed the candle!" - -"Mercy on us! Go on, Tom--go on!" - -"And it seems to me that you said, 'Why, I believe that that door--'" - -"Go ON, Tom!" - -"Just let me study a moment--just a moment. Oh, yes--you said you -believed the door was open." - -"As I'm sitting here, I did! Didn't I, Mary! Go on!" - -"And then--and then--well I won't be certain, but it seems like as if -you made Sid go and--and--" - -"Well? Well? What did I make him do, Tom? What did I make him do?" - -"You made him--you--Oh, you made him shut it." - -"Well, for the land's sake! I never heard the beat of that in all my -days! Don't tell ME there ain't anything in dreams, any more. Sereny -Harper shall know of this before I'm an hour older. I'd like to see her -get around THIS with her rubbage 'bout superstition. Go on, Tom!" - -"Oh, it's all getting just as bright as day, now. Next you said I -warn't BAD, only mischeevous and harum-scarum, and not any more -responsible than--than--I think it was a colt, or something." - -"And so it was! Well, goodness gracious! Go on, Tom!" - -"And then you began to cry." - -"So I did. So I did. Not the first time, neither. And then--" - -"Then Mrs. Harper she began to cry, and said Joe was just the same, -and she wished she hadn't whipped him for taking cream when she'd -throwed it out her own self--" - -"Tom! The sperrit was upon you! You was a prophesying--that's what you -was doing! Land alive, go on, Tom!" - -"Then Sid he said--he said--" - -"I don't think I said anything," said Sid. - -"Yes you did, Sid," said Mary. - -"Shut your heads and let Tom go on! What did he say, Tom?" - -"He said--I THINK he said he hoped I was better off where I was gone -to, but if I'd been better sometimes--" - -"THERE, d'you hear that! It was his very words!" - -"And you shut him up sharp." - -"I lay I did! There must 'a' been an angel there. There WAS an angel -there, somewheres!" - -"And Mrs. Harper told about Joe scaring her with a firecracker, and -you told about Peter and the Painkiller--" - -"Just as true as I live!" - -"And then there was a whole lot of talk 'bout dragging the river for -us, and 'bout having the funeral Sunday, and then you and old Miss -Harper hugged and cried, and she went." - -"It happened just so! It happened just so, as sure as I'm a-sitting in -these very tracks. Tom, you couldn't told it more like if you'd 'a' -seen it! And then what? Go on, Tom!" - -"Then I thought you prayed for me--and I could see you and hear every -word you said. And you went to bed, and I was so sorry that I took and -wrote on a piece of sycamore bark, 'We ain't dead--we are only off -being pirates,' and put it on the table by the candle; and then you -looked so good, laying there asleep, that I thought I went and leaned -over and kissed you on the lips." - -"Did you, Tom, DID you! I just forgive you everything for that!" And -she seized the boy in a crushing embrace that made him feel like the -guiltiest of villains. - -"It was very kind, even though it was only a--dream," Sid soliloquized -just audibly. - -"Shut up, Sid! A body does just the same in a dream as he'd do if he -was awake. Here's a big Milum apple I've been saving for you, Tom, if -you was ever found again--now go 'long to school. I'm thankful to the -good God and Father of us all I've got you back, that's long-suffering -and merciful to them that believe on Him and keep His word, though -goodness knows I'm unworthy of it, but if only the worthy ones got His -blessings and had His hand to help them over the rough places, there's -few enough would smile here or ever enter into His rest when the long -night comes. Go 'long Sid, Mary, Tom--take yourselves off--you've -hendered me long enough." - -The children left for school, and the old lady to call on Mrs. Harper -and vanquish her realism with Tom's marvellous dream. Sid had better -judgment than to utter the thought that was in his mind as he left the -house. It was this: "Pretty thin--as long a dream as that, without any -mistakes in it!" - -What a hero Tom was become, now! He did not go skipping and prancing, -but moved with a dignified swagger as became a pirate who felt that the -public eye was on him. And indeed it was; he tried not to seem to see -the looks or hear the remarks as he passed along, but they were food -and drink to him. Smaller boys than himself flocked at his heels, as -proud to be seen with him, and tolerated by him, as if he had been the -drummer at the head of a procession or the elephant leading a menagerie -into town. Boys of his own size pretended not to know he had been away -at all; but they were consuming with envy, nevertheless. They would -have given anything to have that swarthy suntanned skin of his, and his -glittering notoriety; and Tom would not have parted with either for a -circus. - -At school the children made so much of him and of Joe, and delivered -such eloquent admiration from their eyes, that the two heroes were not -long in becoming insufferably "stuck-up." They began to tell their -adventures to hungry listeners--but they only began; it was not a thing -likely to have an end, with imaginations like theirs to furnish -material. And finally, when they got out their pipes and went serenely -puffing around, the very summit of glory was reached. - -Tom decided that he could be independent of Becky Thatcher now. Glory -was sufficient. He would live for glory. Now that he was distinguished, -maybe she would be wanting to "make up." Well, let her--she should see -that he could be as indifferent as some other people. Presently she -arrived. Tom pretended not to see her. He moved away and joined a group -of boys and girls and began to talk. Soon he observed that she was -tripping gayly back and forth with flushed face and dancing eyes, -pretending to be busy chasing schoolmates, and screaming with laughter -when she made a capture; but he noticed that she always made her -captures in his vicinity, and that she seemed to cast a conscious eye -in his direction at such times, too. It gratified all the vicious -vanity that was in him; and so, instead of winning him, it only "set -him up" the more and made him the more diligent to avoid betraying that -he knew she was about. Presently she gave over skylarking, and moved -irresolutely about, sighing once or twice and glancing furtively and -wistfully toward Tom. Then she observed that now Tom was talking more -particularly to Amy Lawrence than to any one else. She felt a sharp -pang and grew disturbed and uneasy at once. She tried to go away, but -her feet were treacherous, and carried her to the group instead. She -said to a girl almost at Tom's elbow--with sham vivacity: - -"Why, Mary Austin! you bad girl, why didn't you come to Sunday-school?" - -"I did come--didn't you see me?" - -"Why, no! Did you? Where did you sit?" - -"I was in Miss Peters' class, where I always go. I saw YOU." - -"Did you? Why, it's funny I didn't see you. I wanted to tell you about -the picnic." - -"Oh, that's jolly. Who's going to give it?" - -"My ma's going to let me have one." - -"Oh, goody; I hope she'll let ME come." - -"Well, she will. The picnic's for me. She'll let anybody come that I -want, and I want you." - -"That's ever so nice. When is it going to be?" - -"By and by. Maybe about vacation." - -"Oh, won't it be fun! You going to have all the girls and boys?" - -"Yes, every one that's friends to me--or wants to be"; and she glanced -ever so furtively at Tom, but he talked right along to Amy Lawrence -about the terrible storm on the island, and how the lightning tore the -great sycamore tree "all to flinders" while he was "standing within -three feet of it." - -"Oh, may I come?" said Grace Miller. - -"Yes." - -"And me?" said Sally Rogers. - -"Yes." - -"And me, too?" said Susy Harper. "And Joe?" - -"Yes." - -And so on, with clapping of joyful hands till all the group had begged -for invitations but Tom and Amy. Then Tom turned coolly away, still -talking, and took Amy with him. Becky's lips trembled and the tears -came to her eyes; she hid these signs with a forced gayety and went on -chattering, but the life had gone out of the picnic, now, and out of -everything else; she got away as soon as she could and hid herself and -had what her sex call "a good cry." Then she sat moody, with wounded -pride, till the bell rang. She roused up, now, with a vindictive cast -in her eye, and gave her plaited tails a shake and said she knew what -SHE'D do. - -At recess Tom continued his flirtation with Amy with jubilant -self-satisfaction. And he kept drifting about to find Becky and lacerate -her with the performance. At last he spied her, but there was a sudden -falling of his mercury. She was sitting cosily on a little bench behind -the schoolhouse looking at a picture-book with Alfred Temple--and so -absorbed were they, and their heads so close together over the book, -that they did not seem to be conscious of anything in the world besides. -Jealousy ran red-hot through Tom's veins. He began to hate himself for -throwing away the chance Becky had offered for a reconciliation. He -called himself a fool, and all the hard names he could think of. He -wanted to cry with vexation. Amy chatted happily along, as they walked, -for her heart was singing, but Tom's tongue had lost its function. He -did not hear what Amy was saying, and whenever she paused expectantly he -could only stammer an awkward assent, which was as often misplaced as -otherwise. He kept drifting to the rear of the schoolhouse, again and -again, to sear his eyeballs with the hateful spectacle there. He could -not help it. And it maddened him to see, as he thought he saw, that -Becky Thatcher never once suspected that he was even in the land of the -living. But she did see, nevertheless; and she knew she was winning her -fight, too, and was glad to see him suffer as she had suffered. - -Amy's happy prattle became intolerable. Tom hinted at things he had to -attend to; things that must be done; and time was fleeting. But in -vain--the girl chirped on. Tom thought, "Oh, hang her, ain't I ever -going to get rid of her?" At last he must be attending to those -things--and she said artlessly that she would be "around" when school -let out. And he hastened away, hating her for it. - -"Any other boy!" Tom thought, grating his teeth. "Any boy in the whole -town but that Saint Louis smarty that thinks he dresses so fine and is -aristocracy! Oh, all right, I licked you the first day you ever saw -this town, mister, and I'll lick you again! You just wait till I catch -you out! I'll just take and--" - -And he went through the motions of thrashing an imaginary boy ---pummelling the air, and kicking and gouging. "Oh, you do, do you? You -holler 'nough, do you? Now, then, let that learn you!" And so the -imaginary flogging was finished to his satisfaction. - -Tom fled home at noon. His conscience could not endure any more of -Amy's grateful happiness, and his jealousy could bear no more of the -other distress. Becky resumed her picture inspections with Alfred, but -as the minutes dragged along and no Tom came to suffer, her triumph -began to cloud and she lost interest; gravity and absent-mindedness -followed, and then melancholy; two or three times she pricked up her -ear at a footstep, but it was a false hope; no Tom came. At last she -grew entirely miserable and wished she hadn't carried it so far. When -poor Alfred, seeing that he was losing her, he did not know how, kept -exclaiming: "Oh, here's a jolly one! look at this!" she lost patience -at last, and said, "Oh, don't bother me! I don't care for them!" and -burst into tears, and got up and walked away. - -Alfred dropped alongside and was going to try to comfort her, but she -said: - -"Go away and leave me alone, can't you! I hate you!" - -So the boy halted, wondering what he could have done--for she had said -she would look at pictures all through the nooning--and she walked on, -crying. Then Alfred went musing into the deserted schoolhouse. He was -humiliated and angry. He easily guessed his way to the truth--the girl -had simply made a convenience of him to vent her spite upon Tom Sawyer. -He was far from hating Tom the less when this thought occurred to him. -He wished there was some way to get that boy into trouble without much -risk to himself. Tom's spelling-book fell under his eye. Here was his -opportunity. He gratefully opened to the lesson for the afternoon and -poured ink upon the page. - -Becky, glancing in at a window behind him at the moment, saw the act, -and moved on, without discovering herself. She started homeward, now, -intending to find Tom and tell him; Tom would be thankful and their -troubles would be healed. Before she was half way home, however, she -had changed her mind. The thought of Tom's treatment of her when she -was talking about her picnic came scorching back and filled her with -shame. She resolved to let him get whipped on the damaged -spelling-book's account, and to hate him forever, into the bargain. - - - -CHAPTER XIX - -TOM arrived at home in a dreary mood, and the first thing his aunt -said to him showed him that he had brought his sorrows to an -unpromising market: - -"Tom, I've a notion to skin you alive!" - -"Auntie, what have I done?" - -"Well, you've done enough. Here I go over to Sereny Harper, like an -old softy, expecting I'm going to make her believe all that rubbage -about that dream, when lo and behold you she'd found out from Joe that -you was over here and heard all the talk we had that night. Tom, I -don't know what is to become of a boy that will act like that. It makes -me feel so bad to think you could let me go to Sereny Harper and make -such a fool of myself and never say a word." - -This was a new aspect of the thing. His smartness of the morning had -seemed to Tom a good joke before, and very ingenious. It merely looked -mean and shabby now. He hung his head and could not think of anything -to say for a moment. Then he said: - -"Auntie, I wish I hadn't done it--but I didn't think." - -"Oh, child, you never think. You never think of anything but your own -selfishness. You could think to come all the way over here from -Jackson's Island in the night to laugh at our troubles, and you could -think to fool me with a lie about a dream; but you couldn't ever think -to pity us and save us from sorrow." - -"Auntie, I know now it was mean, but I didn't mean to be mean. I -didn't, honest. And besides, I didn't come over here to laugh at you -that night." - -"What did you come for, then?" - -"It was to tell you not to be uneasy about us, because we hadn't got -drownded." - -"Tom, Tom, I would be the thankfullest soul in this world if I could -believe you ever had as good a thought as that, but you know you never -did--and I know it, Tom." - -"Indeed and 'deed I did, auntie--I wish I may never stir if I didn't." - -"Oh, Tom, don't lie--don't do it. It only makes things a hundred times -worse." - -"It ain't a lie, auntie; it's the truth. I wanted to keep you from -grieving--that was all that made me come." - -"I'd give the whole world to believe that--it would cover up a power -of sins, Tom. I'd 'most be glad you'd run off and acted so bad. But it -ain't reasonable; because, why didn't you tell me, child?" - -"Why, you see, when you got to talking about the funeral, I just got -all full of the idea of our coming and hiding in the church, and I -couldn't somehow bear to spoil it. So I just put the bark back in my -pocket and kept mum." - -"What bark?" - -"The bark I had wrote on to tell you we'd gone pirating. I wish, now, -you'd waked up when I kissed you--I do, honest." - -The hard lines in his aunt's face relaxed and a sudden tenderness -dawned in her eyes. - -"DID you kiss me, Tom?" - -"Why, yes, I did." - -"Are you sure you did, Tom?" - -"Why, yes, I did, auntie--certain sure." - -"What did you kiss me for, Tom?" - -"Because I loved you so, and you laid there moaning and I was so sorry." - -The words sounded like truth. The old lady could not hide a tremor in -her voice when she said: - -"Kiss me again, Tom!--and be off with you to school, now, and don't -bother me any more." - -The moment he was gone, she ran to a closet and got out the ruin of a -jacket which Tom had gone pirating in. Then she stopped, with it in her -hand, and said to herself: - -"No, I don't dare. Poor boy, I reckon he's lied about it--but it's a -blessed, blessed lie, there's such a comfort come from it. I hope the -Lord--I KNOW the Lord will forgive him, because it was such -goodheartedness in him to tell it. But I don't want to find out it's a -lie. I won't look." - -She put the jacket away, and stood by musing a minute. Twice she put -out her hand to take the garment again, and twice she refrained. Once -more she ventured, and this time she fortified herself with the -thought: "It's a good lie--it's a good lie--I won't let it grieve me." -So she sought the jacket pocket. A moment later she was reading Tom's -piece of bark through flowing tears and saying: "I could forgive the -boy, now, if he'd committed a million sins!" - - - -CHAPTER XX - -THERE was something about Aunt Polly's manner, when she kissed Tom, -that swept away his low spirits and made him lighthearted and happy -again. He started to school and had the luck of coming upon Becky -Thatcher at the head of Meadow Lane. His mood always determined his -manner. Without a moment's hesitation he ran to her and said: - -"I acted mighty mean to-day, Becky, and I'm so sorry. I won't ever, -ever do that way again, as long as ever I live--please make up, won't -you?" - -The girl stopped and looked him scornfully in the face: - -"I'll thank you to keep yourself TO yourself, Mr. Thomas Sawyer. I'll -never speak to you again." - -She tossed her head and passed on. Tom was so stunned that he had not -even presence of mind enough to say "Who cares, Miss Smarty?" until the -right time to say it had gone by. So he said nothing. But he was in a -fine rage, nevertheless. He moped into the schoolyard wishing she were -a boy, and imagining how he would trounce her if she were. He presently -encountered her and delivered a stinging remark as he passed. She -hurled one in return, and the angry breach was complete. It seemed to -Becky, in her hot resentment, that she could hardly wait for school to -"take in," she was so impatient to see Tom flogged for the injured -spelling-book. If she had had any lingering notion of exposing Alfred -Temple, Tom's offensive fling had driven it entirely away. - -Poor girl, she did not know how fast she was nearing trouble herself. -The master, Mr. Dobbins, had reached middle age with an unsatisfied -ambition. The darling of his desires was, to be a doctor, but poverty -had decreed that he should be nothing higher than a village -schoolmaster. Every day he took a mysterious book out of his desk and -absorbed himself in it at times when no classes were reciting. He kept -that book under lock and key. There was not an urchin in school but was -perishing to have a glimpse of it, but the chance never came. Every boy -and girl had a theory about the nature of that book; but no two -theories were alike, and there was no way of getting at the facts in -the case. Now, as Becky was passing by the desk, which stood near the -door, she noticed that the key was in the lock! It was a precious -moment. She glanced around; found herself alone, and the next instant -she had the book in her hands. The title-page--Professor Somebody's -ANATOMY--carried no information to her mind; so she began to turn the -leaves. She came at once upon a handsomely engraved and colored -frontispiece--a human figure, stark naked. At that moment a shadow fell -on the page and Tom Sawyer stepped in at the door and caught a glimpse -of the picture. Becky snatched at the book to close it, and had the -hard luck to tear the pictured page half down the middle. She thrust -the volume into the desk, turned the key, and burst out crying with -shame and vexation. - -"Tom Sawyer, you are just as mean as you can be, to sneak up on a -person and look at what they're looking at." - -"How could I know you was looking at anything?" - -"You ought to be ashamed of yourself, Tom Sawyer; you know you're -going to tell on me, and oh, what shall I do, what shall I do! I'll be -whipped, and I never was whipped in school." - -Then she stamped her little foot and said: - -"BE so mean if you want to! I know something that's going to happen. -You just wait and you'll see! Hateful, hateful, hateful!"--and she -flung out of the house with a new explosion of crying. - -Tom stood still, rather flustered by this onslaught. Presently he said -to himself: - -"What a curious kind of a fool a girl is! Never been licked in school! -Shucks! What's a licking! That's just like a girl--they're so -thin-skinned and chicken-hearted. Well, of course I ain't going to tell -old Dobbins on this little fool, because there's other ways of getting -even on her, that ain't so mean; but what of it? Old Dobbins will ask -who it was tore his book. Nobody'll answer. Then he'll do just the way -he always does--ask first one and then t'other, and when he comes to the -right girl he'll know it, without any telling. Girls' faces always tell -on them. They ain't got any backbone. She'll get licked. Well, it's a -kind of a tight place for Becky Thatcher, because there ain't any way -out of it." Tom conned the thing a moment longer, and then added: "All -right, though; she'd like to see me in just such a fix--let her sweat it -out!" - -Tom joined the mob of skylarking scholars outside. In a few moments -the master arrived and school "took in." Tom did not feel a strong -interest in his studies. Every time he stole a glance at the girls' -side of the room Becky's face troubled him. Considering all things, he -did not want to pity her, and yet it was all he could do to help it. He -could get up no exultation that was really worthy the name. Presently -the spelling-book discovery was made, and Tom's mind was entirely full -of his own matters for a while after that. Becky roused up from her -lethargy of distress and showed good interest in the proceedings. She -did not expect that Tom could get out of his trouble by denying that he -spilt the ink on the book himself; and she was right. The denial only -seemed to make the thing worse for Tom. Becky supposed she would be -glad of that, and she tried to believe she was glad of it, but she -found she was not certain. When the worst came to the worst, she had an -impulse to get up and tell on Alfred Temple, but she made an effort and -forced herself to keep still--because, said she to herself, "he'll tell -about me tearing the picture sure. I wouldn't say a word, not to save -his life!" - -Tom took his whipping and went back to his seat not at all -broken-hearted, for he thought it was possible that he had unknowingly -upset the ink on the spelling-book himself, in some skylarking bout--he -had denied it for form's sake and because it was custom, and had stuck -to the denial from principle. - -A whole hour drifted by, the master sat nodding in his throne, the air -was drowsy with the hum of study. By and by, Mr. Dobbins straightened -himself up, yawned, then unlocked his desk, and reached for his book, -but seemed undecided whether to take it out or leave it. Most of the -pupils glanced up languidly, but there were two among them that watched -his movements with intent eyes. Mr. Dobbins fingered his book absently -for a while, then took it out and settled himself in his chair to read! -Tom shot a glance at Becky. He had seen a hunted and helpless rabbit -look as she did, with a gun levelled at its head. Instantly he forgot -his quarrel with her. Quick--something must be done! done in a flash, -too! But the very imminence of the emergency paralyzed his invention. -Good!--he had an inspiration! He would run and snatch the book, spring -through the door and fly. But his resolution shook for one little -instant, and the chance was lost--the master opened the volume. If Tom -only had the wasted opportunity back again! Too late. There was no help -for Becky now, he said. The next moment the master faced the school. -Every eye sank under his gaze. There was that in it which smote even -the innocent with fear. There was silence while one might count ten ---the master was gathering his wrath. Then he spoke: "Who tore this book?" - -There was not a sound. One could have heard a pin drop. The stillness -continued; the master searched face after face for signs of guilt. - -"Benjamin Rogers, did you tear this book?" - -A denial. Another pause. - -"Joseph Harper, did you?" - -Another denial. Tom's uneasiness grew more and more intense under the -slow torture of these proceedings. The master scanned the ranks of -boys--considered a while, then turned to the girls: - -"Amy Lawrence?" - -A shake of the head. - -"Gracie Miller?" - -The same sign. - -"Susan Harper, did you do this?" - -Another negative. The next girl was Becky Thatcher. Tom was trembling -from head to foot with excitement and a sense of the hopelessness of -the situation. - -"Rebecca Thatcher" [Tom glanced at her face--it was white with terror] ---"did you tear--no, look me in the face" [her hands rose in appeal] ---"did you tear this book?" - -A thought shot like lightning through Tom's brain. He sprang to his -feet and shouted--"I done it!" - -The school stared in perplexity at this incredible folly. Tom stood a -moment, to gather his dismembered faculties; and when he stepped -forward to go to his punishment the surprise, the gratitude, the -adoration that shone upon him out of poor Becky's eyes seemed pay -enough for a hundred floggings. Inspired by the splendor of his own -act, he took without an outcry the most merciless flaying that even Mr. -Dobbins had ever administered; and also received with indifference the -added cruelty of a command to remain two hours after school should be -dismissed--for he knew who would wait for him outside till his -captivity was done, and not count the tedious time as loss, either. - -Tom went to bed that night planning vengeance against Alfred Temple; -for with shame and repentance Becky had told him all, not forgetting -her own treachery; but even the longing for vengeance had to give way, -soon, to pleasanter musings, and he fell asleep at last with Becky's -latest words lingering dreamily in his ear-- - -"Tom, how COULD you be so noble!" - - - -CHAPTER XXI - -VACATION was approaching. The schoolmaster, always severe, grew -severer and more exacting than ever, for he wanted the school to make a -good showing on "Examination" day. His rod and his ferule were seldom -idle now--at least among the smaller pupils. Only the biggest boys, and -young ladies of eighteen and twenty, escaped lashing. Mr. Dobbins' -lashings were very vigorous ones, too; for although he carried, under -his wig, a perfectly bald and shiny head, he had only reached middle -age, and there was no sign of feebleness in his muscle. As the great -day approached, all the tyranny that was in him came to the surface; he -seemed to take a vindictive pleasure in punishing the least -shortcomings. The consequence was, that the smaller boys spent their -days in terror and suffering and their nights in plotting revenge. They -threw away no opportunity to do the master a mischief. But he kept -ahead all the time. The retribution that followed every vengeful -success was so sweeping and majestic that the boys always retired from -the field badly worsted. At last they conspired together and hit upon a -plan that promised a dazzling victory. They swore in the sign-painter's -boy, told him the scheme, and asked his help. He had his own reasons -for being delighted, for the master boarded in his father's family and -had given the boy ample cause to hate him. The master's wife would go -on a visit to the country in a few days, and there would be nothing to -interfere with the plan; the master always prepared himself for great -occasions by getting pretty well fuddled, and the sign-painter's boy -said that when the dominie had reached the proper condition on -Examination Evening he would "manage the thing" while he napped in his -chair; then he would have him awakened at the right time and hurried -away to school. - -In the fulness of time the interesting occasion arrived. At eight in -the evening the schoolhouse was brilliantly lighted, and adorned with -wreaths and festoons of foliage and flowers. The master sat throned in -his great chair upon a raised platform, with his blackboard behind him. -He was looking tolerably mellow. Three rows of benches on each side and -six rows in front of him were occupied by the dignitaries of the town -and by the parents of the pupils. To his left, back of the rows of -citizens, was a spacious temporary platform upon which were seated the -scholars who were to take part in the exercises of the evening; rows of -small boys, washed and dressed to an intolerable state of discomfort; -rows of gawky big boys; snowbanks of girls and young ladies clad in -lawn and muslin and conspicuously conscious of their bare arms, their -grandmothers' ancient trinkets, their bits of pink and blue ribbon and -the flowers in their hair. All the rest of the house was filled with -non-participating scholars. - -The exercises began. A very little boy stood up and sheepishly -recited, "You'd scarce expect one of my age to speak in public on the -stage," etc.--accompanying himself with the painfully exact and -spasmodic gestures which a machine might have used--supposing the -machine to be a trifle out of order. But he got through safely, though -cruelly scared, and got a fine round of applause when he made his -manufactured bow and retired. - -A little shamefaced girl lisped, "Mary had a little lamb," etc., -performed a compassion-inspiring curtsy, got her meed of applause, and -sat down flushed and happy. - -Tom Sawyer stepped forward with conceited confidence and soared into -the unquenchable and indestructible "Give me liberty or give me death" -speech, with fine fury and frantic gesticulation, and broke down in the -middle of it. A ghastly stage-fright seized him, his legs quaked under -him and he was like to choke. True, he had the manifest sympathy of the -house but he had the house's silence, too, which was even worse than -its sympathy. The master frowned, and this completed the disaster. Tom -struggled awhile and then retired, utterly defeated. There was a weak -attempt at applause, but it died early. - -"The Boy Stood on the Burning Deck" followed; also "The Assyrian Came -Down," and other declamatory gems. Then there were reading exercises, -and a spelling fight. The meagre Latin class recited with honor. The -prime feature of the evening was in order, now--original "compositions" -by the young ladies. Each in her turn stepped forward to the edge of -the platform, cleared her throat, held up her manuscript (tied with -dainty ribbon), and proceeded to read, with labored attention to -"expression" and punctuation. The themes were the same that had been -illuminated upon similar occasions by their mothers before them, their -grandmothers, and doubtless all their ancestors in the female line -clear back to the Crusades. "Friendship" was one; "Memories of Other -Days"; "Religion in History"; "Dream Land"; "The Advantages of -Culture"; "Forms of Political Government Compared and Contrasted"; -"Melancholy"; "Filial Love"; "Heart Longings," etc., etc. - -A prevalent feature in these compositions was a nursed and petted -melancholy; another was a wasteful and opulent gush of "fine language"; -another was a tendency to lug in by the ears particularly prized words -and phrases until they were worn entirely out; and a peculiarity that -conspicuously marked and marred them was the inveterate and intolerable -sermon that wagged its crippled tail at the end of each and every one -of them. No matter what the subject might be, a brain-racking effort -was made to squirm it into some aspect or other that the moral and -religious mind could contemplate with edification. The glaring -insincerity of these sermons was not sufficient to compass the -banishment of the fashion from the schools, and it is not sufficient -to-day; it never will be sufficient while the world stands, perhaps. -There is no school in all our land where the young ladies do not feel -obliged to close their compositions with a sermon; and you will find -that the sermon of the most frivolous and the least religious girl in -the school is always the longest and the most relentlessly pious. But -enough of this. Homely truth is unpalatable. - -Let us return to the "Examination." The first composition that was -read was one entitled "Is this, then, Life?" Perhaps the reader can -endure an extract from it: - - "In the common walks of life, with what delightful - emotions does the youthful mind look forward to some - anticipated scene of festivity! Imagination is busy - sketching rose-tinted pictures of joy. In fancy, the - voluptuous votary of fashion sees herself amid the - festive throng, 'the observed of all observers.' Her - graceful form, arrayed in snowy robes, is whirling - through the mazes of the joyous dance; her eye is - brightest, her step is lightest in the gay assembly. - - "In such delicious fancies time quickly glides by, - and the welcome hour arrives for her entrance into - the Elysian world, of which she has had such bright - dreams. How fairy-like does everything appear to - her enchanted vision! Each new scene is more charming - than the last. But after a while she finds that - beneath this goodly exterior, all is vanity, the - flattery which once charmed her soul, now grates - harshly upon her ear; the ball-room has lost its - charms; and with wasted health and imbittered heart, - she turns away with the conviction that earthly - pleasures cannot satisfy the longings of the soul!" - -And so forth and so on. There was a buzz of gratification from time to -time during the reading, accompanied by whispered ejaculations of "How -sweet!" "How eloquent!" "So true!" etc., and after the thing had closed -with a peculiarly afflicting sermon the applause was enthusiastic. - -Then arose a slim, melancholy girl, whose face had the "interesting" -paleness that comes of pills and indigestion, and read a "poem." Two -stanzas of it will do: - - "A MISSOURI MAIDEN'S FAREWELL TO ALABAMA - - "Alabama, good-bye! I love thee well! - But yet for a while do I leave thee now! - Sad, yes, sad thoughts of thee my heart doth swell, - And burning recollections throng my brow! - For I have wandered through thy flowery woods; - Have roamed and read near Tallapoosa's stream; - Have listened to Tallassee's warring floods, - And wooed on Coosa's side Aurora's beam. - - "Yet shame I not to bear an o'er-full heart, - Nor blush to turn behind my tearful eyes; - 'Tis from no stranger land I now must part, - 'Tis to no strangers left I yield these sighs. - Welcome and home were mine within this State, - Whose vales I leave--whose spires fade fast from me - And cold must be mine eyes, and heart, and tete, - When, dear Alabama! they turn cold on thee!" - -There were very few there who knew what "tete" meant, but the poem was -very satisfactory, nevertheless. - -Next appeared a dark-complexioned, black-eyed, black-haired young -lady, who paused an impressive moment, assumed a tragic expression, and -began to read in a measured, solemn tone: - - "A VISION - - "Dark and tempestuous was night. Around the - throne on high not a single star quivered; but - the deep intonations of the heavy thunder - constantly vibrated upon the ear; whilst the - terrific lightning revelled in angry mood - through the cloudy chambers of heaven, seeming - to scorn the power exerted over its terror by - the illustrious Franklin! Even the boisterous - winds unanimously came forth from their mystic - homes, and blustered about as if to enhance by - their aid the wildness of the scene. - - "At such a time, so dark, so dreary, for human - sympathy my very spirit sighed; but instead thereof, - - "'My dearest friend, my counsellor, my comforter - and guide--My joy in grief, my second bliss - in joy,' came to my side. She moved like one of - those bright beings pictured in the sunny walks - of fancy's Eden by the romantic and young, a - queen of beauty unadorned save by her own - transcendent loveliness. So soft was her step, it - failed to make even a sound, and but for the - magical thrill imparted by her genial touch, as - other unobtrusive beauties, she would have glided - away un-perceived--unsought. A strange sadness - rested upon her features, like icy tears upon - the robe of December, as she pointed to the - contending elements without, and bade me contemplate - the two beings presented." - -This nightmare occupied some ten pages of manuscript and wound up with -a sermon so destructive of all hope to non-Presbyterians that it took -the first prize. This composition was considered to be the very finest -effort of the evening. The mayor of the village, in delivering the -prize to the author of it, made a warm speech in which he said that it -was by far the most "eloquent" thing he had ever listened to, and that -Daniel Webster himself might well be proud of it. - -It may be remarked, in passing, that the number of compositions in -which the word "beauteous" was over-fondled, and human experience -referred to as "life's page," was up to the usual average. - -Now the master, mellow almost to the verge of geniality, put his chair -aside, turned his back to the audience, and began to draw a map of -America on the blackboard, to exercise the geography class upon. But he -made a sad business of it with his unsteady hand, and a smothered -titter rippled over the house. He knew what the matter was, and set -himself to right it. He sponged out lines and remade them; but he only -distorted them more than ever, and the tittering was more pronounced. -He threw his entire attention upon his work, now, as if determined not -to be put down by the mirth. He felt that all eyes were fastened upon -him; he imagined he was succeeding, and yet the tittering continued; it -even manifestly increased. And well it might. There was a garret above, -pierced with a scuttle over his head; and down through this scuttle -came a cat, suspended around the haunches by a string; she had a rag -tied about her head and jaws to keep her from mewing; as she slowly -descended she curved upward and clawed at the string, she swung -downward and clawed at the intangible air. The tittering rose higher -and higher--the cat was within six inches of the absorbed teacher's -head--down, down, a little lower, and she grabbed his wig with her -desperate claws, clung to it, and was snatched up into the garret in an -instant with her trophy still in her possession! And how the light did -blaze abroad from the master's bald pate--for the sign-painter's boy -had GILDED it! - -That broke up the meeting. The boys were avenged. Vacation had come. - - NOTE:--The pretended "compositions" quoted in - this chapter are taken without alteration from a - volume entitled "Prose and Poetry, by a Western - Lady"--but they are exactly and precisely after - the schoolgirl pattern, and hence are much - happier than any mere imitations could be. - - - -CHAPTER XXII - -TOM joined the new order of Cadets of Temperance, being attracted by -the showy character of their "regalia." He promised to abstain from -smoking, chewing, and profanity as long as he remained a member. Now he -found out a new thing--namely, that to promise not to do a thing is the -surest way in the world to make a body want to go and do that very -thing. Tom soon found himself tormented with a desire to drink and -swear; the desire grew to be so intense that nothing but the hope of a -chance to display himself in his red sash kept him from withdrawing -from the order. Fourth of July was coming; but he soon gave that up ---gave it up before he had worn his shackles over forty-eight hours--and -fixed his hopes upon old Judge Frazer, justice of the peace, who was -apparently on his deathbed and would have a big public funeral, since -he was so high an official. During three days Tom was deeply concerned -about the Judge's condition and hungry for news of it. Sometimes his -hopes ran high--so high that he would venture to get out his regalia -and practise before the looking-glass. But the Judge had a most -discouraging way of fluctuating. At last he was pronounced upon the -mend--and then convalescent. Tom was disgusted; and felt a sense of -injury, too. He handed in his resignation at once--and that night the -Judge suffered a relapse and died. Tom resolved that he would never -trust a man like that again. - -The funeral was a fine thing. The Cadets paraded in a style calculated -to kill the late member with envy. Tom was a free boy again, however ---there was something in that. He could drink and swear, now--but found -to his surprise that he did not want to. The simple fact that he could, -took the desire away, and the charm of it. - -Tom presently wondered to find that his coveted vacation was beginning -to hang a little heavily on his hands. - -He attempted a diary--but nothing happened during three days, and so -he abandoned it. - -The first of all the negro minstrel shows came to town, and made a -sensation. Tom and Joe Harper got up a band of performers and were -happy for two days. - -Even the Glorious Fourth was in some sense a failure, for it rained -hard, there was no procession in consequence, and the greatest man in -the world (as Tom supposed), Mr. Benton, an actual United States -Senator, proved an overwhelming disappointment--for he was not -twenty-five feet high, nor even anywhere in the neighborhood of it. - -A circus came. The boys played circus for three days afterward in -tents made of rag carpeting--admission, three pins for boys, two for -girls--and then circusing was abandoned. - -A phrenologist and a mesmerizer came--and went again and left the -village duller and drearier than ever. - -There were some boys-and-girls' parties, but they were so few and so -delightful that they only made the aching voids between ache the harder. - -Becky Thatcher was gone to her Constantinople home to stay with her -parents during vacation--so there was no bright side to life anywhere. - -The dreadful secret of the murder was a chronic misery. It was a very -cancer for permanency and pain. - -Then came the measles. - -During two long weeks Tom lay a prisoner, dead to the world and its -happenings. He was very ill, he was interested in nothing. When he got -upon his feet at last and moved feebly down-town, a melancholy change -had come over everything and every creature. There had been a -"revival," and everybody had "got religion," not only the adults, but -even the boys and girls. Tom went about, hoping against hope for the -sight of one blessed sinful face, but disappointment crossed him -everywhere. He found Joe Harper studying a Testament, and turned sadly -away from the depressing spectacle. He sought Ben Rogers, and found him -visiting the poor with a basket of tracts. He hunted up Jim Hollis, who -called his attention to the precious blessing of his late measles as a -warning. Every boy he encountered added another ton to his depression; -and when, in desperation, he flew for refuge at last to the bosom of -Huckleberry Finn and was received with a Scriptural quotation, his -heart broke and he crept home and to bed realizing that he alone of all -the town was lost, forever and forever. - -And that night there came on a terrific storm, with driving rain, -awful claps of thunder and blinding sheets of lightning. He covered his -head with the bedclothes and waited in a horror of suspense for his -doom; for he had not the shadow of a doubt that all this hubbub was -about him. He believed he had taxed the forbearance of the powers above -to the extremity of endurance and that this was the result. It might -have seemed to him a waste of pomp and ammunition to kill a bug with a -battery of artillery, but there seemed nothing incongruous about the -getting up such an expensive thunderstorm as this to knock the turf -from under an insect like himself. - -By and by the tempest spent itself and died without accomplishing its -object. The boy's first impulse was to be grateful, and reform. His -second was to wait--for there might not be any more storms. - -The next day the doctors were back; Tom had relapsed. The three weeks -he spent on his back this time seemed an entire age. When he got abroad -at last he was hardly grateful that he had been spared, remembering how -lonely was his estate, how companionless and forlorn he was. He drifted -listlessly down the street and found Jim Hollis acting as judge in a -juvenile court that was trying a cat for murder, in the presence of her -victim, a bird. He found Joe Harper and Huck Finn up an alley eating a -stolen melon. Poor lads! they--like Tom--had suffered a relapse. - - - -CHAPTER XXIII - -AT last the sleepy atmosphere was stirred--and vigorously: the murder -trial came on in the court. It became the absorbing topic of village -talk immediately. Tom could not get away from it. Every reference to -the murder sent a shudder to his heart, for his troubled conscience and -fears almost persuaded him that these remarks were put forth in his -hearing as "feelers"; he did not see how he could be suspected of -knowing anything about the murder, but still he could not be -comfortable in the midst of this gossip. It kept him in a cold shiver -all the time. He took Huck to a lonely place to have a talk with him. -It would be some relief to unseal his tongue for a little while; to -divide his burden of distress with another sufferer. Moreover, he -wanted to assure himself that Huck had remained discreet. - -"Huck, have you ever told anybody about--that?" - -"'Bout what?" - -"You know what." - -"Oh--'course I haven't." - -"Never a word?" - -"Never a solitary word, so help me. What makes you ask?" - -"Well, I was afeard." - -"Why, Tom Sawyer, we wouldn't be alive two days if that got found out. -YOU know that." - -Tom felt more comfortable. After a pause: - -"Huck, they couldn't anybody get you to tell, could they?" - -"Get me to tell? Why, if I wanted that half-breed devil to drownd me -they could get me to tell. They ain't no different way." - -"Well, that's all right, then. I reckon we're safe as long as we keep -mum. But let's swear again, anyway. It's more surer." - -"I'm agreed." - -So they swore again with dread solemnities. - -"What is the talk around, Huck? I've heard a power of it." - -"Talk? Well, it's just Muff Potter, Muff Potter, Muff Potter all the -time. It keeps me in a sweat, constant, so's I want to hide som'ers." - -"That's just the same way they go on round me. I reckon he's a goner. -Don't you feel sorry for him, sometimes?" - -"Most always--most always. He ain't no account; but then he hain't -ever done anything to hurt anybody. Just fishes a little, to get money -to get drunk on--and loafs around considerable; but lord, we all do -that--leastways most of us--preachers and such like. But he's kind of -good--he give me half a fish, once, when there warn't enough for two; -and lots of times he's kind of stood by me when I was out of luck." - -"Well, he's mended kites for me, Huck, and knitted hooks on to my -line. I wish we could get him out of there." - -"My! we couldn't get him out, Tom. And besides, 'twouldn't do any -good; they'd ketch him again." - -"Yes--so they would. But I hate to hear 'em abuse him so like the -dickens when he never done--that." - -"I do too, Tom. Lord, I hear 'em say he's the bloodiest looking -villain in this country, and they wonder he wasn't ever hung before." - -"Yes, they talk like that, all the time. I've heard 'em say that if he -was to get free they'd lynch him." - -"And they'd do it, too." - -The boys had a long talk, but it brought them little comfort. As the -twilight drew on, they found themselves hanging about the neighborhood -of the little isolated jail, perhaps with an undefined hope that -something would happen that might clear away their difficulties. But -nothing happened; there seemed to be no angels or fairies interested in -this luckless captive. - -The boys did as they had often done before--went to the cell grating -and gave Potter some tobacco and matches. He was on the ground floor -and there were no guards. - -His gratitude for their gifts had always smote their consciences -before--it cut deeper than ever, this time. They felt cowardly and -treacherous to the last degree when Potter said: - -"You've been mighty good to me, boys--better'n anybody else in this -town. And I don't forget it, I don't. Often I says to myself, says I, -'I used to mend all the boys' kites and things, and show 'em where the -good fishin' places was, and befriend 'em what I could, and now they've -all forgot old Muff when he's in trouble; but Tom don't, and Huck -don't--THEY don't forget him, says I, 'and I don't forget them.' Well, -boys, I done an awful thing--drunk and crazy at the time--that's the -only way I account for it--and now I got to swing for it, and it's -right. Right, and BEST, too, I reckon--hope so, anyway. Well, we won't -talk about that. I don't want to make YOU feel bad; you've befriended -me. But what I want to say, is, don't YOU ever get drunk--then you won't -ever get here. Stand a litter furder west--so--that's it; it's a prime -comfort to see faces that's friendly when a body's in such a muck of -trouble, and there don't none come here but yourn. Good friendly -faces--good friendly faces. Git up on one another's backs and let me -touch 'em. That's it. Shake hands--yourn'll come through the bars, but -mine's too big. Little hands, and weak--but they've helped Muff Potter -a power, and they'd help him more if they could." - -Tom went home miserable, and his dreams that night were full of -horrors. The next day and the day after, he hung about the court-room, -drawn by an almost irresistible impulse to go in, but forcing himself -to stay out. Huck was having the same experience. They studiously -avoided each other. Each wandered away, from time to time, but the same -dismal fascination always brought them back presently. Tom kept his -ears open when idlers sauntered out of the court-room, but invariably -heard distressing news--the toils were closing more and more -relentlessly around poor Potter. At the end of the second day the -village talk was to the effect that Injun Joe's evidence stood firm and -unshaken, and that there was not the slightest question as to what the -jury's verdict would be. - -Tom was out late, that night, and came to bed through the window. He -was in a tremendous state of excitement. It was hours before he got to -sleep. All the village flocked to the court-house the next morning, for -this was to be the great day. Both sexes were about equally represented -in the packed audience. After a long wait the jury filed in and took -their places; shortly afterward, Potter, pale and haggard, timid and -hopeless, was brought in, with chains upon him, and seated where all -the curious eyes could stare at him; no less conspicuous was Injun Joe, -stolid as ever. There was another pause, and then the judge arrived and -the sheriff proclaimed the opening of the court. The usual whisperings -among the lawyers and gathering together of papers followed. These -details and accompanying delays worked up an atmosphere of preparation -that was as impressive as it was fascinating. - -Now a witness was called who testified that he found Muff Potter -washing in the brook, at an early hour of the morning that the murder -was discovered, and that he immediately sneaked away. After some -further questioning, counsel for the prosecution said: - -"Take the witness." - -The prisoner raised his eyes for a moment, but dropped them again when -his own counsel said: - -"I have no questions to ask him." - -The next witness proved the finding of the knife near the corpse. -Counsel for the prosecution said: - -"Take the witness." - -"I have no questions to ask him," Potter's lawyer replied. - -A third witness swore he had often seen the knife in Potter's -possession. - -"Take the witness." - -Counsel for Potter declined to question him. The faces of the audience -began to betray annoyance. Did this attorney mean to throw away his -client's life without an effort? - -Several witnesses deposed concerning Potter's guilty behavior when -brought to the scene of the murder. They were allowed to leave the -stand without being cross-questioned. - -Every detail of the damaging circumstances that occurred in the -graveyard upon that morning which all present remembered so well was -brought out by credible witnesses, but none of them were cross-examined -by Potter's lawyer. The perplexity and dissatisfaction of the house -expressed itself in murmurs and provoked a reproof from the bench. -Counsel for the prosecution now said: - -"By the oaths of citizens whose simple word is above suspicion, we -have fastened this awful crime, beyond all possibility of question, -upon the unhappy prisoner at the bar. We rest our case here." - -A groan escaped from poor Potter, and he put his face in his hands and -rocked his body softly to and fro, while a painful silence reigned in -the court-room. Many men were moved, and many women's compassion -testified itself in tears. Counsel for the defence rose and said: - -"Your honor, in our remarks at the opening of this trial, we -foreshadowed our purpose to prove that our client did this fearful deed -while under the influence of a blind and irresponsible delirium -produced by drink. We have changed our mind. We shall not offer that -plea." [Then to the clerk:] "Call Thomas Sawyer!" - -A puzzled amazement awoke in every face in the house, not even -excepting Potter's. Every eye fastened itself with wondering interest -upon Tom as he rose and took his place upon the stand. The boy looked -wild enough, for he was badly scared. The oath was administered. - -"Thomas Sawyer, where were you on the seventeenth of June, about the -hour of midnight?" - -Tom glanced at Injun Joe's iron face and his tongue failed him. The -audience listened breathless, but the words refused to come. After a -few moments, however, the boy got a little of his strength back, and -managed to put enough of it into his voice to make part of the house -hear: - -"In the graveyard!" - -"A little bit louder, please. Don't be afraid. You were--" - -"In the graveyard." - -A contemptuous smile flitted across Injun Joe's face. - -"Were you anywhere near Horse Williams' grave?" - -"Yes, sir." - -"Speak up--just a trifle louder. How near were you?" - -"Near as I am to you." - -"Were you hidden, or not?" - -"I was hid." - -"Where?" - -"Behind the elms that's on the edge of the grave." - -Injun Joe gave a barely perceptible start. - -"Any one with you?" - -"Yes, sir. I went there with--" - -"Wait--wait a moment. Never mind mentioning your companion's name. We -will produce him at the proper time. Did you carry anything there with -you." - -Tom hesitated and looked confused. - -"Speak out, my boy--don't be diffident. The truth is always -respectable. What did you take there?" - -"Only a--a--dead cat." - -There was a ripple of mirth, which the court checked. - -"We will produce the skeleton of that cat. Now, my boy, tell us -everything that occurred--tell it in your own way--don't skip anything, -and don't be afraid." - -Tom began--hesitatingly at first, but as he warmed to his subject his -words flowed more and more easily; in a little while every sound ceased -but his own voice; every eye fixed itself upon him; with parted lips -and bated breath the audience hung upon his words, taking no note of -time, rapt in the ghastly fascinations of the tale. The strain upon -pent emotion reached its climax when the boy said: - -"--and as the doctor fetched the board around and Muff Potter fell, -Injun Joe jumped with the knife and--" - -Crash! Quick as lightning the half-breed sprang for a window, tore his -way through all opposers, and was gone! - - - -CHAPTER XXIV - -TOM was a glittering hero once more--the pet of the old, the envy of -the young. His name even went into immortal print, for the village -paper magnified him. There were some that believed he would be -President, yet, if he escaped hanging. - -As usual, the fickle, unreasoning world took Muff Potter to its bosom -and fondled him as lavishly as it had abused him before. But that sort -of conduct is to the world's credit; therefore it is not well to find -fault with it. - -Tom's days were days of splendor and exultation to him, but his nights -were seasons of horror. Injun Joe infested all his dreams, and always -with doom in his eye. Hardly any temptation could persuade the boy to -stir abroad after nightfall. Poor Huck was in the same state of -wretchedness and terror, for Tom had told the whole story to the lawyer -the night before the great day of the trial, and Huck was sore afraid -that his share in the business might leak out, yet, notwithstanding -Injun Joe's flight had saved him the suffering of testifying in court. -The poor fellow had got the attorney to promise secrecy, but what of -that? Since Tom's harassed conscience had managed to drive him to the -lawyer's house by night and wring a dread tale from lips that had been -sealed with the dismalest and most formidable of oaths, Huck's -confidence in the human race was well-nigh obliterated. - -Daily Muff Potter's gratitude made Tom glad he had spoken; but nightly -he wished he had sealed up his tongue. - -Half the time Tom was afraid Injun Joe would never be captured; the -other half he was afraid he would be. He felt sure he never could draw -a safe breath again until that man was dead and he had seen the corpse. - -Rewards had been offered, the country had been scoured, but no Injun -Joe was found. One of those omniscient and awe-inspiring marvels, a -detective, came up from St. Louis, moused around, shook his head, -looked wise, and made that sort of astounding success which members of -that craft usually achieve. That is to say, he "found a clew." But you -can't hang a "clew" for murder, and so after that detective had got -through and gone home, Tom felt just as insecure as he was before. - -The slow days drifted on, and each left behind it a slightly lightened -weight of apprehension. - - - -CHAPTER XXV - -THERE comes a time in every rightly-constructed boy's life when he has -a raging desire to go somewhere and dig for hidden treasure. This -desire suddenly came upon Tom one day. He sallied out to find Joe -Harper, but failed of success. Next he sought Ben Rogers; he had gone -fishing. Presently he stumbled upon Huck Finn the Red-Handed. Huck -would answer. Tom took him to a private place and opened the matter to -him confidentially. Huck was willing. Huck was always willing to take a -hand in any enterprise that offered entertainment and required no -capital, for he had a troublesome superabundance of that sort of time -which is not money. "Where'll we dig?" said Huck. - -"Oh, most anywhere." - -"Why, is it hid all around?" - -"No, indeed it ain't. It's hid in mighty particular places, Huck ---sometimes on islands, sometimes in rotten chests under the end of a -limb of an old dead tree, just where the shadow falls at midnight; but -mostly under the floor in ha'nted houses." - -"Who hides it?" - -"Why, robbers, of course--who'd you reckon? Sunday-school -sup'rintendents?" - -"I don't know. If 'twas mine I wouldn't hide it; I'd spend it and have -a good time." - -"So would I. But robbers don't do that way. They always hide it and -leave it there." - -"Don't they come after it any more?" - -"No, they think they will, but they generally forget the marks, or -else they die. Anyway, it lays there a long time and gets rusty; and by -and by somebody finds an old yellow paper that tells how to find the -marks--a paper that's got to be ciphered over about a week because it's -mostly signs and hy'roglyphics." - -"Hyro--which?" - -"Hy'roglyphics--pictures and things, you know, that don't seem to mean -anything." - -"Have you got one of them papers, Tom?" - -"No." - -"Well then, how you going to find the marks?" - -"I don't want any marks. They always bury it under a ha'nted house or -on an island, or under a dead tree that's got one limb sticking out. -Well, we've tried Jackson's Island a little, and we can try it again -some time; and there's the old ha'nted house up the Still-House branch, -and there's lots of dead-limb trees--dead loads of 'em." - -"Is it under all of them?" - -"How you talk! No!" - -"Then how you going to know which one to go for?" - -"Go for all of 'em!" - -"Why, Tom, it'll take all summer." - -"Well, what of that? Suppose you find a brass pot with a hundred -dollars in it, all rusty and gray, or rotten chest full of di'monds. -How's that?" - -Huck's eyes glowed. - -"That's bully. Plenty bully enough for me. Just you gimme the hundred -dollars and I don't want no di'monds." - -"All right. But I bet you I ain't going to throw off on di'monds. Some -of 'em's worth twenty dollars apiece--there ain't any, hardly, but's -worth six bits or a dollar." - -"No! Is that so?" - -"Cert'nly--anybody'll tell you so. Hain't you ever seen one, Huck?" - -"Not as I remember." - -"Oh, kings have slathers of them." - -"Well, I don' know no kings, Tom." - -"I reckon you don't. But if you was to go to Europe you'd see a raft -of 'em hopping around." - -"Do they hop?" - -"Hop?--your granny! No!" - -"Well, what did you say they did, for?" - -"Shucks, I only meant you'd SEE 'em--not hopping, of course--what do -they want to hop for?--but I mean you'd just see 'em--scattered around, -you know, in a kind of a general way. Like that old humpbacked Richard." - -"Richard? What's his other name?" - -"He didn't have any other name. Kings don't have any but a given name." - -"No?" - -"But they don't." - -"Well, if they like it, Tom, all right; but I don't want to be a king -and have only just a given name, like a nigger. But say--where you -going to dig first?" - -"Well, I don't know. S'pose we tackle that old dead-limb tree on the -hill t'other side of Still-House branch?" - -"I'm agreed." - -So they got a crippled pick and a shovel, and set out on their -three-mile tramp. They arrived hot and panting, and threw themselves -down in the shade of a neighboring elm to rest and have a smoke. - -"I like this," said Tom. - -"So do I." - -"Say, Huck, if we find a treasure here, what you going to do with your -share?" - -"Well, I'll have pie and a glass of soda every day, and I'll go to -every circus that comes along. I bet I'll have a gay time." - -"Well, ain't you going to save any of it?" - -"Save it? What for?" - -"Why, so as to have something to live on, by and by." - -"Oh, that ain't any use. Pap would come back to thish-yer town some -day and get his claws on it if I didn't hurry up, and I tell you he'd -clean it out pretty quick. What you going to do with yourn, Tom?" - -"I'm going to buy a new drum, and a sure-'nough sword, and a red -necktie and a bull pup, and get married." - -"Married!" - -"That's it." - -"Tom, you--why, you ain't in your right mind." - -"Wait--you'll see." - -"Well, that's the foolishest thing you could do. Look at pap and my -mother. Fight! Why, they used to fight all the time. I remember, mighty -well." - -"That ain't anything. The girl I'm going to marry won't fight." - -"Tom, I reckon they're all alike. They'll all comb a body. Now you -better think 'bout this awhile. I tell you you better. What's the name -of the gal?" - -"It ain't a gal at all--it's a girl." - -"It's all the same, I reckon; some says gal, some says girl--both's -right, like enough. Anyway, what's her name, Tom?" - -"I'll tell you some time--not now." - -"All right--that'll do. Only if you get married I'll be more lonesomer -than ever." - -"No you won't. You'll come and live with me. Now stir out of this and -we'll go to digging." - -They worked and sweated for half an hour. No result. They toiled -another half-hour. Still no result. Huck said: - -"Do they always bury it as deep as this?" - -"Sometimes--not always. Not generally. I reckon we haven't got the -right place." - -So they chose a new spot and began again. The labor dragged a little, -but still they made progress. They pegged away in silence for some -time. Finally Huck leaned on his shovel, swabbed the beaded drops from -his brow with his sleeve, and said: - -"Where you going to dig next, after we get this one?" - -"I reckon maybe we'll tackle the old tree that's over yonder on -Cardiff Hill back of the widow's." - -"I reckon that'll be a good one. But won't the widow take it away from -us, Tom? It's on her land." - -"SHE take it away! Maybe she'd like to try it once. Whoever finds one -of these hid treasures, it belongs to him. It don't make any difference -whose land it's on." - -That was satisfactory. The work went on. By and by Huck said: - -"Blame it, we must be in the wrong place again. What do you think?" - -"It is mighty curious, Huck. I don't understand it. Sometimes witches -interfere. I reckon maybe that's what's the trouble now." - -"Shucks! Witches ain't got no power in the daytime." - -"Well, that's so. I didn't think of that. Oh, I know what the matter -is! What a blamed lot of fools we are! You got to find out where the -shadow of the limb falls at midnight, and that's where you dig!" - -"Then consound it, we've fooled away all this work for nothing. Now -hang it all, we got to come back in the night. It's an awful long way. -Can you get out?" - -"I bet I will. We've got to do it to-night, too, because if somebody -sees these holes they'll know in a minute what's here and they'll go -for it." - -"Well, I'll come around and maow to-night." - -"All right. Let's hide the tools in the bushes." - -The boys were there that night, about the appointed time. They sat in -the shadow waiting. It was a lonely place, and an hour made solemn by -old traditions. Spirits whispered in the rustling leaves, ghosts lurked -in the murky nooks, the deep baying of a hound floated up out of the -distance, an owl answered with his sepulchral note. The boys were -subdued by these solemnities, and talked little. By and by they judged -that twelve had come; they marked where the shadow fell, and began to -dig. Their hopes commenced to rise. Their interest grew stronger, and -their industry kept pace with it. The hole deepened and still deepened, -but every time their hearts jumped to hear the pick strike upon -something, they only suffered a new disappointment. It was only a stone -or a chunk. At last Tom said: - -"It ain't any use, Huck, we're wrong again." - -"Well, but we CAN'T be wrong. We spotted the shadder to a dot." - -"I know it, but then there's another thing." - -"What's that?". - -"Why, we only guessed at the time. Like enough it was too late or too -early." - -Huck dropped his shovel. - -"That's it," said he. "That's the very trouble. We got to give this -one up. We can't ever tell the right time, and besides this kind of -thing's too awful, here this time of night with witches and ghosts -a-fluttering around so. I feel as if something's behind me all the time; -and I'm afeard to turn around, becuz maybe there's others in front -a-waiting for a chance. I been creeping all over, ever since I got here." - -"Well, I've been pretty much so, too, Huck. They most always put in a -dead man when they bury a treasure under a tree, to look out for it." - -"Lordy!" - -"Yes, they do. I've always heard that." - -"Tom, I don't like to fool around much where there's dead people. A -body's bound to get into trouble with 'em, sure." - -"I don't like to stir 'em up, either. S'pose this one here was to -stick his skull out and say something!" - -"Don't Tom! It's awful." - -"Well, it just is. Huck, I don't feel comfortable a bit." - -"Say, Tom, let's give this place up, and try somewheres else." - -"All right, I reckon we better." - -"What'll it be?" - -Tom considered awhile; and then said: - -"The ha'nted house. That's it!" - -"Blame it, I don't like ha'nted houses, Tom. Why, they're a dern sight -worse'n dead people. Dead people might talk, maybe, but they don't come -sliding around in a shroud, when you ain't noticing, and peep over your -shoulder all of a sudden and grit their teeth, the way a ghost does. I -couldn't stand such a thing as that, Tom--nobody could." - -"Yes, but, Huck, ghosts don't travel around only at night. They won't -hender us from digging there in the daytime." - -"Well, that's so. But you know mighty well people don't go about that -ha'nted house in the day nor the night." - -"Well, that's mostly because they don't like to go where a man's been -murdered, anyway--but nothing's ever been seen around that house except -in the night--just some blue lights slipping by the windows--no regular -ghosts." - -"Well, where you see one of them blue lights flickering around, Tom, -you can bet there's a ghost mighty close behind it. It stands to -reason. Becuz you know that they don't anybody but ghosts use 'em." - -"Yes, that's so. But anyway they don't come around in the daytime, so -what's the use of our being afeard?" - -"Well, all right. We'll tackle the ha'nted house if you say so--but I -reckon it's taking chances." - -They had started down the hill by this time. There in the middle of -the moonlit valley below them stood the "ha'nted" house, utterly -isolated, its fences gone long ago, rank weeds smothering the very -doorsteps, the chimney crumbled to ruin, the window-sashes vacant, a -corner of the roof caved in. The boys gazed awhile, half expecting to -see a blue light flit past a window; then talking in a low tone, as -befitted the time and the circumstances, they struck far off to the -right, to give the haunted house a wide berth, and took their way -homeward through the woods that adorned the rearward side of Cardiff -Hill. - - - -CHAPTER XXVI - -ABOUT noon the next day the boys arrived at the dead tree; they had -come for their tools. Tom was impatient to go to the haunted house; -Huck was measurably so, also--but suddenly said: - -"Lookyhere, Tom, do you know what day it is?" - -Tom mentally ran over the days of the week, and then quickly lifted -his eyes with a startled look in them-- - -"My! I never once thought of it, Huck!" - -"Well, I didn't neither, but all at once it popped onto me that it was -Friday." - -"Blame it, a body can't be too careful, Huck. We might 'a' got into an -awful scrape, tackling such a thing on a Friday." - -"MIGHT! Better say we WOULD! There's some lucky days, maybe, but -Friday ain't." - -"Any fool knows that. I don't reckon YOU was the first that found it -out, Huck." - -"Well, I never said I was, did I? And Friday ain't all, neither. I had -a rotten bad dream last night--dreampt about rats." - -"No! Sure sign of trouble. Did they fight?" - -"No." - -"Well, that's good, Huck. When they don't fight it's only a sign that -there's trouble around, you know. All we got to do is to look mighty -sharp and keep out of it. We'll drop this thing for to-day, and play. -Do you know Robin Hood, Huck?" - -"No. Who's Robin Hood?" - -"Why, he was one of the greatest men that was ever in England--and the -best. He was a robber." - -"Cracky, I wisht I was. Who did he rob?" - -"Only sheriffs and bishops and rich people and kings, and such like. -But he never bothered the poor. He loved 'em. He always divided up with -'em perfectly square." - -"Well, he must 'a' been a brick." - -"I bet you he was, Huck. Oh, he was the noblest man that ever was. -They ain't any such men now, I can tell you. He could lick any man in -England, with one hand tied behind him; and he could take his yew bow -and plug a ten-cent piece every time, a mile and a half." - -"What's a YEW bow?" - -"I don't know. It's some kind of a bow, of course. And if he hit that -dime only on the edge he would set down and cry--and curse. But we'll -play Robin Hood--it's nobby fun. I'll learn you." - -"I'm agreed." - -So they played Robin Hood all the afternoon, now and then casting a -yearning eye down upon the haunted house and passing a remark about the -morrow's prospects and possibilities there. As the sun began to sink -into the west they took their way homeward athwart the long shadows of -the trees and soon were buried from sight in the forests of Cardiff -Hill. - -On Saturday, shortly after noon, the boys were at the dead tree again. -They had a smoke and a chat in the shade, and then dug a little in -their last hole, not with great hope, but merely because Tom said there -were so many cases where people had given up a treasure after getting -down within six inches of it, and then somebody else had come along and -turned it up with a single thrust of a shovel. The thing failed this -time, however, so the boys shouldered their tools and went away feeling -that they had not trifled with fortune, but had fulfilled all the -requirements that belong to the business of treasure-hunting. - -When they reached the haunted house there was something so weird and -grisly about the dead silence that reigned there under the baking sun, -and something so depressing about the loneliness and desolation of the -place, that they were afraid, for a moment, to venture in. Then they -crept to the door and took a trembling peep. They saw a weed-grown, -floorless room, unplastered, an ancient fireplace, vacant windows, a -ruinous staircase; and here, there, and everywhere hung ragged and -abandoned cobwebs. They presently entered, softly, with quickened -pulses, talking in whispers, ears alert to catch the slightest sound, -and muscles tense and ready for instant retreat. - -In a little while familiarity modified their fears and they gave the -place a critical and interested examination, rather admiring their own -boldness, and wondering at it, too. Next they wanted to look up-stairs. -This was something like cutting off retreat, but they got to daring -each other, and of course there could be but one result--they threw -their tools into a corner and made the ascent. Up there were the same -signs of decay. In one corner they found a closet that promised -mystery, but the promise was a fraud--there was nothing in it. Their -courage was up now and well in hand. They were about to go down and -begin work when-- - -"Sh!" said Tom. - -"What is it?" whispered Huck, blanching with fright. - -"Sh!... There!... Hear it?" - -"Yes!... Oh, my! Let's run!" - -"Keep still! Don't you budge! They're coming right toward the door." - -The boys stretched themselves upon the floor with their eyes to -knot-holes in the planking, and lay waiting, in a misery of fear. - -"They've stopped.... No--coming.... Here they are. Don't whisper -another word, Huck. My goodness, I wish I was out of this!" - -Two men entered. Each boy said to himself: "There's the old deaf and -dumb Spaniard that's been about town once or twice lately--never saw -t'other man before." - -"T'other" was a ragged, unkempt creature, with nothing very pleasant -in his face. The Spaniard was wrapped in a serape; he had bushy white -whiskers; long white hair flowed from under his sombrero, and he wore -green goggles. When they came in, "t'other" was talking in a low voice; -they sat down on the ground, facing the door, with their backs to the -wall, and the speaker continued his remarks. His manner became less -guarded and his words more distinct as he proceeded: - -"No," said he, "I've thought it all over, and I don't like it. It's -dangerous." - -"Dangerous!" grunted the "deaf and dumb" Spaniard--to the vast -surprise of the boys. "Milksop!" - -This voice made the boys gasp and quake. It was Injun Joe's! There was -silence for some time. Then Joe said: - -"What's any more dangerous than that job up yonder--but nothing's come -of it." - -"That's different. Away up the river so, and not another house about. -'Twon't ever be known that we tried, anyway, long as we didn't succeed." - -"Well, what's more dangerous than coming here in the daytime!--anybody -would suspicion us that saw us." - -"I know that. But there warn't any other place as handy after that -fool of a job. I want to quit this shanty. I wanted to yesterday, only -it warn't any use trying to stir out of here, with those infernal boys -playing over there on the hill right in full view." - -"Those infernal boys" quaked again under the inspiration of this -remark, and thought how lucky it was that they had remembered it was -Friday and concluded to wait a day. They wished in their hearts they -had waited a year. - -The two men got out some food and made a luncheon. After a long and -thoughtful silence, Injun Joe said: - -"Look here, lad--you go back up the river where you belong. Wait there -till you hear from me. I'll take the chances on dropping into this town -just once more, for a look. We'll do that 'dangerous' job after I've -spied around a little and think things look well for it. Then for -Texas! We'll leg it together!" - -This was satisfactory. Both men presently fell to yawning, and Injun -Joe said: - -"I'm dead for sleep! It's your turn to watch." - -He curled down in the weeds and soon began to snore. His comrade -stirred him once or twice and he became quiet. Presently the watcher -began to nod; his head drooped lower and lower, both men began to snore -now. - -The boys drew a long, grateful breath. Tom whispered: - -"Now's our chance--come!" - -Huck said: - -"I can't--I'd die if they was to wake." - -Tom urged--Huck held back. At last Tom rose slowly and softly, and -started alone. But the first step he made wrung such a hideous creak -from the crazy floor that he sank down almost dead with fright. He -never made a second attempt. The boys lay there counting the dragging -moments till it seemed to them that time must be done and eternity -growing gray; and then they were grateful to note that at last the sun -was setting. - -Now one snore ceased. Injun Joe sat up, stared around--smiled grimly -upon his comrade, whose head was drooping upon his knees--stirred him -up with his foot and said: - -"Here! YOU'RE a watchman, ain't you! All right, though--nothing's -happened." - -"My! have I been asleep?" - -"Oh, partly, partly. Nearly time for us to be moving, pard. What'll we -do with what little swag we've got left?" - -"I don't know--leave it here as we've always done, I reckon. No use to -take it away till we start south. Six hundred and fifty in silver's -something to carry." - -"Well--all right--it won't matter to come here once more." - -"No--but I'd say come in the night as we used to do--it's better." - -"Yes: but look here; it may be a good while before I get the right -chance at that job; accidents might happen; 'tain't in such a very good -place; we'll just regularly bury it--and bury it deep." - -"Good idea," said the comrade, who walked across the room, knelt down, -raised one of the rearward hearth-stones and took out a bag that -jingled pleasantly. He subtracted from it twenty or thirty dollars for -himself and as much for Injun Joe, and passed the bag to the latter, -who was on his knees in the corner, now, digging with his bowie-knife. - -The boys forgot all their fears, all their miseries in an instant. -With gloating eyes they watched every movement. Luck!--the splendor of -it was beyond all imagination! Six hundred dollars was money enough to -make half a dozen boys rich! Here was treasure-hunting under the -happiest auspices--there would not be any bothersome uncertainty as to -where to dig. They nudged each other every moment--eloquent nudges and -easily understood, for they simply meant--"Oh, but ain't you glad NOW -we're here!" - -Joe's knife struck upon something. - -"Hello!" said he. - -"What is it?" said his comrade. - -"Half-rotten plank--no, it's a box, I believe. Here--bear a hand and -we'll see what it's here for. Never mind, I've broke a hole." - -He reached his hand in and drew it out-- - -"Man, it's money!" - -The two men examined the handful of coins. They were gold. The boys -above were as excited as themselves, and as delighted. - -Joe's comrade said: - -"We'll make quick work of this. There's an old rusty pick over amongst -the weeds in the corner the other side of the fireplace--I saw it a -minute ago." - -He ran and brought the boys' pick and shovel. Injun Joe took the pick, -looked it over critically, shook his head, muttered something to -himself, and then began to use it. The box was soon unearthed. It was -not very large; it was iron bound and had been very strong before the -slow years had injured it. The men contemplated the treasure awhile in -blissful silence. - -"Pard, there's thousands of dollars here," said Injun Joe. - -"'Twas always said that Murrel's gang used to be around here one -summer," the stranger observed. - -"I know it," said Injun Joe; "and this looks like it, I should say." - -"Now you won't need to do that job." - -The half-breed frowned. Said he: - -"You don't know me. Least you don't know all about that thing. 'Tain't -robbery altogether--it's REVENGE!" and a wicked light flamed in his -eyes. "I'll need your help in it. When it's finished--then Texas. Go -home to your Nance and your kids, and stand by till you hear from me." - -"Well--if you say so; what'll we do with this--bury it again?" - -"Yes. [Ravishing delight overhead.] NO! by the great Sachem, no! -[Profound distress overhead.] I'd nearly forgot. That pick had fresh -earth on it! [The boys were sick with terror in a moment.] What -business has a pick and a shovel here? What business with fresh earth -on them? Who brought them here--and where are they gone? Have you heard -anybody?--seen anybody? What! bury it again and leave them to come and -see the ground disturbed? Not exactly--not exactly. We'll take it to my -den." - -"Why, of course! Might have thought of that before. You mean Number -One?" - -"No--Number Two--under the cross. The other place is bad--too common." - -"All right. It's nearly dark enough to start." - -Injun Joe got up and went about from window to window cautiously -peeping out. Presently he said: - -"Who could have brought those tools here? Do you reckon they can be -up-stairs?" - -The boys' breath forsook them. Injun Joe put his hand on his knife, -halted a moment, undecided, and then turned toward the stairway. The -boys thought of the closet, but their strength was gone. The steps came -creaking up the stairs--the intolerable distress of the situation woke -the stricken resolution of the lads--they were about to spring for the -closet, when there was a crash of rotten timbers and Injun Joe landed -on the ground amid the debris of the ruined stairway. He gathered -himself up cursing, and his comrade said: - -"Now what's the use of all that? If it's anybody, and they're up -there, let them STAY there--who cares? If they want to jump down, now, -and get into trouble, who objects? It will be dark in fifteen minutes ---and then let them follow us if they want to. I'm willing. In my -opinion, whoever hove those things in here caught a sight of us and -took us for ghosts or devils or something. I'll bet they're running -yet." - -Joe grumbled awhile; then he agreed with his friend that what daylight -was left ought to be economized in getting things ready for leaving. -Shortly afterward they slipped out of the house in the deepening -twilight, and moved toward the river with their precious box. - -Tom and Huck rose up, weak but vastly relieved, and stared after them -through the chinks between the logs of the house. Follow? Not they. -They were content to reach ground again without broken necks, and take -the townward track over the hill. They did not talk much. They were too -much absorbed in hating themselves--hating the ill luck that made them -take the spade and the pick there. But for that, Injun Joe never would -have suspected. He would have hidden the silver with the gold to wait -there till his "revenge" was satisfied, and then he would have had the -misfortune to find that money turn up missing. Bitter, bitter luck that -the tools were ever brought there! - -They resolved to keep a lookout for that Spaniard when he should come -to town spying out for chances to do his revengeful job, and follow him -to "Number Two," wherever that might be. Then a ghastly thought -occurred to Tom. - -"Revenge? What if he means US, Huck!" - -"Oh, don't!" said Huck, nearly fainting. - -They talked it all over, and as they entered town they agreed to -believe that he might possibly mean somebody else--at least that he -might at least mean nobody but Tom, since only Tom had testified. - -Very, very small comfort it was to Tom to be alone in danger! Company -would be a palpable improvement, he thought. - - - -CHAPTER XXVII - -THE adventure of the day mightily tormented Tom's dreams that night. -Four times he had his hands on that rich treasure and four times it -wasted to nothingness in his fingers as sleep forsook him and -wakefulness brought back the hard reality of his misfortune. As he lay -in the early morning recalling the incidents of his great adventure, he -noticed that they seemed curiously subdued and far away--somewhat as if -they had happened in another world, or in a time long gone by. Then it -occurred to him that the great adventure itself must be a dream! There -was one very strong argument in favor of this idea--namely, that the -quantity of coin he had seen was too vast to be real. He had never seen -as much as fifty dollars in one mass before, and he was like all boys -of his age and station in life, in that he imagined that all references -to "hundreds" and "thousands" were mere fanciful forms of speech, and -that no such sums really existed in the world. He never had supposed -for a moment that so large a sum as a hundred dollars was to be found -in actual money in any one's possession. If his notions of hidden -treasure had been analyzed, they would have been found to consist of a -handful of real dimes and a bushel of vague, splendid, ungraspable -dollars. - -But the incidents of his adventure grew sensibly sharper and clearer -under the attrition of thinking them over, and so he presently found -himself leaning to the impression that the thing might not have been a -dream, after all. This uncertainty must be swept away. He would snatch -a hurried breakfast and go and find Huck. Huck was sitting on the -gunwale of a flatboat, listlessly dangling his feet in the water and -looking very melancholy. Tom concluded to let Huck lead up to the -subject. If he did not do it, then the adventure would be proved to -have been only a dream. - -"Hello, Huck!" - -"Hello, yourself." - -Silence, for a minute. - -"Tom, if we'd 'a' left the blame tools at the dead tree, we'd 'a' got -the money. Oh, ain't it awful!" - -"'Tain't a dream, then, 'tain't a dream! Somehow I most wish it was. -Dog'd if I don't, Huck." - -"What ain't a dream?" - -"Oh, that thing yesterday. I been half thinking it was." - -"Dream! If them stairs hadn't broke down you'd 'a' seen how much dream -it was! I've had dreams enough all night--with that patch-eyed Spanish -devil going for me all through 'em--rot him!" - -"No, not rot him. FIND him! Track the money!" - -"Tom, we'll never find him. A feller don't have only one chance for -such a pile--and that one's lost. I'd feel mighty shaky if I was to see -him, anyway." - -"Well, so'd I; but I'd like to see him, anyway--and track him out--to -his Number Two." - -"Number Two--yes, that's it. I been thinking 'bout that. But I can't -make nothing out of it. What do you reckon it is?" - -"I dono. It's too deep. Say, Huck--maybe it's the number of a house!" - -"Goody!... No, Tom, that ain't it. If it is, it ain't in this -one-horse town. They ain't no numbers here." - -"Well, that's so. Lemme think a minute. Here--it's the number of a -room--in a tavern, you know!" - -"Oh, that's the trick! They ain't only two taverns. We can find out -quick." - -"You stay here, Huck, till I come." - -Tom was off at once. He did not care to have Huck's company in public -places. He was gone half an hour. He found that in the best tavern, No. -2 had long been occupied by a young lawyer, and was still so occupied. -In the less ostentatious house, No. 2 was a mystery. The -tavern-keeper's young son said it was kept locked all the time, and he -never saw anybody go into it or come out of it except at night; he did -not know any particular reason for this state of things; had had some -little curiosity, but it was rather feeble; had made the most of the -mystery by entertaining himself with the idea that that room was -"ha'nted"; had noticed that there was a light in there the night before. - -"That's what I've found out, Huck. I reckon that's the very No. 2 -we're after." - -"I reckon it is, Tom. Now what you going to do?" - -"Lemme think." - -Tom thought a long time. Then he said: - -"I'll tell you. The back door of that No. 2 is the door that comes out -into that little close alley between the tavern and the old rattle trap -of a brick store. Now you get hold of all the door-keys you can find, -and I'll nip all of auntie's, and the first dark night we'll go there -and try 'em. And mind you, keep a lookout for Injun Joe, because he -said he was going to drop into town and spy around once more for a -chance to get his revenge. If you see him, you just follow him; and if -he don't go to that No. 2, that ain't the place." - -"Lordy, I don't want to foller him by myself!" - -"Why, it'll be night, sure. He mightn't ever see you--and if he did, -maybe he'd never think anything." - -"Well, if it's pretty dark I reckon I'll track him. I dono--I dono. -I'll try." - -"You bet I'll follow him, if it's dark, Huck. Why, he might 'a' found -out he couldn't get his revenge, and be going right after that money." - -"It's so, Tom, it's so. I'll foller him; I will, by jingoes!" - -"Now you're TALKING! Don't you ever weaken, Huck, and I won't." - - - -CHAPTER XXVIII - -THAT night Tom and Huck were ready for their adventure. They hung -about the neighborhood of the tavern until after nine, one watching the -alley at a distance and the other the tavern door. Nobody entered the -alley or left it; nobody resembling the Spaniard entered or left the -tavern door. The night promised to be a fair one; so Tom went home with -the understanding that if a considerable degree of darkness came on, -Huck was to come and "maow," whereupon he would slip out and try the -keys. But the night remained clear, and Huck closed his watch and -retired to bed in an empty sugar hogshead about twelve. - -Tuesday the boys had the same ill luck. Also Wednesday. But Thursday -night promised better. Tom slipped out in good season with his aunt's -old tin lantern, and a large towel to blindfold it with. He hid the -lantern in Huck's sugar hogshead and the watch began. An hour before -midnight the tavern closed up and its lights (the only ones -thereabouts) were put out. No Spaniard had been seen. Nobody had -entered or left the alley. Everything was auspicious. The blackness of -darkness reigned, the perfect stillness was interrupted only by -occasional mutterings of distant thunder. - -Tom got his lantern, lit it in the hogshead, wrapped it closely in the -towel, and the two adventurers crept in the gloom toward the tavern. -Huck stood sentry and Tom felt his way into the alley. Then there was a -season of waiting anxiety that weighed upon Huck's spirits like a -mountain. He began to wish he could see a flash from the lantern--it -would frighten him, but it would at least tell him that Tom was alive -yet. It seemed hours since Tom had disappeared. Surely he must have -fainted; maybe he was dead; maybe his heart had burst under terror and -excitement. In his uneasiness Huck found himself drawing closer and -closer to the alley; fearing all sorts of dreadful things, and -momentarily expecting some catastrophe to happen that would take away -his breath. There was not much to take away, for he seemed only able to -inhale it by thimblefuls, and his heart would soon wear itself out, the -way it was beating. Suddenly there was a flash of light and Tom came -tearing by him: "Run!" said he; "run, for your life!" - -He needn't have repeated it; once was enough; Huck was making thirty -or forty miles an hour before the repetition was uttered. The boys -never stopped till they reached the shed of a deserted slaughter-house -at the lower end of the village. Just as they got within its shelter -the storm burst and the rain poured down. As soon as Tom got his breath -he said: - -"Huck, it was awful! I tried two of the keys, just as soft as I could; -but they seemed to make such a power of racket that I couldn't hardly -get my breath I was so scared. They wouldn't turn in the lock, either. -Well, without noticing what I was doing, I took hold of the knob, and -open comes the door! It warn't locked! I hopped in, and shook off the -towel, and, GREAT CAESAR'S GHOST!" - -"What!--what'd you see, Tom?" - -"Huck, I most stepped onto Injun Joe's hand!" - -"No!" - -"Yes! He was lying there, sound asleep on the floor, with his old -patch on his eye and his arms spread out." - -"Lordy, what did you do? Did he wake up?" - -"No, never budged. Drunk, I reckon. I just grabbed that towel and -started!" - -"I'd never 'a' thought of the towel, I bet!" - -"Well, I would. My aunt would make me mighty sick if I lost it." - -"Say, Tom, did you see that box?" - -"Huck, I didn't wait to look around. I didn't see the box, I didn't -see the cross. I didn't see anything but a bottle and a tin cup on the -floor by Injun Joe; yes, I saw two barrels and lots more bottles in the -room. Don't you see, now, what's the matter with that ha'nted room?" - -"How?" - -"Why, it's ha'nted with whiskey! Maybe ALL the Temperance Taverns have -got a ha'nted room, hey, Huck?" - -"Well, I reckon maybe that's so. Who'd 'a' thought such a thing? But -say, Tom, now's a mighty good time to get that box, if Injun Joe's -drunk." - -"It is, that! You try it!" - -Huck shuddered. - -"Well, no--I reckon not." - -"And I reckon not, Huck. Only one bottle alongside of Injun Joe ain't -enough. If there'd been three, he'd be drunk enough and I'd do it." - -There was a long pause for reflection, and then Tom said: - -"Lookyhere, Huck, less not try that thing any more till we know Injun -Joe's not in there. It's too scary. Now, if we watch every night, we'll -be dead sure to see him go out, some time or other, and then we'll -snatch that box quicker'n lightning." - -"Well, I'm agreed. I'll watch the whole night long, and I'll do it -every night, too, if you'll do the other part of the job." - -"All right, I will. All you got to do is to trot up Hooper Street a -block and maow--and if I'm asleep, you throw some gravel at the window -and that'll fetch me." - -"Agreed, and good as wheat!" - -"Now, Huck, the storm's over, and I'll go home. It'll begin to be -daylight in a couple of hours. You go back and watch that long, will -you?" - -"I said I would, Tom, and I will. I'll ha'nt that tavern every night -for a year! I'll sleep all day and I'll stand watch all night." - -"That's all right. Now, where you going to sleep?" - -"In Ben Rogers' hayloft. He lets me, and so does his pap's nigger man, -Uncle Jake. I tote water for Uncle Jake whenever he wants me to, and -any time I ask him he gives me a little something to eat if he can -spare it. That's a mighty good nigger, Tom. He likes me, becuz I don't -ever act as if I was above him. Sometime I've set right down and eat -WITH him. But you needn't tell that. A body's got to do things when -he's awful hungry he wouldn't want to do as a steady thing." - -"Well, if I don't want you in the daytime, I'll let you sleep. I won't -come bothering around. Any time you see something's up, in the night, -just skip right around and maow." - - - -CHAPTER XXIX - -THE first thing Tom heard on Friday morning was a glad piece of news ---Judge Thatcher's family had come back to town the night before. Both -Injun Joe and the treasure sunk into secondary importance for a moment, -and Becky took the chief place in the boy's interest. He saw her and -they had an exhausting good time playing "hi-spy" and "gully-keeper" -with a crowd of their school-mates. The day was completed and crowned -in a peculiarly satisfactory way: Becky teased her mother to appoint -the next day for the long-promised and long-delayed picnic, and she -consented. The child's delight was boundless; and Tom's not more -moderate. The invitations were sent out before sunset, and straightway -the young folks of the village were thrown into a fever of preparation -and pleasurable anticipation. Tom's excitement enabled him to keep -awake until a pretty late hour, and he had good hopes of hearing Huck's -"maow," and of having his treasure to astonish Becky and the picnickers -with, next day; but he was disappointed. No signal came that night. - -Morning came, eventually, and by ten or eleven o'clock a giddy and -rollicking company were gathered at Judge Thatcher's, and everything -was ready for a start. It was not the custom for elderly people to mar -the picnics with their presence. The children were considered safe -enough under the wings of a few young ladies of eighteen and a few -young gentlemen of twenty-three or thereabouts. The old steam ferryboat -was chartered for the occasion; presently the gay throng filed up the -main street laden with provision-baskets. Sid was sick and had to miss -the fun; Mary remained at home to entertain him. The last thing Mrs. -Thatcher said to Becky, was: - -"You'll not get back till late. Perhaps you'd better stay all night -with some of the girls that live near the ferry-landing, child." - -"Then I'll stay with Susy Harper, mamma." - -"Very well. And mind and behave yourself and don't be any trouble." - -Presently, as they tripped along, Tom said to Becky: - -"Say--I'll tell you what we'll do. 'Stead of going to Joe Harper's -we'll climb right up the hill and stop at the Widow Douglas'. She'll -have ice-cream! She has it most every day--dead loads of it. And she'll -be awful glad to have us." - -"Oh, that will be fun!" - -Then Becky reflected a moment and said: - -"But what will mamma say?" - -"How'll she ever know?" - -The girl turned the idea over in her mind, and said reluctantly: - -"I reckon it's wrong--but--" - -"But shucks! Your mother won't know, and so what's the harm? All she -wants is that you'll be safe; and I bet you she'd 'a' said go there if -she'd 'a' thought of it. I know she would!" - -The Widow Douglas' splendid hospitality was a tempting bait. It and -Tom's persuasions presently carried the day. So it was decided to say -nothing anybody about the night's programme. Presently it occurred to -Tom that maybe Huck might come this very night and give the signal. The -thought took a deal of the spirit out of his anticipations. Still he -could not bear to give up the fun at Widow Douglas'. And why should he -give it up, he reasoned--the signal did not come the night before, so -why should it be any more likely to come to-night? The sure fun of the -evening outweighed the uncertain treasure; and, boy-like, he determined -to yield to the stronger inclination and not allow himself to think of -the box of money another time that day. - -Three miles below town the ferryboat stopped at the mouth of a woody -hollow and tied up. The crowd swarmed ashore and soon the forest -distances and craggy heights echoed far and near with shoutings and -laughter. All the different ways of getting hot and tired were gone -through with, and by-and-by the rovers straggled back to camp fortified -with responsible appetites, and then the destruction of the good things -began. After the feast there was a refreshing season of rest and chat -in the shade of spreading oaks. By-and-by somebody shouted: - -"Who's ready for the cave?" - -Everybody was. Bundles of candles were procured, and straightway there -was a general scamper up the hill. The mouth of the cave was up the -hillside--an opening shaped like a letter A. Its massive oaken door -stood unbarred. Within was a small chamber, chilly as an ice-house, and -walled by Nature with solid limestone that was dewy with a cold sweat. -It was romantic and mysterious to stand here in the deep gloom and look -out upon the green valley shining in the sun. But the impressiveness of -the situation quickly wore off, and the romping began again. The moment -a candle was lighted there was a general rush upon the owner of it; a -struggle and a gallant defence followed, but the candle was soon -knocked down or blown out, and then there was a glad clamor of laughter -and a new chase. But all things have an end. By-and-by the procession -went filing down the steep descent of the main avenue, the flickering -rank of lights dimly revealing the lofty walls of rock almost to their -point of junction sixty feet overhead. This main avenue was not more -than eight or ten feet wide. Every few steps other lofty and still -narrower crevices branched from it on either hand--for McDougal's cave -was but a vast labyrinth of crooked aisles that ran into each other and -out again and led nowhere. It was said that one might wander days and -nights together through its intricate tangle of rifts and chasms, and -never find the end of the cave; and that he might go down, and down, -and still down, into the earth, and it was just the same--labyrinth -under labyrinth, and no end to any of them. No man "knew" the cave. -That was an impossible thing. Most of the young men knew a portion of -it, and it was not customary to venture much beyond this known portion. -Tom Sawyer knew as much of the cave as any one. - -The procession moved along the main avenue some three-quarters of a -mile, and then groups and couples began to slip aside into branch -avenues, fly along the dismal corridors, and take each other by -surprise at points where the corridors joined again. Parties were able -to elude each other for the space of half an hour without going beyond -the "known" ground. - -By-and-by, one group after another came straggling back to the mouth -of the cave, panting, hilarious, smeared from head to foot with tallow -drippings, daubed with clay, and entirely delighted with the success of -the day. Then they were astonished to find that they had been taking no -note of time and that night was about at hand. The clanging bell had -been calling for half an hour. However, this sort of close to the day's -adventures was romantic and therefore satisfactory. When the ferryboat -with her wild freight pushed into the stream, nobody cared sixpence for -the wasted time but the captain of the craft. - -Huck was already upon his watch when the ferryboat's lights went -glinting past the wharf. He heard no noise on board, for the young -people were as subdued and still as people usually are who are nearly -tired to death. He wondered what boat it was, and why she did not stop -at the wharf--and then he dropped her out of his mind and put his -attention upon his business. The night was growing cloudy and dark. Ten -o'clock came, and the noise of vehicles ceased, scattered lights began -to wink out, all straggling foot-passengers disappeared, the village -betook itself to its slumbers and left the small watcher alone with the -silence and the ghosts. Eleven o'clock came, and the tavern lights were -put out; darkness everywhere, now. Huck waited what seemed a weary long -time, but nothing happened. His faith was weakening. Was there any use? -Was there really any use? Why not give it up and turn in? - -A noise fell upon his ear. He was all attention in an instant. The -alley door closed softly. He sprang to the corner of the brick store. -The next moment two men brushed by him, and one seemed to have -something under his arm. It must be that box! So they were going to -remove the treasure. Why call Tom now? It would be absurd--the men -would get away with the box and never be found again. No, he would -stick to their wake and follow them; he would trust to the darkness for -security from discovery. So communing with himself, Huck stepped out -and glided along behind the men, cat-like, with bare feet, allowing -them to keep just far enough ahead not to be invisible. - -They moved up the river street three blocks, then turned to the left -up a cross-street. They went straight ahead, then, until they came to -the path that led up Cardiff Hill; this they took. They passed by the -old Welshman's house, half-way up the hill, without hesitating, and -still climbed upward. Good, thought Huck, they will bury it in the old -quarry. But they never stopped at the quarry. They passed on, up the -summit. They plunged into the narrow path between the tall sumach -bushes, and were at once hidden in the gloom. Huck closed up and -shortened his distance, now, for they would never be able to see him. -He trotted along awhile; then slackened his pace, fearing he was -gaining too fast; moved on a piece, then stopped altogether; listened; -no sound; none, save that he seemed to hear the beating of his own -heart. The hooting of an owl came over the hill--ominous sound! But no -footsteps. Heavens, was everything lost! He was about to spring with -winged feet, when a man cleared his throat not four feet from him! -Huck's heart shot into his throat, but he swallowed it again; and then -he stood there shaking as if a dozen agues had taken charge of him at -once, and so weak that he thought he must surely fall to the ground. He -knew where he was. He knew he was within five steps of the stile -leading into Widow Douglas' grounds. Very well, he thought, let them -bury it there; it won't be hard to find. - -Now there was a voice--a very low voice--Injun Joe's: - -"Damn her, maybe she's got company--there's lights, late as it is." - -"I can't see any." - -This was that stranger's voice--the stranger of the haunted house. A -deadly chill went to Huck's heart--this, then, was the "revenge" job! -His thought was, to fly. Then he remembered that the Widow Douglas had -been kind to him more than once, and maybe these men were going to -murder her. He wished he dared venture to warn her; but he knew he -didn't dare--they might come and catch him. He thought all this and -more in the moment that elapsed between the stranger's remark and Injun -Joe's next--which was-- - -"Because the bush is in your way. Now--this way--now you see, don't -you?" - -"Yes. Well, there IS company there, I reckon. Better give it up." - -"Give it up, and I just leaving this country forever! Give it up and -maybe never have another chance. I tell you again, as I've told you -before, I don't care for her swag--you may have it. But her husband was -rough on me--many times he was rough on me--and mainly he was the -justice of the peace that jugged me for a vagrant. And that ain't all. -It ain't a millionth part of it! He had me HORSEWHIPPED!--horsewhipped -in front of the jail, like a nigger!--with all the town looking on! -HORSEWHIPPED!--do you understand? He took advantage of me and died. But -I'll take it out of HER." - -"Oh, don't kill her! Don't do that!" - -"Kill? Who said anything about killing? I would kill HIM if he was -here; but not her. When you want to get revenge on a woman you don't -kill her--bosh! you go for her looks. You slit her nostrils--you notch -her ears like a sow!" - -"By God, that's--" - -"Keep your opinion to yourself! It will be safest for you. I'll tie -her to the bed. If she bleeds to death, is that my fault? I'll not cry, -if she does. My friend, you'll help me in this thing--for MY sake ---that's why you're here--I mightn't be able alone. If you flinch, I'll -kill you. Do you understand that? And if I have to kill you, I'll kill -her--and then I reckon nobody'll ever know much about who done this -business." - -"Well, if it's got to be done, let's get at it. The quicker the -better--I'm all in a shiver." - -"Do it NOW? And company there? Look here--I'll get suspicious of you, -first thing you know. No--we'll wait till the lights are out--there's -no hurry." - -Huck felt that a silence was going to ensue--a thing still more awful -than any amount of murderous talk; so he held his breath and stepped -gingerly back; planted his foot carefully and firmly, after balancing, -one-legged, in a precarious way and almost toppling over, first on one -side and then on the other. He took another step back, with the same -elaboration and the same risks; then another and another, and--a twig -snapped under his foot! His breath stopped and he listened. There was -no sound--the stillness was perfect. His gratitude was measureless. Now -he turned in his tracks, between the walls of sumach bushes--turned -himself as carefully as if he were a ship--and then stepped quickly but -cautiously along. When he emerged at the quarry he felt secure, and so -he picked up his nimble heels and flew. Down, down he sped, till he -reached the Welshman's. He banged at the door, and presently the heads -of the old man and his two stalwart sons were thrust from windows. - -"What's the row there? Who's banging? What do you want?" - -"Let me in--quick! I'll tell everything." - -"Why, who are you?" - -"Huckleberry Finn--quick, let me in!" - -"Huckleberry Finn, indeed! It ain't a name to open many doors, I -judge! But let him in, lads, and let's see what's the trouble." - -"Please don't ever tell I told you," were Huck's first words when he -got in. "Please don't--I'd be killed, sure--but the widow's been good -friends to me sometimes, and I want to tell--I WILL tell if you'll -promise you won't ever say it was me." - -"By George, he HAS got something to tell, or he wouldn't act so!" -exclaimed the old man; "out with it and nobody here'll ever tell, lad." - -Three minutes later the old man and his sons, well armed, were up the -hill, and just entering the sumach path on tiptoe, their weapons in -their hands. Huck accompanied them no further. He hid behind a great -bowlder and fell to listening. There was a lagging, anxious silence, -and then all of a sudden there was an explosion of firearms and a cry. - -Huck waited for no particulars. He sprang away and sped down the hill -as fast as his legs could carry him. - - - -CHAPTER XXX - -AS the earliest suspicion of dawn appeared on Sunday morning, Huck -came groping up the hill and rapped gently at the old Welshman's door. -The inmates were asleep, but it was a sleep that was set on a -hair-trigger, on account of the exciting episode of the night. A call -came from a window: - -"Who's there!" - -Huck's scared voice answered in a low tone: - -"Please let me in! It's only Huck Finn!" - -"It's a name that can open this door night or day, lad!--and welcome!" - -These were strange words to the vagabond boy's ears, and the -pleasantest he had ever heard. He could not recollect that the closing -word had ever been applied in his case before. The door was quickly -unlocked, and he entered. Huck was given a seat and the old man and his -brace of tall sons speedily dressed themselves. - -"Now, my boy, I hope you're good and hungry, because breakfast will be -ready as soon as the sun's up, and we'll have a piping hot one, too ---make yourself easy about that! I and the boys hoped you'd turn up and -stop here last night." - -"I was awful scared," said Huck, "and I run. I took out when the -pistols went off, and I didn't stop for three mile. I've come now becuz -I wanted to know about it, you know; and I come before daylight becuz I -didn't want to run across them devils, even if they was dead." - -"Well, poor chap, you do look as if you'd had a hard night of it--but -there's a bed here for you when you've had your breakfast. No, they -ain't dead, lad--we are sorry enough for that. You see we knew right -where to put our hands on them, by your description; so we crept along -on tiptoe till we got within fifteen feet of them--dark as a cellar -that sumach path was--and just then I found I was going to sneeze. It -was the meanest kind of luck! I tried to keep it back, but no use ---'twas bound to come, and it did come! I was in the lead with my pistol -raised, and when the sneeze started those scoundrels a-rustling to get -out of the path, I sung out, 'Fire boys!' and blazed away at the place -where the rustling was. So did the boys. But they were off in a jiffy, -those villains, and we after them, down through the woods. I judge we -never touched them. They fired a shot apiece as they started, but their -bullets whizzed by and didn't do us any harm. As soon as we lost the -sound of their feet we quit chasing, and went down and stirred up the -constables. They got a posse together, and went off to guard the river -bank, and as soon as it is light the sheriff and a gang are going to -beat up the woods. My boys will be with them presently. I wish we had -some sort of description of those rascals--'twould help a good deal. -But you couldn't see what they were like, in the dark, lad, I suppose?" - -"Oh yes; I saw them down-town and follered them." - -"Splendid! Describe them--describe them, my boy!" - -"One's the old deaf and dumb Spaniard that's ben around here once or -twice, and t'other's a mean-looking, ragged--" - -"That's enough, lad, we know the men! Happened on them in the woods -back of the widow's one day, and they slunk away. Off with you, boys, -and tell the sheriff--get your breakfast to-morrow morning!" - -The Welshman's sons departed at once. As they were leaving the room -Huck sprang up and exclaimed: - -"Oh, please don't tell ANYbody it was me that blowed on them! Oh, -please!" - -"All right if you say it, Huck, but you ought to have the credit of -what you did." - -"Oh no, no! Please don't tell!" - -When the young men were gone, the old Welshman said: - -"They won't tell--and I won't. But why don't you want it known?" - -Huck would not explain, further than to say that he already knew too -much about one of those men and would not have the man know that he -knew anything against him for the whole world--he would be killed for -knowing it, sure. - -The old man promised secrecy once more, and said: - -"How did you come to follow these fellows, lad? Were they looking -suspicious?" - -Huck was silent while he framed a duly cautious reply. Then he said: - -"Well, you see, I'm a kind of a hard lot,--least everybody says so, -and I don't see nothing agin it--and sometimes I can't sleep much, on -account of thinking about it and sort of trying to strike out a new way -of doing. That was the way of it last night. I couldn't sleep, and so I -come along up-street 'bout midnight, a-turning it all over, and when I -got to that old shackly brick store by the Temperance Tavern, I backed -up agin the wall to have another think. Well, just then along comes -these two chaps slipping along close by me, with something under their -arm, and I reckoned they'd stole it. One was a-smoking, and t'other one -wanted a light; so they stopped right before me and the cigars lit up -their faces and I see that the big one was the deaf and dumb Spaniard, -by his white whiskers and the patch on his eye, and t'other one was a -rusty, ragged-looking devil." - -"Could you see the rags by the light of the cigars?" - -This staggered Huck for a moment. Then he said: - -"Well, I don't know--but somehow it seems as if I did." - -"Then they went on, and you--" - -"Follered 'em--yes. That was it. I wanted to see what was up--they -sneaked along so. I dogged 'em to the widder's stile, and stood in the -dark and heard the ragged one beg for the widder, and the Spaniard -swear he'd spile her looks just as I told you and your two--" - -"What! The DEAF AND DUMB man said all that!" - -Huck had made another terrible mistake! He was trying his best to keep -the old man from getting the faintest hint of who the Spaniard might -be, and yet his tongue seemed determined to get him into trouble in -spite of all he could do. He made several efforts to creep out of his -scrape, but the old man's eye was upon him and he made blunder after -blunder. Presently the Welshman said: - -"My boy, don't be afraid of me. I wouldn't hurt a hair of your head -for all the world. No--I'd protect you--I'd protect you. This Spaniard -is not deaf and dumb; you've let that slip without intending it; you -can't cover that up now. You know something about that Spaniard that -you want to keep dark. Now trust me--tell me what it is, and trust me ---I won't betray you." - -Huck looked into the old man's honest eyes a moment, then bent over -and whispered in his ear: - -"'Tain't a Spaniard--it's Injun Joe!" - -The Welshman almost jumped out of his chair. In a moment he said: - -"It's all plain enough, now. When you talked about notching ears and -slitting noses I judged that that was your own embellishment, because -white men don't take that sort of revenge. But an Injun! That's a -different matter altogether." - -During breakfast the talk went on, and in the course of it the old man -said that the last thing which he and his sons had done, before going -to bed, was to get a lantern and examine the stile and its vicinity for -marks of blood. They found none, but captured a bulky bundle of-- - -"Of WHAT?" - -If the words had been lightning they could not have leaped with a more -stunning suddenness from Huck's blanched lips. His eyes were staring -wide, now, and his breath suspended--waiting for the answer. The -Welshman started--stared in return--three seconds--five seconds--ten ---then replied: - -"Of burglar's tools. Why, what's the MATTER with you?" - -Huck sank back, panting gently, but deeply, unutterably grateful. The -Welshman eyed him gravely, curiously--and presently said: - -"Yes, burglar's tools. That appears to relieve you a good deal. But -what did give you that turn? What were YOU expecting we'd found?" - -Huck was in a close place--the inquiring eye was upon him--he would -have given anything for material for a plausible answer--nothing -suggested itself--the inquiring eye was boring deeper and deeper--a -senseless reply offered--there was no time to weigh it, so at a venture -he uttered it--feebly: - -"Sunday-school books, maybe." - -Poor Huck was too distressed to smile, but the old man laughed loud -and joyously, shook up the details of his anatomy from head to foot, -and ended by saying that such a laugh was money in a-man's pocket, -because it cut down the doctor's bill like everything. Then he added: - -"Poor old chap, you're white and jaded--you ain't well a bit--no -wonder you're a little flighty and off your balance. But you'll come -out of it. Rest and sleep will fetch you out all right, I hope." - -Huck was irritated to think he had been such a goose and betrayed such -a suspicious excitement, for he had dropped the idea that the parcel -brought from the tavern was the treasure, as soon as he had heard the -talk at the widow's stile. He had only thought it was not the treasure, -however--he had not known that it wasn't--and so the suggestion of a -captured bundle was too much for his self-possession. But on the whole -he felt glad the little episode had happened, for now he knew beyond -all question that that bundle was not THE bundle, and so his mind was -at rest and exceedingly comfortable. In fact, everything seemed to be -drifting just in the right direction, now; the treasure must be still -in No. 2, the men would be captured and jailed that day, and he and Tom -could seize the gold that night without any trouble or any fear of -interruption. - -Just as breakfast was completed there was a knock at the door. Huck -jumped for a hiding-place, for he had no mind to be connected even -remotely with the late event. The Welshman admitted several ladies and -gentlemen, among them the Widow Douglas, and noticed that groups of -citizens were climbing up the hill--to stare at the stile. So the news -had spread. The Welshman had to tell the story of the night to the -visitors. The widow's gratitude for her preservation was outspoken. - -"Don't say a word about it, madam. There's another that you're more -beholden to than you are to me and my boys, maybe, but he don't allow -me to tell his name. We wouldn't have been there but for him." - -Of course this excited a curiosity so vast that it almost belittled -the main matter--but the Welshman allowed it to eat into the vitals of -his visitors, and through them be transmitted to the whole town, for he -refused to part with his secret. When all else had been learned, the -widow said: - -"I went to sleep reading in bed and slept straight through all that -noise. Why didn't you come and wake me?" - -"We judged it warn't worth while. Those fellows warn't likely to come -again--they hadn't any tools left to work with, and what was the use of -waking you up and scaring you to death? My three negro men stood guard -at your house all the rest of the night. They've just come back." - -More visitors came, and the story had to be told and retold for a -couple of hours more. - -There was no Sabbath-school during day-school vacation, but everybody -was early at church. The stirring event was well canvassed. News came -that not a sign of the two villains had been yet discovered. When the -sermon was finished, Judge Thatcher's wife dropped alongside of Mrs. -Harper as she moved down the aisle with the crowd and said: - -"Is my Becky going to sleep all day? I just expected she would be -tired to death." - -"Your Becky?" - -"Yes," with a startled look--"didn't she stay with you last night?" - -"Why, no." - -Mrs. Thatcher turned pale, and sank into a pew, just as Aunt Polly, -talking briskly with a friend, passed by. Aunt Polly said: - -"Good-morning, Mrs. Thatcher. Good-morning, Mrs. Harper. I've got a -boy that's turned up missing. I reckon my Tom stayed at your house last -night--one of you. And now he's afraid to come to church. I've got to -settle with him." - -Mrs. Thatcher shook her head feebly and turned paler than ever. - -"He didn't stay with us," said Mrs. Harper, beginning to look uneasy. -A marked anxiety came into Aunt Polly's face. - -"Joe Harper, have you seen my Tom this morning?" - -"No'm." - -"When did you see him last?" - -Joe tried to remember, but was not sure he could say. The people had -stopped moving out of church. Whispers passed along, and a boding -uneasiness took possession of every countenance. Children were -anxiously questioned, and young teachers. They all said they had not -noticed whether Tom and Becky were on board the ferryboat on the -homeward trip; it was dark; no one thought of inquiring if any one was -missing. One young man finally blurted out his fear that they were -still in the cave! Mrs. Thatcher swooned away. Aunt Polly fell to -crying and wringing her hands. - -The alarm swept from lip to lip, from group to group, from street to -street, and within five minutes the bells were wildly clanging and the -whole town was up! The Cardiff Hill episode sank into instant -insignificance, the burglars were forgotten, horses were saddled, -skiffs were manned, the ferryboat ordered out, and before the horror -was half an hour old, two hundred men were pouring down highroad and -river toward the cave. - -All the long afternoon the village seemed empty and dead. Many women -visited Aunt Polly and Mrs. Thatcher and tried to comfort them. They -cried with them, too, and that was still better than words. All the -tedious night the town waited for news; but when the morning dawned at -last, all the word that came was, "Send more candles--and send food." -Mrs. Thatcher was almost crazed; and Aunt Polly, also. Judge Thatcher -sent messages of hope and encouragement from the cave, but they -conveyed no real cheer. - -The old Welshman came home toward daylight, spattered with -candle-grease, smeared with clay, and almost worn out. He found Huck -still in the bed that had been provided for him, and delirious with -fever. The physicians were all at the cave, so the Widow Douglas came -and took charge of the patient. She said she would do her best by him, -because, whether he was good, bad, or indifferent, he was the Lord's, -and nothing that was the Lord's was a thing to be neglected. The -Welshman said Huck had good spots in him, and the widow said: - -"You can depend on it. That's the Lord's mark. He don't leave it off. -He never does. Puts it somewhere on every creature that comes from his -hands." - -Early in the forenoon parties of jaded men began to straggle into the -village, but the strongest of the citizens continued searching. All the -news that could be gained was that remotenesses of the cavern were -being ransacked that had never been visited before; that every corner -and crevice was going to be thoroughly searched; that wherever one -wandered through the maze of passages, lights were to be seen flitting -hither and thither in the distance, and shoutings and pistol-shots sent -their hollow reverberations to the ear down the sombre aisles. In one -place, far from the section usually traversed by tourists, the names -"BECKY & TOM" had been found traced upon the rocky wall with -candle-smoke, and near at hand a grease-soiled bit of ribbon. Mrs. -Thatcher recognized the ribbon and cried over it. She said it was the -last relic she should ever have of her child; and that no other memorial -of her could ever be so precious, because this one parted latest from -the living body before the awful death came. Some said that now and -then, in the cave, a far-away speck of light would glimmer, and then a -glorious shout would burst forth and a score of men go trooping down the -echoing aisle--and then a sickening disappointment always followed; the -children were not there; it was only a searcher's light. - -Three dreadful days and nights dragged their tedious hours along, and -the village sank into a hopeless stupor. No one had heart for anything. -The accidental discovery, just made, that the proprietor of the -Temperance Tavern kept liquor on his premises, scarcely fluttered the -public pulse, tremendous as the fact was. In a lucid interval, Huck -feebly led up to the subject of taverns, and finally asked--dimly -dreading the worst--if anything had been discovered at the Temperance -Tavern since he had been ill. - -"Yes," said the widow. - -Huck started up in bed, wild-eyed: - -"What? What was it?" - -"Liquor!--and the place has been shut up. Lie down, child--what a turn -you did give me!" - -"Only tell me just one thing--only just one--please! Was it Tom Sawyer -that found it?" - -The widow burst into tears. "Hush, hush, child, hush! I've told you -before, you must NOT talk. You are very, very sick!" - -Then nothing but liquor had been found; there would have been a great -powwow if it had been the gold. So the treasure was gone forever--gone -forever! But what could she be crying about? Curious that she should -cry. - -These thoughts worked their dim way through Huck's mind, and under the -weariness they gave him he fell asleep. The widow said to herself: - -"There--he's asleep, poor wreck. Tom Sawyer find it! Pity but somebody -could find Tom Sawyer! Ah, there ain't many left, now, that's got hope -enough, or strength enough, either, to go on searching." - - - -CHAPTER XXXI - -NOW to return to Tom and Becky's share in the picnic. They tripped -along the murky aisles with the rest of the company, visiting the -familiar wonders of the cave--wonders dubbed with rather -over-descriptive names, such as "The Drawing-Room," "The Cathedral," -"Aladdin's Palace," and so on. Presently the hide-and-seek frolicking -began, and Tom and Becky engaged in it with zeal until the exertion -began to grow a trifle wearisome; then they wandered down a sinuous -avenue holding their candles aloft and reading the tangled web-work of -names, dates, post-office addresses, and mottoes with which the rocky -walls had been frescoed (in candle-smoke). Still drifting along and -talking, they scarcely noticed that they were now in a part of the cave -whose walls were not frescoed. They smoked their own names under an -overhanging shelf and moved on. Presently they came to a place where a -little stream of water, trickling over a ledge and carrying a limestone -sediment with it, had, in the slow-dragging ages, formed a laced and -ruffled Niagara in gleaming and imperishable stone. Tom squeezed his -small body behind it in order to illuminate it for Becky's -gratification. He found that it curtained a sort of steep natural -stairway which was enclosed between narrow walls, and at once the -ambition to be a discoverer seized him. Becky responded to his call, -and they made a smoke-mark for future guidance, and started upon their -quest. They wound this way and that, far down into the secret depths of -the cave, made another mark, and branched off in search of novelties to -tell the upper world about. In one place they found a spacious cavern, -from whose ceiling depended a multitude of shining stalactites of the -length and circumference of a man's leg; they walked all about it, -wondering and admiring, and presently left it by one of the numerous -passages that opened into it. This shortly brought them to a bewitching -spring, whose basin was incrusted with a frostwork of glittering -crystals; it was in the midst of a cavern whose walls were supported by -many fantastic pillars which had been formed by the joining of great -stalactites and stalagmites together, the result of the ceaseless -water-drip of centuries. Under the roof vast knots of bats had packed -themselves together, thousands in a bunch; the lights disturbed the -creatures and they came flocking down by hundreds, squeaking and -darting furiously at the candles. Tom knew their ways and the danger of -this sort of conduct. He seized Becky's hand and hurried her into the -first corridor that offered; and none too soon, for a bat struck -Becky's light out with its wing while she was passing out of the -cavern. The bats chased the children a good distance; but the fugitives -plunged into every new passage that offered, and at last got rid of the -perilous things. Tom found a subterranean lake, shortly, which -stretched its dim length away until its shape was lost in the shadows. -He wanted to explore its borders, but concluded that it would be best -to sit down and rest awhile, first. Now, for the first time, the deep -stillness of the place laid a clammy hand upon the spirits of the -children. Becky said: - -"Why, I didn't notice, but it seems ever so long since I heard any of -the others." - -"Come to think, Becky, we are away down below them--and I don't know -how far away north, or south, or east, or whichever it is. We couldn't -hear them here." - -Becky grew apprehensive. - -"I wonder how long we've been down here, Tom? We better start back." - -"Yes, I reckon we better. P'raps we better." - -"Can you find the way, Tom? It's all a mixed-up crookedness to me." - -"I reckon I could find it--but then the bats. If they put our candles -out it will be an awful fix. Let's try some other way, so as not to go -through there." - -"Well. But I hope we won't get lost. It would be so awful!" and the -girl shuddered at the thought of the dreadful possibilities. - -They started through a corridor, and traversed it in silence a long -way, glancing at each new opening, to see if there was anything -familiar about the look of it; but they were all strange. Every time -Tom made an examination, Becky would watch his face for an encouraging -sign, and he would say cheerily: - -"Oh, it's all right. This ain't the one, but we'll come to it right -away!" - -But he felt less and less hopeful with each failure, and presently -began to turn off into diverging avenues at sheer random, in desperate -hope of finding the one that was wanted. He still said it was "all -right," but there was such a leaden dread at his heart that the words -had lost their ring and sounded just as if he had said, "All is lost!" -Becky clung to his side in an anguish of fear, and tried hard to keep -back the tears, but they would come. At last she said: - -"Oh, Tom, never mind the bats, let's go back that way! We seem to get -worse and worse off all the time." - -"Listen!" said he. - -Profound silence; silence so deep that even their breathings were -conspicuous in the hush. Tom shouted. The call went echoing down the -empty aisles and died out in the distance in a faint sound that -resembled a ripple of mocking laughter. - -"Oh, don't do it again, Tom, it is too horrid," said Becky. - -"It is horrid, but I better, Becky; they might hear us, you know," and -he shouted again. - -The "might" was even a chillier horror than the ghostly laughter, it -so confessed a perishing hope. The children stood still and listened; -but there was no result. Tom turned upon the back track at once, and -hurried his steps. It was but a little while before a certain -indecision in his manner revealed another fearful fact to Becky--he -could not find his way back! - -"Oh, Tom, you didn't make any marks!" - -"Becky, I was such a fool! Such a fool! I never thought we might want -to come back! No--I can't find the way. It's all mixed up." - -"Tom, Tom, we're lost! we're lost! We never can get out of this awful -place! Oh, why DID we ever leave the others!" - -She sank to the ground and burst into such a frenzy of crying that Tom -was appalled with the idea that she might die, or lose her reason. He -sat down by her and put his arms around her; she buried her face in his -bosom, she clung to him, she poured out her terrors, her unavailing -regrets, and the far echoes turned them all to jeering laughter. Tom -begged her to pluck up hope again, and she said she could not. He fell -to blaming and abusing himself for getting her into this miserable -situation; this had a better effect. She said she would try to hope -again, she would get up and follow wherever he might lead if only he -would not talk like that any more. For he was no more to blame than -she, she said. - -So they moved on again--aimlessly--simply at random--all they could do -was to move, keep moving. For a little while, hope made a show of -reviving--not with any reason to back it, but only because it is its -nature to revive when the spring has not been taken out of it by age -and familiarity with failure. - -By-and-by Tom took Becky's candle and blew it out. This economy meant -so much! Words were not needed. Becky understood, and her hope died -again. She knew that Tom had a whole candle and three or four pieces in -his pockets--yet he must economize. - -By-and-by, fatigue began to assert its claims; the children tried to -pay attention, for it was dreadful to think of sitting down when time -was grown to be so precious, moving, in some direction, in any -direction, was at least progress and might bear fruit; but to sit down -was to invite death and shorten its pursuit. - -At last Becky's frail limbs refused to carry her farther. She sat -down. Tom rested with her, and they talked of home, and the friends -there, and the comfortable beds and, above all, the light! Becky cried, -and Tom tried to think of some way of comforting her, but all his -encouragements were grown threadbare with use, and sounded like -sarcasms. Fatigue bore so heavily upon Becky that she drowsed off to -sleep. Tom was grateful. He sat looking into her drawn face and saw it -grow smooth and natural under the influence of pleasant dreams; and -by-and-by a smile dawned and rested there. The peaceful face reflected -somewhat of peace and healing into his own spirit, and his thoughts -wandered away to bygone times and dreamy memories. While he was deep in -his musings, Becky woke up with a breezy little laugh--but it was -stricken dead upon her lips, and a groan followed it. - -"Oh, how COULD I sleep! I wish I never, never had waked! No! No, I -don't, Tom! Don't look so! I won't say it again." - -"I'm glad you've slept, Becky; you'll feel rested, now, and we'll find -the way out." - -"We can try, Tom; but I've seen such a beautiful country in my dream. -I reckon we are going there." - -"Maybe not, maybe not. Cheer up, Becky, and let's go on trying." - -They rose up and wandered along, hand in hand and hopeless. They tried -to estimate how long they had been in the cave, but all they knew was -that it seemed days and weeks, and yet it was plain that this could not -be, for their candles were not gone yet. A long time after this--they -could not tell how long--Tom said they must go softly and listen for -dripping water--they must find a spring. They found one presently, and -Tom said it was time to rest again. Both were cruelly tired, yet Becky -said she thought she could go a little farther. She was surprised to -hear Tom dissent. She could not understand it. They sat down, and Tom -fastened his candle to the wall in front of them with some clay. -Thought was soon busy; nothing was said for some time. Then Becky broke -the silence: - -"Tom, I am so hungry!" - -Tom took something out of his pocket. - -"Do you remember this?" said he. - -Becky almost smiled. - -"It's our wedding-cake, Tom." - -"Yes--I wish it was as big as a barrel, for it's all we've got." - -"I saved it from the picnic for us to dream on, Tom, the way grown-up -people do with wedding-cake--but it'll be our--" - -She dropped the sentence where it was. Tom divided the cake and Becky -ate with good appetite, while Tom nibbled at his moiety. There was -abundance of cold water to finish the feast with. By-and-by Becky -suggested that they move on again. Tom was silent a moment. Then he -said: - -"Becky, can you bear it if I tell you something?" - -Becky's face paled, but she thought she could. - -"Well, then, Becky, we must stay here, where there's water to drink. -That little piece is our last candle!" - -Becky gave loose to tears and wailings. Tom did what he could to -comfort her, but with little effect. At length Becky said: - -"Tom!" - -"Well, Becky?" - -"They'll miss us and hunt for us!" - -"Yes, they will! Certainly they will!" - -"Maybe they're hunting for us now, Tom." - -"Why, I reckon maybe they are. I hope they are." - -"When would they miss us, Tom?" - -"When they get back to the boat, I reckon." - -"Tom, it might be dark then--would they notice we hadn't come?" - -"I don't know. But anyway, your mother would miss you as soon as they -got home." - -A frightened look in Becky's face brought Tom to his senses and he saw -that he had made a blunder. Becky was not to have gone home that night! -The children became silent and thoughtful. In a moment a new burst of -grief from Becky showed Tom that the thing in his mind had struck hers -also--that the Sabbath morning might be half spent before Mrs. Thatcher -discovered that Becky was not at Mrs. Harper's. - -The children fastened their eyes upon their bit of candle and watched -it melt slowly and pitilessly away; saw the half inch of wick stand -alone at last; saw the feeble flame rise and fall, climb the thin -column of smoke, linger at its top a moment, and then--the horror of -utter darkness reigned! - -How long afterward it was that Becky came to a slow consciousness that -she was crying in Tom's arms, neither could tell. All that they knew -was, that after what seemed a mighty stretch of time, both awoke out of -a dead stupor of sleep and resumed their miseries once more. Tom said -it might be Sunday, now--maybe Monday. He tried to get Becky to talk, -but her sorrows were too oppressive, all her hopes were gone. Tom said -that they must have been missed long ago, and no doubt the search was -going on. He would shout and maybe some one would come. He tried it; -but in the darkness the distant echoes sounded so hideously that he -tried it no more. - -The hours wasted away, and hunger came to torment the captives again. -A portion of Tom's half of the cake was left; they divided and ate it. -But they seemed hungrier than before. The poor morsel of food only -whetted desire. - -By-and-by Tom said: - -"SH! Did you hear that?" - -Both held their breath and listened. There was a sound like the -faintest, far-off shout. Instantly Tom answered it, and leading Becky -by the hand, started groping down the corridor in its direction. -Presently he listened again; again the sound was heard, and apparently -a little nearer. - -"It's them!" said Tom; "they're coming! Come along, Becky--we're all -right now!" - -The joy of the prisoners was almost overwhelming. Their speed was -slow, however, because pitfalls were somewhat common, and had to be -guarded against. They shortly came to one and had to stop. It might be -three feet deep, it might be a hundred--there was no passing it at any -rate. Tom got down on his breast and reached as far down as he could. -No bottom. They must stay there and wait until the searchers came. They -listened; evidently the distant shoutings were growing more distant! a -moment or two more and they had gone altogether. The heart-sinking -misery of it! Tom whooped until he was hoarse, but it was of no use. He -talked hopefully to Becky; but an age of anxious waiting passed and no -sounds came again. - -The children groped their way back to the spring. The weary time -dragged on; they slept again, and awoke famished and woe-stricken. Tom -believed it must be Tuesday by this time. - -Now an idea struck him. There were some side passages near at hand. It -would be better to explore some of these than bear the weight of the -heavy time in idleness. He took a kite-line from his pocket, tied it to -a projection, and he and Becky started, Tom in the lead, unwinding the -line as he groped along. At the end of twenty steps the corridor ended -in a "jumping-off place." Tom got down on his knees and felt below, and -then as far around the corner as he could reach with his hands -conveniently; he made an effort to stretch yet a little farther to the -right, and at that moment, not twenty yards away, a human hand, holding -a candle, appeared from behind a rock! Tom lifted up a glorious shout, -and instantly that hand was followed by the body it belonged to--Injun -Joe's! Tom was paralyzed; he could not move. He was vastly gratified -the next moment, to see the "Spaniard" take to his heels and get -himself out of sight. Tom wondered that Joe had not recognized his -voice and come over and killed him for testifying in court. But the -echoes must have disguised the voice. Without doubt, that was it, he -reasoned. Tom's fright weakened every muscle in his body. He said to -himself that if he had strength enough to get back to the spring he -would stay there, and nothing should tempt him to run the risk of -meeting Injun Joe again. He was careful to keep from Becky what it was -he had seen. He told her he had only shouted "for luck." - -But hunger and wretchedness rise superior to fears in the long run. -Another tedious wait at the spring and another long sleep brought -changes. The children awoke tortured with a raging hunger. Tom believed -that it must be Wednesday or Thursday or even Friday or Saturday, now, -and that the search had been given over. He proposed to explore another -passage. He felt willing to risk Injun Joe and all other terrors. But -Becky was very weak. She had sunk into a dreary apathy and would not be -roused. She said she would wait, now, where she was, and die--it would -not be long. She told Tom to go with the kite-line and explore if he -chose; but she implored him to come back every little while and speak -to her; and she made him promise that when the awful time came, he -would stay by her and hold her hand until all was over. - -Tom kissed her, with a choking sensation in his throat, and made a -show of being confident of finding the searchers or an escape from the -cave; then he took the kite-line in his hand and went groping down one -of the passages on his hands and knees, distressed with hunger and sick -with bodings of coming doom. - - - -CHAPTER XXXII - -TUESDAY afternoon came, and waned to the twilight. The village of St. -Petersburg still mourned. The lost children had not been found. Public -prayers had been offered up for them, and many and many a private -prayer that had the petitioner's whole heart in it; but still no good -news came from the cave. The majority of the searchers had given up the -quest and gone back to their daily avocations, saying that it was plain -the children could never be found. Mrs. Thatcher was very ill, and a -great part of the time delirious. People said it was heartbreaking to -hear her call her child, and raise her head and listen a whole minute -at a time, then lay it wearily down again with a moan. Aunt Polly had -drooped into a settled melancholy, and her gray hair had grown almost -white. The village went to its rest on Tuesday night, sad and forlorn. - -Away in the middle of the night a wild peal burst from the village -bells, and in a moment the streets were swarming with frantic half-clad -people, who shouted, "Turn out! turn out! they're found! they're -found!" Tin pans and horns were added to the din, the population massed -itself and moved toward the river, met the children coming in an open -carriage drawn by shouting citizens, thronged around it, joined its -homeward march, and swept magnificently up the main street roaring -huzzah after huzzah! - -The village was illuminated; nobody went to bed again; it was the -greatest night the little town had ever seen. During the first half-hour -a procession of villagers filed through Judge Thatcher's house, seized -the saved ones and kissed them, squeezed Mrs. Thatcher's hand, tried to -speak but couldn't--and drifted out raining tears all over the place. - -Aunt Polly's happiness was complete, and Mrs. Thatcher's nearly so. It -would be complete, however, as soon as the messenger dispatched with -the great news to the cave should get the word to her husband. Tom lay -upon a sofa with an eager auditory about him and told the history of -the wonderful adventure, putting in many striking additions to adorn it -withal; and closed with a description of how he left Becky and went on -an exploring expedition; how he followed two avenues as far as his -kite-line would reach; how he followed a third to the fullest stretch of -the kite-line, and was about to turn back when he glimpsed a far-off -speck that looked like daylight; dropped the line and groped toward it, -pushed his head and shoulders through a small hole, and saw the broad -Mississippi rolling by! And if it had only happened to be night he would -not have seen that speck of daylight and would not have explored that -passage any more! He told how he went back for Becky and broke the good -news and she told him not to fret her with such stuff, for she was -tired, and knew she was going to die, and wanted to. He described how he -labored with her and convinced her; and how she almost died for joy when -she had groped to where she actually saw the blue speck of daylight; how -he pushed his way out at the hole and then helped her out; how they sat -there and cried for gladness; how some men came along in a skiff and Tom -hailed them and told them their situation and their famished condition; -how the men didn't believe the wild tale at first, "because," said they, -"you are five miles down the river below the valley the cave is in" ---then took them aboard, rowed to a house, gave them supper, made them -rest till two or three hours after dark and then brought them home. - -Before day-dawn, Judge Thatcher and the handful of searchers with him -were tracked out, in the cave, by the twine clews they had strung -behind them, and informed of the great news. - -Three days and nights of toil and hunger in the cave were not to be -shaken off at once, as Tom and Becky soon discovered. They were -bedridden all of Wednesday and Thursday, and seemed to grow more and -more tired and worn, all the time. Tom got about, a little, on -Thursday, was down-town Friday, and nearly as whole as ever Saturday; -but Becky did not leave her room until Sunday, and then she looked as -if she had passed through a wasting illness. - -Tom learned of Huck's sickness and went to see him on Friday, but -could not be admitted to the bedroom; neither could he on Saturday or -Sunday. He was admitted daily after that, but was warned to keep still -about his adventure and introduce no exciting topic. The Widow Douglas -stayed by to see that he obeyed. At home Tom learned of the Cardiff -Hill event; also that the "ragged man's" body had eventually been found -in the river near the ferry-landing; he had been drowned while trying -to escape, perhaps. - -About a fortnight after Tom's rescue from the cave, he started off to -visit Huck, who had grown plenty strong enough, now, to hear exciting -talk, and Tom had some that would interest him, he thought. Judge -Thatcher's house was on Tom's way, and he stopped to see Becky. The -Judge and some friends set Tom to talking, and some one asked him -ironically if he wouldn't like to go to the cave again. Tom said he -thought he wouldn't mind it. The Judge said: - -"Well, there are others just like you, Tom, I've not the least doubt. -But we have taken care of that. Nobody will get lost in that cave any -more." - -"Why?" - -"Because I had its big door sheathed with boiler iron two weeks ago, -and triple-locked--and I've got the keys." - -Tom turned as white as a sheet. - -"What's the matter, boy! Here, run, somebody! Fetch a glass of water!" - -The water was brought and thrown into Tom's face. - -"Ah, now you're all right. What was the matter with you, Tom?" - -"Oh, Judge, Injun Joe's in the cave!" - - - -CHAPTER XXXIII - -WITHIN a few minutes the news had spread, and a dozen skiff-loads of -men were on their way to McDougal's cave, and the ferryboat, well -filled with passengers, soon followed. Tom Sawyer was in the skiff that -bore Judge Thatcher. - -When the cave door was unlocked, a sorrowful sight presented itself in -the dim twilight of the place. Injun Joe lay stretched upon the ground, -dead, with his face close to the crack of the door, as if his longing -eyes had been fixed, to the latest moment, upon the light and the cheer -of the free world outside. Tom was touched, for he knew by his own -experience how this wretch had suffered. His pity was moved, but -nevertheless he felt an abounding sense of relief and security, now, -which revealed to him in a degree which he had not fully appreciated -before how vast a weight of dread had been lying upon him since the day -he lifted his voice against this bloody-minded outcast. - -Injun Joe's bowie-knife lay close by, its blade broken in two. The -great foundation-beam of the door had been chipped and hacked through, -with tedious labor; useless labor, too, it was, for the native rock -formed a sill outside it, and upon that stubborn material the knife had -wrought no effect; the only damage done was to the knife itself. But if -there had been no stony obstruction there the labor would have been -useless still, for if the beam had been wholly cut away Injun Joe could -not have squeezed his body under the door, and he knew it. So he had -only hacked that place in order to be doing something--in order to pass -the weary time--in order to employ his tortured faculties. Ordinarily -one could find half a dozen bits of candle stuck around in the crevices -of this vestibule, left there by tourists; but there were none now. The -prisoner had searched them out and eaten them. He had also contrived to -catch a few bats, and these, also, he had eaten, leaving only their -claws. The poor unfortunate had starved to death. In one place, near at -hand, a stalagmite had been slowly growing up from the ground for ages, -builded by the water-drip from a stalactite overhead. The captive had -broken off the stalagmite, and upon the stump had placed a stone, -wherein he had scooped a shallow hollow to catch the precious drop -that fell once in every three minutes with the dreary regularity of a -clock-tick--a dessertspoonful once in four and twenty hours. That drop -was falling when the Pyramids were new; when Troy fell; when the -foundations of Rome were laid; when Christ was crucified; when the -Conqueror created the British empire; when Columbus sailed; when the -massacre at Lexington was "news." It is falling now; it will still be -falling when all these things shall have sunk down the afternoon of -history, and the twilight of tradition, and been swallowed up in the -thick night of oblivion. Has everything a purpose and a mission? Did -this drop fall patiently during five thousand years to be ready for -this flitting human insect's need? and has it another important object -to accomplish ten thousand years to come? No matter. It is many and -many a year since the hapless half-breed scooped out the stone to catch -the priceless drops, but to this day the tourist stares longest at that -pathetic stone and that slow-dropping water when he comes to see the -wonders of McDougal's cave. Injun Joe's cup stands first in the list of -the cavern's marvels; even "Aladdin's Palace" cannot rival it. - -Injun Joe was buried near the mouth of the cave; and people flocked -there in boats and wagons from the towns and from all the farms and -hamlets for seven miles around; they brought their children, and all -sorts of provisions, and confessed that they had had almost as -satisfactory a time at the funeral as they could have had at the -hanging. - -This funeral stopped the further growth of one thing--the petition to -the governor for Injun Joe's pardon. The petition had been largely -signed; many tearful and eloquent meetings had been held, and a -committee of sappy women been appointed to go in deep mourning and wail -around the governor, and implore him to be a merciful ass and trample -his duty under foot. Injun Joe was believed to have killed five -citizens of the village, but what of that? If he had been Satan himself -there would have been plenty of weaklings ready to scribble their names -to a pardon-petition, and drip a tear on it from their permanently -impaired and leaky water-works. - -The morning after the funeral Tom took Huck to a private place to have -an important talk. Huck had learned all about Tom's adventure from the -Welshman and the Widow Douglas, by this time, but Tom said he reckoned -there was one thing they had not told him; that thing was what he -wanted to talk about now. Huck's face saddened. He said: - -"I know what it is. You got into No. 2 and never found anything but -whiskey. Nobody told me it was you; but I just knowed it must 'a' ben -you, soon as I heard 'bout that whiskey business; and I knowed you -hadn't got the money becuz you'd 'a' got at me some way or other and -told me even if you was mum to everybody else. Tom, something's always -told me we'd never get holt of that swag." - -"Why, Huck, I never told on that tavern-keeper. YOU know his tavern -was all right the Saturday I went to the picnic. Don't you remember you -was to watch there that night?" - -"Oh yes! Why, it seems 'bout a year ago. It was that very night that I -follered Injun Joe to the widder's." - -"YOU followed him?" - -"Yes--but you keep mum. I reckon Injun Joe's left friends behind him, -and I don't want 'em souring on me and doing me mean tricks. If it -hadn't ben for me he'd be down in Texas now, all right." - -Then Huck told his entire adventure in confidence to Tom, who had only -heard of the Welshman's part of it before. - -"Well," said Huck, presently, coming back to the main question, -"whoever nipped the whiskey in No. 2, nipped the money, too, I reckon ---anyways it's a goner for us, Tom." - -"Huck, that money wasn't ever in No. 2!" - -"What!" Huck searched his comrade's face keenly. "Tom, have you got on -the track of that money again?" - -"Huck, it's in the cave!" - -Huck's eyes blazed. - -"Say it again, Tom." - -"The money's in the cave!" - -"Tom--honest injun, now--is it fun, or earnest?" - -"Earnest, Huck--just as earnest as ever I was in my life. Will you go -in there with me and help get it out?" - -"I bet I will! I will if it's where we can blaze our way to it and not -get lost." - -"Huck, we can do that without the least little bit of trouble in the -world." - -"Good as wheat! What makes you think the money's--" - -"Huck, you just wait till we get in there. If we don't find it I'll -agree to give you my drum and every thing I've got in the world. I -will, by jings." - -"All right--it's a whiz. When do you say?" - -"Right now, if you say it. Are you strong enough?" - -"Is it far in the cave? I ben on my pins a little, three or four days, -now, but I can't walk more'n a mile, Tom--least I don't think I could." - -"It's about five mile into there the way anybody but me would go, -Huck, but there's a mighty short cut that they don't anybody but me -know about. Huck, I'll take you right to it in a skiff. I'll float the -skiff down there, and I'll pull it back again all by myself. You -needn't ever turn your hand over." - -"Less start right off, Tom." - -"All right. We want some bread and meat, and our pipes, and a little -bag or two, and two or three kite-strings, and some of these -new-fangled things they call lucifer matches. I tell you, many's -the time I wished I had some when I was in there before." - -A trifle after noon the boys borrowed a small skiff from a citizen who -was absent, and got under way at once. When they were several miles -below "Cave Hollow," Tom said: - -"Now you see this bluff here looks all alike all the way down from the -cave hollow--no houses, no wood-yards, bushes all alike. But do you see -that white place up yonder where there's been a landslide? Well, that's -one of my marks. We'll get ashore, now." - -They landed. - -"Now, Huck, where we're a-standing you could touch that hole I got out -of with a fishing-pole. See if you can find it." - -Huck searched all the place about, and found nothing. Tom proudly -marched into a thick clump of sumach bushes and said: - -"Here you are! Look at it, Huck; it's the snuggest hole in this -country. You just keep mum about it. All along I've been wanting to be -a robber, but I knew I'd got to have a thing like this, and where to -run across it was the bother. We've got it now, and we'll keep it -quiet, only we'll let Joe Harper and Ben Rogers in--because of course -there's got to be a Gang, or else there wouldn't be any style about it. -Tom Sawyer's Gang--it sounds splendid, don't it, Huck?" - -"Well, it just does, Tom. And who'll we rob?" - -"Oh, most anybody. Waylay people--that's mostly the way." - -"And kill them?" - -"No, not always. Hive them in the cave till they raise a ransom." - -"What's a ransom?" - -"Money. You make them raise all they can, off'n their friends; and -after you've kept them a year, if it ain't raised then you kill them. -That's the general way. Only you don't kill the women. You shut up the -women, but you don't kill them. They're always beautiful and rich, and -awfully scared. You take their watches and things, but you always take -your hat off and talk polite. They ain't anybody as polite as robbers ---you'll see that in any book. Well, the women get to loving you, and -after they've been in the cave a week or two weeks they stop crying and -after that you couldn't get them to leave. If you drove them out they'd -turn right around and come back. It's so in all the books." - -"Why, it's real bully, Tom. I believe it's better'n to be a pirate." - -"Yes, it's better in some ways, because it's close to home and -circuses and all that." - -By this time everything was ready and the boys entered the hole, Tom -in the lead. They toiled their way to the farther end of the tunnel, -then made their spliced kite-strings fast and moved on. A few steps -brought them to the spring, and Tom felt a shudder quiver all through -him. He showed Huck the fragment of candle-wick perched on a lump of -clay against the wall, and described how he and Becky had watched the -flame struggle and expire. - -The boys began to quiet down to whispers, now, for the stillness and -gloom of the place oppressed their spirits. They went on, and presently -entered and followed Tom's other corridor until they reached the -"jumping-off place." The candles revealed the fact that it was not -really a precipice, but only a steep clay hill twenty or thirty feet -high. Tom whispered: - -"Now I'll show you something, Huck." - -He held his candle aloft and said: - -"Look as far around the corner as you can. Do you see that? There--on -the big rock over yonder--done with candle-smoke." - -"Tom, it's a CROSS!" - -"NOW where's your Number Two? 'UNDER THE CROSS,' hey? Right yonder's -where I saw Injun Joe poke up his candle, Huck!" - -Huck stared at the mystic sign awhile, and then said with a shaky voice: - -"Tom, less git out of here!" - -"What! and leave the treasure?" - -"Yes--leave it. Injun Joe's ghost is round about there, certain." - -"No it ain't, Huck, no it ain't. It would ha'nt the place where he -died--away out at the mouth of the cave--five mile from here." - -"No, Tom, it wouldn't. It would hang round the money. I know the ways -of ghosts, and so do you." - -Tom began to fear that Huck was right. Misgivings gathered in his -mind. But presently an idea occurred to him-- - -"Lookyhere, Huck, what fools we're making of ourselves! Injun Joe's -ghost ain't a going to come around where there's a cross!" - -The point was well taken. It had its effect. - -"Tom, I didn't think of that. But that's so. It's luck for us, that -cross is. I reckon we'll climb down there and have a hunt for that box." - -Tom went first, cutting rude steps in the clay hill as he descended. -Huck followed. Four avenues opened out of the small cavern which the -great rock stood in. The boys examined three of them with no result. -They found a small recess in the one nearest the base of the rock, with -a pallet of blankets spread down in it; also an old suspender, some -bacon rind, and the well-gnawed bones of two or three fowls. But there -was no money-box. The lads searched and researched this place, but in -vain. Tom said: - -"He said UNDER the cross. Well, this comes nearest to being under the -cross. It can't be under the rock itself, because that sets solid on -the ground." - -They searched everywhere once more, and then sat down discouraged. -Huck could suggest nothing. By-and-by Tom said: - -"Lookyhere, Huck, there's footprints and some candle-grease on the -clay about one side of this rock, but not on the other sides. Now, -what's that for? I bet you the money IS under the rock. I'm going to -dig in the clay." - -"That ain't no bad notion, Tom!" said Huck with animation. - -Tom's "real Barlow" was out at once, and he had not dug four inches -before he struck wood. - -"Hey, Huck!--you hear that?" - -Huck began to dig and scratch now. Some boards were soon uncovered and -removed. They had concealed a natural chasm which led under the rock. -Tom got into this and held his candle as far under the rock as he -could, but said he could not see to the end of the rift. He proposed to -explore. He stooped and passed under; the narrow way descended -gradually. He followed its winding course, first to the right, then to -the left, Huck at his heels. Tom turned a short curve, by-and-by, and -exclaimed: - -"My goodness, Huck, lookyhere!" - -It was the treasure-box, sure enough, occupying a snug little cavern, -along with an empty powder-keg, a couple of guns in leather cases, two -or three pairs of old moccasins, a leather belt, and some other rubbish -well soaked with the water-drip. - -"Got it at last!" said Huck, ploughing among the tarnished coins with -his hand. "My, but we're rich, Tom!" - -"Huck, I always reckoned we'd get it. It's just too good to believe, -but we HAVE got it, sure! Say--let's not fool around here. Let's snake -it out. Lemme see if I can lift the box." - -It weighed about fifty pounds. Tom could lift it, after an awkward -fashion, but could not carry it conveniently. - -"I thought so," he said; "THEY carried it like it was heavy, that day -at the ha'nted house. I noticed that. I reckon I was right to think of -fetching the little bags along." - -The money was soon in the bags and the boys took it up to the cross -rock. - -"Now less fetch the guns and things," said Huck. - -"No, Huck--leave them there. They're just the tricks to have when we -go to robbing. We'll keep them there all the time, and we'll hold our -orgies there, too. It's an awful snug place for orgies." - -"What orgies?" - -"I dono. But robbers always have orgies, and of course we've got to -have them, too. Come along, Huck, we've been in here a long time. It's -getting late, I reckon. I'm hungry, too. We'll eat and smoke when we -get to the skiff." - -They presently emerged into the clump of sumach bushes, looked warily -out, found the coast clear, and were soon lunching and smoking in the -skiff. As the sun dipped toward the horizon they pushed out and got -under way. Tom skimmed up the shore through the long twilight, chatting -cheerily with Huck, and landed shortly after dark. - -"Now, Huck," said Tom, "we'll hide the money in the loft of the -widow's woodshed, and I'll come up in the morning and we'll count it -and divide, and then we'll hunt up a place out in the woods for it -where it will be safe. Just you lay quiet here and watch the stuff till -I run and hook Benny Taylor's little wagon; I won't be gone a minute." - -He disappeared, and presently returned with the wagon, put the two -small sacks into it, threw some old rags on top of them, and started -off, dragging his cargo behind him. When the boys reached the -Welshman's house, they stopped to rest. Just as they were about to move -on, the Welshman stepped out and said: - -"Hallo, who's that?" - -"Huck and Tom Sawyer." - -"Good! Come along with me, boys, you are keeping everybody waiting. -Here--hurry up, trot ahead--I'll haul the wagon for you. Why, it's not -as light as it might be. Got bricks in it?--or old metal?" - -"Old metal," said Tom. - -"I judged so; the boys in this town will take more trouble and fool -away more time hunting up six bits' worth of old iron to sell to the -foundry than they would to make twice the money at regular work. But -that's human nature--hurry along, hurry along!" - -The boys wanted to know what the hurry was about. - -"Never mind; you'll see, when we get to the Widow Douglas'." - -Huck said with some apprehension--for he was long used to being -falsely accused: - -"Mr. Jones, we haven't been doing nothing." - -The Welshman laughed. - -"Well, I don't know, Huck, my boy. I don't know about that. Ain't you -and the widow good friends?" - -"Yes. Well, she's ben good friends to me, anyway." - -"All right, then. What do you want to be afraid for?" - -This question was not entirely answered in Huck's slow mind before he -found himself pushed, along with Tom, into Mrs. Douglas' drawing-room. -Mr. Jones left the wagon near the door and followed. - -The place was grandly lighted, and everybody that was of any -consequence in the village was there. The Thatchers were there, the -Harpers, the Rogerses, Aunt Polly, Sid, Mary, the minister, the editor, -and a great many more, and all dressed in their best. The widow -received the boys as heartily as any one could well receive two such -looking beings. They were covered with clay and candle-grease. Aunt -Polly blushed crimson with humiliation, and frowned and shook her head -at Tom. Nobody suffered half as much as the two boys did, however. Mr. -Jones said: - -"Tom wasn't at home, yet, so I gave him up; but I stumbled on him and -Huck right at my door, and so I just brought them along in a hurry." - -"And you did just right," said the widow. "Come with me, boys." - -She took them to a bedchamber and said: - -"Now wash and dress yourselves. Here are two new suits of clothes ---shirts, socks, everything complete. They're Huck's--no, no thanks, -Huck--Mr. Jones bought one and I the other. But they'll fit both of you. -Get into them. We'll wait--come down when you are slicked up enough." - -Then she left. - - - -CHAPTER XXXIV - -HUCK said: "Tom, we can slope, if we can find a rope. The window ain't -high from the ground." - -"Shucks! what do you want to slope for?" - -"Well, I ain't used to that kind of a crowd. I can't stand it. I ain't -going down there, Tom." - -"Oh, bother! It ain't anything. I don't mind it a bit. I'll take care -of you." - -Sid appeared. - -"Tom," said he, "auntie has been waiting for you all the afternoon. -Mary got your Sunday clothes ready, and everybody's been fretting about -you. Say--ain't this grease and clay, on your clothes?" - -"Now, Mr. Siddy, you jist 'tend to your own business. What's all this -blow-out about, anyway?" - -"It's one of the widow's parties that she's always having. This time -it's for the Welshman and his sons, on account of that scrape they -helped her out of the other night. And say--I can tell you something, -if you want to know." - -"Well, what?" - -"Why, old Mr. Jones is going to try to spring something on the people -here to-night, but I overheard him tell auntie to-day about it, as a -secret, but I reckon it's not much of a secret now. Everybody knows ---the widow, too, for all she tries to let on she don't. Mr. Jones was -bound Huck should be here--couldn't get along with his grand secret -without Huck, you know!" - -"Secret about what, Sid?" - -"About Huck tracking the robbers to the widow's. I reckon Mr. Jones -was going to make a grand time over his surprise, but I bet you it will -drop pretty flat." - -Sid chuckled in a very contented and satisfied way. - -"Sid, was it you that told?" - -"Oh, never mind who it was. SOMEBODY told--that's enough." - -"Sid, there's only one person in this town mean enough to do that, and -that's you. If you had been in Huck's place you'd 'a' sneaked down the -hill and never told anybody on the robbers. You can't do any but mean -things, and you can't bear to see anybody praised for doing good ones. -There--no thanks, as the widow says"--and Tom cuffed Sid's ears and -helped him to the door with several kicks. "Now go and tell auntie if -you dare--and to-morrow you'll catch it!" - -Some minutes later the widow's guests were at the supper-table, and a -dozen children were propped up at little side-tables in the same room, -after the fashion of that country and that day. At the proper time Mr. -Jones made his little speech, in which he thanked the widow for the -honor she was doing himself and his sons, but said that there was -another person whose modesty-- - -And so forth and so on. He sprung his secret about Huck's share in the -adventure in the finest dramatic manner he was master of, but the -surprise it occasioned was largely counterfeit and not as clamorous and -effusive as it might have been under happier circumstances. However, -the widow made a pretty fair show of astonishment, and heaped so many -compliments and so much gratitude upon Huck that he almost forgot the -nearly intolerable discomfort of his new clothes in the entirely -intolerable discomfort of being set up as a target for everybody's gaze -and everybody's laudations. - -The widow said she meant to give Huck a home under her roof and have -him educated; and that when she could spare the money she would start -him in business in a modest way. Tom's chance was come. He said: - -"Huck don't need it. Huck's rich." - -Nothing but a heavy strain upon the good manners of the company kept -back the due and proper complimentary laugh at this pleasant joke. But -the silence was a little awkward. Tom broke it: - -"Huck's got money. Maybe you don't believe it, but he's got lots of -it. Oh, you needn't smile--I reckon I can show you. You just wait a -minute." - -Tom ran out of doors. The company looked at each other with a -perplexed interest--and inquiringly at Huck, who was tongue-tied. - -"Sid, what ails Tom?" said Aunt Polly. "He--well, there ain't ever any -making of that boy out. I never--" - -Tom entered, struggling with the weight of his sacks, and Aunt Polly -did not finish her sentence. Tom poured the mass of yellow coin upon -the table and said: - -"There--what did I tell you? Half of it's Huck's and half of it's mine!" - -The spectacle took the general breath away. All gazed, nobody spoke -for a moment. Then there was a unanimous call for an explanation. Tom -said he could furnish it, and he did. The tale was long, but brimful of -interest. There was scarcely an interruption from any one to break the -charm of its flow. When he had finished, Mr. Jones said: - -"I thought I had fixed up a little surprise for this occasion, but it -don't amount to anything now. This one makes it sing mighty small, I'm -willing to allow." - -The money was counted. The sum amounted to a little over twelve -thousand dollars. It was more than any one present had ever seen at one -time before, though several persons were there who were worth -considerably more than that in property. - - - -CHAPTER XXXV - -THE reader may rest satisfied that Tom's and Huck's windfall made a -mighty stir in the poor little village of St. Petersburg. So vast a -sum, all in actual cash, seemed next to incredible. It was talked -about, gloated over, glorified, until the reason of many of the -citizens tottered under the strain of the unhealthy excitement. Every -"haunted" house in St. Petersburg and the neighboring villages was -dissected, plank by plank, and its foundations dug up and ransacked for -hidden treasure--and not by boys, but men--pretty grave, unromantic -men, too, some of them. Wherever Tom and Huck appeared they were -courted, admired, stared at. The boys were not able to remember that -their remarks had possessed weight before; but now their sayings were -treasured and repeated; everything they did seemed somehow to be -regarded as remarkable; they had evidently lost the power of doing and -saying commonplace things; moreover, their past history was raked up -and discovered to bear marks of conspicuous originality. The village -paper published biographical sketches of the boys. - -The Widow Douglas put Huck's money out at six per cent., and Judge -Thatcher did the same with Tom's at Aunt Polly's request. Each lad had -an income, now, that was simply prodigious--a dollar for every week-day -in the year and half of the Sundays. It was just what the minister got ---no, it was what he was promised--he generally couldn't collect it. A -dollar and a quarter a week would board, lodge, and school a boy in -those old simple days--and clothe him and wash him, too, for that -matter. - -Judge Thatcher had conceived a great opinion of Tom. He said that no -commonplace boy would ever have got his daughter out of the cave. When -Becky told her father, in strict confidence, how Tom had taken her -whipping at school, the Judge was visibly moved; and when she pleaded -grace for the mighty lie which Tom had told in order to shift that -whipping from her shoulders to his own, the Judge said with a fine -outburst that it was a noble, a generous, a magnanimous lie--a lie that -was worthy to hold up its head and march down through history breast to -breast with George Washington's lauded Truth about the hatchet! Becky -thought her father had never looked so tall and so superb as when he -walked the floor and stamped his foot and said that. She went straight -off and told Tom about it. - -Judge Thatcher hoped to see Tom a great lawyer or a great soldier some -day. He said he meant to look to it that Tom should be admitted to the -National Military Academy and afterward trained in the best law school -in the country, in order that he might be ready for either career or -both. - -Huck Finn's wealth and the fact that he was now under the Widow -Douglas' protection introduced him into society--no, dragged him into -it, hurled him into it--and his sufferings were almost more than he -could bear. The widow's servants kept him clean and neat, combed and -brushed, and they bedded him nightly in unsympathetic sheets that had -not one little spot or stain which he could press to his heart and know -for a friend. He had to eat with a knife and fork; he had to use -napkin, cup, and plate; he had to learn his book, he had to go to -church; he had to talk so properly that speech was become insipid in -his mouth; whithersoever he turned, the bars and shackles of -civilization shut him in and bound him hand and foot. - -He bravely bore his miseries three weeks, and then one day turned up -missing. For forty-eight hours the widow hunted for him everywhere in -great distress. The public were profoundly concerned; they searched -high and low, they dragged the river for his body. Early the third -morning Tom Sawyer wisely went poking among some old empty hogsheads -down behind the abandoned slaughter-house, and in one of them he found -the refugee. Huck had slept there; he had just breakfasted upon some -stolen odds and ends of food, and was lying off, now, in comfort, with -his pipe. He was unkempt, uncombed, and clad in the same old ruin of -rags that had made him picturesque in the days when he was free and -happy. Tom routed him out, told him the trouble he had been causing, -and urged him to go home. Huck's face lost its tranquil content, and -took a melancholy cast. He said: - -"Don't talk about it, Tom. I've tried it, and it don't work; it don't -work, Tom. It ain't for me; I ain't used to it. The widder's good to -me, and friendly; but I can't stand them ways. She makes me get up just -at the same time every morning; she makes me wash, they comb me all to -thunder; she won't let me sleep in the woodshed; I got to wear them -blamed clothes that just smothers me, Tom; they don't seem to any air -git through 'em, somehow; and they're so rotten nice that I can't set -down, nor lay down, nor roll around anywher's; I hain't slid on a -cellar-door for--well, it 'pears to be years; I got to go to church and -sweat and sweat--I hate them ornery sermons! I can't ketch a fly in -there, I can't chaw. I got to wear shoes all Sunday. The widder eats by -a bell; she goes to bed by a bell; she gits up by a bell--everything's -so awful reg'lar a body can't stand it." - -"Well, everybody does that way, Huck." - -"Tom, it don't make no difference. I ain't everybody, and I can't -STAND it. It's awful to be tied up so. And grub comes too easy--I don't -take no interest in vittles, that way. I got to ask to go a-fishing; I -got to ask to go in a-swimming--dern'd if I hain't got to ask to do -everything. Well, I'd got to talk so nice it wasn't no comfort--I'd got -to go up in the attic and rip out awhile, every day, to git a taste in -my mouth, or I'd a died, Tom. The widder wouldn't let me smoke; she -wouldn't let me yell, she wouldn't let me gape, nor stretch, nor -scratch, before folks--" [Then with a spasm of special irritation and -injury]--"And dad fetch it, she prayed all the time! I never see such a -woman! I HAD to shove, Tom--I just had to. And besides, that school's -going to open, and I'd a had to go to it--well, I wouldn't stand THAT, -Tom. Looky here, Tom, being rich ain't what it's cracked up to be. It's -just worry and worry, and sweat and sweat, and a-wishing you was dead -all the time. Now these clothes suits me, and this bar'l suits me, and -I ain't ever going to shake 'em any more. Tom, I wouldn't ever got into -all this trouble if it hadn't 'a' ben for that money; now you just take -my sheer of it along with your'n, and gimme a ten-center sometimes--not -many times, becuz I don't give a dern for a thing 'thout it's tollable -hard to git--and you go and beg off for me with the widder." - -"Oh, Huck, you know I can't do that. 'Tain't fair; and besides if -you'll try this thing just a while longer you'll come to like it." - -"Like it! Yes--the way I'd like a hot stove if I was to set on it long -enough. No, Tom, I won't be rich, and I won't live in them cussed -smothery houses. I like the woods, and the river, and hogsheads, and -I'll stick to 'em, too. Blame it all! just as we'd got guns, and a -cave, and all just fixed to rob, here this dern foolishness has got to -come up and spile it all!" - -Tom saw his opportunity-- - -"Lookyhere, Huck, being rich ain't going to keep me back from turning -robber." - -"No! Oh, good-licks; are you in real dead-wood earnest, Tom?" - -"Just as dead earnest as I'm sitting here. But Huck, we can't let you -into the gang if you ain't respectable, you know." - -Huck's joy was quenched. - -"Can't let me in, Tom? Didn't you let me go for a pirate?" - -"Yes, but that's different. A robber is more high-toned than what a -pirate is--as a general thing. In most countries they're awful high up -in the nobility--dukes and such." - -"Now, Tom, hain't you always ben friendly to me? You wouldn't shet me -out, would you, Tom? You wouldn't do that, now, WOULD you, Tom?" - -"Huck, I wouldn't want to, and I DON'T want to--but what would people -say? Why, they'd say, 'Mph! Tom Sawyer's Gang! pretty low characters in -it!' They'd mean you, Huck. You wouldn't like that, and I wouldn't." - -Huck was silent for some time, engaged in a mental struggle. Finally -he said: - -"Well, I'll go back to the widder for a month and tackle it and see if -I can come to stand it, if you'll let me b'long to the gang, Tom." - -"All right, Huck, it's a whiz! Come along, old chap, and I'll ask the -widow to let up on you a little, Huck." - -"Will you, Tom--now will you? That's good. If she'll let up on some of -the roughest things, I'll smoke private and cuss private, and crowd -through or bust. When you going to start the gang and turn robbers?" - -"Oh, right off. We'll get the boys together and have the initiation -to-night, maybe." - -"Have the which?" - -"Have the initiation." - -"What's that?" - -"It's to swear to stand by one another, and never tell the gang's -secrets, even if you're chopped all to flinders, and kill anybody and -all his family that hurts one of the gang." - -"That's gay--that's mighty gay, Tom, I tell you." - -"Well, I bet it is. And all that swearing's got to be done at -midnight, in the lonesomest, awfulest place you can find--a ha'nted -house is the best, but they're all ripped up now." - -"Well, midnight's good, anyway, Tom." - -"Yes, so it is. And you've got to swear on a coffin, and sign it with -blood." - -"Now, that's something LIKE! Why, it's a million times bullier than -pirating. I'll stick to the widder till I rot, Tom; and if I git to be -a reg'lar ripper of a robber, and everybody talking 'bout it, I reckon -she'll be proud she snaked me in out of the wet." - - - -CONCLUSION - -SO endeth this chronicle. It being strictly a history of a BOY, it -must stop here; the story could not go much further without becoming -the history of a MAN. When one writes a novel about grown people, he -knows exactly where to stop--that is, with a marriage; but when he -writes of juveniles, he must stop where he best can. - -Most of the characters that perform in this book still live, and are -prosperous and happy. Some day it may seem worth while to take up the -story of the younger ones again and see what sort of men and women they -turned out to be; therefore it will be wisest not to reveal any of that -part of their lives at present. - - - - - -End of the Project Gutenberg EBook of The Adventures of Tom Sawyer, Complete -by Mark Twain (Samuel Clemens) diff --git a/internal/compress/testdata/case1.bin b/internal/compress/testdata/case1.bin deleted file mode 100644 index 723b4bc2..00000000 Binary files a/internal/compress/testdata/case1.bin and /dev/null differ diff --git a/internal/compress/testdata/case2.bin b/internal/compress/testdata/case2.bin deleted file mode 100644 index c34928f3..00000000 --- a/internal/compress/testdata/case2.bin +++ /dev/null @@ -1 +0,0 @@ -5555555555555054072307277407224532200600565 \ No newline at end of file diff --git a/internal/compress/testdata/case3.bin b/internal/compress/testdata/case3.bin deleted file mode 100644 index c44f88d4..00000000 --- a/internal/compress/testdata/case3.bin +++ /dev/null @@ -1 +0,0 @@ -1502302578000010610834044101581512311545562525 \ No newline at end of file diff --git a/internal/compress/testdata/crash1.bin b/internal/compress/testdata/crash1.bin deleted file mode 100644 index d483846c..00000000 Binary files a/internal/compress/testdata/crash1.bin and /dev/null differ diff --git a/internal/compress/testdata/crash2.bin b/internal/compress/testdata/crash2.bin deleted file mode 100644 index 721a6531..00000000 --- a/internal/compress/testdata/crash2.bin +++ /dev/null @@ -1 +0,0 @@ -313254615470505 \ No newline at end of file diff --git a/internal/compress/testdata/crash3.bin b/internal/compress/testdata/crash3.bin deleted file mode 100644 index 3e3d493a..00000000 --- a/internal/compress/testdata/crash3.bin +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/internal/compress/testdata/crash4.bin b/internal/compress/testdata/crash4.bin deleted file mode 100644 index 725b093f..00000000 --- a/internal/compress/testdata/crash4.bin +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/internal/compress/testdata/crash5.bin b/internal/compress/testdata/crash5.bin deleted file mode 100644 index a521217b..00000000 --- a/internal/compress/testdata/crash5.bin +++ /dev/null @@ -1 +0,0 @@ -55511151231257827021181583404541015625 \ No newline at end of file diff --git a/internal/compress/testdata/dec-crash6.bin b/internal/compress/testdata/dec-crash6.bin deleted file mode 100644 index d7832124..00000000 Binary files a/internal/compress/testdata/dec-crash6.bin and /dev/null differ diff --git a/internal/compress/testdata/dec-hang1.bin b/internal/compress/testdata/dec-hang1.bin deleted file mode 100644 index e07926fb..00000000 --- a/internal/compress/testdata/dec-hang1.bin +++ /dev/null @@ -1 +0,0 @@ -$ \ No newline at end of file diff --git a/internal/compress/testdata/dec-hang2.bin b/internal/compress/testdata/dec-hang2.bin deleted file mode 100644 index c1484ef8..00000000 --- a/internal/compress/testdata/dec-hang2.bin +++ /dev/null @@ -1 +0,0 @@ -|CO \ No newline at end of file diff --git a/internal/compress/testdata/dec-hang3.bin b/internal/compress/testdata/dec-hang3.bin deleted file mode 100644 index 3bd97e9c..00000000 --- a/internal/compress/testdata/dec-hang3.bin +++ /dev/null @@ -1 +0,0 @@ -VV \ No newline at end of file diff --git a/internal/compress/testdata/dec-symlen1.bin b/internal/compress/testdata/dec-symlen1.bin deleted file mode 100644 index c5589c99..00000000 Binary files a/internal/compress/testdata/dec-symlen1.bin and /dev/null differ diff --git a/internal/compress/testdata/e.txt b/internal/compress/testdata/e.txt deleted file mode 100644 index 5ca186f1..00000000 --- a/internal/compress/testdata/e.txt +++ /dev/null @@ -1 +0,0 @@ -2.7182818284590452353602874713526624977572470936999595749669676277240766303535475945713821785251664274274663919320030599218174135966290435729003342952605956307381323286279434907632338298807531952510190115738341879307021540891499348841675092447614606680822648001684774118537423454424371075390777449920695517027618386062613313845830007520449338265602976067371132007093287091274437470472306969772093101416928368190255151086574637721112523897844250569536967707854499699679468644549059879316368892300987931277361782154249992295763514822082698951936680331825288693984964651058209392398294887933203625094431173012381970684161403970198376793206832823764648042953118023287825098194558153017567173613320698112509961818815930416903515988885193458072738667385894228792284998920868058257492796104841984443634632449684875602336248270419786232090021609902353043699418491463140934317381436405462531520961836908887070167683964243781405927145635490613031072085103837505101157477041718986106873969655212671546889570350354021234078498193343210681701210056278802351930332247450158539047304199577770935036604169973297250886876966403555707162268447162560798826517871341951246652010305921236677194325278675398558944896970964097545918569563802363701621120477427228364896134225164450781824423529486363721417402388934412479635743702637552944483379980161254922785092577825620926226483262779333865664816277251640191059004916449982893150566047258027786318641551956532442586982946959308019152987211725563475463964479101459040905862984967912874068705048958586717479854667757573205681288459205413340539220001137863009455606881667400169842055804033637953764520304024322566135278369511778838638744396625322498506549958862342818997077332761717839280349465014345588970719425863987727547109629537415211151368350627526023264847287039207643100595841166120545297030236472549296669381151373227536450988890313602057248176585118063036442812314965507047510254465011727211555194866850800368532281831521960037356252794495158284188294787610852639813955990067376482922443752871846245780361929819713991475644882626039033814418232625150974827987779964373089970388867782271383605772978824125611907176639465070633045279546618550966661856647097113444740160704626215680717481877844371436988218559670959102596862002353718588748569652200050311734392073211390803293634479727355955277349071783793421637012050054513263835440001863239914907054797780566978533580489669062951194324730995876552368128590413832411607226029983305353708761389396391779574540161372236187893652605381558415871869255386061647798340254351284396129460352913325942794904337299085731580290958631382683291477116396337092400316894586360606458459251269946557248391865642097526850823075442545993769170419777800853627309417101634349076964237222943523661255725088147792231519747780605696725380171807763603462459278778465850656050780844211529697521890874019660906651803516501792504619501366585436632712549639908549144200014574760819302212066024330096412704894390397177195180699086998606636583232278709376502260149291011517177635944602023249300280401867723910288097866605651183260043688508817157238669842242201024950551881694803221002515426494639812873677658927688163598312477886520141174110913601164995076629077943646005851941998560162647907615321038727557126992518275687989302761761146162549356495903798045838182323368612016243736569846703785853305275833337939907521660692380533698879565137285593883499894707416181550125397064648171946708348197214488898790676503795903669672494992545279033729636162658976039498576741397359441023744329709355477982629614591442936451428617158587339746791897571211956187385783644758448423555581050025611492391518893099463428413936080383091662818811503715284967059741625628236092168075150177725387402564253470879089137291722828611515915683725241630772254406337875931059826760944203261924285317018781772960235413060672136046000389661093647095141417185777014180606443636815464440053316087783143174440811949422975599314011888683314832802706553833004693290115744147563139997221703804617092894579096271662260740718749975359212756084414737823303270330168237193648002173285734935947564334129943024850235732214597843282641421684878721673367010615094243456984401873312810107945127223737886126058165668053714396127888732527373890392890506865324138062796025930387727697783792868409325365880733988457218746021005311483351323850047827169376218004904795597959290591655470505777514308175112698985188408718564026035305583737832422924185625644255022672155980274012617971928047139600689163828665277009752767069777036439260224372841840883251848770472638440379530166905465937461619323840363893131364327137688841026811219891275223056256756254701725086349765367288605966752740868627407912856576996313789753034660616669804218267724560530660773899624218340859882071864682623215080288286359746839654358856685503773131296587975810501214916207656769950659715344763470320853215603674828608378656803073062657633469774295634643716709397193060876963495328846833613038829431040800296873869117066666146800015121143442256023874474325250769387077775193299942137277211258843608715834835626961661980572526612206797540621062080649882918454395301529982092503005498257043390553570168653120526495614857249257386206917403695213533732531666345466588597286659451136441370331393672118569553952108458407244323835586063106806964924851232632699514603596037297253198368423363904632136710116192821711150282801604488058802382031981493096369596735832742024988245684941273860566491352526706046234450549227581151709314921879592718001940968866986837037302200475314338181092708030017205935530520700706072233999463990571311587099635777359027196285061146514837526209565346713290025994397663114545902685898979115837093419370441155121920117164880566945938131183843765620627846310490346293950029458341164824114969758326011800731699437393506966295712410273239138741754923071862454543222039552735295240245903805744502892246886285336542213815722131163288112052146489805180092024719391710555390113943316681515828843687606961102505171007392762385553386272553538830960671644662370922646809671254061869502143176211668140097595281493907222601112681153108387317617323235263605838173151034595736538223534992935822836851007810884634349983518404451704270189381994243410090575376257767571118090088164183319201962623416288166521374717325477727783488774366518828752156685719506371936565390389449366421764003121527870222366463635755503565576948886549500270853923617105502131147413744106134445544192101336172996285694899193369184729478580729156088510396781959429833186480756083679551496636448965592948187851784038773326247051945050419847742014183947731202815886845707290544057510601285258056594703046836344592652552137008068752009593453607316226118728173928074623094685367823106097921599360019946237993434210687813497346959246469752506246958616909178573976595199392993995567542714654910456860702099012606818704984178079173924071945996323060254707901774527513186809982284730860766536866855516467702911336827563107223346726113705490795365834538637196235856312618387156774118738527722922594743373785695538456246801013905727871016512966636764451872465653730402443684140814488732957847348490003019477888020460324660842875351848364959195082888323206522128104190448047247949291342284951970022601310430062410717971502793433263407995960531446053230488528972917659876016667811937932372453857209607582277178483361613582612896226118129455927462767137794487586753657544861407611931125958512655759734573015333642630767985443385761715333462325270572005303988289499034259566232975782488735029259166825894456894655992658454762694528780516501720674785417887982276806536650641910973434528878338621726156269582654478205672987756426325321594294418039943217000090542650763095588465895171709147607437136893319469090981904501290307099566226620303182649365733698419555776963787624918852865686607600566025605445711337286840205574416030837052312242587223438854123179481388550075689381124935386318635287083799845692619981794523364087429591180747453419551420351726184200845509170845682368200897739455842679214273477560879644279202708312150156406341341617166448069815483764491573900121217041547872591998943825364950514771379399147205219529079396137621107238494290616357604596231253506068537651423115349665683715116604220796394466621163255157729070978473156278277598788136491951257483328793771571459091064841642678309949723674420175862269402159407924480541255360431317992696739157542419296607312393763542139230617876753958711436104089409966089471418340698362993675362621545247298464213752891079884381306095552622720837518629837066787224430195793793786072107254277289071732854874374355781966511716618330881129120245204048682200072344035025448202834254187884653602591506445271657700044521097735585897622655484941621714989532383421600114062950718490427789258552743035221396835679018076406042138307308774460170842688272261177180842664333651780002171903449234264266292261456004337383868335555343453004264818473989215627086095650629340405264943244261445665921291225648893569655009154306426134252668472594914314239398845432486327461842846655985332312210466259890141712103446084271616619001257195870793217569698544013397622096749454185407118446433946990162698351607848924514058940946395267807354579700307051163682519487701189764002827648414160587206184185297189154019688253289309149665345753571427318482016384644832499037886069008072709327673127581966563941148961716832980455139729506687604740915420428429993541025829113502241690769431668574242522509026939034814856451303069925199590436384028429267412573422447765584177886171737265462085498294498946787350929581652632072258992368768457017823038096567883112289305809140572610865884845873101658151167533327674887014829167419701512559782572707406431808601428149024146780472327597684269633935773542930186739439716388611764209004068663398856841681003872389214483176070116684503887212364367043314091155733280182977988736590916659612402021778558854876176161989370794380056663364884365089144805571039765214696027662583599051987042300179465536788567430285974600143785483237068701190078499404930918919181649327259774030074879681484882342932023012128032327460392219687528340516906974194257614673978110715464186273369091584973185011183960482533518748438923177292613543024932562896371361977285456622924461644497284597867711574125670307871885109336344480149675240618536569532074170533486782754827815415561966911055101472799040386897220465550833170782394808785990501947563108984124144672821865459971596639015641941751820935932616316888380132758752601460507676098392625726411120135288591317848299475682472564885533357279772205543568126302535748216585414000805314820697137262149755576051890481622376790414926742600071045922695314835188137463887104273544767623577933993970632396604969145303273887874557905934937772320142954803345000695256980935282887783710670585567749481373858630385762823040694005665340584887527005308832459182183494318049834199639981458773435863115940570443683515285383609442955964360676090221741896883548131643997437764158365242234642619597390455450680695232850751868719449064767791886720306418630751053512149851051207313846648717547518382979990189317751550639981016466414592102406838294603208535554058147159273220677567669213664081505900806952540610628536408293276621931939933861623836069111767785448236129326858199965239275488427435414402884536455595124735546139403154952097397051896240157976832639450633230452192645049651735466775699295718989690470902730288544945416699791992948038254980285946029052763145580316514066229171223429375806143993484914362107993576737317948964252488813720435579287511385856973381976083524423240466778020948399639946684833774706725483618848273000648319163826022110555221246733323184463005504481849916996622087746140216157021029603318588727333298779352570182393861244026868339555870607758169954398469568540671174444932479519572159419645863736126915526457574786985964242176592896862383506370433939811671397544736228625506803682664135541448048997721373174119199970017293907303350869020922519124447393278376156321810842898207706974138707053266117683698647741787180202729412982310888796831880854367327806879771659111654224453806625861711729498038248879986504061563975629936962809358189761491017145343556659542757064194408833816841111166200759787244137082333917886114708228657531078536674695018462140736493917366254937783014074302668422150335117736471853872324040421037907750266020114814935482228916663640782450166815341213505278578539332606110249802273093636740213515386431693015267460536064351732154701091440650878823636764236831187390937464232609021646365627553976834019482932795750624399645272578624400375983422050808935129023122475970644105678361870877172333555465482598906861201410107222465904008553798235253885171623518256518482203125214950700378300411216212126052726059944320443056274522916128891766814160639131235975350390320077529587392412476451850809163911459296071156344204347133544720981178461451077872399140606290228276664309264900592249810291068759434533858330391178747575977065953570979640012224092199031158229259667913153991561438070129260780197022589662923368154312499412259460023399472228171056603931877226800493833148980338548909468685130789292064242819174795866199944411196208730498064385006852620258432842085582338566936649849720817046135376163584015342840674118587581546514598270228676671855309311923340191286170613364873183197560812569460089402953094429119590295968563923037689976327462283900735457144596414108229285922239332836210192822937243590283003884445701383771632056518351970100115722010956997890484964453434612129224964732356126321951155701565824427661599326463155806672053127596948538057364208384918887095176052287817339462747644656858900936266123311152910816041524100214195937349786431661556732702792109593543055579732660554677963552005378304619540636971842916168582734122217145885870814274090248185446421774876925093328785670674677381226752831653559245204578070541352576903253522738963847495646255940378924925007624386893776475310102323746733771474581625530698032499033676455430305274561512961214585944432150749051491453950981001388737926379964873728396416897555132275962011838248650746985492038097691932606437608743209385602815642849756549307909733854185583515789409814007691892389063090542534883896831762904120212949167195811935791203162514344096503132835216728021372415947344095498316138322505486708172221475138425166790445416617303200820330902895488808516797258495813407132180533988828139346049850532340472595097214331492586604248511405819579711564191458842833000525684776874305916390494306871343118796189637475503362820939949343690321031976898112055595369465424704173323895394046035325396758354395350516720261647961347790912327995264929045151148307923369382166010702872651938143844844532639517394110131152502750465749343063766541866128915264446926222884366299462732467958736383501937142786471398054038215513463223702071533134887083174146591492406359493020921122052610312390682941345696785958518393491382340884274312419099152870804332809132993078936867127413922890033069995875921815297612482409116951587789964090352577345938248232053055567238095022266790439614231852991989181065554412477204508510210071522352342792531266930108270633942321762570076323139159349709946933241013908779161651226804414809765618979735043151396066913258379033748620836695475083280318786707751177525663963479259219733577949555498655214193398170268639987388347010255262052312317215254062571636771270010760912281528326508984359568975961038372157726831170734552250194121701541318793651818502020877326906133592182000762327269503283827391243828198170871168108951187896746707073377869592565542713340052326706040004348843432902760360498027862160749469654989210474443927871934536701798673920803845633723311983855862638008516345597194441994344624761123844617615736242015935078520825600604101556889899501732554337298073561699861101908472096600708320280569917042590103876928658336557728758684250492690370934262028022399861803400211320742198642917383679176232826444645756330336556777374808644109969141827774253417010988435853189339175934511574023847292909015468559163792696196841000676598399744972047287881831200233383298030567865480871476464512824264478216644266616732096012564794514827125671326697067367144617795643752391742928503987022583734069852309190464967260243411270345611114149835783901793499713790913696706497637127248466613279908254305449295528594932793818341607827091326680865655921102733746700132583428715240835661522165574998431236278287106649401564670141943713823863454729606978693335973109537126499416282656463708490580151538205338326511289504938566468752921135932220265681856418260827538790002407915892646028490894922299966167437731347776134150965262448332709343898412056926145108857812249139616912534202918139898683901335795857624435194008943955180554746554000051766240202825944828833811886381749594284892013520090951007864941868256009273977667585642598378587497776669563350170748579027248701370264203283965756348010818356182372177082236423186591595883669487322411726504487268392328453010991677518376831599821263237123854357312681202445175401852132663740538802901249728180895021553100673598184430429105288459323064725590442355960551978839325930339572934663055160430923785677229293537208416693134575284011873746854691620648991164726909428982971065606801805807843600461866223562874591385185904416250663222249561448724413813849763797102676020845531824111963927941069619465426480006761727618115630063644321116224837379105623611358836334550102286170517890440570419577859833348463317921904494652923021469259756566389965893747728751393377105569802455757436190501772466214587592374418657530064998056688376964229825501195065837843125232135309371235243969149662310110328243570065781487677299160941153954063362752423712935549926713485031578238899567545287915578420483105749330060197958207739558522807307048950936235550769837881926357141779338750216344391014187576711938914416277109602859415809719913429313295145924373636456473035037374538503489286113141638094752301745088784885645741275003353303416138096560043105860548355773946625033230034341587814634602169235079216111013148948281895391028916816328709309713184139815427678818067628650978085718262117003140003377301581536334149093237034703637513354537634521050370995452942055232078817449370937677056009306353645510913481627378204985657055608784211964039972344556458607689515569686899384896439195225232309703301037277227710870564912966121061494072782442033414057441446459968236966118878411656290355117839944070961772567164919790168195234523807446299877664824873753313018142763910519234685081979001796519907050490865237442841652776611425351538665162781316090964802801234493372427866930894827913465443931965254154829494577875758599482099181824522449312077768250830768282335001597040419199560509705364696473142448453825888112602753909548852639708652339052941829691802357120545328231809270356491743371932080628731303589640570873779967845174740515317401384878082881006046388936711640477755985481263907504747295012609419990373721246201677030517790352952793168766305099837441859803498821239340919805055103821539827677291373138006715339240126954586376422065097810852907639079727841301764553247527073788764069366420012194745702358295481365781809867944020220280822637957006755393575808086318932075864444206644691649334467698180811716568665213389686173592450920801465312529777966137198695916451869432324246404401672381978020728394418264502183131483366019384891972317817154372192103946638473715630226701801343515930442853848941825678870721238520597263859224934763623122188113706307506918260109689069251417142514218153491532129077723748506635489170892850760234351768218355008829647410655814882049239533702270536705630750317499788187009989251020178015601042277836283644323729779929935160925884515772055232896978333126427671291093993103773425910592303277652667641874842441076564447767097790392324958416348527735171981064673837142742974468992320406932506062834468937543016787815320616009057693404906146176607094380110915443261929000745209895959201159412324102274845482605404361871836330268992858623582145643879695210235266673372434423091577183277565800211928270391042391966426911155333594569685782817020325495552528875464466074620294766116004435551604735044292127916358748473501590215522120388281168021413865865168464569964810015633741255098479730138656275460161279246359783661480163871602794405482710196290774543628092612567507181773641749763254436773503632580004042919906963117397787875081560227368824967077635559869284901628768699628053790181848148810833946900016380791075960745504688912686792812391148880036720729730801354431325347713094186717178607522981373539126772812593958220524289991371690685650421575056729991274177149279608831502358697816190894908487717722503860872618384947939757440664912760518878124233683125467278331513186758915668300679210215947336858591201395360301678110413444411030903388761520488296909104689167671555373346622545575975202624771242796225983278405833585897671474205724047439720232895903726148688388003174146490203843590358527993123871042845981608996101945691646983837718267264685264869172948414153004604004299585035164101899027529366867431834955447458124140190754681607770977920579383895378192128847409929537040546962226547278807248685508046571043123854873351653070570784584243335550958221912862797205455466267099131902370311779690892786623112661337671178512943059323281605826535623848164192144732543731002062738466812351691016359252588256806438946389880872735284406462208149513862275239938938734905082625472417781702582044129853760499827899020083498387362992498125742354568439023012261733665820546785671147973065077035475620567428300187473019197310881157516777005071432012726354601912460800451608108641835539669946936947322271670748972850464195392966434725254724357659192969949061670189061433616907056148280980363243454128229968275980226694045642181328624517549652147221620839824594576613342710564957193564431561774500828376935700995419541839029151033187933907614207467028867968594985439789457300768939890070073924697461812855764662265412913204052279071212820653775058280040897163467163709024906774736309136904002615646432159560910851092445162454420141442641660181385990017417408244245378610158433361777292580611159192008414091888191208858207627011483671760749046980914443057262211104583300789331698191603917150622792986282709446275915009683226345073725451366858172483498470080840163868209726371345205439802277866337293290829914010645589761697455978409211409167684020269370229231743334499986901841510888993165125090001163719114994852024821586396216294981753094623047604832399379391002142532996476235163569009445086058091202459904612118623318278614464727795523218635916551883057930657703331498510068357135624341881884405780028844018129031378653794869614630467726914552953690154167025838032477842272417994513653582260971652588356712133519546838335349801503269359798167463231847628306340588324731228951257944267639877946713121042763380872695738609314631539148548792514028885025189788076023838995615684850391995855029256054176767663145354058496296796781349420116003325874431438746248313850214980401681940795687219268462617287403480967931949965604299190281810597603263251746405016454606266765529010639868703668263299050577706266397868453584384057673298268163448646707439990917504018892319267557518354054956017732907127219134577524905771512773358423314008356080926962298894163047287780054743798498545562870729968407382937218623831766524716090967192007237658894226186550487552614557855898773008703234726418384831040394818743616224455286163287628541175946460497027724490799275146445792982549802258601001772437840167723166802004162547244179415547810554178036773553354467030326469619447560812831933095679685582771932031205941616693902049665352189672822671972640029493307384717544753761937017882976382487233361813499414541694736549254840633793674361541081593464960431603544354737728802361047743115330785159902977771499610274627769759612488879448609863349422852847651310277926279743981957617505591300993377368240510902583759345170015340522266144077237050890044496613295859536020556034009492820943862994618834790932894161098856594954213114335608810239423706087108026465913203560121875933791639666437282836752328391688865373751335794859860107569374889645657187292540448508624449947816273842517229343960137212406286783636675845331904743954740664015260871940915743955282773904303868772728262065663129387459875317749973799293043294371763801856280061141619563942414312254397099163565102848315765427037906837175764870230052388197498746636856292655058222887713221781440489538099681072143012394693530931524054081215705402274414521876541901428386744260011889041724570537470755550581632831687247110220353727166112304857340460879272501694701067831178927095527253222125224361673343366384756590949728221809418684074238351567868893421148203905824224324264643630201441787982022116248471657468291146315407563770222740135841109076078464780070182766336227978104546331131294044833570134869585165267459515187680033395522410548181767867772152798270250117195816577603549732923724732067853690257536233971216884390878879262188202305529937132397194333083536231248870386416194361506529551267334207198502259771408638122015980894363561808597010080081622557455039101321981979045520049618583777721048046635533806616517023595097133203631578945644487800945620369784973459902004606886572701865867757842758530645706617127194967371083950603267501532435909029491516973738110897934782297684100117657987098185725131372267749706609250481876835516003714638685918913011736805218743265426063700710595364425062760458252336880552521181566417553430681181548267844169315284408461087588214317641649835663127518728182948655658524206852221830755306118393326934164459415342651778653397980580828158806300749952897558204686612590853678738603318442905510689778698417735603118111677563872589911516803236547002987989628986181014596471307916144369564690909518788574398821730583884980809523077569358851616027719521488998358632323127308909861560777386006984035267826785387215920936255817889813416247486456433211043194821421299793188104636399541496539441501383868748384870224681829391860319598667962363489309283087840712400431022706137591368056518861313458307990705003607588327248867879324093380071864152853317943535073401891193638546730000660453783784472469288830546979000131248952100446949032058838294923613919284305249167833012980192255157050378521810552961623637523647962685751660066539364142273063001648652613891842243501797455993616794063303522111829071597538821839777552812981538570168702202620274678647916644030729018445497956399844836807851997088201407769199261674991148329821854382718946282165387064858588646221611410343570342878862979083418871606214430014533275029715104673156021000043869510583773779766003460887624861640938645252177935289947578496255243925598620521409052346250847830487046492688313289470553891357290706967599556298586669559721686506052072801342104355762779184021797626656484580261591407173477009039475168017709900129391137881248534255949312866653465033728846390649968460644741907524313323903404908195233044389559060547854954620263256676813262435925020249516275607080900436460421497025691488555265022810327762115842282433269528629137662675481993546118143913367579700141255870143319434764035725376914388899683088262844616425575034001428982557620386364384137906519612917777354183694676232982904981261717676191554292570438432239918482261744350470199171258214687683172646078959690569981353264435973965173473319484798758064137926885413552523275720457329477215706850016950046959758389373527538622664943456437071610511521617176237598050900553232154896062817794302268640579555845730600598376482703339859420098582351400179507104569019191359062304102336798080907240196312675268916362136351032648077232914950859151265812143823371072949148088472355286394195993455684156344577951727033374238129903260198160571971183950662758220321837136059718025940870615534713104482272716848395524105913605919812444978458110854511231668173534838253724825347636777581712867205865148285317273569069839935110763432091319780314031658897379628301178409806410175016511072932907832177487566289310650383806093372841399226733384778203302020700517188941706465146238366720632742644336612174011766914919235570905644803016342294301837655263108450172510307540942604409687066288066265900569082451407632599158164499361455172452057020443093722305550217222299706209749268609762787409626448772056043078634808885709143464793241536214303199965695610753570417207285334250171325558818113295504095217830139465216436594262960768570585698507157151317262928960072587601564840556088613165411835958628710665496282599535127193244635791046554389165150954187306071015034430609582302257455974944275067630926322529966338219395202927917973247094559691016402983683080426309910481567503623509654924302589575273521412445149542462972258510120707802110188106722347972579330653187713438466713807546383471635428854957610942841898601794658721444495198801550804042506452191484989920400007310672369944655246020908767882300064337725657385010969899058191290957079866699453765080407917852438222041070599278889267745752084287526377986730360561230710723922581504781379172731261234878334034473833573601973235946604273704635201327182592410906040097638585857716958419563109577748529579836844756803121874818202833941887076311731615289811756429711334181497218078040465077657204457082859417475114926179367379999220181789399433337731146911970737861041963986422166045588965683206701337505745038872111332436739840284188639147633491695114032583475841514170325690161784931455706904169858050217798497637014758914810543205854914100662201721719726878930012101267481270235940855162601689425111458499658315589660460091525797881670384625905383256920520425791378948827579603278877535466861441826827797651258953563761485994485049706638406266121957141911063246061774180577212381659872472432252969098533628440799030007594546281549235506086481557928961969617060715201589825299772803520002610888814176506636216905928021516429198484077446143617891415191517976537848282687018750030264867608433204658525470555882410254654806040437372771834769014720664234434374255514129178503032471263418076525187802925534774001104853996960549926508093910691337614841834884596365621526610332239417467064368340504749943339802285610313083038484571294767389856293937641914407036507544622061186499127249643799875806537850203753189972618014404667793050140301580709266213229273649718653952866567538572115133606114457222800851183757899219543063413692302293139751143702404830227357629039911794499248480915071002444078482866598579406525539141041497342780203520135419925977628178182825372022920108186449448349255421793982723279357095828748597126780783134286180750497175747373730296280477376908932558914598141724852658299510882230055223242218586191394795184220131553319634363922684259164168669438122537135960710031743651959027712571604588486044820674410935215327906816032054215967959066411120187618531256710150212239401285668608469435937408158536481912528004920724042172170913983123118054043277015835629513656274610248827706488865037765175678806872498861657094846665770674577000207144332525555736557083150320019082992096545498737419756608619533492312940263904930982014700371161829485939931199955070455381196711289367735249958182011774799788636393286405807810818657337668157893827656450642917396685579555053188715314552353070355994740186225988149854660737787698781542360397080977412361518245964026869979609564523828584235953564615185448165799966460648261396618720304839119560250381111550938420209894591555760083897989949964566262540514195610780090298667014635238532066032574466820259430618801773091109212741138269148784355679352572808875543164693077235363768226036080174040660997151176880434927489197133087822951123746632635635328517394189466510943745768270782209928468034684157443127739811044186762032954475468077511126663685479944460934809992951875666499902261686019672053749149951226823637895865245462813439289338365156536992413109638102559114643923805213907862893561660998836479175633176725856523591069520326895990054884753424160586689820067483163174286329119633399132709086065074595260357157323069712106423424081597068328707624437165532750228797802598690981111226558888151520837482450034463046505984569690276166958278982913613535306291331427881888249342136442417833519319786543940201465328083410341785272489879050919932369270996567133507711905899945951923990615156165480300145359212550696405345263823452155999210578191371030188979206408883974767667144727314254467923500524618849237455307575734902707342496298879996942094595961008702501329453325358045689285707241207965919809225550560061971283541270202072583994171175520920820151096509526685113897577150810849443508285458749912943857563115668324566827992991861539009255871716840495663991959154034218364537212023678608655364745175654879318925644085274489190918193411667583563439758886046349413111875241038425467937999203546910411935443113219136068129657568583611774564654674861061988591414805799318725367531243470335482637527081353105570818049642498584646147973467599315946514787025065271083508782350656532331797738656666181652390017664988485456054961300215776115255813396184027067814900350252876823607822107397102339146870159735868589015297010347780503292154014359595298683404657471756232196640515401477953167461726208727304820634652469109953327375561090578378455945469160223687689641425960164689647106348074109928546482353083540132332924864037318003195202317476206537726163717445360549726690601711176761047774971666890152163838974311714180622222345718567941507299526201086205084783127474791909996889937275229053674785020500038630036526218800670926674104806027341997756660029427941090400064654281074454007616429525362460261476180471744322889953285828397762184600967669267581270302806519535452053173536808954589902180783145775891280203970053633193821100095443241244197949192916205234421346395653840781209416214835001155883618421164283992454027590719621537570187067083731012246141362048926555668109467076386536083015847614512581588569610030337081197058344452874666198891534664244887911940711423940115986970795745946337170243268484864632018986352827092313047089215684758207753034387689978702323438584381125011714013265769320554911860153519551654627941175593967947958810333935413289702528893533748106257875620364294270257512121137330213811951395756419122685155962476203282038726342066227347868223036522019655729325905068134849292299647248229359787842720945578267329975853818536442370617353517653060396801087899490506654491544577952166038552398013798104340564182403396162494910454712104839439200945914647542424785991096900046541371091630096785951563947332190934511838669964622788855817353221326876634958059123761251203010983867841195725887799206041260049865895027247133146763722204388398558347770112599424691208308595666787531942465131444389971195968105937957532155524204659410081418351120174196853432672343271868099625045432475688702055341969199545300952644398446384346598830418262932239295612610045884644244285011551557765935780379565026806130721758672048541797157896401554276881090475899564605488362989140226580026134158039480357971019004151547655018391755772677897148793477372747525743898158705040701968215101218826088040084551332795162841280679678965570163917067779841529149397403158167896865448841319046368332179115059107813898261026271979696826411179918656038993895418928488851750122504754778999508544083983800725431468842988412616042682248823097788556495765424017114510393927980290997604904428832198976751320535115230545666467143795931915272680278210241540629795828828466355623580986725638200565215519951793551069127710538552661926903526081367717666435071213453983711357500975854405939558661737828297120544693182260401670308530911657973113259516101749193468250063285777004686987177255226525708428745733039859744230639751837209975339055095883623642814493247460522424051972825153787541962759327436278819283740253185668545040893929401040561666867664402868211607294830305236465560955351079987185041352121321534713770667681396211443891632403235741573773787908838267618458756361026435182951815392455211729022985278518025598478407179607904114472041476091765804302984501746867981277584971731733287305281134969591668387877072315968334322509070204019030503595891994666652037530271923764252552910347950343816357721698115464329245608951158732012675424975710520894362639501382962152214033621065422821876739580121286442788547491928976959315766891987305176388698461503354594898541849550251690616888419122873385522699976822609645007504500096116866129171093180282355042553653997166054753907348915189650027442328981181709248273610863801576007240601649547082331349361582435128299050405405333992577071321011503713898695076713447940748097845416328110406350804863393555238405735580863718763530261867971725608155328716436111474875107033512913923595452951407437943144900950809932872153235195999616750297532475931909938012968640379783553559071355708369947311923538531051736669154087312467233440702525006918026747725078958903448856673081487299464807786497709361969389290891718228134002845552513917355978456150353144603409441211512001738697261466786933733154341007587514908295822756919350542184106448264951943804240543255345965248373785310657979037977505031436474651422484768831323479762673689855474944277949916560108528257618964374464656819789319422077536824661110427671936481836360534108748971066866318805026555929568123959680449295166615409802610781691689418764353363449482900125929366840591370059526914934421861891742142561071896846626335874414976973921566392767687720145153302241853125308442727245771161505550519076276250016522166274796257424425420546785767478190959486500575711016264847833741198041625940813327229905891486422127968042984725356237202887830051788539737909455265135144073130049869453403245984236934627060242579432563660640597549471239092372458126154582526667304702319359866523378856244229188278436440434628094888288712101968642736370461639297485616780079779959696843367730352483047478240669928277140069031660709951473154191919911453182543906294573298686613524886500574780251977607442660798300291573030523199052185718628543687577860915726925232573171665625274275808460620177046433101212443409281314659760221360416223031167750085960128475289259463348312408766740128170543067985261868949895004918275008304998926472034986965363326210919830621495095877228260815566702155693484634079776879525038204442326697479264829899016938511552124688935873289878336267819361764023681714606495185508780596635354698788205094762016350757090024201498400967867845405354130050482404996646978558002628931826518708714613909521454987992300431779500489569529280112698632533646737179519363094399609176354568799002814515169743717518330632232942199132137614506411391269837128970829395360832883050256072727563548374205497856659895469089938558918441085605111510354367477810778500572718180809661542709143010161515013086522842238721618109043183163796046431523184434669799904865336375319295967726080853457652274714047941973192220960296582500937408249714373040087376988068797038047223488825819819025644086847749767508999164153502160223967816357097637814023962825054332801828798160046910336602415904504637333597488119998663995617171089911809851197616486499233594328274275983382931099806461605360243604040848379619072542165869409486682092396143083817303621520642297839982533698027039931804024928814430649614747600087654305571672697259114631990688823893005380061568007730984416061355843701277573463708822073792921409548717956947854414951731561828176343929570234710460088230637509877521391223419548471196982303169544468045517922669260631327498272520906329003279972932906827204647650366969765227673645419031639887433042226322021325368176044169612053532174352764937901877252263626883107879345194133825996368795020985033021472307603375442346871647223795507794130304865403488955400210765171630884759704098331306109510294140865574071074640401937347718815339902047036749084359309086354777210564861918603858715882024476138160390378532660185842568914109194464566162667753712365992832481865739251429498555141512136758288423285957759412684479036912662015308418041737698963759002546999454131659341985624780714434977201991702665380714107259910648709897259362243300706760476097690456341576573395549588448948093604077155688747288451838106069038026528318275560395905381507241627615047252487759578650784894547389096573312763852962664517004459626327934637721151028545472312880039058405918498833810711366073657536918428084655898982349219315205257478363855266205400703561310260405145079325925798227406012199249391735122145336707913500607486561657301854049217477162051678486507913573336334257685988361252720250944019430674728667983441293018131344299088234006652915385763779110955708000600143579956351811596764725075668367726052352939773016348235753572874236648294604770429166438403558846422370760111774821079625901180265548868995181239470625954254584491340203400196442965370643088660925268811549596291166168612036195319253262662271108142149856132646467211954801142455133946382385908540917878668826947602781853283155445565265933912487885639504644196022475186011405239187543742526581685003052301877096152411653980646785444273124462179491306502631062903402737260479940181929954454297256377507172705659271779285537195547433852182309492703218343678206382655341157162788603990157495208065443409462446634653253581574814022471260618973060860559065082163068709634119751925774318683671722139063093061019303182326666420628155129647685313861018672921889347039342072245556791239578260248978371473556820782675452142687314252252601795889759116238720807580527221031327444754083319215135934526961397220564699247718289310588394769170851420631557192703636345039529604362885088555160008371973526383838996789184600327073682083234847108471706160879195227388252347506380811606090840124222431476103563328940609282430125462013806032608121942876847907192546246309055749298781661271916548229644317263587524548607563020667656942355342774617635549231817456159185668061686428714964129290560130053913469569829490891003991259088290348791943368696942620662946948514931472688923571615032405542263391673583102728579723061998175868700492227418629077079508809336215346303842967525604369606110193842723883107587771653594778681499030978765900869583480043137176832954871752604714113064847270887246697164585218774442100900090916189819413456305028950484575822161887397443918833085509908566008543102796375247476265353031558684515120283396640547496946343986288291957510384781539068343717740714095628337554413567955424664601335663617305811711646062717854078898495334329100315985673932305693426085376230981047171826940937686754301837015557540822371538037838383342702379535934403549452173960327095407712107332936507766465603712364707109272580867897181182493799540477008369348889220963814281561595610931815183701135104790176383595168144627670903450457460997444500166918675661035889313483800512736411157304599205955471122443903196476642761038164285918037488354360663299436899730090925177601162043761411616688128178292382311221745850238080733727204908880095181889576314103157447684338100457385008523652069340710078955916549813037292944462306371284357984809871964143085146878525033128989319500645722582281175483887671061073178169281242483613796475692482076321356427357261609825142445262515952514875273805633150964052552659776922077806644338105562443538136258941809788015677378951310313157361136026047890761945591820289365770116416881703644242694283057457471567494391573593353763114830246668754727566653059819746822346578699972291792416156043557665183382167059157867799311835820189855730344883681934418305987021880502259192818047775223884407167894780414701414651073580452021499197980812095692195622632313741870979731320870864552236740416185590793816745658234353037283309503729022429802768451559528656923189798000383061378732434546500582722712325031420712488100290697226311129067629080951145758060270806092801504406139446350643069742785469477459876821004441453438033759717384777232052065301037861326418823586036569054773343070911759152582503029410738914441818378779490613137536794654893375260322906277631983337976816641721083140551864133302224787118511817036598365960493964571491686005656771360533192423185262166760222073368844844409234470948568027905894191829969467724456269443308241243846160408284006424867072583661011433404214473683453638496544701067827313169538435919120440283949541956874453676459875488726170687163109591315801609722382049772577307454562979127906177531663252857205858766376754282917933549923678212008601904369428956102301731743150352204665675088491593025926618816581008701658499456495586855628208747248318351516339189292646558880593601275151838235485893426165223086697314511412035659916934103076974774451947043836739600076578628245472064617380804602903639144493859012422380173377038154675297645596518492676039300171943042511794045679862114630138402371099347243455794730048929825402680821621522346560274258486595687074510352794291633405915025075992398611224340312056999780516223878772230396359709132856830486160362127579561601328561866388146004722200580017580282279272167842720649966956840905752590774886105493806116954293569077377792821084159737469613143291808510446953973485067590503662391722108732333169909603363771705474725026941732982890400239372879549386540463828596742216318201530139629734398479588628632934746650690284066719018081265539973675916799759010867483920062877888531102781695087545740384607594616919584610655963327283485609570305572502494416337066573150237126843581984154103154401008430380631442183776750349813408169325201240813452285974626715177152223063741359255747513535160669108359443999692315898156732033027129284241219651936303734407981204656795322986357374589031654007016472204989445629050395873788912680565516464274460174738175296313458739390484560414203426465560422112239134631023161290836446988901247285192778589195228773637440432659264672239982186452797664826673070168802722052338600372842903155828454593854349099449420750911108532138744823216151007808922516285123275724355101999038195993350032641446053470357293073912578481757987468353429629749652545426864234949270336399427519354240001973125098882419600095766257217621860474573769577649582201796258392376391717855799468922496750179251915218219624653575570564228220399546682648329822996167217080156801080799777126517156274295763666959661983507435667132218383358509536665806605597148376773866922551603463644386269977295750658468929599809168949981898588529537874489519527097766262684177088590284321676352132630838812766335363319004134332844347630067982023716933653652880580156390360562722752187272454764258840995216482554453662083811789117725225682611478014242896970967121967502094421226279437073328703410646312100557376727450271638975234111426287828736758358819056742163061523416789476056879277154789714326222041069587947186435439940738639948986836168919377836648327137363654676901173760246643082285362494712605173293777247276797635865806019396287718060679122426813922872134061694882029506831654589707623668302556167559477498715183426989208952182644710514911419441192277010977616645850068963849426165593473112961064282379048216056210094265076173838082479030510998790719611852832556787472942907151041468948104916751035295897242381802288151276582257190705537652455285511598636421244284176256230139538669970308943645907600684938040875210854159851278070333207779865635907968462191534944587677170063778573171211036517486371634098385626541555573292664616402279791195975248525300376741774056125700303625811704838385391207273191845064713669122576415213769896260940351804147432053600369234179035440735703058314741623452840188940808983125191307741823338981880316339159565954543405777784331681162551898060409183018907512170192983622897099598983405484962284289398469847938668614293324543983592637036699355184231661615244505980576745765335552338715678211466689996845227042954589710922163652573965950289645637766038988037941517917867910675199009966139206238732318786758420544279396366759104126821843375015743069045967947046685602358283919759975285865384338189120042853787549302768972168199113340697282255535300044743958830079799736518459131437946494086272149669719100359399974735262764126125995350902609540048669398955899487421379590802893196914845826873123710180229775301190684280440780938156598081694611679374425663244656799606363751546304833112722231812338371779800439731087402647536582575657351059978314264831879619843765495877803685261751835391844920488198629786329743136948511780579298636452193232481339393090754566368038513630619718033957979522539508697432546502659123585049283028832934489284591373621624852528877442891851104093746333590660233239711922814450735588373324057814862662207486215513375036775585494138678352928273109003823116855374520901095101174796663003330352534143230024288248051396631446632656081582045216883922312025671065388459503224002320453633895521539919011035217362720909565500846486605368975498478995875596103167696587161281951919668893326641203784750417081752273735270989343717167642329956935697166213782736138899530515711822960896394055380431939398453970864418654291655853168697537052760701061488025700785387150835779480952313152747735711713643356413242974208137266896149109564214803567792270566625834289773407718710649866150447478726164249976671481383053947984958938064202886667951943482750168192023591633247099185942520392818083953020434979919361853380201407072481627304313418985942503858404365993281651941497377286729589582881907490040331593436076189609669494800067194371424058105327517721952474344983414191979918179909864631583246021516575531754156198940698289315745851842783390581029411600498699307751428513021286202539508732388779357409781288187000829944831476678183644656510024467827445695591845768068704978044824105799710771577579093525803824227377612436908709875189149049904225568041463131309240101049368241449253427992201346380538342369643767428862595140146178201810734100565466708236854312816339049676558789901487477972479202502227218169405159042170892104287552188658308608452708423928652597536146290037780167001654671681605343292907573031466562485809639550080023347676187068086526878722783177420214068980703410506200235273632267291964034093571225623659496432076928058165514428643204955256838543079254299909353199329432966018220787933122323225928276556048763399988478426451731890365879756498207607478270258861409976050788036706732268192473513646356758611212953074644777149423343867876705824452296605797007134458987594126654609414211447540007211790607458330686866231309155780005966522736183536340439991445294960728379007338249976020630448806064574892740547730693971337007962746135534442514745423654662752252624869916077111131569725392943756732215758704952417232428206555322808868670153681482911738542735797154157943689491063759749151524510096986573825654899585216747260540468342338610760823605782941948009334370046866568258579827323875158302566720152604684361412652956519894291184887986819088277339147282063794512260294515707367105637720023427811802621502691790400488001808901847311751199425460594416773315777951735444490965752131026306836047140331442314298077895617051256930051804287472368435536402764392777908638966566390166776625678575354239947427919442544664643315554138265543388487778859972063679660692327601733858843763144148113561693030468420017434061395220072403658812798249143261731617813894970955038369479594617979829257740992171922783223006387384996138434398468502234780438733784470928703890536420557474836284616809363650973790900204118525835525201575239280826462555785658190226958376345342663420946214426672453987171047721482128157607275305173330963455909323664528978019175132987747952929099598069790148515839540444283988381797511245355548426126784217797728268989735007954505834273726937288386902125284843370917479603207479554080911491866208687184899550445210616155437083299502854903659617362726552868081324793106686855857401668022408227992433394360936223390321499357262507480617409173636062365464458476384647869520547719533384203403990244761056010612777546471464177412625548519830144627405538601855708359981544891286863480720710061787059669365218674805943569985859699554089329219507269337550235821561424994538234781138316591662683103065194730233419384164076823699357668723462219641322516076261161976034708844046473083172682611277723613381938490606534404043904909864126903479263503943531836741051762565704797064478004684323069430241749029731181951132935746854550484711078742905499870600373983113761544808189067620753424526993443755719446665453524088287267537759197074526286322840219629557247932987132852479994638938924943286917770190128914220188747760484939855471168524810559991574441551507431214406120333762869533792439547155394213121021954430556748370425907553004950664994802614794524739012802842646689229455664958621308118913500279654910344806150170407268010067948926855360944990373928383520627992820181576427054962997401900837493444950600754365525758905546552402103412862124809003162941975876195941956592556732874237856112669741771367104424821916671499611728903944393665340294226514575682907490402153401026923964977275904729573320027982816062130523130658731513076913832317193626664465502290735017347656293033318520949298475227462534564256702254695786484819977513326393221579478212493307051107367474918016345667888810782101151826314878755138027101379868751299375133303843885631415175908928986956197561123025310875057188962535763225834275763348421016668109884514141469311719314272028007223449941999003964948245457520704922091620614222912795322688239046498239081592961111003756999529251250673688233852648213896986384052437049402152187547825163347082430303521036927849762517317825860862215614519165573478940019558704784741658847364803865995119651409542615026615147651220820245816010801218275982577477652393859159165067449846149161165153821266726927461290533753163055654440793427876550267301214578324885948736899073512166118397877342715872870912311383472485146035661382188014840560716074652441118841800734067898587159273982452147328317214621907330492060817440914125388918087968538960627860118193099489240811702350413554126823863744341209267781729790694714759018264824761112414556423937732224538665992861551475342773370683344173073150805440138894084087253197595538897613986400165639906934600670780501058567196636796167140097031535132386972899001749862948883362389858632127176571330142071330179992326381982094042993377790345261665892577931395405145369730429462079488033141099249907113241694504241391265397274078984953073730364134893688060340009640631540701820289244667315059736321311926231179142794944897281477264038321021720718017561601025111179022163703476297572233435788863537030535008357679180120653016668316780269873860755423748298548246360981608957670421903145684942967286646362305101773132268579232832164818921732941553151386988781837232271364011755881332524294135348699384658137175857614330952147617551708342432434174779579226338663454959438736807839569911987059388085500837507984051126658973018149321061950769007587519836861526164087252594820126991923916722273718430385263107266000047367872474915828601694439920041571102706081507270147619679971490141639274282889578424398001497985658130305740620028554097382687819891158955487586486645709231721825870342960508203415938806006561845735081804032347750084214100574577342802985404049555529215986404933246481040773076611691605586804857302606467764258503301836174306413323887707999698641372275526317649662882467901094531117120243890323410259937511584651917675138077575448307953064925086002835629697045016137935696266759775923436166369375035368699454550392874449940328328128905560530091416446608691247256021455381248285307613556149618444364923014290938289373215312818797541139219415606631622784836152140668972661027123715779503062132916001988806369127647416567067485490795342762338253943990022498972883660263920518704790601584084302914787302246651371144395418253441269003331181914268070735159284180415100555199146564934872796969351992963117195821262627236458009708099166752820365818699111948365866102758375863322993225541477479210421324166848264953111826527351008031659958888814809945737293785681411438021523876706455063233067233939551964260397443829874822322662036352861302543796600943104500158604854027036789711934695579989189112302233381602302236277726084846296189550730850698061500281436425336666311433321645213882557346329366870956708432252564333895997812402164189946978348320376011613913855499933990786652305860332060641949298931012423081105800169745975038516887112037747631577311831360002742502722451570906304496369230938382329175076469684003556425503797106891999812319602533733677437970687713814747552190142928586781724044248049323750330957002929126630316970587409214456472022710796484778657310660832173093768033821742156446602190335203981531618935787083561603302255162155107179460621892674335641960083663483835896703409115513087820138723494714321400450513941428998350576038799343355677628023346565854351219361896876831439866735726040869511136649881229957801618882834124004126142251475184552502502640896823664946401177803776799157180146386554733265278569418005501363433953502870836220605121839418516239153709790768084909674194289061134979961034672077354959593868862427986411437928435620575955500144308051267664432183688321434583708549082240014585748228606859593502657405750939203135881722442164955416889785558265198046245527898343289578416968890756237467281044803018524217706136533236073856228166664597654076844715963930782091017090763377917711485205493367936868430832404126789220929930411890501756484917499452393770674524578019171841679541825554377930299249277892416277257788147974770446005423669346157135208417428211847353652367573702352791459837645712257646122605628127852169580892808988394594406165340521932514843306105322700231133680378433377389724881307874325614952744243584753011150345103737688223837573804282007358586938044331529253129961025096113761670187568525921208929131354473196308440066835155160913925692912175784379179004808848023029304392630921342768601226558630456913133560978156776098711809238440656353136182676923761613389237802972720736243967239854144480757286813436768000573823963610796223140429490728058551444771338682314499547929338131259971996894072233847404542592316639781608209399269744676323921370773991899853301483814622364299493902073285072098040905300059160091641710175605409814301906444379905831277826625762288108104414704097708248077905168225857235732665234414956169007985520848841886027352780861218049418060017941147110410688703738674378147161236141950474056521041002268987858525470689031657094677131822113205505046579701869337769278257145248837213394613987859786320048011792814546859096532616616068403160077901584946840224344163938313618742275417712170336151163782359059685168880561304838542087505126933144171705880517278127917564053282929427357971823360842784676292324980318169828654166132873909074116734612367109059236155113860447246378721244612580406931724769152219217409096880209008801535633471775664392125733993165330324425899852598966724744126503608416484160724482125980550754851232313331300621490042708542735985913041306918279258584509440150719217604794274047740253314305451367710311947544521321732225875550489799267468541529538871443696399406391099267018219539890685186755868574434469213792094590683677929528246795437302263472495359466300235998990248299853826140395410812427393530207575128774273992824866921285637240069184859771126480352376025469714309316636539718514623865421671429236191647402172547787238964043145364190541101514371773797752463632741619269990461595895793940622986041489302535678633503526382069821487003578061101552210224486633247184367035502326672749787730470216165019711937442505629639916559369593557640005236360445141148916155147776301876302136068825296274460238077523189646894043033182148655637014692476427395401909403584437251915352134557610698046469739424511797999048754951422010043090235713636892619493763602673645872492900162675597083797995647487354531686531900176427222751039446099641439322672532108666047912598938351926694497553568096931962642014042788365702610390456105151611792018698900673027082384103280213487456720062839744828713298223957579105420819286308176631987048287388639069922461848323992902685392499812367091421613488781501234093387999776097433615750910992585468475923085725368613605356762146929424264323906626708602846163376051573599050869800314239735368928435294958099434465414316189806451480849292695749412903363373410480943579407321266012450796613789442208485840536446021616517885568969302685188950832476793300404851688934411125834396590422211152736276278672366665845757559585409486248261694480201791748223085835007862255216359325125768382924978090431102048708975715033330963651576804501966025215527080352103848176167004443740572131294252820989545456276344353575741673638980108310579931697917916718271145837435222026387771805250290791645414791173616253155840768495583288190293564201219633684854080865928095131505012602919562576032932512847250469881908146475324342363863860247943921015193235101390117789997483527186469346024554247028375300033725403910085997650987642832802908445662021678362267272292737780213652404028817217012490974899454430826861772239385250883760749742195942655217301733355851389407457348144161511380845358039740277795072051893487170722955427683655826706766313911972211811528466502223383490906676554168336907959409404576472940901354356409277969379842065738891481990225399022315913388145851487225126560927576795873759207013915029216513720851137197522734365458411622066281660256333632074449918511469174455062297146086578736313585389023662557285424516018080487167823688885575325066254262367702604215835160174851981885460860036597606743233346410471991027562358645341748631726556391320606407754779439671383653877377610828300019937359760370467245737880967939894493795829602910746901609451288456550071458091887879542641820145369659962842686882363495879277007025298960996798975941955735253914237782443302746708282008722602053415292735847582937522487377937899136764642153727843553986244015856488692101644781661602962113570056638347990334049623875941092886778920270077504951511405782565295015024484968204744379710872943108541684540513016310902267112951959140520827546866418137305837933236150599142045255880213558474751516267815309465541240524091663857551298894834797423322854504140527354235070335984964593699534959698554244978249586929179182415068053002553370412778703476446244329205906832901886692400222391918714603175399666877477960121790688623311002908668305431787009355066944389131913333586368037447530664502418437136030852288582121720231274167009740351431532131803978033680228154223490183737494117973254478594157962104378787072154814091725163615415163381388912588517924237727229603497305533840942889918919161186249580560073570527227874940321250645426206304469470804277945973817146810395192821550688079136701210109944220737024613687196031491162370967939354636396448139025711768057799751751298979667073292674886430097398814873780767363792886767781170520534367705731566895899181530825761606591843760505051704242093231358724816618683821026679970982966436224723644898648976857100173643547336955619347638598187756855912376232580849341570570863450733443976604780386678461711520325115528237161469200634713570383377229877321365028868868859434051205798386937002783312365427450532283462669786446920780944052138528653384627970748017872477988461146015077617116261800781557915472305214759943058006652042710117125674185860274188801377931279938153727692612114066810156521441903567333926116697140453812010040811760123270513163743154487571768761575554916236601762880220601068655524141619314312671535587154866747899398685510873576261006923021359580838145290642217792987748784161516349497309700794368305080955621264592795333690631936594413261117944256602433064619312002953123619348034504503004315096798588111896950537335671086336886944665564112662287921812114121425167348136472449021275252555647623248505638391391630760976364990288930588053406631352470996993362568102360392264043588787550723319888417590521211390376609272658409023873553418516426444865247805763826160023858280693148922231457758783791564902227590699346481624734399733206013058796068136378152964615963260698744961105368384203105364183675373594176373955988088591188920114871545460924735613515979992999722298041707112256996310945945097765566409972722824015293663094891067963296735505830412258608050740410916678539569261234499102819759563955711753011823480304181029089719655278245770283085321733741593938595853203645590564229716679900322284081259569032886928291260139267587858284765599075828016611120063145411315144108875767081854894287737618991537664505164279985451077400771946398046265077776614053524831090497899859510873112620613018757108643735744708366215377470972660188656210681516328000908086198554303597948479869789466434027029290899143432223920333487108261968698934611177160561910681226015874410833093070377506876977485840324132474643763087889666151972556180371472590029550718424245405129246729039791532535999005557334600111693557020225722442772950263840538309433999383388018839553821540371447394465152512354603526742382254148328248990134023054550811390236768038649723899924257800315803725555410178461863478690646045865826036072306952576113184134225274786464852363324759102670562466350802553058142201552282050989197818420425028259521880098846231828512448393059455162005455907776121981297954040150653985341579053629101777939776957892084510979265382905626736402636703151957650493344879513766262192237185642999150828898080904189181015450813145034385734032579549707819385285699926238835221520814478940626889936085239827537174490903769904145555260249190126341431327373827075950390882531223536876389814182564965563294518709637484074360669912550026080424160562533591856230955376566866124027875883101021495284600804805028045254063691285010599912421270508133194975917146762267305044225075915290251742774636494555052325186322411388406191257012917881384181566918237215400893603475101448554254698937834239606460813666829750019379115061709452680984785152862123171377897417492087541064556959508967969794980679770961683057941674310519254486327358885118436597143583348756027405400165571178309126113117314169066606067613797690123141099672013123730329707678988740099317309687380126740538923612230370779727025191340850390101739924877352408881040807749924412635346413181858792480760553268122881584307471326768283097203149049868884456187976015468233715478415429742230166504759393312132256510189175368566338139736836336126010908419590215582111816677413843969205870515074254852744810154541079359513596653630049188769523677579147319184225806802539818418929888943038224766186405856591859943091324575886587044653095332668532261321209825839180538360814144791320319699276037194760191286674308615217243049852806380129834255379486287824758850820609389214668693729881191560115633701248675404205911464930888219050248857645752083363921499441937170268576222251074166230901665867067714568862793343153513505688216165112807318529333124070912343832502302341169501745502360505475824093175657701604884577017762183184615567978427541088499501610912720817913532406784267161792013428902861583277304794830971705537485109380418091491750245433432217445924133037928381694330975012918544596923388733288616144238100112755828623259628572648121538348900698511503485369544461542161283241700533583180520082915722904696365553178152398468725451306350506984981006205514844020769539324155096762680887603572463913955278222246439122592651921288446961107463586148252820017348957533954255019475442643148903233373926763409115527189768429887783617346613535388507656327107814312435018965109238453660236940276060642119384227665755210663671879603217527184404651560427289869560206997012906367847161654793068868305846508082886614111979138822898112498261434559408961813509226857611474609406147937240008842153535862052780125014270055274468359151840373309373580494342483940467505708347927948338133276237937844629209323999417593374917899786484958148818865149169302451512835579818112344900827168644548306546633975256079615935830821400021951611342337058359111545217293721664061708131602078213341260356852013161345136871600980378712556766143923146458085652084039744217352744813741215277475202259244561520365608268890193913957991844109971588312780020898275935898106482117936157951837937026741451400902833064466209280549839169261068975151083963132117128513257434964510681479694782619701483204392206140109523453209269311762298139422044308117317394338867965739135764377642819353621467837436136161591167926578700137748127848510041447845416464568496606699139509524527949914769441031612575776863713634644477006787131066832417871556281779122339077841275184193161188155887229676749605752053192594847679397486414128879475647133049543555044790277128690095643357913405127375570391806822344718167939329121448449553897728696601037841520390662890781218240141299368590465146519209198605347788576842696538459445700169758422531241268031418456268722581132040056433413524302102739213788415250475704533878002467378571470021087314693254557923134757243640544448132093266582986850659125571745568328831440322798049274104403921761438405750750288608423536966715191668510428001748971774811216784160854454400190449242294333666338347684438072624307319019363571067447363413698467328522605570126450123348367412135721830146848071241856625742852208909104583727386227300781566668914250733456373259567253354316171586533339843321723688126003809020585719930855573100508771533737446465211874481748868710652311198691114058503492239156755462142467550498676710264926176510110766876596258810039163948397811986615585196216487695936398904500383258041054420595482859955239065758108017936807080830518996468540836412752905182813744878769639548306385089756146421874889271294890398025623046812175145502330254086076115859321603465240763923593699949180470780496764486889980902123735780457040380820770357387588525976042434608851075199334470112741787878845674656640471901619633546770714090590826954225196409446319547658653032104723804625249971910690110456227579220926904132753699634145768795242244563973018311291451151322757841320376225862458224784696669785947914981610522628786944136373683125108310682898766123782697506343047263278453719024447970975017396831214493357290791648779915089163278018852504558488782722376705263811803792477835540018117452957747339714012352011459901984753358434861297092928529424139865507522507808919352104173963493428604871342370429572757862549365917805401652536330410692033704691093097588782938291296447890613200063096560747882082122140978472301680600835812336957051454650181292694364578357815608503303392466039553797630836137289498678842851139853615593352782103740733076818433040893624460576706096188294529171362940967592507631348636606011346115980434147450705511490716640635688739020690279453438236930531133440901381392849163507484449076828386687476663619303412376248380175840467851210698290605196112357188811150723607303158506622574566366740720668999061320627793994112805759798332878792144188725498543014546662945079670707688135022230580562225942983096887732856788971494623888272184647618153045844390967248232348259587963698908456664795754200195991919240707615823002328977439748112690476546256873684352229063217889227643289360535947903046811114130586348244566489159211382258867880972564351646404364328416076247766114349880319792230537889671148058968061594279189647401954989466232962162567264739015818692956765601444248501821713300527995551312539849919933907083138030214072556753022600033565715934283182650908979350869698950542635843046765145668997627989606295925119763672907762567862769469947280606094290314917493590511523235698715397127866718077578671910380368991445381484562682604003456798248689847811138328054940490519768008320299631757043011485087384048591850157264392187414592464617404735275250506783992273121600117160338604710710015235631159734711153198198710616109850375758965576728904060387168114313084172893710817412764581206119054145955378853200366615264923610030157044627231777788649806700723598889528747481372190175074700005571108178930354895017924552067329003818814068686247959272205591627902292600592107710510448103392878991286820705448979977319695574374529708195463942431669050083984398993036790655541596099324867822475424361758944371791403787168166189093900243862038610001362193667280872414291108080291896093127526202667881902085595708111853836166128848729527875143202956393295910508349687029060692838441522579419764824996318479414814660898281725690484184326061946254276693688953540732363428302189694947766126078346328490315128061501009539164530614554234923393806214007779256337619373052025699319099789404390847443596972052065999017828537676265683558625452697455260991024576619614037537859594506363227095122489241931813728141668427013096050734578659047904243852086508154491350136491698639048125666610843702294730266721499164849610746803261583352580352858275799038584091667618877199539888680431991650866887781701439663176815592262016991396613153738021294160006906947533431677802632207226265881842757216055461439677336258462997385077307751473833315101468395296411397329672457933540390136107395245686243008096720460995545708974893048753897955544443791303790422346037768729236001386569593952300768091377768847789746299699489949016141866131552200856673695770822720338936659590666350594330040363762591189195691561626122704788696510356062748423100605472091437069471661080277379848576543481249822444235828329813543645124092220896643987201997945619030397327254617823136363375927622656301565813545578319730419339269008282952718252138855126583037630477490625995514925943105307478901043009876580816508144862607975129633326675259272351611791836777128931053144471668835182920514343609292493191180249366051791485330421043899773019267686085347768149502299280938065840007311767895491286098112311307002535600347898600653805084532572431553654422067661352337408211307834360326940015926958459588297845649462271300855594293344520727007718206398887404742186697709349647758173683580193168322111365547392288184271373843690526638607662451284299368435082612881367358536293873792369928837047900484722240370919885912556341130849457067599032002751632513926694249485692320904596897775676762684224768120033279577059394613185252356456291805905295974791266162882381429824622654141067246487216174351317397697122228010100668178786776119825961537643641828573481088089988571570279722274734750248439022607880448075724807701621064670166965100202654371260046641935546165838945950143502160890185703558173661823437491622669077311800121188299737319891006060966841193266075165452741829459541189277264192546108246351931647783837078295218389645376236304858042774417907169146356546201215125418664885396161542055152375000426794253417764590821513675258479774465114750438460596325820468809667795709044645884673847481638045635188183210386594798204376334738389017759714236223057776395541011294523488098341476645559342209402059733452337956309441446698222457026367119493286653989491344225517746402732596722993581333110831711807234044326813737231209669052411856734897392234152750707954137453460386506786693396236535556479102508529284294227710593056660625152290924148057080971159783458351173168204129645967070633303569271821496292272073250126955216172649821895790908865085382490848904421755530946832055636316431893917626269931034289485184392539670922412565933079102365485294162132200251193795272480340133135247014182195618419055761030190199521647459734401211601239235679307823190770288415814605647291481745105388060109787505925537152356112290181284710137917215124667428500061818271276125025241876177485994084521492727902567005925854431027704636911098800554312457229683836980470864041706010966962231877065395275783874454229129966623016408054769705821417128636329650130416501278156397799631957412627634011130135082721772287129164002237230234809031485343677016544959380750634285293053131127965945266651960426350406454862543383772209428482543536823186182982713182489884498260285705690699045790998144649193654563259496570044689011049923939218088155626191834404362264965506449848521612498442375928443642612004256628602157801140467879662339228190804577624109076487087406157070486658398144845855803277997327929143195789110373530019873110486895656281917362036703039179710646309906285483702836118486672219457621775034511770110458001291255925462680537427727378863726783016568351092332280649908459179620305691566806180826586923920561895421631986004793961133953226395999749526798801074576466538377400437463695133685671362553184054638475191646737948743270916620098057717103475575333102702706317395612448413745782734376330101853438497450236265733191742446567787499665000938706441886733491099877926005340862442833450486907338279348425305698737469497333364267191968992849534561045719338665222471536681145666596959735075972188416698767321649331898967182978657974612216573922404856900225324160367805329990925438960169901664189038843548375648056012628830409421321300206164540821986138099462721214327234457806819925823202851398237118926541234460723597174777907172041523181575194793527456442984630888846385381068621715274531612303165705848974316209831401326306699896632888532682145204083110738032052784669279984003137878996525635126885368435559620598057278951754498694219326972133205286374577983487319388899574634252048213337552584571056619586932031563299451502519194559691231437579991138301656117185508816658756751184338145761060365142858427872190232598107834593970738225147111878311540875777560020664124562293239116606733386480367086953749244898068000217666674827426925968686433731916548717750106343608307376281613984107392410037196754833838054369880310983922140260514297591221159148505938770679068701351029862207502287721123345624421024715163941251258954337788492834236361124473822814504596821452253550035968325337489186278678359443979041598043992124889848660795045011701169092519383155609441705397900600291315024253848282782826223304151370929502192196508374714697845805550615914539506437316401173317807741497557116733034632008408954066541694665746735785483133770133628948904397670025863002540635264006601631712883920305576358989492412827022489373848906764385339931878608019223108328847459816417701264089078551777830131616162049792779670521847212730327970738223860581986744668610994383049960437407323195784473254857416239738852016202384784256163512597161783106850156299135559874758848151014815490937380933394074455700842090155903853444962128368313687375166780513082594599771257467939781491953642874321122421579851584491669362551569370916855252644720786527971466476760328471332985501945689772758983450586004316822658631176606237201721007922216410188299330808409384014213759697185976897042759041500946595252763487628135867117352364964121058854934496645898651826545634382851159137631569519895230262881794959971545221250667461174394884433312659432286710965281109501693028351496524082850120190831078678067061851145740970787563117610746428835593915985421673115153096948758378955979586132649569817205284291038172721213138681565524428109871168862743968021885581515367531218374119972919471325465199144188500672036481975944167950887487934416759598361960010994838744709079104099785974656112459851972157558134628546189728615020774374529539536929655449012953097288963767713353842429715394179547179095580120134210175150931491664699052366350233024087218654727629639065723341455005903913890253699317155917179823065162679744711857951506573868504088229934804445549850597823297898617029498418376255258757455303112991914341109413088238114443068843062655305601658801408561023324210300218460588586954418502977463085858496130037238190325162225570729975710727306066072916922978033647048840958711228045188511908718588299514331534128549297173849768523136276076868494780364948299904475715771141080958058141208956059471668626290036145602625334863284986816039463372436667112964460292915746181117789169695839947080954788863503281129626899231110099889317815313946681882028368363373822281414974006917942192888817139116283910295684918233358930813360131488748366464224381776081007739183393749346933644748150564933649323157235306109385796839902153381449126925350768211098738352197507736653475499431740580563099143218212547336281359488317681489194306530426029773885492974570569448783077945878865062970895499843760181694031056909587141386804846359853684034105948341788438963179956468815791937174656705047441528027712541569401365862097760735632832966564135817028088013546326104892768731829917950379944446328158595181380144716817284996793061814177131912099236282922612543236071226270324572637946863533391758737446552006008819975294017572421299723542069630427857950608911113416534893431149175314953530067419744979017235181671568754163484949491289001739377451431928382431183263265079530371177806185851153508809998200482761808307209649636476943066172549186143700971387567940218696710148540307471561091358933165600167252126542502898612259306484105898847129649230941215144563947889999327145875969555737090855150648002321476443037232466147111552578583071024936898814562568786834745518893385181791667579054210421036349316257870476543126790661216644142285017446278477132740595579600648343288827864837043456066966456899746910373987712891593313271266247505582258634928427718355831641593667712218537642376222104779338956378722902509543014182257180331300148113377736941508488867501893156994849838936052666818012783912005801431596441910546663236810148207799356523056490420711364192200177189107935243234322761787712568251126481332974354926568682748715986654943041648468220593921673359485057849622807932422649812705271398407720995707236227009245067665680069149966555737866411877079767754867028786431817941521796178310655030287157272282250812017060713380339641841211253856248920130010782462165136989511064611133562443838185366273563783436921279354709230119655914915800561707258518503167289370411936374780625824298250726464801821523430268081486978164824349353456855843696378384153838051184406043696871666416514036129729992912630842812149152469877429332305214999981829046119471676727503742221367186614654042534463141660649871499001000660041544868437352208483059495953182872280520828676300361091734508632133033647289584176588755345227938480297724485711815574893561311524926772006362198369980664159549388683836411891430443767715498026544959061738265591178545999378510861446014967645550103653971251138583505085112442517772923814396233043724036032603181442991365750246012787514117944901305803452199992701148071712847770301254994886841867572975189214295652512486943983729047410363121899124217339550688778643130750024823361832738729697376598820053895902935486054979802320400472236873557411858132734337978931582039412878989728973298812553514507641535360519462112217000676321611195841029252568536561813138784086477147099724553013170761712163186600291464501378587854802096244703771373587720086738054108140042311418525803293267396324596914044834665722042880679280616029884043400536534009706581694636096660911110968789751801325224478246957913251892122653056085866541115373584912790254654369020869419871125588453729063224423222287139122012248769976837147645598526739225904997885514250047585260297929306159913444898341973583316070107516452301310796620382579278533125161760789984630103493496981494261055367836366022561213767081421091373531780682420175737470287189310207606953355721704357535177461573524838432101571399813798596607129664438314791296359275429627129436142685922138993054980645399144588692472767598544271527788443836760149912897358259961869729756588978741082189422337344547375227693199222635973520722998387368484349176841191020246627479579564349615012657433845758638834735832242535328142047826934473129971189346354502994681747128179298167439644524956655532311649920677163664580318205849626132234652606175413532444702007661807418914040158148560001030119994109595492321434406067634769713089513389171050503856336503545166431774489640061738861761193622676890576955693918707703942304940038440622614449572516631017080642923345170422426679607075404028551182398361531383751432493056398381877995594942545196756559181968690885283434886050828529642437578712929439366177362830136595872723080969468398938676366226456791132977469812675226595621009318322081754694778878755356188335083870248295346078597023609865656376722755704495258739871812593441903785275571333409842450127258596692434317689018966145404453679047136294238156127656824247864736176671770647002431119711090007474065945650315375044177982192306323700872039212085499569681061379189029961178936752146022386905665481382858280449537530160921422195940638787074787991194920898374091788534417523064715030278397979864517336625329511775105559014160459873338186887977858817291976604516353353556047648420520888811722831990044504284486852338334530105533929637308039738230604714104525470094899407601215247602819963846343554852932377161410869591950786873276075400085220065031871239272857835807010762542769655355964789450166013816295177908531139811092831583216931563867459747449584385282701658246192092219529134323496779345585613140207765996142546463288677356891785576835169608392864188830094883324700447958316931533832382377876344426323456301679513671047510469669001217777128065522453689371871451567394733440447280450959433090683667110655953338602938000999949010642769859623260401863733572846679531229683156358145420890540651226419162015504500430562136991850941034609601030543816694795964585804425194905110733387679946734471718615647723811737035654917628707589456035519195603962301157866323750234725054461073979402475184415558178087962822231972692984516683306919505079993357259165675557294585962182052650473353712351623662770479333289322136141858785972771685682725303734836891911847197133753088446777943274857148827821608844765700041403499921376794209627560883081509438030705666022764678117533361028187800710219794428777313146387857817205661409023041499923248268982477222109852189758140879763486146763606368674611966620347304608917277240045953051376938375381543486981101990651706961774052218247422657652138152740612699012706880875386408669901461740890540981877671880076124151967064152117653084325544261017536348281196837493395825742541244634247233586360777980960199745187758845459645895956779558869098404768259253477849930457883128541747079059795909431627722327844578918694214929451540174214623240300841907975296782445969183509474202123617940309048634960534054931299919496087957952586977170236680033862505764938088740994009589948109397983231108838769236490221499111120870639202892490698435333152727991330986335454324971441378059132240814960156485679843966464780280409057580889190254236606774500413415794312112501275232250148067232979652230488493751166084976116412777395311302041566848265531411348993243747890268935173904043294851610659785832253168204202834993641595980197343889883020994152152288611175126686173051956249367180053845637855129171848417841594797435580617856680758491080185805695567990185198397660693358224779136504562705766735170961550493338390452612404395517449136885115987454340932040102218982707539212403241042424451570052968378815749468441508011138612561164102477190903050040240662278945607061512108266146098662040425010583978098192019726759010749924884966139441184159734610382401178556739080566483321039073867083298691078093495828888707110651559651222542929154212923108071159723275797510859911398076844732639426419452063138217862260999160086752446265457028969067192282283045169111363652774517975842147102219099906257373383472726498678244401048998507631630668050267115944636293525120269424810854530602810627264236538250773340575475701704367039596467715959261029438313074897245505729085688496091346323165819468660587092144653716755655531962091865952628448253731353698162517351930115341581171353292035873164168839107994000677266031617527582917398395852606454113318985505747847121053505795649095931672167565624818782002769963734155880000867852567422461511406015760115910256449002264980039498403358091309140197877843650167960167465370287466062584346329708303725980494653589318912163976013193079476972058034710553111117215859219066231028099212084069283091906017370764654655683413207556315315006453462321007133584907633048328153458698497332599801187479664273140279381289961720524540674695271948079930396730194274036466594154400092799908634806622334906695224044652158992864203435098858422692019340575496840904812955522654754650713532842543496616084954788090727649930252702815067862810825243222979985391759845188868387004477101866772159439708514664612871148749531862180941719676843144666435175837688436786081446319641912566574047718699160915550910878919431253671945651261878486910876729910565595155159739659034383628124629118117760949411880105946336671039049777312004243578115790429823045072038322781246413671297959415082918378213212876890545963586369344879749784841123274921331663162812456388238288715648447883142417650147980187858215768793063001153788998014623690135803753306246148576074932567807682651045738059018831237617271889933790487113395588485234240255002352200613574914318259142479829367775490496399350755839668967578364316618369307625603528602940662803255416535431518013714821941772672244005268401996533334184004345525296592918502940131600651124395297874364222806977720437363717873457948420238745151249157913139411148608416429347958793681868609689684640858334131017858142710955416293375915178392341303110543328703526599993904966822112768158316511246866451167351378214345336650598328347443536290312393672084593164394941881138607974670134709640378534907149089842317891739783650654751982883367395714360000003439863363212091718954899055748693397700245632475954504411422582410783866837655467400137324322809113692670682805397549111166171102397437749479335174036135005397581475520834285772800986189401984375446435081498218360112577632447389452051636938585136484259964518361856989088721789764694721246807900330925083496645841656554261294195108847197209106605105540933731954888406444080280579549008076040034154662137669606444293774985897353625591959618552448187940317374508256072895120945456562159540405425814886929842786582357673195799285293120866275922366115137445767916063621675267440451221051052090834707443986137829082352772895849625656881972792768694795806100573787084121444815034797422312103295359297822377134077549545477791813823542607184617108389097825964406170543546968567030745411634244134486308676327949177682923093183221341455482591367202823284396549001805653203960795517074496039006696990334199278212696767771835209083959545341866777944872740383733381985235884202840150981579594685874537989503257362809837592216229258598599123843993575573285028613155970362934249814178056461615863415338635077223269996508860870999964899373049307170967888740149746147542880387421250689212155876692242387434701120990859082164073576380817386959755176083877600277517253037133445654852635661720197563001580049790223419586738061442401502436288957503206533690825756785507020555105572381878574650371086308158185862815883054564662297694803970618265491385181326737485227188267917919091354407852685476254126683398240534022469989966652573155637645862251862823092085424412805997628505488913098331761884983352975136073772030571342739638126588567405013841074788943393996603591853934198416322617654857376671943132840050626295140357877264680649549355746326408186979718630218760025813995719923601345374229758918285167511358171472625828596940798518571870075823122317068134867930884899275181661399609753105295773584618525865211893339375771859916335112163441037910451845019023066893064178977808158101360449495409665363660370075881004450265734935127707426742578608784898185628869980851665713320835842613381142623855420315774246613108873106318111989880289722849790551075148403702290580483052731884959994156606537314021296702220821915862905952604040620011815269664910068587592655660567562963361434230232810747488395040380984981860056164646099819257616235478710913832967563761506732550860683433720438748186791668975746563456020002562889601191100980453350423842063824039434163502977688802779835087481178298349417211674919425601608685332435385951152061809031241698182079314615062073826097180458265687043623935757495737332781578904386011378078508110273049446611821957450170106059384336519458628360682108585130499820420578458577175933849015564447305834515291412561679970569657426139901681932056241927977282026714297258700193234337873153939403115411184101414292741703537542003698760608765500109345299007034032401334806388514095769557147190364152027721127070187421548123931953220997506553022646844227700020589045922742423904937051507367764629844971682121994198274794049092601715727439368569721862936007387077810797440975556627807371228030350048829843919546433753355787895064018998685060281902452191177018634505171087023903398550540704454189088472042376499749035038518949505897971286631644699407490959473411581934618336692169573605081585080837952036335619947691937965065016808710250735070825260046821242820434367245824478859256555487861614478717581068572356895150707602217433511627331709472765932413249132702425519391509083601346239612335001086614623850633127072987745618984384288764099836164964775714638573247333226653894523588365972955159905187411779288608760239306160016168434070611663449248395156319152882728822831375458678269830696691220130954815935450754923554167766876455212545681242936427474153815692219503331560151614492247512488957534835926226263545406704767033866410025277276800886383266629488582740369655329362236090572479794734434077704284318507901973469071141230364111729224929307731939309795452877412451183953480382210373644697046967493042810911797232448615413264031578430955396671061468083815548947146733652483679138566431084747848676243012018489329109615281108087617422779131629345494425395422727309645057976122885347393189600810965202090151104579377602529543130188938184010247010134929317443562883578609861545691161669857388024973756940558138630581099823372565164920155443216861690537054630176154809626620800633059320775897175589925862195462096455464624399535391743228225433267174308492508396461328929584567927365409119947616225155964704061297047759818551878441419948614013153859322060745185909608884280218943358691959604936409651570327527570641500776261323783648149005245481413195989296398441371781402764122087644989688629798910870164270169014007825748311598976330612951195680427485317886333041169767175063822135213839779138443325644288490872919067009802496281560626258636942322658490628628035057282983101266919109637258378149363774960594515216932644945188292639525772348420077356021656909077097264985642831778694777804964343991762549216500608626285329471055602670413384500507827390640287529864161287496473708235188892189612641279553536442286955430551308700009878557534223100547153412810957024870812654319123261956462149376527526356402127388765103883255007364899937167183280028398832319373301564123277185395654932422977953016534830128490677845037490891749347389015649588574802194996722621185874361039774946338633057887487405540005440439344888192044102134790034598411927024921557026873700970995205391930979319495883265922171508324621942300185974396706491149559411733728199869021311629886680267446443489233020607003821262841723679627307191405008084085703978151998148822390059948911946474438682533745889962375133378280532928272016815977970066488394482446332210928320504045983008943565954267256879714918703447338237767914829203283196838105907715727191903042365315650957464549643425328069510396558733549803850995143463506175361480050195045201350200180281506933241918267855737764414097080945745624854867704904368368717590918057269794010465019484853146726642978667687697789291431128505043098192949736165944259471754765135205245072597538577958372797702972231435199958499522344049394502115428867244188717409524554771867484911475031801773304689909317974472957035192387686405544278134169807249382219749124257510162187439772902147704638010731470653154201300583810458905006764557332998149945854655105526374914354195867992595981412218735238407957416123372264063860431988936249867649693592569592128495906254446474331759999685163660305216426770428154681777589339252115538590526823311608302751194384823861552852465010329467297198112105314125898165100120742688143577590825227466863206188376830450921784582526239594189673003640808624233657620979111641766331328852352062487922978959456450333733139422384778582717195412347860434376165241568717943562570215636666680088531006728947033079540804583324192188488870712275670333173939262509073556164513677064199539111948881240659821685787131385056850623094155206877987539740658484250135205615103489821873770245063583314243624807432542464195984647411575625441010389671576677263196442524931941806472423789334668561083789808830313571333157729435664956078125304917594015895146954965223118559669048559467607968190167266634650186182955669893965019614544401768162810604465068448139561667220729261210164692339016793399632833013163850830967942792934551268435760356901970523138364640961311774904600772840862214747547653221505518116489887879087780918009050706040061220010051271575991225725282523378026809030528461581739558198122397010092017202251606352922464781615533532275453264543087093320924631855976580561717446840450048285353396546862678852330044967795580761661801833668792312510460809773895565488962815089519622093675058841609752282328250433712970186608193748968699961301486924694482420723632912367052542145464162968910442981633373266871675946715392611950649224725627254543274193495995569590243279097174392258098103601486364409101491734183079646345064833303404765711827040276868271418084574998493392039317445402616663674646668754385093967129918067471909885312710726724428584870694307099756567949198418996425748884764622030325637751112534060087936904565779272035205921345924272965206683338510673615276261016026647772485083344719891986802656197236420847504962661607797092906844757798251795569758235084371746103310387911789239441630112634077535773520558040066982523191225570519133631407211349723226549151062961739050617857127509403623146700931176133132018631158730886798239298009805089491510788371194099750375473674305745187265414016446924576792185753680363289139664155342066705623272936001177781498886100830877849571709880858667023104043242526785955562077310543072298032125941107957349146684680220501816192150766649106862033378713826058987655210423668198670177861672671972374156917880001690656659046965316154923604061891820982414006103779407166342002735828911994182647812782659666207030384795881442790246669264032799404016800137293477301530941805070587421153284642203006550763966756168318897005152026656649929417382840327305940740147117478464839241225676523593418554066440983706083636457657081801664285044258224551650808864421212113914352453935225522162483791737330329812349528984098613273709957407786789349311975204237925022851375880436791854547836416773151821457226504640800104202100410766027807729152555503218182387221708112766208665317651926458452495269685376314437998340336947124447247796973890514941120010934140073794061859447165516612674930799374705772930521750426383798367668159183589049652163726492960837147204067428996276720315410211504333742057182854090136325721437592054640471894328548696883599785122262130812989581571391597464534806099601555877223193450760315411663112963843719400333736013305526352571490454327925190794007111504785378036370897340146753465517470747096935814912797188187854376797751675927822300312945518595042883902735494672667647506072643698761394806879080593531793001711000214417701504495496412454361656210150919997862972495905809191825255486358703529320142005857057855419217730505342687533799076038746689684283402648733290888881745453047194740939258407362058242849349024756883352446212456101562729065130618520732925434179252299417447855189995098959999877410951464170076989305620163502192692653166599093238118295411937545448509428621839424186218067457128099385258842631930670182098008050900019819621758458932516877698594110522845465835679362969619219080897536813210484518784516230623911878024604050824909336069998094776253792973597037759066145994638578378211017122446355845171941670344732162722443265914858595797823752976323442911242311368603724514438765801271594060878788638511089680883165505046309006148832545452819908256238805872042843941834687865142541377686054291079721004271658 diff --git a/internal/compress/testdata/endnonzero.bin b/internal/compress/testdata/endnonzero.bin deleted file mode 100644 index cf08368a..00000000 Binary files a/internal/compress/testdata/endnonzero.bin and /dev/null differ diff --git a/internal/compress/testdata/endzerobits.bin b/internal/compress/testdata/endzerobits.bin deleted file mode 100644 index d9952f92..00000000 Binary files a/internal/compress/testdata/endzerobits.bin and /dev/null differ diff --git a/internal/compress/testdata/fse-artifact3.bin b/internal/compress/testdata/fse-artifact3.bin deleted file mode 100644 index 0607a9e7..00000000 Binary files a/internal/compress/testdata/fse-artifact3.bin and /dev/null differ diff --git a/internal/compress/testdata/gettysburg.txt b/internal/compress/testdata/gettysburg.txt deleted file mode 100644 index 2c9bcde3..00000000 --- a/internal/compress/testdata/gettysburg.txt +++ /dev/null @@ -1,29 +0,0 @@ - Four score and seven years ago our fathers brought forth on -this continent, a new nation, conceived in Liberty, and dedicated -to the proposition that all men are created equal. - Now we are engaged in a great Civil War, testing whether that -nation, or any nation so conceived and so dedicated, can long -endure. - We are met on a great battle-field of that war. - We have come to dedicate a portion of that field, as a final -resting place for those who here gave their lives that that -nation might live. It is altogether fitting and proper that -we should do this. - But, in a larger sense, we can not dedicate - we can not -consecrate - we can not hallow - this ground. - The brave men, living and dead, who struggled here, have -consecrated it, far above our poor power to add or detract. -The world will little note, nor long remember what we say here, -but it can never forget what they did here. - It is for us the living, rather, to be dedicated here to the -unfinished work which they who fought here have thus far so -nobly advanced. It is rather for us to be here dedicated to -the great task remaining before us - that from these honored -dead we take increased devotion to that cause for which they -gave the last full measure of devotion - - that we here highly resolve that these dead shall not have -died in vain - that this nation, under God, shall have a new -birth of freedom - and that government of the people, by the -people, for the people, shall not perish from this earth. - -Abraham Lincoln, November 19, 1863, Gettysburg, Pennsylvania diff --git a/internal/compress/testdata/html.txt b/internal/compress/testdata/html.txt deleted file mode 100644 index a8603543..00000000 --- a/internal/compress/testdata/html.txt +++ /dev/null @@ -1,1183 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Crdit Agricole Alpes Provence (Bouches-du-Rhne, Hautes Alpes et Vaucluse) - Crdit Agricole Alpes Provence (Bouches-du-Rhne, Hautes Alpes et Vaucluse) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - -
- - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - -
- © Crdit Agricole 2011 -
- -
- -
- - - - - \ No newline at end of file diff --git a/internal/compress/testdata/normcount2.bin b/internal/compress/testdata/normcount2.bin deleted file mode 100644 index 39050dae..00000000 --- a/internal/compress/testdata/normcount2.bin +++ /dev/null @@ -1 +0,0 @@ -868000113000000fd9F055125272181835410155551151-0_0040Y2BW4K_0x_j4L___e__331ms__QSlz_I__ \ No newline at end of file diff --git a/internal/compress/testdata/pi.txt b/internal/compress/testdata/pi.txt deleted file mode 100644 index ca99bbc2..00000000 --- a/internal/compress/testdata/pi.txt +++ /dev/null @@ -1 +0,0 @@ -3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679821480865132823066470938446095505822317253594081284811174502841027019385211055596446229489549303819644288109756659334461284756482337867831652712019091456485669234603486104543266482133936072602491412737245870066063155881748815209209628292540917153643678925903600113305305488204665213841469519415116094330572703657595919530921861173819326117931051185480744623799627495673518857527248912279381830119491298336733624406566430860213949463952247371907021798609437027705392171762931767523846748184676694051320005681271452635608277857713427577896091736371787214684409012249534301465495853710507922796892589235420199561121290219608640344181598136297747713099605187072113499999983729780499510597317328160963185950244594553469083026425223082533446850352619311881710100031378387528865875332083814206171776691473035982534904287554687311595628638823537875937519577818577805321712268066130019278766111959092164201989380952572010654858632788659361533818279682303019520353018529689957736225994138912497217752834791315155748572424541506959508295331168617278558890750983817546374649393192550604009277016711390098488240128583616035637076601047101819429555961989467678374494482553797747268471040475346462080466842590694912933136770289891521047521620569660240580381501935112533824300355876402474964732639141992726042699227967823547816360093417216412199245863150302861829745557067498385054945885869269956909272107975093029553211653449872027559602364806654991198818347977535663698074265425278625518184175746728909777727938000816470600161452491921732172147723501414419735685481613611573525521334757418494684385233239073941433345477624168625189835694855620992192221842725502542568876717904946016534668049886272327917860857843838279679766814541009538837863609506800642251252051173929848960841284886269456042419652850222106611863067442786220391949450471237137869609563643719172874677646575739624138908658326459958133904780275900994657640789512694683983525957098258226205224894077267194782684826014769909026401363944374553050682034962524517493996514314298091906592509372216964615157098583874105978859597729754989301617539284681382686838689427741559918559252459539594310499725246808459872736446958486538367362226260991246080512438843904512441365497627807977156914359977001296160894416948685558484063534220722258284886481584560285060168427394522674676788952521385225499546667278239864565961163548862305774564980355936345681743241125150760694794510965960940252288797108931456691368672287489405601015033086179286809208747609178249385890097149096759852613655497818931297848216829989487226588048575640142704775551323796414515237462343645428584447952658678210511413547357395231134271661021359695362314429524849371871101457654035902799344037420073105785390621983874478084784896833214457138687519435064302184531910484810053706146806749192781911979399520614196634287544406437451237181921799983910159195618146751426912397489409071864942319615679452080951465502252316038819301420937621378559566389377870830390697920773467221825625996615014215030680384477345492026054146659252014974428507325186660021324340881907104863317346496514539057962685610055081066587969981635747363840525714591028970641401109712062804390397595156771577004203378699360072305587631763594218731251471205329281918261861258673215791984148488291644706095752706957220917567116722910981690915280173506712748583222871835209353965725121083579151369882091444210067510334671103141267111369908658516398315019701651511685171437657618351556508849099898599823873455283316355076479185358932261854896321329330898570642046752590709154814165498594616371802709819943099244889575712828905923233260972997120844335732654893823911932597463667305836041428138830320382490375898524374417029132765618093773444030707469211201913020330380197621101100449293215160842444859637669838952286847831235526582131449576857262433441893039686426243410773226978028073189154411010446823252716201052652272111660396665573092547110557853763466820653109896526918620564769312570586356620185581007293606598764861179104533488503461136576867532494416680396265797877185560845529654126654085306143444318586769751456614068007002378776591344017127494704205622305389945613140711270004078547332699390814546646458807972708266830634328587856983052358089330657574067954571637752542021149557615814002501262285941302164715509792592309907965473761255176567513575178296664547791745011299614890304639947132962107340437518957359614589019389713111790429782856475032031986915140287080859904801094121472213179476477726224142548545403321571853061422881375850430633217518297986622371721591607716692547487389866549494501146540628433663937900397692656721463853067360965712091807638327166416274888800786925602902284721040317211860820419000422966171196377921337575114959501566049631862947265473642523081770367515906735023507283540567040386743513622224771589150495309844489333096340878076932599397805419341447377441842631298608099888687413260472156951623965864573021631598193195167353812974167729478672422924654366800980676928238280689964004824354037014163149658979409243237896907069779422362508221688957383798623001593776471651228935786015881617557829735233446042815126272037343146531977774160319906655418763979293344195215413418994854447345673831624993419131814809277771038638773431772075456545322077709212019051660962804909263601975988281613323166636528619326686336062735676303544776280350450777235547105859548702790814356240145171806246436267945612753181340783303362542327839449753824372058353114771199260638133467768796959703098339130771098704085913374641442822772634659470474587847787201927715280731767907707157213444730605700733492436931138350493163128404251219256517980694113528013147013047816437885185290928545201165839341965621349143415956258658655705526904965209858033850722426482939728584783163057777560688876446248246857926039535277348030480290058760758251047470916439613626760449256274204208320856611906254543372131535958450687724602901618766795240616342522577195429162991930645537799140373404328752628889639958794757291746426357455254079091451357111369410911939325191076020825202618798531887705842972591677813149699009019211697173727847684726860849003377024242916513005005168323364350389517029893922334517220138128069650117844087451960121228599371623130171144484640903890644954440061986907548516026327505298349187407866808818338510228334508504860825039302133219715518430635455007668282949304137765527939751754613953984683393638304746119966538581538420568533862186725233402830871123282789212507712629463229563989898935821167456270102183564622013496715188190973038119800497340723961036854066431939509790190699639552453005450580685501956730229219139339185680344903982059551002263535361920419947455385938102343955449597783779023742161727111723643435439478221818528624085140066604433258885698670543154706965747458550332323342107301545940516553790686627333799585115625784322988273723198987571415957811196358330059408730681216028764962867446047746491599505497374256269010490377819868359381465741268049256487985561453723478673303904688383436346553794986419270563872931748723320837601123029911367938627089438799362016295154133714248928307220126901475466847653576164773794675200490757155527819653621323926406160136358155907422020203187277605277219005561484255518792530343513984425322341576233610642506390497500865627109535919465897514131034822769306247435363256916078154781811528436679570611086153315044521274739245449454236828860613408414863776700961207151249140430272538607648236341433462351897576645216413767969031495019108575984423919862916421939949072362346468441173940326591840443780513338945257423995082965912285085558215725031071257012668302402929525220118726767562204154205161841634847565169998116141010029960783869092916030288400269104140792886215078424516709087000699282120660418371806535567252532567532861291042487761825829765157959847035622262934860034158722980534989650226291748788202734209222245339856264766914905562842503912757710284027998066365825488926488025456610172967026640765590429099456815065265305371829412703369313785178609040708667114965583434347693385781711386455873678123014587687126603489139095620099393610310291616152881384379099042317473363948045759314931405297634757481193567091101377517210080315590248530906692037671922033229094334676851422144773793937517034436619910403375111735471918550464490263655128162288244625759163330391072253837421821408835086573917715096828874782656995995744906617583441375223970968340800535598491754173818839994469748676265516582765848358845314277568790029095170283529716344562129640435231176006651012412006597558512761785838292041974844236080071930457618932349229279650198751872127267507981255470958904556357921221033346697499235630254947802490114195212382815309114079073860251522742995818072471625916685451333123948049470791191532673430282441860414263639548000448002670496248201792896476697583183271314251702969234889627668440323260927524960357996469256504936818360900323809293459588970695365349406034021665443755890045632882250545255640564482465151875471196218443965825337543885690941130315095261793780029741207665147939425902989695946995565761218656196733786236256125216320862869222103274889218654364802296780705765615144632046927906821207388377814233562823608963208068222468012248261177185896381409183903673672220888321513755600372798394004152970028783076670944474560134556417254370906979396122571429894671543578468788614445812314593571984922528471605049221242470141214780573455105008019086996033027634787081081754501193071412233908663938339529425786905076431006383519834389341596131854347546495569781038293097164651438407007073604112373599843452251610507027056235266012764848308407611830130527932054274628654036036745328651057065874882256981579367897669742205750596834408697350201410206723585020072452256326513410559240190274216248439140359989535394590944070469120914093870012645600162374288021092764579310657922955249887275846101264836999892256959688159205600101655256375678566722796619885782794848855834397518744545512965634434803966420557982936804352202770984294232533022576341807039476994159791594530069752148293366555661567873640053666564165473217043903521329543529169414599041608753201868379370234888689479151071637852902345292440773659495630510074210871426134974595615138498713757047101787957310422969066670214498637464595280824369445789772330048764765241339075920434019634039114732023380715095222010682563427471646024335440051521266932493419673977041595683753555166730273900749729736354964533288869844061196496162773449518273695588220757355176651589855190986665393549481068873206859907540792342402300925900701731960362254756478940647548346647760411463233905651343306844953979070903023460461470961696886885014083470405460742958699138296682468185710318879065287036650832431974404771855678934823089431068287027228097362480939962706074726455399253994428081137369433887294063079261595995462624629707062594845569034711972996409089418059534393251236235508134949004364278527138315912568989295196427287573946914272534366941532361004537304881985517065941217352462589548730167600298865925786628561249665523533829428785425340483083307016537228563559152534784459818313411290019992059813522051173365856407826484942764411376393866924803118364453698589175442647399882284621844900877769776312795722672655562596282542765318300134070922334365779160128093179401718598599933849235495640057099558561134980252499066984233017350358044081168552653117099570899427328709258487894436460050410892266917835258707859512983441729535195378855345737426085902908176515578039059464087350612322611200937310804854852635722825768203416050484662775045003126200800799804925485346941469775164932709504934639382432227188515974054702148289711177792376122578873477188196825462981268685817050740272550263329044976277894423621674119186269439650671515779586756482399391760426017633870454990176143641204692182370764887834196896861181558158736062938603810171215855272668300823834046564758804051380801633638874216371406435495561868964112282140753302655100424104896783528588290243670904887118190909494533144218287661810310073547705498159680772009474696134360928614849417850171807793068108546900094458995279424398139213505586422196483491512639012803832001097738680662877923971801461343244572640097374257007359210031541508936793008169980536520276007277496745840028362405346037263416554259027601834840306811381855105979705664007509426087885735796037324514146786703688098806097164258497595138069309449401515422221943291302173912538355915031003330325111749156969174502714943315155885403922164097229101129035521815762823283182342548326111912800928252561902052630163911477247331485739107775874425387611746578671169414776421441111263583553871361011023267987756410246824032264834641766369806637857681349204530224081972785647198396308781543221166912246415911776732253264335686146186545222681268872684459684424161078540167681420808850280054143613146230821025941737562389942075713627516745731891894562835257044133543758575342698699472547031656613991999682628247270641336222178923903176085428943733935618891651250424404008952719837873864805847268954624388234375178852014395600571048119498842390606136957342315590796703461491434478863604103182350736502778590897578272731305048893989009923913503373250855982655867089242612429473670193907727130706869170926462548423240748550366080136046689511840093668609546325002145852930950000907151058236267293264537382104938724996699339424685516483261134146110680267446637334375340764294026682973865220935701626384648528514903629320199199688285171839536691345222444708045923966028171565515656661113598231122506289058549145097157553900243931535190902107119457300243880176615035270862602537881797519478061013715004489917210022201335013106016391541589578037117792775225978742891917915522417189585361680594741234193398420218745649256443462392531953135103311476394911995072858430658361935369329699289837914941939406085724863968836903265564364216644257607914710869984315733749648835292769328220762947282381537409961545598798259891093717126218283025848112389011968221429457667580718653806506487026133892822994972574530332838963818439447707794022843598834100358385423897354243956475556840952248445541392394100016207693636846776413017819659379971557468541946334893748439129742391433659360410035234377706588867781139498616478747140793263858738624732889645643598774667638479466504074111825658378878454858148962961273998413442726086061872455452360643153710112746809778704464094758280348769758948328241239292960582948619196670918958089833201210318430340128495116203534280144127617285830243559830032042024512072872535581195840149180969253395075778400067465526031446167050827682772223534191102634163157147406123850425845988419907611287258059113935689601431668283176323567325417073420817332230462987992804908514094790368878687894930546955703072619009502076433493359106024545086453628935456862958531315337183868265617862273637169757741830239860065914816164049449650117321313895747062088474802365371031150898427992754426853277974311395143574172219759799359685252285745263796289612691572357986620573408375766873884266405990993505000813375432454635967504844235284874701443545419576258473564216198134073468541117668831186544893776979566517279662326714810338643913751865946730024434500544995399742372328712494834706044063471606325830649829795510109541836235030309453097335834462839476304775645015008507578949548931393944899216125525597701436858943585877526379625597081677643800125436502371412783467926101995585224717220177723700417808419423948725406801556035998390548985723546745642390585850216719031395262944554391316631345308939062046784387785054239390524731362012947691874975191011472315289326772533918146607300089027768963114810902209724520759167297007850580717186381054967973100167870850694207092232908070383263453452038027860990556900134137182368370991949516489600755049341267876436746384902063964019766685592335654639138363185745698147196210841080961884605456039038455343729141446513474940784884423772175154334260306698831768331001133108690421939031080143784334151370924353013677631084913516156422698475074303297167469640666531527035325467112667522460551199581831963763707617991919203579582007595605302346267757943936307463056901080114942714100939136913810725813781357894005599500183542511841721360557275221035268037357265279224173736057511278872181908449006178013889710770822931002797665935838758909395688148560263224393726562472776037890814458837855019702843779362407825052704875816470324581290878395232453237896029841669225489649715606981192186584926770403956481278102179913217416305810554598801300484562997651121241536374515005635070127815926714241342103301566165356024733807843028655257222753049998837015348793008062601809623815161366903341111386538510919367393835229345888322550887064507539473952043968079067086806445096986548801682874343786126453815834280753061845485903798217994599681154419742536344399602902510015888272164745006820704193761584547123183460072629339550548239557137256840232268213012476794522644820910235647752723082081063518899152692889108455571126603965034397896278250016110153235160519655904211844949907789992007329476905868577878720982901352956613978884860509786085957017731298155314951681467176959760994210036183559138777817698458758104466283998806006162298486169353373865787735983361613384133853684211978938900185295691967804554482858483701170967212535338758621582310133103877668272115726949518179589754693992642197915523385766231676275475703546994148929041301863861194391962838870543677743224276809132365449485366768000001065262485473055861598999140170769838548318875014293890899506854530765116803337322265175662207526951791442252808165171667766727930354851542040238174608923283917032754257508676551178593950027933895920576682789677644531840404185540104351348389531201326378369283580827193783126549617459970567450718332065034556644034490453627560011250184335607361222765949278393706478426456763388188075656121689605041611390390639601620221536849410926053876887148379895599991120991646464411918568277004574243434021672276445589330127781586869525069499364610175685060167145354315814801054588605645501332037586454858403240298717093480910556211671546848477803944756979804263180991756422809873998766973237695737015808068229045992123661689025962730430679316531149401764737693873514093361833216142802149763399189835484875625298752423873077559555955465196394401821840998412489826236737714672260616336432964063357281070788758164043814850188411431885988276944901193212968271588841338694346828590066640806314077757725705630729400492940302420498416565479736705485580445865720227637840466823379852827105784319753541795011347273625774080213476826045022851579795797647467022840999561601569108903845824502679265942055503958792298185264800706837650418365620945554346135134152570065974881916341359556719649654032187271602648593049039787489589066127250794828276938953521753621850796297785146188432719223223810158744450528665238022532843891375273845892384422535472653098171578447834215822327020690287232330053862163479885094695472004795231120150432932266282727632177908840087861480221475376578105819702226309717495072127248479478169572961423658595782090830733233560348465318730293026659645013718375428897557971449924654038681799213893469244741985097334626793321072686870768062639919361965044099542167627840914669856925715074315740793805323925239477557441591845821562518192155233709607483329234921034514626437449805596103307994145347784574699992128599999399612281615219314888769388022281083001986016549416542616968586788372609587745676182507275992950893180521872924610867639958916145855058397274209809097817293239301067663868240401113040247007350857828724627134946368531815469690466968693925472519413992914652423857762550047485295476814795467007050347999588867695016124972282040303995463278830695976249361510102436555352230690612949388599015734661023712235478911292547696176005047974928060721268039226911027772261025441492215765045081206771735712027180242968106203776578837166909109418074487814049075517820385653909910477594141321543284406250301802757169650820964273484146957263978842560084531214065935809041271135920041975985136254796160632288736181367373244506079244117639975974619383584574915988097667447093006546342423460634237474666080431701260052055928493695941434081468529815053947178900451835755154125223590590687264878635752541911288877371766374860276606349603536794702692322971868327717393236192007774522126247518698334951510198642698878471719396649769070825217423365662725928440620430214113719922785269984698847702323823840055655517889087661360130477098438611687052310553149162517283732728676007248172987637569816335415074608838663640693470437206688651275688266149730788657015685016918647488541679154596507234287730699853713904300266530783987763850323818215535597323530686043010675760838908627049841888595138091030423595782495143988590113185835840667472370297149785084145853085781339156270760356390763947311455495832266945702494139831634332378975955680856836297253867913275055542524491943589128405045226953812179131914513500993846311774017971512283785460116035955402864405902496466930707769055481028850208085800878115773817191741776017330738554758006056014337743299012728677253043182519757916792969965041460706645712588834697979642931622965520168797300035646304579308840327480771811555330909887025505207680463034608658165394876951960044084820659673794731680864156456505300498816164905788311543454850526600698230931577765003780704661264706021457505793270962047825615247145918965223608396645624105195510522357239739512881816405978591427914816542632892004281609136937773722299983327082082969955737727375667615527113922588055201898876201141680054687365580633471603734291703907986396522961312801782679717289822936070288069087768660593252746378405397691848082041021944719713869256084162451123980620113184541244782050110798760717155683154078865439041210873032402010685341947230476666721749869868547076781205124736792479193150856444775379853799732234456122785843296846647513336573692387201464723679427870042503255589926884349592876124007558756946413705625140011797133166207153715436006876477318675587148783989081074295309410605969443158477539700943988394914432353668539209946879645066533985738887866147629443414010498889931600512076781035886116602029611936396821349607501116498327856353161451684576956871090029997698412632665023477167286573785790857466460772283415403114415294188047825438761770790430001566986776795760909966936075594965152736349811896413043311662774712338817406037317439705406703109676765748695358789670031925866259410510533584384656023391796749267844763708474978333655579007384191473198862713525954625181604342253729962863267496824058060296421146386436864224724887283434170441573482481833301640566959668866769563491416328426414974533349999480002669987588815935073578151958899005395120853510357261373640343675347141048360175464883004078464167452167371904831096767113443494819262681110739948250607394950735031690197318521195526356325843390998224986240670310768318446607291248747540316179699411397387765899868554170318847788675929026070043212666179192235209382278788809886335991160819235355570464634911320859189796132791319756490976000139962344455350143464268604644958624769094347048293294140411146540923988344435159133201077394411184074107684981066347241048239358274019449356651610884631256785297769734684303061462418035852933159734583038455410337010916767763742762102137013548544509263071901147318485749233181672072137279355679528443925481560913728128406333039373562420016045664557414588166052166608738748047243391212955877763906969037078828527753894052460758496231574369171131761347838827194168606625721036851321566478001476752310393578606896111259960281839309548709059073861351914591819510297327875571049729011487171897180046961697770017913919613791417162707018958469214343696762927459109940060084983568425201915593703701011049747339493877885989417433031785348707603221982970579751191440510994235883034546353492349826883624043327267415540301619505680654180939409982020609994140216890900708213307230896621197755306659188141191577836272927461561857103721724710095214236964830864102592887457999322374955191221951903424452307535133806856807354464995127203174487195403976107308060269906258076020292731455252078079914184290638844373499681458273372072663917670201183004648190002413083508846584152148991276106513741539435657211390328574918769094413702090517031487773461652879848235338297260136110984514841823808120540996125274580881099486972216128524897425555516076371675054896173016809613803811914361143992106380050832140987604599309324851025168294467260666138151745712559754953580239983146982203613380828499356705575524712902745397762140493182014658008021566536067765508783804304134310591804606800834591136640834887408005741272586704792258319127415739080914383138456424150940849133918096840251163991936853225557338966953749026620923261318855891580832455571948453875628786128859004106006073746501402627824027346962528217174941582331749239683530136178653673760642166778137739951006589528877427662636841830680190804609849809469763667335662282915132352788806157768278159588669180238940333076441912403412022316368577860357276941541778826435238131905028087018575047046312933353757285386605888904583111450773942935201994321971171642235005644042979892081594307167019857469273848653833436145794634175922573898588001698014757420542995801242958105456510831046297282937584161162532562516572498078492099897990620035936509934721582965174135798491047111660791587436986541222348341887722929446335178653856731962559852026072947674072616767145573649812105677716893484917660771705277187601199908144113058645577910525684304811440261938402322470939249802933550731845890355397133088446174107959162511714864874468611247605428673436709046678468670274091881014249711149657817724279347070216688295610877794405048437528443375108828264771978540006509704033021862556147332117771174413350281608840351781452541964320309576018694649088681545285621346988355444560249556668436602922195124830910605377201980218310103270417838665447181260397190688462370857518080035327047185659499476124248110999288679158969049563947624608424065930948621507690314987020673533848349550836366017848771060809804269247132410009464014373603265645184566792456669551001502298330798496079949882497061723674493612262229617908143114146609412341593593095854079139087208322733549572080757165171876599449856937956238755516175754380917805280294642004472153962807463602113294255916002570735628126387331060058910652457080244749375431841494014821199962764531068006631183823761639663180931444671298615527598201451410275600689297502463040173514891945763607893528555053173314164570504996443890936308438744847839616840518452732884032345202470568516465716477139323775517294795126132398229602394548579754586517458787713318138752959809412174227300352296508089177705068259248822322154938048371454781647213976820963320508305647920482085920475499857320388876391601995240918938945576768749730856955958010659526503036266159750662225084067428898265907510637563569968211510949669744580547288693631020367823250182323708459790111548472087618212477813266330412076216587312970811230758159821248639807212407868878114501655825136178903070860870198975889807456643955157415363193191981070575336633738038272152798849350397480015890519420879711308051233933221903466249917169150948541401871060354603794643379005890957721180804465743962806186717861017156740967662080295766577051291209907944304632892947306159510430902221439371849560634056189342513057268291465783293340524635028929175470872564842600349629611654138230077313327298305001602567240141851520418907011542885799208121984493156999059182011819733500126187728036812481995877070207532406361259313438595542547781961142935163561223496661522614735399674051584998603552953329245752388810136202347624669055816438967863097627365504724348643071218494373485300606387644566272186661701238127715621379746149861328744117714552444708997144522885662942440230184791205478498574521634696448973892062401943518310088283480249249085403077863875165911302873958787098100772718271874529013972836614842142871705531796543076504534324600536361472618180969976933486264077435199928686323835088756683595097265574815431940195576850437248001020413749831872259677387154958399718444907279141965845930083942637020875635398216962055324803212267498911402678528599673405242031091797899905718821949391320753431707980023736590985375520238911643467185582906853711897952626234492483392496342449714656846591248918556629589329909035239233333647435203707701010843880032907598342170185542283861617210417603011645918780539367447472059985023582891833692922337323999480437108419659473162654825748099482509991833006976569367159689364493348864744213500840700660883597235039532340179582557036016936990988671132109798897070517280755855191269930673099250704070245568507786790694766126298082251633136399521170984528092630375922426742575599892892783704744452189363203489415521044597261883800300677617931381399162058062701651024458869247649246891924612125310275731390840470007143561362316992371694848132554200914530410371354532966206392105479824392125172540132314902740585892063217589494345489068463993137570910346332714153162232805522972979538018801628590735729554162788676498274186164218789885741071649069191851162815285486794173638906653885764229158342500673612453849160674137340173572779956341043326883569507814931378007362354180070619180267328551191942676091221035987469241172837493126163395001239599240508454375698507957046222664619000103500490183034153545842833764378111988556318777792537201166718539541835984438305203762819440761594106820716970302285152250573126093046898423433152732131361216582808075212631547730604423774753505952287174402666389148817173086436111389069420279088143119448799417154042103412190847094080254023932942945493878640230512927119097513536000921971105412096683111516328705423028470073120658032626417116165957613272351566662536672718998534199895236884830999302757419916463841427077988708874229277053891227172486322028898425125287217826030500994510824783572905691988555467886079462805371227042466543192145281760741482403827835829719301017888345674167811398954750448339314689630763396657226727043393216745421824557062524797219978668542798977992339579057581890622525473582205236424850783407110144980478726691990186438822932305382318559732869780922253529591017341407334884761005564018242392192695062083183814546983923664613639891012102177095976704908305081854704194664371312299692358895384930136356576186106062228705599423371631021278457446463989738188566746260879482018647487672727222062676465338099801966883680994159075776852639865146253336312450536402610569605513183813174261184420189088853196356986962795036738424313011331753305329802016688817481342988681585577810343231753064784983210629718425184385534427620128234570716988530518326179641178579608888150329602290705614476220915094739035946646916235396809201394578175891088931992112260073928149169481615273842736264298098234063200244024495894456129167049508235812487391799648641133480324757775219708932772262349486015046652681439877051615317026696929704928316285504212898146706195331970269507214378230476875280287354126166391708245925170010714180854800636923259462019002278087409859771921805158532147392653251559035410209284665925299914353791825314545290598415817637058927906909896911164381187809435371521332261443625314490127454772695739393481546916311624928873574718824071503995009446731954316193855485207665738825139639163576723151005556037263394867208207808653734942440115799667507360711159351331959197120948964717553024531364770942094635696982226673775209945168450643623824211853534887989395673187806606107885440005508276570305587448541805778891719207881423351138662929667179643468760077047999537883387870348718021842437342112273940255717690819603092018240188427057046092622564178375265263358324240661253311529423457965569502506810018310900411245379015332966156970522379210325706937051090830789479999004999395322153622748476603613677697978567386584670936679588583788795625946464891376652199588286933801836011932368578558558195556042156250883650203322024513762158204618106705195330653060606501054887167245377942831338871631395596905832083416898476065607118347136218123246227258841990286142087284956879639325464285343075301105285713829643709990356948885285190402956047346131138263878897551788560424998748316382804046848618938189590542039889872650697620201995548412650005394428203930127481638158530396439925470201672759328574366661644110962566337305409219519675148328734808957477775278344221091073111351828046036347198185655572957144747682552857863349342858423118749440003229690697758315903858039353521358860079600342097547392296733310649395601812237812854584317605561733861126734780745850676063048229409653041118306671081893031108871728167519579675347188537229309616143204006381322465841111157758358581135018569047815368938137718472814751998350504781297718599084707621974605887423256995828892535041937958260616211842368768511418316068315867994601652057740529423053601780313357263267054790338401257305912339601880137825421927094767337191987287385248057421248921183470876629667207272325650565129333126059505777727542471241648312832982072361750574673870128209575544305968395555686861188397135522084452852640081252027665557677495969626612604565245684086139238265768583384698499778726706555191854468698469478495734622606294219624557085371272776523098955450193037732166649182578154677292005212667143463209637891852323215018976126034373684067194193037746880999296877582441047878123266253181845960453853543839114496775312864260925211537673258866722604042523491087026958099647595805794663973419064010036361904042033113579336542426303561457009011244800890020801478056603710154122328891465722393145076071670643556827437743965789067972687438473076346451677562103098604092717090951280863090297385044527182892749689212106670081648583395537735919136950153162018908887484210798706899114804669270650940762046502772528650728905328548561433160812693005693785417861096969202538865034577183176686885923681488475276498468821949739729707737187188400414323127636504814531122850990020742409255859252926103021067368154347015252348786351643976235860419194129697690405264832347009911154242601273438022089331096686367898694977994001260164227609260823493041180643829138347354679725399262338791582998486459271734059225620749105308531537182911681637219395188700957788181586850464507699343940987433514431626330317247747486897918209239480833143970840673084079589358108966564775859905563769525232653614424780230826811831037735887089240613031336477371011628214614661679404090518615260360092521947218890918107335871964142144478654899528582343947050079830388538860831035719306002771194558021911942899922722353458707566246926177663178855144350218287026685610665003531050216318206017609217984684936863161293727951873078972637353717150256378733579771808184878458866504335824377004147710414934927438457587107159731559439426412570270965125108115548247939403597681188117282472158250109496096625393395380922195591918188552678062149923172763163218339896938075616855911752998450132067129392404144593862398809381240452191484831646210147389182510109096773869066404158973610476436500068077105656718486281496371118832192445663945814491486165500495676982690308911185687986929470513524816091743243015383684707292898982846022237301452655679898627767968091469798378268764311598832109043715611299766521539635464420869197567370005738764978437686287681792497469438427465256316323005551304174227341646455127812784577772457520386543754282825671412885834544435132562054464241011037955464190581168623059644769587054072141985212106734332410756767575818456990693046047522770167005684543969234041711089888993416350585157887353430815520811772071880379104046983069578685473937656433631979786803671873079693924236321448450354776315670255390065423117920153464977929066241508328858395290542637687668968805033317227800185885069736232403894700471897619347344308437443759925034178807972235859134245813144049847701732361694719765715353197754997162785663119046912609182591249890367654176979903623755286526375733763526969344354400473067198868901968147428767790866979688522501636949856730217523132529265375896415171479559538784278499866456302878831962099830494519874396369070682762657485810439112232618794059941554063270131989895703761105323606298674803779153767511583043208498720920280929752649812569163425000522908872646925284666104665392171482080130502298052637836426959733707053922789153510568883938113249757071331029504430346715989448786847116438328050692507766274500122003526203709466023414648998390252588830148678162196775194583167718762757200505439794412459900771152051546199305098386982542846407255540927403132571632640792934183342147090412542533523248021932277075355546795871638358750181593387174236061551171013123525633485820365146141870049205704372018261733194715700867578539336078622739558185797587258744102542077105475361294047460100094095444959662881486915903899071865980563617137692227290764197755177720104276496949611056220592502420217704269622154958726453989227697660310524980855759471631075870133208861463266412591148633881220284440694169488261529577625325019870359870674380469821942056381255833436421949232275937221289056420943082352544084110864545369404969271494003319782861318186188811118408257865928757426384450059944229568586460481033015388911499486935436030221810943466764000022362550573631294626296096198760564259963946138692330837196265954739234624134597795748524647837980795693198650815977675350553918991151335252298736112779182748542008689539658359421963331502869561192012298889887006079992795411188269023078913107603617634779489432032102773359416908650071932804017163840644987871753756781185321328408216571107549528294974936214608215583205687232185574065161096274874375098092230211609982633033915469494644491004515280925089745074896760324090768983652940657920198315265410658136823791984090645712468948470209357761193139980246813405200394781949866202624008902150166163813538381515037735022966074627952910384068685569070157516624192987244482719429331004854824454580718897633003232525821581280327467962002814762431828622171054352898348208273451680186131719593324711074662228508710666117703465352839577625997744672185715816126411143271794347885990892808486694914139097716736900277758502686646540565950394867841110790116104008572744562938425494167594605487117235946429105850909950214958793112196135908315882620682332156153086833730838173279328196983875087083483880463884784418840031847126974543709373298362402875197920802321878744882872843727378017827008058782410749357514889978911739746129320351081432703251409030487462262942344327571260086642508333187688650756429271605525289544921537651751492196367181049435317858383453865255656640657251363575064353236508936790431702597878177190314867963840828810209461490079715137717099061954969640070867667102330048672631475510537231757114322317411411680622864206388906210192355223546711662137499693269321737043105987225039456574924616978260970253359475020913836673772894438696400028110344026084712899000746807764844088711341352503367877316797709372778682166117865344231732264637847697875144332095340001650692130546476890985050203015044880834261845208730530973189492916425322933612431514306578264070283898409841602950309241897120971601649265613413433422298827909921786042679812457285345801338260995877178113102167340256562744007296834066198480676615805021691833723680399027931606420436812079900316264449146190219458229690992122788553948783538305646864881655562294315673128274390826450611628942803501661336697824051770155219626522725455850738640585299830379180350432876703809252167907571204061237596327685674845079151147313440001832570344920909712435809447900462494313455028900680648704293534037436032625820535790118395649089354345101342969617545249573960621490288728932792520696535386396443225388327522499605986974759882329916263545973324445163755334377492928990581175786355555626937426910947117002165411718219750519831787137106051063795558588905568852887989084750915764639074693619881507814685262133252473837651192990156109189777922008705793396463827490680698769168197492365624226087154176100430608904377976678519661891404144925270480881971498801542057787006521594009289777601330756847966992955433656139847738060394368895887646054983871478968482805384701730871117761159663505039979343869339119789887109156541709133082607647406305711411098839388095481437828474528838368079418884342666222070438722887413947801017721392281911992365405516395893474263953824829609036900288359327745855060801317988407162446563997948275783650195514221551339281978226984278638391679715091262410548725700924070045488485692950448110738087996547481568913935380943474556972128919827177020766613602489581468119133614121258783895577357194986317210844398901423948496659251731388171602663261931065366535041473070804414939169363262373767777095850313255990095762731957308648042467701212327020533742667053142448208168130306397378736642483672539837487690980602182785786216512738563513290148903509883270617258932575363993979055729175160097615459044771692265806315111028038436017374742152476085152099016158582312571590733421736576267142390478279587281505095633092802668458937649649770232973641319060982740633531089792464242134583740901169391964250459128813403498810635400887596820054408364386516617880557608956896727531538081942077332597917278437625661184319891025007491829086475149794003160703845549465385946027452447466812314687943441610993338908992638411847425257044572517459325738989565185716575961481266020310797628254165590506042479114016957900338356574869252800743025623419498286467914476322774005529460903940177536335655471931000175430047504719144899841040015867946179241610016454716551337074073950260442769538553834397550548871099785205401175169747581344926079433689543783221172450687344231989878844128542064742809735625807066983106979935260693392135685881391214807354728463227784908087002467776303605551232386656295178853719673034634701222939581606792509153217489030840886516061119011498443412350124646928028805996134283511884715449771278473361766285062169778717743824362565711779450064477718370221999106695021656757644044997940765037999954845002710665987813603802314126836905783190460792765297277694043613023051787080546511542469395265127101052927070306673024447125973939950514628404767431363739978259184541176413327906460636584152927019030276017339474866960348694976541752429306040727005059039503148522921392575594845078867977925253931765156416197168443524369794447355964260633391055126826061595726217036698506473281266724521989060549880280782881429796336696744124805982192146339565745722102298677599746738126069367069134081559412016115960190237753525556300606247983261249881288192937343476862689219239777833910733106588256813777172328315329082525092733047850724977139448333892552081175608452966590553940965568541706001179857293813998258319293679100391844099286575605993598910002969864460974714718470101531283762631146774209145574041815908800064943237855839308530828305476076799524357391631221886057549673832243195650655460852881201902363644712703748634421727257879503428486312944916318475347531435041392096108796057730987201352484075057637199253650470908582513936863463863368042891767107602111159828875539940120076013947033661793715396306139863655492213741597905119083588290097656647300733879314678913181465109316761575821351424860442292445304113160652700974330088499034675405518640677342603583409608605533747362760935658853109760994238347382222087292464497684560579562516765574088410321731345627735856052358236389532038534024842273371639123973215995440828421666636023296545694703577184873442034227706653837387506169212768015766181095420097708363604361110592409117889540338021426523948929686439808926114635414571535194342850721353453018315875628275733898268898523557799295727645229391567477566676051087887648453493636068278050564622813598885879259940946446041705204470046315137975431737187756039815962647501410906658866162180038266989961965580587208639721176995219466789857011798332440601811575658074284182910615193917630059194314434605154047710570054339000182453117733718955857603607182860506356479979004139761808955363669603162193113250223851791672055180659263518036251214575926238369348222665895576994660491938112486609099798128571823494006615552196112207203092277646200999315244273589488710576623894693889446495093960330454340842102462401048723328750081749179875543879387381439894238011762700837196053094383940063756116458560943129517597713935396074322792489221267045808183313764165818269562105872892447740035947009268662659651422050630078592002488291860839743732353849083964326147000532423540647042089499210250404726781059083644007466380020870126664209457181702946752278540074508552377720890581683918446592829417018288233014971554235235911774818628592967605048203864343108779562892925405638946621948268711042828163893975711757786915430165058602965217459581988878680408110328432739867198621306205559855266036405046282152306154594474489908839081999738747452969810776201487134000122535522246695409315213115337915798026979555710508507473874750758068765376445782524432638046143042889235934852961058269382103498000405248407084403561167817170512813378805705643450616119330424440798260377951198548694559152051960093041271007277849301555038895360338261929343797081874320949914159593396368110627557295278004254863060054523839151068998913578820019411786535682149118528207852130125518518493711503422159542244511900207393539627400208110465530207932867254740543652717595893500716336076321614725815407642053020045340183572338292661915308354095120226329165054426123619197051613839357326693760156914429944943744856809775696303129588719161129294681884936338647392747601226964158848900965717086160598147204467428664208765334799858222090619802173211614230419477754990738738567941189824660913091691772274207233367635032678340586301930193242996397204445179288122854478211953530898910125342975524727635730226281382091807439748671453590778633530160821559911314144205091447293535022230817193663509346865858656314855575862447818620108711889760652969899269328178705576435143382060141077329261063431525337182243385263520217735440715281898137698755157574546939727150488469793619500477720970561793913828989845327426227288647108883270173723258818244658436249580592560338105215606206155713299156084892064340303395262263451454283678698288074251422567451806184149564686111635404971897682154227722479474033571527436819409892050113653400123846714296551867344153741615042563256713430247655125219218035780169240326699541746087592409207004669340396510178134857835694440760470232540755557764728450751826890418293966113310160131119077398632462778219023650660374041606724962490137433217246454097412995570529142438208076098364823465973886691349919784013108015581343979194852830436739012482082444814128095443773898320059864909159505322857914576884962578665885999179867520554558099004556461178755249370124553217170194282884617402736649978475508294228020232901221630102309772151569446427909802190826689868834263071609207914085197695235553488657743425277531197247430873043619511396119080030255878387644206085044730631299277888942729189727169890575925244679660189707482960949190648764693702750773866432391919042254290235318923377293166736086996228032557185308919284403805071030064776847863243191000223929785255372375566213644740096760539439838235764606992465260089090624105904215453927904411529580345334500256244101006359530039598864466169595626351878060688513723462707997327233134693971456285542615467650632465676620279245208581347717608521691340946520307673391841147504140168924121319826881568664561485380287539331160232292555618941042995335640095786495340935115266454024418775949316930560448686420862757201172319526405023099774567647838488973464317215980626787671838005247696884084989185086149003432403476742686245952395890358582135006450998178244636087317754378859677672919526111213859194725451400301180503437875277664402762618941017576872680428176623860680477885242887430259145247073950546525135339459598789619778911041890292943818567205070964606263541732944649576612651953495701860015412623962286413897796733329070567376962156498184506842263690367849555970026079867996261019039331263768556968767029295371162528005543100786408728939225714512481135778627664902425161990277471090335933309304948380597856628844787441469841499067123764789582263294904679812089984857163571087831191848630254501620929805829208334813638405421720056121989353669371336733392464416125223196943471206417375491216357008573694397305979709719726666642267431117762176403068681310351899112271339724036887000996862922546465006385288620393800504778276912835603372548255793912985251506829969107754257647488325341412132800626717094009098223529657957997803018282428490221470748111124018607613415150387569830918652780658896682362523937845272634530420418802508442363190383318384550522367992357752929106925043261446950109861088899914658551881873582528164302520939285258077969737620845637482114433988162710031703151334402309526351929588680690821355853680161000213740851154484912685841268695899174149133820578492800698255195740201818105641297250836070356851055331787840829000041552511865779453963317538532092149720526607831260281961164858098684587525129997404092797683176639914655386108937587952214971731728131517932904431121815871023518740757222100123768721944747209349312324107065080618562372526732540733324875754482967573450019321902199119960797989373383673242576103938985349278777473980508080015544764061053522202325409443567718794565430406735896491017610775948364540823486130254718476485189575836674399791508512858020607820554462991723202028222914886959399729974297471155371858924238493855858595407438104882624648788053304271463011941589896328792678327322456103852197011130466587100500083285177311776489735230926661234588873102883515626446023671996644554727608310118788389151149340939344750073025855814756190881398752357812331342279866503522725367171230756861045004548970360079569827626392344107146584895780241408158405229536937499710665594894459246286619963556350652623405339439142111271810691052290024657423604130093691889255865784668461215679554256605416005071276641766056874274200329577160643448606201239821698271723197826816628249938714995449137302051843669076723577400053932662622760323659751718925901801104290384274185507894887438832703063283279963007200698012244365116394086922220745320244624121155804354542064215121585056896157356414313068883443185280853975927734433655384188340303517822946253702015782157373265523185763554098954033236382319219892171177449469403678296185920803403867575834111518824177439145077366384071880489358256868542011645031357633355509440319236720348651010561049872726472131986543435450409131859513145181276437310438972507004981987052176272494065214619959232142314439776546708351714749367986186552791715824080651063799500184295938799158350171580759883784962257398512129810326379376218322456594236685376799113140108043139732335449090824910499143325843298821033984698141715756010829706583065211347076803680695322971990599904451209087275776225351040902392888779424630483280319132710495478599180196967835321464441189260631526618167443193550817081875477050802654025294109218264858213857526688155584113198560022135158887210365696087515063187533002942118682221893775546027227291290504292259787710667873840000616772154638441292371193521828499824350920891801685572798156421858191197490985730570332667646460728757430565372602768982373259745084479649545648030771598153955827779139373601717422996027353102768719449444917939785144631597314435351850491413941557329382048542123508173912549749819308714396615132942045919380106231421774199184060180347949887691051557905554806953878540066453375981862846419905220452803306263695626490910827627115903856995051246529996062855443838330327638599800792922846659503551211245284087516229060262011857775313747949362055496401073001348853150735487353905602908933526400713274732621960311773433943673385759124508149335736911664541281788171454023054750667136518258284898099512139193995633241336556777098003081910272040997148687418134667006094051021462690280449159646545330107754695413088714165312544813061192407821188690056027781824235022696189344352547633573536485619363254417756613981703930632872166905722259745209192917262199844409646158269456380239502837121686446561785235565164127712826918688615572716201474934052276946595712198314943381622114006936307430444173284786101777743837977037231795255434107223445512555589998646183876764903972461167959018100035098928641204195163551108763204267612979826529425882951141275841262732790798807559751851576841264742209479721843309352972665210015662514552994745127631550917636730259462132930190402837954246323258550301096706922720227074863419005438302650681214142135057154175057508639907673946335146209082888934938376439399256900604067311422093312195936202982972351163259386772241477911629572780752395056251581603133359382311500518626890530658368129988108663263271980611271548858798093487912913707498230575929091862939195014721197586067270092547718025750337730799397134539532646195269996596385654917590458333585799102012713204583903200853878881633637685182083727885131175227769609787962142372162545214591281831798216044111311671406914827170981015457781939202311563871950805024679725792497605772625913328559726371211201905720771409148645074094926718035815157571514050397610963846755569298970383547314100223802583468767350129775413279532060971154506484212185936490997917766874774481882870632315515865032898164228288232746866106592732197907162384642153489852476216789050260998045266483929542357287343977680495774091449538391575565485459058976495198513801007958010783759945775299196700547602252552034453988712538780171960718164078124847847257912407824544361682345239570689514272269750431873633263011103053423335821609333191218806608268341428910415173247216053355849993224548730778822905252324234861531520976938461042582849714963475341837562003014915703279685301868631572488401526639835689563634657435321783493199825542117308467745297085839507616458229630324424328237737450517028560698067889521768198156710781633405266759539424926280756968326107495323390536223090807081455919837355377748742029039018142937311529334644468151212945097596534306284215319445727118614900017650558177095302468875263250119705209476159416768727784472000192789137251841622857783792284439084301181121496366424659033634194540657183544771912446621259392656620306888520055599121235363718226922531781458792593750441448933981608657900876165024635197045828895481793756681046474614105142498870252139936870509372305447734112641354892806841059107716677821238332810262185587751312721179344448201440425745083063944738363793906283008973306241380614589414227694747931665717623182472168350678076487573420491557628217583972975134478990696589532548940335615613167403276472469212505759116251529654568544633498114317670257295661844775487469378464233737238981920662048511894378868224807279352022501796545343757274163910791972952950812942922205347717304184477915673991738418311710362524395716152714669005814700002633010452643547865903290733205468338872078735444762647925297690170912007874183736735087713376977683496344252419949951388315074877537433849458259765560996555954318040920178497184685497370696212088524377013853757681416632722412634423982152941645378000492507262765150789085071265997036708726692764308377229685985169122305037462744310852934305273078865283977335246017463527703205938179125396915621063637625882937571373840754406468964783100704580613446731271591194608435935825987782835266531151065041623295329047772174083559349723758552138048305090009646676088301540612824308740645594431853413755220166305812111033453120745086824339432159043594430312431227471385842030390106070940315235556172767994160020393975099897629335325855575624808996691829864222677502360193257974726742578211119734709402357457222271212526852384295874273501563660093188045493338989741571490544182559738080871565281430102670460284316819230392535297795765862414392701549740879273131051636119137577008929564823323648298263024607975875767745377160102490804624301856524161756655600160859121534556267602192689982855377872583145144082654583484409478463178777374794653580169960779405568701192328608041130904629350871827125934668712766694873899824598527786499569165464029458935064964335809824765965165142090986755203808309203230487342703468288751604071546653834619611223013759451579252696743642531927390036038608236450762698827497618723575476762889950752114804852527950845033958570838130476937881321123674281319487950228066320170022460331989671970649163741175854851878484012054844672588851401562725019821719066960812627785485964818369621410721714214986361918774754509650308957099470934337856981674465828267911940611956037845397855839240761276344105766751024307559814552786167815949657062559755074306521085301597908073343736079432866757890533483669555486803913433720156498834220893399971641479746938696905480089193067138057171505857307148815649920714086758259602876056459782423770242469805328056632787041926768467116266879463486950464507420219373945259262668613552940624781361206202636498199999498405143868285258956342264328707663299304891723400725471764188685351372332667877921738347541480022803392997357936152412755829569276837231234798989446274330454566790062032420516396282588443085438307201495672106460533238537203143242112607424485845094580494081820927639140008540422023556260218564348994145439950410980591817948882628052066441086319001688568155169229486203010738897181007709290590480749092427141018933542818429995988169660993836961644381528877214085268088757488293258735809905670755817017949161906114001908553744882726200936685604475596557476485674008177381703307380305476973609786543859382187220583902344443508867499866506040645874346005331827436296177862518081893144363251205107094690813586440519229512932450078833398788429339342435126343365204385812912834345297308652909783300671261798130316794385535726296998740359570458452230856390098913179475948752126397078375944861139451960286751210561638976008880092746115860800207803341591451797073036835196977766076373785333012024120112046988609209339085365773222392412449051532780950955866459477634482269986074813297302630975028812103517723124465095349653693090018637764094094349837313251321862080214809922685502948454661814715557444709669530177690434272031892770604717784527939160472281534379803539679861424370956683221491465438014593829277393396032754048009552231816667380357183932757077142046723838624617803976292377131209580789363841447929802588065522129262093623930637313496640186619510811583471173312025805866727639992763579078063818813069156366274125431259589936119647626101405563503399523140323113819656236327198961837254845333702062563464223952766943568376761368711962921818754576081617053031590728828700712313666308722754918661395773730546065997437810987649802414011242142773668082751390959313404155826266789510846776118665957660165998178089414985754976284387856100263796543178313634025135814161151902096499133548733131115022700681930135929595971640197196053625033558479980963488718039111612813595968565478868325856437896173159762002419621552896297904819822199462269487137462444729093456470028537694958859591606789282491054412515996300781368367490209374915732896270028656829344431342347351239298259166739503425995868970697267332582735903121288746660451461487850346142827765991608090398652575717263081833494441820193533385071292345774375579344062178711330063106003324053991693682603746176638565758877580201229366353270267100681261825172914608202541892885935244491070138206211553827793565296914576502048643282865557934707209634807372692141186895467322767751335690190153723669036865389161291688887876407525493494249733427181178892759931596719354758988097924525262363659036320070854440784544797348291802082044926670634420437555325050527522833778887040804033531923407685630109347772125639088640413101073817853338316038135280828119040832564401842053746792992622037698718018061122624490909242641985820861751177113789051609140381575003366424156095216328197122335023167422600567941281406217219641842705784328959802882335059828208196666249035857789940333152274817776952843681630088531769694783690580671064828083598046698841098135158654906933319522394363287923990534810987830274500172065433699066117784554364687723631844464768069142828004551074686645392805399409108754939166095731619715033166968309929466349142798780842257220697148875580637480308862995118473187124777291910070227588893486939456289515802965372150409603107761289831263589964893410247036036645058687287589051406841238124247386385427908282733827973326885504935874303160274749063129572349742611221517417153133618622410913869500688835898962349276317316478340077460886655598733382113829928776911495492184192087771606068472874673681886167507221017261103830671787856694812948785048943063086169948798703160515884108282351274153538513365895332948629494495061868514779105804696039069372662670386512905201137810858616188886947957607413585534585151768051973334433495230120395770739623771316030242887200537320998253008977618973129817881944671731160647231476248457551928732782825127182446807824215216469567819294098238926284943760248852279003620219386696482215628093605373178040863727268426696421929946819214908701707533361094791381804063287387593848269535583077395761447997270003472880182785281389503217986345216111066608839314053226944905455527867894417579202440021450780192099804461382547805858048442416404775031536054906591430078158372430123137511562284015838644270890718284816757527123846782459534334449622010096071051370608461801187543120725491334994247617115633321408934609156561550600317384218701570226103101916603887064661438897736318780940711527528174689576401581047016965247557740891644568677717158500583269943401677202156767724068128366565264122982439465133197359199709403275938502669557470231813203243716420586141033606524536939160050644953060161267822648942437397166717661231048975031885732165554988342121802846912529086101485527815277625623750456375769497734336846015607727035509629049392487088406281067943622418704747008368842671022558302403599841645951122485272633632645114017395248086194635840783753556885622317115520947223065437092606797351000565549381224575483728545711797393615756167641692895805257297522338558611388322171107362265816218842443178857488798109026653793426664216990914056536432249301334867988154886628665052346997235574738424830590423677143278792316422403877764330192600192284778313837632536121025336935812624086866699738275977365682227907215832478888642369346396164363308730139814211430306008730666164803678984091335926293402304324974926887831643602681011309570716141912830686577323532639653677390317661361315965553584999398600565155921936759977717933019744688148371103206503693192894521402650915465184309936553493337183425298433679915939417466223900389527673813330617747629574943868716978453767219493506590875711917720875477107189937960894774512654757501871194870738736785890200617373321075693302216320628432065671192096950585761173961632326217708945426214609858410237813215817727602222738133495410481003073275107799948991977963883530734443457532975914263768405442264784216063122769646967156473999043715903323906560726644116438605404838847161912109008701019130726071044114143241976796828547885524779476481802959736049439700479596040292746299203572099761950140348315380947714601056333446998820822120587281510729182971211917876424880354672316916541852256729234429187128163232596965413548589577133208339911288775917226115273379010341362085614577992398778325083550730199818459025958355989260553299673770491722454935329683300002230181517226575787524058832249085821280089747909326100762578770428656006996176212176845478996440705066241710213327486796237430229155358200780141165348065647488230615003392068983794766255036549822805329662862117930628430170492402301985719978948836897183043805182174419147660429752437251683435411217038631379411422095295885798060152938752753799030938871683572095760715221900279379292786303637268765822681241993384808166021603722154710143007377537792699069587121289288019052031601285861825494413353820784883465311632650407642428390870121015194231961652268422003711230464300673442064747718021353070124098860353399152667923871101706221865883573781210935179775604425634694999787251125440854522274810914874307259869602040275941178942581281882159952359658979181144077653354321757595255536158128001163846720319346507296807990793963714961774312119402021297573125165253768017359101557338153772001952444543620071848475663415407442328621060997613243487548847434539665981338717466093020535070271952983943271425371155766600025784423031073429551533945060486222764966687624079324353192992639253731076892135352572321080889819339168668278948281170472624501948409700975760920983724090074717973340788141825195842598096241747610138252643955135259311885045636264188300338539652435997416931322894719878308427600401368074703904097238473945834896186539790594118599310356168436869219485382055780395773881360679549900085123259442529724486666766834641402189915944565309423440650667851948417766779470472041958822043295380326310537494883122180391279678446100139726753892195119117836587662528083690053249004597410947068772912328214304635337283519953648274325833119144459017809607782883583730111857543659958982724531925310588115026307542571493943024453931870179923608166611305426253995833897942971602070338767815033010280120095997252222280801423571094760351925544434929986767817891045559063015953809761875920358937341978962358931125983902598310267193304189215109689156225069659119828323455503059081730735195503721665870288053992138576037035377105178021280129566841984140362872725623214428754302210909472721073474134975514190737043318276626177275996888826027225247133683353452816692779591328861381766349857728936900965749562287103024362590772412219094300871755692625758065709912016659622436080242870024547362036394841255954881727272473653467783647201918303998717627037515724649922289467932322693619177641614618795613956699567783068290316589699430767333508234990790624100202506134057344300695745474682175690441651540636584680463692621274211075399042188716127617787014258864825775223889184599523376292377915585744549477361295525952226578636462118377598473700347971408206994145580719080213590732269233100831759510659019121294795408603640757358750205890208704579670007055262505811420663907459215273309406823649441590891009220296680523325266198911311842016291631076894084723564366808182168657219688268358402785500782804043453710183651096951782335743030504852653738073531074185917705610397395062640355442275156101107261779370634723804990666922161971194259120445084641746383589938239946517395509000859479990136026674261494290066467115067175422177038774507673563742154782905911012619157555870238957001405117822646989944917908301795475876760168094100135837613578591356924455647764464178667115391951357696104864922490083446715486383054477914330097680486878348184672733758436892724310447406807685278625585165092088263813233623148733336714764520450876627614950389949504809560460989604329123358348859990294526400284994280878624039811814884767301216754161106629995553668193123287425702063738352020086863691311733469731741219153633246745325630871347302792174956227014687325867891734558379964351358800959350877556356248810493852999007675135513527792412429277488565888566513247302514710210575352516511814850902750476845518252096331899068527614435138213662152368890578786699432288816028377482035506016029894009119713850179871683633744139275973644017007014763706655703504338121113576415018451821413619823495159601064752712575935185304332875537783057509567425442684712219618709178560783936144511383335649103256405733898667178123972237519316430617013859539474367843392670986712452211189690840236327411496601243483098929941738030588417166613073040067588380432111555379440605497721705942821514886165672771240903387727745629097110134885184374118695655449745736845218066982911045058004299887953899027804383596282409421860556287788428802127553884803728640019441614257499904272009595204654170598104989967504511936471172772220436102614079750809686975176600237187748348016120310234680567112644766123747627852190241202569943534716226660893675219833111813511146503854895025120655772636145473604426859498074396932331297127377157347099713952291182653485155587137336629120242714302503763269501350911612952993785864681307226486008270881333538193703682598867893321238327053297625857382790097826460545598555131836688844628265133798491667839409761353766251798258249663458771950124384040359140849209733754642474488176184070023569580177410177696925077814893386672557898564589851056891960924398841569280696983352240225634570497312245269354193837004843183357196516626721575524193401933099018319309196582920969656247667683659647019595754739345514337413708761517323677204227385674279170698204549953095918872434939524094441678998846319845504852393662972079777452814399418256789457795712552426826089940863317371538896262889629402112108884427376568624527612130371017300785135715404533041507959447776143597437803742436646973247138410492124314138903579092416036406314038149831481905251720937103964026808994832572297954564042701757722904173234796073618787889913318305843069394825961318713816423467218730845133877219086975104942843769325024981656673816260615941768252509993741672883951744066932549653403101452225316189009235376486378482881344209870048096227171226407489571939002918573307460104360729190945767994614929290427981687729426487729952858434647775386906950148984133924540394144680263625402118614317031251117577642829914644533408920976961699098372652361768745605894704968170136974909523072082682887890730190018253425805343421705928713931737993142410852647390948284596418093614138475831136130576108462366837237695913492615824516221552134879244145041756848064120636520170386330129532777699023118648020067556905682295016354931992305914246396217025329747573114094220180199368035026495636955866425906762685687372110339156793839895765565193177883000241613539562437777840801748819373095020699900890899328088397430367736595524891300156633294077907139615464534088791510300651321934486673248275907946807879819425019582622320395131252014109960531260696555404248670549986786923021746989009547850725672978794769888831093487464426400718183160331655511534276155622405474473378049246214952133258527698847336269182649174338987824789278468918828054669982303689939783413747587025805716349413568433929396068192061773331791738208562436433635359863494496890781064019674074436583667071586924521182997893804077137501290858646578905771426833582768978554717687184427726120509266486102051535642840632368481807287940717127966820060727559555904040233178749447346454760628189541512139162918444297651066947969354016866010055196077687335396511614930937570968554559381513789569039251014953265628147011998326992200066392875374713135236421589265126204072887716578358405219646054105435443642166562244565042999010256586927279142752931172082793937751326106052881235373451068372939893580871243869385934389175713376300720319760816604464683937725806909237297523486702916910426369262090199605204121024077648190316014085863558427609537086558164273995349346546314504040199528537252004957805254656251154109252437991326262713609099402902262062836752132305065183934057450112099341464918433323646569371725914489324159006242020612885732926133596808726500045628284557574596592120530341310111827501306961509835515632004310784601906565493806542525229161991819959602752327702249855738824899882707465936355768582560518068964285376850772012220347920993936179268206590142165615925306737944568949070853263568196831861772268249911472615732035807646298116244013316737892788689229032593349861797021994981925739617673075834417098559222170171825712777534491508205278430904619460835217402005838672849709411023266953921445461066215006410674740207009189911951376466904481267253691537162290791385403937560077835153374167747942100384002308951850994548779039346122220865060160500351776264831611153325587705073541279249909859373473787081194253055121436979749914951860535920403830235716352727630874693219622190064260886183676103346002255477477813641012691906569686495012688376296907233961276287223041141813610060264044030035996988919945827397624114613744804059697062576764723766065541618574690527229238228275186799156983390747671146103022776606020061246876477728819096791613354019881402757992174167678799231603963569492851513633647219540611171767387372555728522940054361785176502307544693869307873499110352182532929726044553210797887711449898870911511237250604238753734841257086064069052058452122754533848008205302450456517669518576913200042816758054924811780519832646032445792829730129105318385636821206215531288668564956512613892261367064093953334570526986959692350353094224543865278677673027540402702246384483553239914751363441044050092330361271496081355490531539021002299595756583705381261965683144286057956696622154721695620870013727768536960840704833325132793112232507148630206951245395003735723346807094656483089209801534878705633491092366057554050864111521441481434630437273271045027768661953107858323334857840297160925215326092558932655600672124359464255065996771770388445396181632879614460817789272171836908880126778207430106422524634807454300476492885553409062185153654355474125476152769772667769772777058315801412185688011705028365275543214803488004442979998062157904564161957212784508928489806426497427090579129069217807298769477975112447305991406050629946894280931034216416629935614828130998870745292716048433630818404126469637925843094185442216359084576146078558562473814931427078266215185541603870206876980461747400808324343665382354555109449498431093494759944672673665352517662706772194183191977196378015702169933675083760057163454643671776723387588643405644871566964321041282595645349841388412890420682047007615596916843038999348366793542549210328113363184722592305554383058206941675629992013373175489122037230349072681068534454035993561823576312837767640631013125335212141994611869350833176587852047112364331226765129964171325217513553261867681942338790365468908001827135283584888444111761234101179918709236507184857856221021104009776994453121795022479578069506532965940383987369907240797679040826794007618729547835963492793904576973661643405359792219285870574957481696694062334272619733518136626063735982575552496509807260123668283605928341855848026958413772558970883789942910549800331113884603401939166122186696058491571485733568286149500019097591125218800396419762163559375743718011480559442298730418196808085647265713547612831629200449880315402105530597076666362749328308916880932359290081787411985738317192616728834918402429721290434965526942726402559641463525914348400675867690350382320572934132981593533044446496829441367323442158380761694831219333119819061096142952201536170298575105594326461468505452684975764807808009221335811378197749271768545075538328768874474591593731162470601091244609829424841287520224462594477638749491997840446829257360968534549843266536862844489365704111817793806441616531223600214918768769467398407517176307516849856359201486892943105940202457969622924566644881967576294349535326382171613395757790766370764569570259738800438415805894336137106551859987600754924187211714889295221737721146081154344982665479872580056674724051122007383459271575727715218589946948117940644466399432370044291140747218180224825837736017346685300744985564715420036123593397312914458591522887408719508708632218837288262822884631843717261903305777147651564143822306791847386039147683108141358275755853643597721650028277803713422869688787349795096031108899196143386664068450697420787700280509367203387232629637856038653216432348815557557018469089074647879122436375556668678067610544955017260791142930831285761254481944449473244819093795369008206384631678225064809531810406570254327604385703505922818919878065865412184299217273720955103242251079718077833042609086794273428955735559252723805511440438001239041687716445180226491681641927401106451622431101700056691121733189423400547959684669804298017362570406733282129962153684881404102194463424646220745575643960452985313071409084608499653767803793201899140865814662175319337665970114330608625009829566917638846056762972931464911493704624469351984039534449135141193667933301936617663652555149174982307987072280860859626112660504289296966535652516688885572112276802772743708917389639772257564890533401038855931125679991516589025016486961427207005916056166159702451989051832969278935550303934681219761582183980483960562523091462638447386296039848924386187298507775928792722068554807210497817653286210187476766897248841139560349480376727036316921007350834073865261684507482496448597428134936480372426116704266870831925040997615319076855770327421785010006441984124207396400139603601583810565928413684574119102736420274163723488214524101347716529603128408658419787951116511529827814620379139855006399960326591248525308493690313130100799977191362230866011099929142871249388541612038020411340188887219693477904497527454288072803509305828754420755134816660927879353566521255620139988249628478726214432362853676502591450468377635282587652139156480972141929675549384375582600253168536356731379262475878049445944183429172756988376226261846365452743497662411138451305481449836311789784489732076719508784158618879692955819733250699951402601511675529750575437810242238957925786562128432731202200716730574069286869363930186765958251326499145950260917069347519408975357464016830811798846452473618956056479426358070562563281189269663026479535951097127659136233180866921535788607812759910537171402204506186075374866306350591483916467656723205714516886170790984695932236724946737583099607042589220481550799132752088583781117685214269334786921895240622657921043620348852926267984013953216458791151579050460579710838983371864038024417511347226472547010794793996953554669619726763255229914654933499663234185951450360980344092212206712567698723427940708857070474293173329188523896721971353924492426178641188637790962814486917869468177591717150669111480020759432012061969637795103227089029566085562225452602610460736131368869009281721068198618553780982018471154163630326265699283424155023600978046417108525537612728905335045506135684143775854429677977014660294387687225115363801191758154028120818255606485410787933598921064427244898618961629413418001295130683638609294100083136673372153008352696235737175330738653338204842190308186449184093723944033405244909554558016406460761581010301767488475017661908692946098769201691202181688291040870709560951470416921147027413390052253340834812870353031023919699978597413908593605433599697075604460134242453682496098772581311024732798562072126572499003468293886872304895562253204463602639854225258416464324271611419817802482595563544907219226583863662663750835944314877635156145710745528016159677048442714194435183275698407552677926411261765250615965235457187956673170913319358761628255920783080185206890151504713340386100310055914817852110384754542933389188444120517943969970194112695119526564919594189975418393234647424290702718875223534393673633663200307232747037407123982562024662651974090199762452056198557625760008708173083288344381831070054514493545885422678578551915372292379555494333410174420169600090696415612732297770221217951868376359082255128816470021992348864043959153018464004714321186360622527011541122283802778538911098490201342741014121559769965438877197485376431158229838533123071751132961904559007938064276695819014842627991221792947987348901868471676503827328552059082984529806259250352128451925927986593506132961946796252373972565584157853744567558998032405492186962888490332560851455344391660226257775512916200772796852629387937530454181080729285891989715381797343496187232927614747850192611450413274873242970583408471112333746274617274626582415324271059322506255302314738759251724787322881491455915605036334575424233779160374952502493022351481961381162563911415610326844958072508273431765944054098269765269344579863479709743124498271933113863873159636361218623497261409556079920628316999420072054811525353393946076850019909886553861433495781650089961649079678142901148387645682174914075623767618453775144031475411206760160726460556859257799322070337333398916369504346690694828436629980037414527627716547623825546170883189810868806847853705536480469350958818025360529740793538676511195079373282083146268960071075175520614433784114549950136432446328193346389050936545714506900864483440180428363390513578157273973334537284263372174065775771079830517555721036795976901889958494130195999573017901240193908681356585539661941371794487632079868800371607303220547423572266896801882123424391885984168972277652194032493227314793669234004848976059037958094696041754279613782553781223947646147832926976545162290281701100437846038756544151739433960048915318817576650500951697402415644771293656614253949368884230517400129920556854289853897942669956777027089146513736892206104415481662156804219838476730871787590279209175900695273456682026513373111518000181434120962601658629821076663523361774007837783423709152644063054071807843358061072961105550020415131696373046849213356837265400307509829089364612047891114753037049893952833457824082817386441322710002968311940203323456420826473276233830294639378998375836554559919340866235090967961134004867027123176526663710778725111860354037554487418693519733656621772359229396776463251562023487570113795712096237723431370212031004965152111976013176419408203437348512852602913334915125083119802850177855710725373149139215709105130965059885999931560863655477403551898166733535880048214665099741433761182777723351910741217572841592580872591315074606025634903777263373914461377038021318347447301113032670296917335047701632106616227830027269283365584011791419447808748253360714403296252285775009808599609040936312635621328162071453406104224112083010008587264252112262480142647519426184325853386753874054743491072710049754281159466017136122590440158991600229827801796035194080046513534752698777609527839984368086908989197839693532179980139135442552717910225397010810632143048511378291498511381969143043497500189980681644412123273328307192824362406733196554692677851193152775113446468905504248113361434984604849051258345683266441528489713972376040328212660253516693914082049947320486021627759791771234751097502403078935759937715095021751693555827072533911892334070223832077585802137174778378778391015234132098489423459613692340497998279304144463162707214796117456975719681239291913740982925805561955207434243295982898980529233366415419256367380689494201471241340525072204061794355252555225008748790086568314542835167750542294803274783044056438581591952666758282929705226127628711040134801787224801789684052407924360582742467443076721645270313451354167649668901274786801010295133862698649748212118629040337691568576240699296372493097201628707200189835423690364149270236961938547372480329855045112089192879829874467864129159417531675602533435310626745254507114181483239880607297140234725520713490798398982355268723950909365667878992383712578976248755990443228895388377317348941122757071410959790047919301046740750411435381782464630795989555638991884773781341347070246747362112048986226991888517456251732519341352038115863350123913054441910073628447567514161050410973505852762044489190978901984315485280533985777844313933883994310444465669244550885946314081751220331390681596592510546858013133838152176418210433429788826119630443111388796258746090226130900849975430395771243230616906262919403921439740270894777663702488155499322458825979020631257436910946393252806241642476868495455324938017639371615636847859823715902385421265840615367228607131702674740131145261063765383390315921943469817605358380310612887852051546933639241088467632009567089718367490578163085158138161966882222047570437590614338040725853862083565176998426774523195824182683698270160237414938363496629351576854061397342746470899685618170160551104880971554859118617189668025973541705423985135560018720335079060946421271143993196046527424050882225359773481519135438571253258540493946010865793798058620143366078825219717809025817370870916460452727977153509910340736425020386386718220522879694458387652947951048660717390229327455426785669776865939923416834122274663015062155320502655341460995249356050854921756549134830958906536175693817637473644183378974229700703545206663170929607591989627732423090252397443861014263098687733913882518684316501027964911497737582888913450341148865948670215492101084328080783428089417298008983297536940644969903125399863919581601468995220880662285408414864274786281975546629278814621607171381880180840572084715868906836919393381864278454537956719272397972364651667592011057995663962598535512763558768140213409829016296873429850792471846056874828331381259161962476156902875901072733103299140623864608333378638257926302391590003557609032477281338887339178096966601469615031754226751125993315529674213336300222964906480934582008181061802100227664580400278213336758573019011371754672763059044353131319036092489097246427928455549913490005180295707082919052556781889913899625138662319380053611346224294610248954072404857123256628888931722116432947816190554868054943441034090680716088028227959686950133643814268252170472870863010137301155236861416908375675747637239763185757038109443390564564468524183028148107998376918512127201935044041804604721626939445788377090105974693219720558114078775989772072009689382249303236830515862657281114637996983137517937623215111252349734305240622105244234353732905655163406669506165892878218707756794176080712973781335187117931650033155523822487730653444179453415395202424449703410120874072188109388268167512042299404948179449472732894770111574139441228455521828424922240658752689172272780607116754046973008037039618787796694882555614674384392570115829546661358678671897661297311267200072971553613027503556167817765442287442114729881614802705243806817653573275578602505847084013208837932816008769081300492491473682517035382219619039014999523495387105997351143478292339499187936608692301375596368532373806703591144243268561512109404259582639301678017128669239283231057658851714020211196957064799814031505633045141564414623163763809904402816256917576489142569714163598439317433270237812336938043012892626375382667795034169334323607500248175741808750388475094939454896209740485442635637164995949920980884294790363666297526003243856352945844728944547166209297495496616877414120882130477022816116456044007236351581149729739218966737382647204722642221242016560150284971306332795814302516013694825567014780935790889657134926158161346901806965089556310121218491805847922720691871696316330044858020102860657858591269974637661741463934159569539554203314628026518951167938074573315759846086173702687867602943677780500244673391332431669880354073232388281847501051641331189537036488422690270478052742490603492082954755054003457160184072574536938145531175354210726557835615499874447480427323457880061873149341566046352979779455075359304795687209316724536547208381685855606043801977030764246083489876101345709394877002946175792061952549255757109038525171488525265671045349813419803390641529876343695420256080277614421914318921393908834543131769685101840103844472348948869520981943531906506555354617335814045544837884752526253949665869992058417652780125341033896469818642430034146791380619028059607854888010789705516946215228773090104467462497979992627120951684779568482583341402266477210843362437593741610536734041954738964197895425335036301861400951534766961476255651873823292468547356935802896011536791787303553159378363082248615177770541577576561759358512016692943111138863582159667618830326104164651714846979385422621687161400122378213779774131268977266712992025922017408770076956283473932201088159356286281928563571893384958850603853158179760679479840878360975960149733420572704603521790605647603285569276273495182203236144112584182426247712012035776388895974318232827871314608053533574494297621796789034568169889553518504478325616380709476951699086247100019748809205009521943632378719764870339223811540363475488626845956159755193765410115014067001226927474393888589943859730245414801061235908036274585288493563251585384383242493252666087588908318700709100237377106576985056433928854337658342596750653715005333514489908293887737352051459333049626531415141386124437935885070944688045486975358170212908490787347806814366323322819415827345671356443171537967818058195852464840084032909981943781718177302317003989733050495387356116261023999433259780126893432605584710278764901070923443884634011735556865903585244919370181041626208504299258697435817098133894045934471937493877624232409852832762266604942385129709453245586252103600829286649724174919141988966129558076770979594795306013119159011773943104209049079424448868513086844493705909026006120649425744710353547657859242708130410618546219881830090634588187038755856274911587375421064667951346487586771543838018521348281915812462599335160198935595167968932852205824799421034512715877163345222995418839680448835529753361286837225935390079201666941339091168758803988828869216002373257361588207163516271332810518187602104852180675526648673908900907195138058626735124312215691637902277328705410842037841525683288718046987952513073266340278519059417338920358540395677035611329354482585628287610610698229721420961993509331312171187891078766872044548876089410174798647137882462153955933333275562009439580434537919782280590395959927436913793778664940964048777841748336432684026282932406260081908081804390914556351936856063045089142289645219987798849347477729132797266027658401667890136490508741142126861969862044126965282981087045479861559545338021201155646979976785738920186243599326777689454060508218838227909833627167124490026761178498264377033002081844590009717235204331994708242098771514449751017055643029542821819670009202515615844174205933658148134902693111517093872260026458630561325605792560927332265579346280805683443921373688405650434307396574061017779370141424615493070741360805442100295600095663588977899267630517718781943706761498217564186590116160865408635391513039201316805769034172596453692350806417446562351523929050409479953184074862151210561833854566176652606393713658802521666223576132201941701372664966073252010771947931265282763302413805164907174565964853748354669194523580315301969160480994606814904037819829732360930087135760798621425422096419004367905479049930078372421581954535418371129368658430553842717628035279128821129308351575656599944741788438381565148434229858704245592434693295232821803508333726283791830216591836181554217157448465778420134329982594566884558266171979012180849480332448787258183774805522268151011371745368417870280274452442905474518234674919564188551244421337783521423865979925988203287085109338386829906571994614906290257427686038850511032638544540419184958866538545040571323629681069146814847869659166861842756798460041868762298055562963045953227923051616721591968675849523635298935788507746081537321454642984792310511676357749494622952569497660359473962430995343310404994209677883827002714478494069037073249106444151696053256560586778757417472110827435774315194060757983563629143326397812218946287447798119807225646714664054850131009656786314880090303749338875364183165134982546694673316118123364854397649325026179549357204305402182974871251107404011611405899911093062492312813116340549262571356721818628932786138833718028535056503591952741400869510926167541476792668032109237467087213606278332922386413619594121339278036118276324106004740971111048140003623342714514483334641675466354699731494756643423659493496845884551524150756376605086632827424794136062876041290644913828519456402643153225858624043141838669590633245063000392213192647625962691510904457695301444054618037857503036686212462278639752746667870121003392984873375014475600322100622358029343774955032037012738468163061026570300872275462966796880890587127676361066225722352229739206443093524327228100859973095132528630601105497915644791845004618046762408928925680912930592960642357021061524646205023248966593987324933967376952023991760898474571843531936646529125848064480196520162838795189499336759241485626136995945307287254532463291529110128763770605570609531377527751867923292134955245133089867969165129073841302167573238637575820080363575728002754490327953079900799442541108725693188014667935595834676432868876966610097395749967836593397846346959948950610490383647409504695226063858046758073069912290474089879166872117147527644711604401952718169508289733537148530928937046384420893299771125856840846608339934045689026787516008775461267988015465856522061210953490796707365539702576199431376639960606061106406959330828171876426043573425361756943784848495250108266488395159700490598380812105221111091943323951136051446459834210799058082093716464523127704023160072138543723461267260997870385657091998507595634613248460188409850194287687902268734556500519121546544063829253851276317663922050938345204300773017029940362615434001322763910912988327863920412300445551684054889809080779174636092439334912641164240093880746356607262336695842764583698268734815881961058571835767462009650526065929263548291499045768307210893245857073701660717398194485028842603963660746031184786225831056580870870305567595861341700745402965687634774176431051751036732869245558582082372038601781739405175130437994868822320044378043103170921034261674998000073016094814586374488778522273076330495383944345382770608760763542098445008306247630253572781032783461766970544287155315340016497076657195985041748199087201490875686037783591994719343352772947285537925787684832301101859365800717291186967617655053775030293033830706448912811412025506150896411007623824574488655182581058140345320124754723269087547507078577659732542844459353044992070014538748948226556442223696365544194225441338212225477497535494624827680533336983284156138692363443358553868471111430498248398991803165458638289353799130535222833430137953372954016257623228081138499491876144141322933767106563492528814528239506209022357876684650116660097382753660405446941653422239052108314585847035529352219928272760574821266065291385530345549744551470344939486863429459658431024190785923680224560763936784166270518555178702904073557304620639692453307795782245949710420188043000183881429008173039450507342787013124466860092778581811040911511729374873627887874907465285565434748886831064110051023020875107768918781525622735251550379532444857787277617001964853703555167655209119339343762866284619844026295252183678522367475108809781507098978413086245881522660963551401874495836926917799047120726494905737264286005211403581231076006699518536124862746756375896225299116496066876508261734178484789337295056739007878617925351440621045366250640463728815698232317500596261080921955211150859302955654967538862612972339914628358476048627627027309739202001432248707582337354915246085608210328882974183906478869923273691360048837436615223517058437705545210815513361262142911815615301758882573594892507108879262128641392443309383797333867806131795237315266773820858024701433527009243803266951742119507670884326346442749127558907746863582162166042741315170212458586056233631493164646913946562497471741958354218607748711057338458433689939645913740603382159352243594751626239188685307822821763983237306180204246560477527943104796189724299533029792497481684052893791044947004590864991872727345413508101983881864673609392571930511968645601855782450218231065889437986522432050677379966196955472440585922417953006820451795370043472451762893566770508490213107736625751697335527462302943031203596260953423574397249659211010657817826108745318874803187430823573699195156340957162700992444929749105489851519658664740148225106335367949737142510229341882585117371994499115097583746130105505064197721531929354875371191630262030328588658528480193509225875775597425276584011721342323648084027143356367542046375182552524944329657043861387865901965738802868401894087672816714137033661732650120578653915780703088714261519075001492576112927675193096728453971160213606303090542243966320674323582797889332324405779199278484633339777737655901870574806828678347965624146102899508487399692970750432753029972872297327934442988646412725348160603779707298299173029296308695801996312413304939350493325412355071054461182591141116454534710329881047844067780138077131465400099386306481266614330858206811395838319169545558259426895769841428893743467084107946318932539106963955780706021245974898293564613560788983472419979478564362042094613412387613198865352358312996862268948608408456655606876954501274486631405054735351746873009806322780468912246821460806727627708402402266155485024008952891657117617439020337584877842911289623247059191874691042005848326140677333751027195653994697162517248312230633919328707983800748485726516123434933273356664473358556430235280883924348278760886164943289399166399210488307847777048045728491456303353265070029588906265915498509407972767567129795010098229476228961891591441520032283878773485130979081019129267227103778898053964156362364169154985768408398468861684375407065121039062506128107663799047908879674778069738473170475253442156390387201238806323688037017949308954900776331523063548374256816653361606641980030188287123767481898330246836371488309259283375902278942588060087286038859168849730693948020511221766359138251524278670094406942355120201568377778851824670025651708509249623747726813694284350062938814429987905301056217375459182679973217735029368928065210025396268807498092643458011655715886700443503976505323478287327368840863540002740676783821963522226539290939807367391364082898722017776747168118195856133721583119054682936083236976113450281757830202934845982925000895682630271263295866292147653142233351793093387951357095346377183684092444422096319331295620305575517340067973740614162107923633423805646850092037167152642556371853889571416419772387422610596667396997173168169415435095283193556417705668622215217991151355639707143312893657553844648326201206424338016955862698561022460646069330793847858814367407000599769703649019273328826135329363112403650698652160638987250267238087403396744397830258296894256896741864336134979475245526291426522842419243083388103580053787023999542172113686550275341362211693140694669513186928102574795985605145005021715913317751609957865551981886193211282110709442287240442481153406055895958355815232012184605820563592699303478851132068626627588771446035996656108430725696500563064489187599466596772847171539573612108180841547273142661748933134174632662354222072600146012701206934639520564445543291662986660783089068118790090815295063626782075614388815781351134695366303878412092346942868730839320432333872775496805210302821544324723388845215343727250128589747691460808314404125868181540049187772287869801853454537006526655649170915429522756709222217474112062720656622989806032891672068743654948246108697367225547404812889242471854323605753411672850757552057131156697954584887398742228135887985840783135060548290551482785294891121905383195624228719484759407859398047901094194070671764439032730712135887385049993638838205501683402777496070276844880281912220636888636811043569529300652195528261526991271637277388418993287130563464688227398288763198645709836308917786487086676185485680047672552675414742851028145807403152992197814557756843681110185317498167016426647884090262682824448258027532094549915104518517716546311804904567985713257528117913656278158111288816562285876030875974963849435275676612168959261485030785362045274507752950631012480341804584059432926079854435620093708091821523920371790678121992280496069738238743312626730306795943960954957189577217915597300588693646845576676092450906088202212235719254536715191834872587423919410890444115959932760044506556206461164655665487594247369252336955993030355095817626176231849561906494839673002037763874369343999829430209147073618947932692762445186560239559053705128978163455423320114975994896278424327483788032701418676952621180975006405149755889650293004867605208010491537885413909424531691719987628941277221129464568294860281493181560249677887949813777216229359437811004448060797672429276249510784153446429150842764520002042769470698041775832209097020291657347251582904630910359037842977572651720877244740952267166306005469716387943171196873484688738186656751279298575016363411314627530499019135646823804329970695770150789337728658035712790913767420805655493624646 diff --git a/internal/compress/testdata/pngdata.bin b/internal/compress/testdata/pngdata.bin deleted file mode 100644 index f48a75c9..00000000 Binary files a/internal/compress/testdata/pngdata.bin and /dev/null differ diff --git a/internal/compress/testdata/sharnd.out b/internal/compress/testdata/sharnd.out deleted file mode 100644 index b474465e..00000000 Binary files a/internal/compress/testdata/sharnd.out and /dev/null differ diff --git a/internal/compress/zlib/reader.go b/internal/compress/zlib/reader.go deleted file mode 100644 index 78df4f56..00000000 --- a/internal/compress/zlib/reader.go +++ /dev/null @@ -1,182 +0,0 @@ -// Copyright 2009 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -/* -Package zlib implements reading and writing of zlib format compressed data, -as specified in RFC 1950. - -This package differs from the standard library's compress/zlib package -in that it pools readers and writers to reduce allocations. - -Note that closing a reader or writer causes it to be returned to a pool -for reuse. Therefore, the caller must not retain references to a -reader or writer after closing it; in the standard library's -compress/zlib package, it is legal to Reset a closed reader or writer -and continue using it; that is not allowed here, so there is simply no -Resetter interface. - -The implementation provides filters that uncompress during reading -and compress during writing. For example, to write compressed data -to a buffer: - - var b bytes.Buffer - w := zlib.NewWriter(&b) - w.Write([]byte("hello, world\n")) - w.Close() - -and to read that data back: - - r, err := zlib.NewReader(&b) - io.Copy(os.Stdout, r) - r.Close() -*/ -package zlib - -import ( - "encoding/binary" - "errors" - "hash" - "io" - "sync" - - "codeberg.org/lindenii/furgit/internal/compress/flate" - "codeberg.org/lindenii/furgit/internal/intconv" -) - -const ( - zlibDeflate = 8 - zlibMaxWindow = 7 -) - -var ( - // ErrChecksum is returned when reading ZLIB data that has an invalid checksum. - ErrChecksum = errors.New("zlib: invalid checksum") - // ErrDictionary is returned when reading ZLIB data that has an invalid dictionary. - ErrDictionary = errors.New("zlib: invalid dictionary") - // ErrHeader is returned when reading ZLIB data that has an invalid header. - ErrHeader = errors.New("zlib: invalid header") -) - -//nolint:gochecknoglobals -var readerPool = sync.Pool{ - New: func() any { - r := new(Reader) - - return r - }, -} - -// Reader reads and verifies one zlib stream. -// -// Reader implements io.ReadCloser. -type Reader struct { - r flate.Reader - decompressor io.ReadCloser - digest hash.Hash32 - headerRead uint64 - trailerRead uint64 - err error - scratch [4]byte -} - -// NewReader creates a new ReadCloser. -// Reads from the returned ReadCloser read and decompress data from r. -// If r does not implement [io.ByteReader], the decompressor may read more -// data than necessary from r. -// It is the caller's responsibility to call Close on the ReadCloser when done. -func NewReader(r io.Reader) (*Reader, error) { - return NewReaderDict(r, nil) -} - -// NewReaderDict is like [NewReader] but uses a preset dictionary. -// NewReaderDict ignores the dictionary if the compressed data does not refer to it. -// If the compressed data refers to a different dictionary, NewReaderDict returns [ErrDictionary]. -func NewReaderDict(r io.Reader, dict []byte) (*Reader, error) { - v := readerPool.Get() - - z, ok := v.(*Reader) - if !ok { - panic("zlib: pool returned unexpected type") - } - - err := z.reset(r, dict) - if err != nil { - return nil, err - } - - return z, nil -} - -// Read decompresses bytes from receiver into p. -func (z *Reader) Read(p []byte) (int, error) { - if z.err != nil { - return 0, z.err - } - - var n int - - n, z.err = z.decompressor.Read(p) - - _, err := z.digest.Write(p[0:n]) - if err != nil { - z.err = err - - return n, z.err - } - - if !errors.Is(z.err, io.EOF) { - // In the normal case we return here. - return n, z.err - } - - // Finished file; check checksum. - readN, err := io.ReadFull(z.r, z.scratch[0:4]) - - readNUint64, convErr := intconv.IntToUint64(readN) - if convErr != nil { - z.err = convErr - - return n, z.err - } - - z.trailerRead += readNUint64 - - if err != nil { - if errors.Is(err, io.EOF) { - err = io.ErrUnexpectedEOF - } - - z.err = err - - return n, z.err - } - // ZLIB (RFC 1950) is big-endian, unlike GZIP (RFC 1952). - checksum := binary.BigEndian.Uint32(z.scratch[:4]) - if checksum != z.digest.Sum32() { - z.err = ErrChecksum - - return n, z.err - } - - return n, io.EOF -} - -// Close does not close the wrapped [io.Reader] originally passed to [NewReader]. -// In order for the ZLIB checksum to be verified, the reader must be -// fully consumed until the [io.EOF]. -// Close returns the instance to a global pool; you MUST NOT keep references after Close. -func (z *Reader) Close() error { - if z.err != nil && !errors.Is(z.err, io.EOF) { - return z.err - } - - z.err = z.decompressor.Close() - if z.err != nil { - return z.err - } - - readerPool.Put(z) - - return nil -} diff --git a/internal/compress/zlib/reader_reset.go b/internal/compress/zlib/reader_reset.go deleted file mode 100644 index 0d531896..00000000 --- a/internal/compress/zlib/reader_reset.go +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright 2009 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package zlib - -import ( - "bufio" - "encoding/binary" - "errors" - "io" - - "codeberg.org/lindenii/furgit/internal/adler32" - "codeberg.org/lindenii/furgit/internal/compress/flate" - "codeberg.org/lindenii/furgit/internal/intconv" -) - -// reset resets receiver to read a new zlib stream. -func (z *Reader) reset(r io.Reader, dict []byte) error { - *z = Reader{decompressor: z.decompressor} - - var input flate.Reader - if fr, ok := r.(flate.Reader); ok { - input = fr - } else { - input = bufio.NewReader(r) - } - - z.r = input - - // Read the header (RFC 1950 section 2.2.). - readN, err := io.ReadFull(z.r, z.scratch[0:2]) - - readNUint64, convErr := intconv.IntToUint64(readN) - if convErr != nil { - z.err = convErr - - return z.err - } - - z.headerRead += readNUint64 - - z.err = err - if z.err != nil { - if errors.Is(z.err, io.EOF) { - z.err = io.ErrUnexpectedEOF - } - - return z.err - } - - h := binary.BigEndian.Uint16(z.scratch[:2]) - if (z.scratch[0]&0x0f != zlibDeflate) || (z.scratch[0]>>4 > zlibMaxWindow) || (h%31 != 0) { - z.err = ErrHeader - - return z.err - } - - haveDict := z.scratch[1]&0x20 != 0 - if haveDict { //nolint:nestif - readN, z.err = io.ReadFull(z.r, z.scratch[0:4]) - - readNUint64, err := intconv.IntToUint64(readN) - if err != nil { - z.err = err - - return z.err - } - - z.headerRead += readNUint64 - if z.err != nil { - if errors.Is(z.err, io.EOF) { - z.err = io.ErrUnexpectedEOF - } - - return z.err - } - - checksum := binary.BigEndian.Uint32(z.scratch[:4]) - if checksum != adler32.Checksum(dict) { - z.err = ErrDictionary - - return z.err - } - } - - if z.decompressor != nil { - resetter, ok := z.decompressor.(flate.Resetter) - if !ok { - panic("zlib: pooled decompressor does not implement flate.Resetter") - } - - z.err = resetter.Reset(z.r, dict) - if z.err != nil { - return z.err - } - - z.digest = adler32.New() - - return nil - } - - if haveDict { - z.decompressor = flate.NewReaderDict(z.r, dict) - } else { - z.decompressor = flate.NewReader(z.r) - } - - z.digest = adler32.New() - - return nil -} diff --git a/internal/compress/zlib/reader_test.go b/internal/compress/zlib/reader_test.go deleted file mode 100644 index 2cfa8a97..00000000 --- a/internal/compress/zlib/reader_test.go +++ /dev/null @@ -1,200 +0,0 @@ -// Copyright 2009 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package zlib_test - -import ( - "bytes" - "errors" - "io" - "testing" - - "codeberg.org/lindenii/furgit/internal/compress/zlib" -) - -type zlibTest struct { - desc string - raw string - compressed []byte - dict []byte - err error -} - -// Compare-to-golden test data was generated by the ZLIB example program at -// https://www.zlib.net/zpipe.c - -//nolint:gochecknoglobals -var zlibTests = []zlibTest{ - { - "truncated empty", - "", - []byte{}, - nil, - io.ErrUnexpectedEOF, - }, - { - "truncated dict", - "", - []byte{0x78, 0xbb}, - []byte{0x00}, - io.ErrUnexpectedEOF, - }, - { - "truncated checksum", - "", - []byte{ - 0x78, 0xbb, 0x00, 0x01, 0x00, 0x01, 0xca, 0x48, - 0xcd, 0xc9, 0xc9, 0xd7, 0x51, 0x28, 0xcf, 0x2f, - 0xca, 0x49, 0x01, 0x04, 0x00, 0x00, 0xff, 0xff, - }, - []byte{0x00}, - io.ErrUnexpectedEOF, - }, - { - "empty", - "", - []byte{0x78, 0x9c, 0x03, 0x00, 0x00, 0x00, 0x00, 0x01}, - nil, - nil, - }, - { - "goodbye", - "goodbye, world", - []byte{ - 0x78, 0x9c, 0x4b, 0xcf, 0xcf, 0x4f, 0x49, 0xaa, - 0x4c, 0xd5, 0x51, 0x28, 0xcf, 0x2f, 0xca, 0x49, - 0x01, 0x00, 0x28, 0xa5, 0x05, 0x5e, - }, - nil, - nil, - }, - { - "bad header (CINFO)", - "", - []byte{0x88, 0x98, 0x03, 0x00, 0x00, 0x00, 0x00, 0x01}, - nil, - zlib.ErrHeader, - }, - { - "bad header (FCHECK)", - "", - []byte{0x78, 0x9f, 0x03, 0x00, 0x00, 0x00, 0x00, 0x01}, - nil, - zlib.ErrHeader, - }, - { - "bad checksum", - "", - []byte{0x78, 0x9c, 0x03, 0x00, 0x00, 0x00, 0x00, 0xff}, - nil, - zlib.ErrChecksum, - }, - { - "not enough data", - "", - []byte{0x78, 0x9c, 0x03, 0x00, 0x00, 0x00}, - nil, - io.ErrUnexpectedEOF, - }, - { - "excess data is silently ignored", - "", - []byte{ - 0x78, 0x9c, 0x03, 0x00, 0x00, 0x00, 0x00, 0x01, - 0x78, 0x9c, 0xff, - }, - nil, - nil, - }, - { - "dictionary", - "Hello, World!\n", - []byte{ - 0x78, 0xbb, 0x1c, 0x32, 0x04, 0x27, 0xf3, 0x00, - 0xb1, 0x75, 0x20, 0x1c, 0x45, 0x2e, 0x00, 0x24, - 0x12, 0x04, 0x74, - }, - []byte{ - 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x57, 0x6f, 0x72, 0x6c, 0x64, 0x0a, - }, - nil, - }, - { - "wrong dictionary", - "", - []byte{ - 0x78, 0xbb, 0x1c, 0x32, 0x04, 0x27, 0xf3, 0x00, - 0xb1, 0x75, 0x20, 0x1c, 0x45, 0x2e, 0x00, 0x24, - 0x12, 0x04, 0x74, - }, - []byte{ - 0x48, 0x65, 0x6c, 0x6c, - }, - zlib.ErrDictionary, - }, - { - "truncated zlib stream amid raw-block", - "hello", - []byte{ - 0x78, 0x9c, 0x00, 0x0c, 0x00, 0xf3, 0xff, 0x68, 0x65, 0x6c, 0x6c, 0x6f, - }, - nil, - io.ErrUnexpectedEOF, - }, - { - "truncated zlib stream amid fixed-block", - "He", - []byte{ - 0x78, 0x9c, 0xf2, 0x48, 0xcd, - }, - nil, - io.ErrUnexpectedEOF, - }, -} - -func TestDecompressor(t *testing.T) { - t.Parallel() - - b := new(bytes.Buffer) - for _, tt := range zlibTests { - in := bytes.NewReader(tt.compressed) - - zr, err := zlib.NewReaderDict(in, tt.dict) - if err != nil { - if !errors.Is(err, tt.err) { - t.Errorf("%s: NewReader: %s", tt.desc, err) - } - - continue - } - - // Read and verify correctness of data. - b.Reset() - - n, err := io.Copy(b, zr) - if err != nil { - if !errors.Is(err, tt.err) { - t.Errorf("%s: io.Copy: %v want %v", tt.desc, err, tt.err) - } - - continue - } - - s := b.String() - if s != tt.raw { - t.Errorf("%s: got %d-byte %q want %d-byte %q", tt.desc, n, s, len(tt.raw), tt.raw) - } - - // Check for sticky errors. - n1, err := zr.Read([]byte{0}) - if n1 != 0 || !errors.Is(err, io.EOF) { - t.Errorf("%s: Read() = (%d, %v), want (0, io.EOF)", tt.desc, n, err) - } - - err = zr.Close() - if err != nil { - t.Errorf("%s: Close() = %v, want nil", tt.desc, err) - } - } -} diff --git a/internal/compress/zlib/writer.go b/internal/compress/zlib/writer.go deleted file mode 100644 index 05808eb6..00000000 --- a/internal/compress/zlib/writer.go +++ /dev/null @@ -1,205 +0,0 @@ -// Copyright 2009 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package zlib - -import ( - "encoding/binary" - "fmt" - "hash" - "io" - "sync" - - "codeberg.org/lindenii/furgit/internal/compress/flate" -) - -// These constants are copied from the [flate] package, so that code that imports -// [compress/zlib] does not also have to import [compress/flate]. -const ( - NoCompression = flate.NoCompression - BestSpeed = flate.BestSpeed - BestCompression = flate.BestCompression - DefaultCompression = flate.DefaultCompression - HuffmanOnly = flate.HuffmanOnly -) - -// A Writer takes data written to it and writes the compressed -// form of that data to an underlying writer (see [NewWriter]). -type Writer struct { - w io.Writer - level int - dict []byte - compressor *flate.Writer - digest hash.Hash32 - err error - scratch [4]byte - wroteHeader bool -} - -//nolint:gochecknoglobals -var writerPool = sync.Pool{ - New: func() any { - return new(Writer) - }, -} - -// NewWriter creates a new [Writer]. -// Writes to the returned Writer are compressed and written to w. -// -// It is the caller's responsibility to call Close on the Writer when done. -// Writes may be buffered and not flushed until Close. -func NewWriter(w io.Writer) *Writer { - z, _ := NewWriterLevelDict(w, DefaultCompression, nil) - - return z -} - -// NewWriterLevel is like [NewWriter] but specifies the compression level instead -// of assuming [DefaultCompression]. -// -// The compression level can be [DefaultCompression], [NoCompression], [HuffmanOnly] -// or any integer value between [BestSpeed] and [BestCompression] inclusive. -// The error returned will be nil if the level is valid. -func NewWriterLevel(w io.Writer, level int) (*Writer, error) { - return NewWriterLevelDict(w, level, nil) -} - -// NewWriterLevelDict is like [NewWriterLevel] but specifies a dictionary to -// compress with. -// -// The dictionary may be nil. If not, its contents should not be modified until -// the Writer is closed. -func NewWriterLevelDict(w io.Writer, level int, dict []byte) (*Writer, error) { - if level < HuffmanOnly || level > BestCompression { - return nil, fmt.Errorf("zlib: invalid compression level: %d", level) - } - - v := writerPool.Get() - - z, ok := v.(*Writer) - if !ok { - panic("zlib: pool returned unexpected type") - } - - // flate.Writer can only be Reset with the same level/dictionary mode. - // Reuse it only when the configuration is unchanged and dictionary-free. - reuseCompressor := z.compressor != nil && z.level == level && z.dict == nil && dict == nil - if !reuseCompressor { - z.compressor = nil - } - - if z.digest != nil { - z.digest.Reset() - } - - *z = Writer{ - w: w, - level: level, - dict: dict, - compressor: z.compressor, - digest: z.digest, - } - if z.compressor != nil { - z.compressor.Reset(w) - } - - return z, nil -} - -// Reset clears the state of the [Writer] z such that it is equivalent to its -// initial state from [NewWriterLevel] or [NewWriterLevelDict], but instead writing -// to w. -func (z *Writer) Reset(w io.Writer) { - z.w = w - // z.level and z.dict left unchanged. - if z.compressor != nil { - z.compressor.Reset(w) - } - - if z.digest != nil { - z.digest.Reset() - } - - z.err = nil - z.scratch = [4]byte{} - z.wroteHeader = false -} - -// Write writes a compressed form of p to the underlying [io.Writer]. The -// compressed bytes are not necessarily flushed until the [Writer] is closed or -// explicitly flushed. -func (z *Writer) Write(p []byte) (n int, err error) { - if !z.wroteHeader { - z.err = z.writeHeader() - } - - if z.err != nil { - return 0, z.err - } - - if len(p) == 0 { - return 0, nil - } - - n, err = z.compressor.Write(p) - if err != nil { - z.err = err - - return n, err - } - - _, err = z.digest.Write(p) - if err != nil { - z.err = err - - return 0, z.err - } - - return n, err -} - -// Flush flushes the Writer to its underlying [io.Writer]. -func (z *Writer) Flush() error { - if !z.wroteHeader { - z.err = z.writeHeader() - } - - if z.err != nil { - return z.err - } - - z.err = z.compressor.Flush() - - return z.err -} - -// Close closes the Writer, flushing any unwritten data to the underlying -// [io.Writer], but does not close the underlying io.Writer. -func (z *Writer) Close() error { - if !z.wroteHeader { - z.err = z.writeHeader() - } - - if z.err != nil { - return z.err - } - - z.err = z.compressor.Close() - if z.err != nil { - return z.err - } - - checksum := z.digest.Sum32() - // ZLIB (RFC 1950) is big-endian, unlike GZIP (RFC 1952). - binary.BigEndian.PutUint32(z.scratch[:], checksum) - - _, z.err = z.w.Write(z.scratch[0:4]) - if z.err != nil { - return z.err - } - - writerPool.Put(z) - - return nil -} diff --git a/internal/compress/zlib/writer_header.go b/internal/compress/zlib/writer_header.go deleted file mode 100644 index 43d3bdf5..00000000 --- a/internal/compress/zlib/writer_header.go +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright 2009 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package zlib - -import ( - "encoding/binary" - - "codeberg.org/lindenii/furgit/internal/adler32" - "codeberg.org/lindenii/furgit/internal/compress/flate" -) - -// writeHeader writes the ZLIB header. -func (z *Writer) writeHeader() (err error) { - z.wroteHeader = true - // ZLIB has a two-byte header (as documented in RFC 1950). - // The first four bits is the CINFO (compression info), which is 7 for the default deflate window size. - // The next four bits is the CM (compression method), which is 8 for deflate. - z.scratch[0] = 0x78 - // The next two bits is the FLEVEL (compression level). The four values are: - // 0=fastest, 1=fast, 2=default, 3=best. - // The next bit, FDICT, is set if a dictionary is given. - // The final five FCHECK bits form a mod-31 checksum. - switch z.level { - case -2, 0, 1: - z.scratch[1] = 0 << 6 - case 2, 3, 4, 5: - z.scratch[1] = 1 << 6 - case 6, -1: - z.scratch[1] = 2 << 6 - case 7, 8, 9: - z.scratch[1] = 3 << 6 - default: - panic("unreachable") - } - - if z.dict != nil { - z.scratch[1] |= 1 << 5 - } - - z.scratch[1] += uint8(31 - binary.BigEndian.Uint16(z.scratch[:2])%31) //#nosec G115 - - _, err = z.w.Write(z.scratch[0:2]) - if err != nil { - return err - } - - if z.dict != nil { - // The next four bytes are the Adler-32 checksum of the dictionary. - binary.BigEndian.PutUint32(z.scratch[:], adler32.Checksum(z.dict)) - - _, err = z.w.Write(z.scratch[0:4]) - if err != nil { - return err - } - } - - if z.compressor == nil { - // Initialize deflater unless the Writer is being reused - // after a Reset call. - z.compressor, err = flate.NewWriterDict(z.w, z.level, z.dict) - if err != nil { - return err - } - - z.digest = adler32.New() - } - - return nil -} diff --git a/internal/compress/zlib/writer_test.go b/internal/compress/zlib/writer_test.go deleted file mode 100644 index 541aac65..00000000 --- a/internal/compress/zlib/writer_test.go +++ /dev/null @@ -1,248 +0,0 @@ -// Copyright 2009 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package zlib_test - -import ( - "bytes" - "fmt" - "io" - "os" - "path/filepath" - "testing" - - "codeberg.org/lindenii/furgit/internal/compress/zlib" -) - -//nolint:gochecknoglobals -var filenames = []string{ - "../testdata/gettysburg.txt", - "../testdata/e.txt", - "../testdata/pi.txt", -} - -//nolint:gochecknoglobals -var data = []string{ - "test a reasonable sized string that can be compressed", -} - -func testdataRoot(t *testing.T) *os.Root { - t.Helper() - - root, err := os.OpenRoot("../testdata") - if err != nil { - t.Fatalf("open testdata root: %v", err) - } - - return root -} - -// Tests that compressing and then decompressing the given file at the given compression level and dictionary -// yields equivalent bytes to the original file. -func testFileLevelDict(t *testing.T, fn string, level int, d string) { - t.Helper() - - root := testdataRoot(t) - - defer func() { - err := root.Close() - if err != nil { - t.Fatalf("%s (level=%d, dict=%q): close testdata root: %v", fn, level, d, err) - } - }() - - // Read the file, as golden output. - b0, err := root.ReadFile(filepath.Base(fn)) - if err != nil { - t.Errorf("%s (level=%d, dict=%q): %v", fn, level, d, err) - - return - } - - testLevelDict(t, fn, b0, level, d) -} - -func testLevelDict(t *testing.T, fn string, b0 []byte, level int, d string) { - t.Helper() - - // Make dictionary, if given. - var dict []byte - if d != "" { - dict = []byte(d) - } - - // Push data through a pipe that compresses at the write end, and decompresses at the read end. - piper, pipew := io.Pipe() - - defer func() { - err := piper.Close() - if err != nil { - t.Fatalf("%s (level=%d, dict=%q): close piper: %v", fn, level, d, err) - } - }() - - go func() { - defer func() { - err := pipew.Close() - if err != nil { - t.Errorf("%s (level=%d, dict=%q): close pipew: %v", fn, level, d, err) - } - }() - - zlibw, err := zlib.NewWriterLevelDict(pipew, level, dict) - if err != nil { - t.Errorf("%s (level=%d, dict=%q): %v", fn, level, d, err) - - return - } - - defer func() { - err := zlibw.Close() - if err != nil { - t.Errorf("%s (level=%d, dict=%q): close zlibw: %v", fn, level, d, err) - } - }() - - _, err = zlibw.Write(b0) - if err != nil { - t.Errorf("%s (level=%d, dict=%q): %v", fn, level, d, err) - - return - } - }() - - zlibr, err := zlib.NewReaderDict(piper, dict) - if err != nil { - t.Errorf("%s (level=%d, dict=%q): %v", fn, level, d, err) - - return - } - - defer func() { - err := zlibr.Close() - if err != nil { - t.Fatalf("%s (level=%d, dict=%q): close zlibr: %v", fn, level, d, err) - } - }() - - // Compare the decompressed data. - b1, err1 := io.ReadAll(zlibr) - if err1 != nil { - t.Errorf("%s (level=%d, dict=%q): %v", fn, level, d, err1) - - return - } - - if len(b0) != len(b1) { - t.Errorf("%s (level=%d, dict=%q): length mismatch %d versus %d", fn, level, d, len(b0), len(b1)) - - return - } - - for i := range b0 { - if b0[i] != b1[i] { - t.Errorf("%s (level=%d, dict=%q): mismatch at %d, 0x%02x versus 0x%02x\n", fn, level, d, i, b0[i], b1[i]) - - return - } - } -} - -func TestWriter(t *testing.T) { - t.Parallel() - - for i, s := range data { - b := []byte(s) - tag := fmt.Sprintf("#%d", i) - testLevelDict(t, tag, b, zlib.DefaultCompression, "") - testLevelDict(t, tag, b, zlib.NoCompression, "") - testLevelDict(t, tag, b, zlib.HuffmanOnly, "") - - for level := zlib.BestSpeed; level <= zlib.BestCompression; level++ { - testLevelDict(t, tag, b, level, "") - } - } -} - -func TestWriterBig(t *testing.T) { - t.Parallel() - - for i, fn := range filenames { - testFileLevelDict(t, fn, zlib.DefaultCompression, "") - testFileLevelDict(t, fn, zlib.NoCompression, "") - testFileLevelDict(t, fn, zlib.HuffmanOnly, "") - - for level := zlib.BestSpeed; level <= zlib.BestCompression; level++ { - testFileLevelDict(t, fn, level, "") - - if level >= 1 && testing.Short() { - break - } - } - - if i == 0 && testing.Short() { - break - } - } -} - -func TestWriterDict(t *testing.T) { - t.Parallel() - - const dictionary = "0123456789." - for i, fn := range filenames { - testFileLevelDict(t, fn, zlib.DefaultCompression, dictionary) - testFileLevelDict(t, fn, zlib.NoCompression, dictionary) - testFileLevelDict(t, fn, zlib.HuffmanOnly, dictionary) - - for level := zlib.BestSpeed; level <= zlib.BestCompression; level++ { - testFileLevelDict(t, fn, level, dictionary) - - if level >= 1 && testing.Short() { - break - } - } - - if i == 0 && testing.Short() { - break - } - } -} - -func TestWriterDictIsUsed(t *testing.T) { - t.Parallel() - - var ( - input = []byte("Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.") - buf bytes.Buffer - ) - - compressor, err := zlib.NewWriterLevelDict(&buf, zlib.BestCompression, input) - if err != nil { - t.Errorf("error in NewWriterLevelDict: %s", err) - - return - } - - _, err = compressor.Write(input) - if err != nil { - t.Errorf("error in compressor.Write: %s", err) - - return - } - - err = compressor.Close() - if err != nil { - t.Errorf("error in compressor.Close: %s", err) - - return - } - - const expectedMaxSize = 25 - - output := buf.Bytes() - if len(output) > expectedMaxSize { - t.Errorf("result too large (got %d, want <= %d bytes). Is the dictionary being used?", len(output), expectedMaxSize) - } -} diff --git a/internal/cpu/LICENSE b/internal/cpu/LICENSE deleted file mode 100644 index 2a7cf70d..00000000 --- a/internal/cpu/LICENSE +++ /dev/null @@ -1,27 +0,0 @@ -Copyright 2009 The Go Authors. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google LLC nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/internal/cpu/cpu.go b/internal/cpu/cpu.go deleted file mode 100644 index ab4cc0bd..00000000 --- a/internal/cpu/cpu.go +++ /dev/null @@ -1,9 +0,0 @@ -// Package cpu provides routines for CPU feature detection. -package cpu - -// X86 contains x86 CPU feature flags detected at runtime. -// -//nolint:gochecknoglobals -var X86 struct { - HasAVX2 bool -} diff --git a/internal/cpu/cpu_amd64.go b/internal/cpu/cpu_amd64.go deleted file mode 100644 index 7fe59633..00000000 --- a/internal/cpu/cpu_amd64.go +++ /dev/null @@ -1,34 +0,0 @@ -//go:build amd64 && !purego - -package cpu - -const ( - cpuidOSXSAVE = 1 << 27 - cpuidAVX = 1 << 28 - cpuidAVX2 = 1 << 5 -) - -// cpuid is implemented in cpu_amd64.s. -func cpuid(eaxArg, ecxArg uint32) (eax, ebx, ecx, edx uint32) - -// xgetbv with ecx = 0 is implemented in cpu_amd64.s. -func xgetbv() (eax, edx uint32) - -func init() { //nolint:gochecknoinits - maxID, _, _, _ := cpuid(0, 0) - if maxID < 7 { - return - } - - _, _, ecx1, _ := cpuid(1, 0) - - osSupportsAVX := false - - if ecx1&cpuidOSXSAVE != 0 { - eax, _ := xgetbv() - osSupportsAVX = eax&(1<<1) != 0 && eax&(1<<2) != 0 - } - - _, ebx7, _, _ := cpuid(7, 0) - X86.HasAVX2 = osSupportsAVX && ecx1&cpuidAVX != 0 && ebx7&cpuidAVX2 != 0 -} diff --git a/internal/cpu/cpu_amd64.s b/internal/cpu/cpu_amd64.s deleted file mode 100644 index 250a34e2..00000000 --- a/internal/cpu/cpu_amd64.s +++ /dev/null @@ -1,22 +0,0 @@ -//go:build amd64 && !purego - -#include "textflag.h" - -// func cpuid(eaxArg, ecxArg uint32) (eax, ebx, ecx, edx uint32) -TEXT ·cpuid(SB), NOSPLIT, $0-24 - MOVL eaxArg+0(FP), AX - MOVL ecxArg+4(FP), CX - CPUID - MOVL AX, eax+8(FP) - MOVL BX, ebx+12(FP) - MOVL CX, ecx+16(FP) - MOVL DX, edx+20(FP) - RET - -// func xgetbv() (eax, edx uint32) -TEXT ·xgetbv(SB), NOSPLIT, $0-8 - MOVL $0, CX - XGETBV - MOVL AX, eax+0(FP) - MOVL DX, edx+4(FP) - RET diff --git a/internal/cpu/cpu_other.go b/internal/cpu/cpu_other.go deleted file mode 100644 index 969c68ef..00000000 --- a/internal/cpu/cpu_other.go +++ /dev/null @@ -1,3 +0,0 @@ -//go:build !amd64 || purego - -package cpu diff --git a/internal/doc.go b/internal/doc.go deleted file mode 100644 index 0c4a6161..00000000 --- a/internal/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package internal provides private packages and helpers. -package internal diff --git a/internal/intconv/doc.go b/internal/intconv/doc.go deleted file mode 100644 index fc1f7428..00000000 --- a/internal/intconv/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package intconv provides checked integer conversion helpers. -package intconv diff --git a/internal/intconv/i64_i32.go b/internal/intconv/i64_i32.go deleted file mode 100644 index 485e7895..00000000 --- a/internal/intconv/i64_i32.go +++ /dev/null @@ -1,15 +0,0 @@ -package intconv - -import ( - "fmt" - "math" -) - -// Int64ToInt32 converts v to int32, returning an error if it overflows. -func Int64ToInt32(v int64) (int32, error) { - if v < math.MinInt32 || v > math.MaxInt32 { - return 0, fmt.Errorf("intconv: int64 %d overflows int32", v) - } - - return int32(v), nil -} diff --git a/internal/intconv/i64_u64.go b/internal/intconv/i64_u64.go deleted file mode 100644 index 4c9b56c5..00000000 --- a/internal/intconv/i64_u64.go +++ /dev/null @@ -1,12 +0,0 @@ -package intconv - -import "fmt" - -// Int64ToUint64 converts v to uint64, returning an error if v is negative. -func Int64ToUint64(v int64) (uint64, error) { - if v < 0 { - return 0, fmt.Errorf("intconv: int64 %d is negative", v) - } - - return uint64(v), nil -} diff --git a/internal/intconv/i_u32.go b/internal/intconv/i_u32.go deleted file mode 100644 index 5354010c..00000000 --- a/internal/intconv/i_u32.go +++ /dev/null @@ -1,15 +0,0 @@ -package intconv - -import ( - "fmt" - "math" -) - -// IntToUint32 converts v to uint32, returning an error if it overflows. -func IntToUint32(v int) (uint32, error) { - if v < 0 || v > math.MaxUint32 { - return 0, fmt.Errorf("intconv: int %d overflows uint32", v) - } - - return uint32(v), nil -} diff --git a/internal/intconv/i_u64.go b/internal/intconv/i_u64.go deleted file mode 100644 index a94a162c..00000000 --- a/internal/intconv/i_u64.go +++ /dev/null @@ -1,12 +0,0 @@ -package intconv - -import "fmt" - -// IntToUint64 converts v to uint64, returning an error if v is negative. -func IntToUint64(v int) (uint64, error) { - if v < 0 { - return 0, fmt.Errorf("intconv: int %d is negative", v) - } - - return uint64(v), nil -} diff --git a/internal/intconv/se_u8_u32.go b/internal/intconv/se_u8_u32.go deleted file mode 100644 index bef34268..00000000 --- a/internal/intconv/se_u8_u32.go +++ /dev/null @@ -1,10 +0,0 @@ -package intconv - -// SignExtendByteToUint32 sign-extends b as a signed 8-bit integer into uint32. -func SignExtendByteToUint32(b byte) uint32 { - if b&0x80 == 0 { - return uint32(b) - } - - return 0xFFFFFF00 | uint32(b) -} diff --git a/internal/intconv/u32_i.go b/internal/intconv/u32_i.go deleted file mode 100644 index a0f88724..00000000 --- a/internal/intconv/u32_i.go +++ /dev/null @@ -1,15 +0,0 @@ -package intconv - -import ( - "fmt" - "math" -) - -// Uint32ToInt converts v to int, returning an error if it overflows. -func Uint32ToInt(v uint32) (int, error) { - if uint64(v) > uint64(math.MaxInt) { - return 0, fmt.Errorf("intconv: uint32 %d overflows int", v) - } - - return int(v), nil -} diff --git a/internal/intconv/u32_u8.go b/internal/intconv/u32_u8.go deleted file mode 100644 index 13aee55b..00000000 --- a/internal/intconv/u32_u8.go +++ /dev/null @@ -1,15 +0,0 @@ -package intconv - -import ( - "fmt" - "math" -) - -// Uint32ToUint8 converts v to uint8, returning an error if it overflows. -func Uint32ToUint8(v uint32) (uint8, error) { - if v > math.MaxUint8 { - return 0, fmt.Errorf("intconv: uint32 %d overflows uint8", v) - } - - return uint8(v), nil -} diff --git a/internal/intconv/u64_i.go b/internal/intconv/u64_i.go deleted file mode 100644 index 45b88d53..00000000 --- a/internal/intconv/u64_i.go +++ /dev/null @@ -1,15 +0,0 @@ -package intconv - -import ( - "fmt" - "math" -) - -// Uint64ToInt converts v to int, returning an error if it overflows. -func Uint64ToInt(v uint64) (int, error) { - if v > uint64(math.MaxInt) { - return 0, fmt.Errorf("intconv: uint64 %d overflows int", v) - } - - return int(v), nil -} diff --git a/internal/intconv/u64_i64.go b/internal/intconv/u64_i64.go deleted file mode 100644 index 59b26a73..00000000 --- a/internal/intconv/u64_i64.go +++ /dev/null @@ -1,15 +0,0 @@ -package intconv - -import ( - "fmt" - "math" -) - -// Uint64ToInt64 converts v to int64, returning an error if it overflows. -func Uint64ToInt64(v uint64) (int64, error) { - if v > math.MaxInt64 { - return 0, fmt.Errorf("intconv: uint64 %d overflows int64", v) - } - - return int64(v), nil -} diff --git a/internal/intconv/uptr_int.go b/internal/intconv/uptr_int.go deleted file mode 100644 index fa832147..00000000 --- a/internal/intconv/uptr_int.go +++ /dev/null @@ -1,15 +0,0 @@ -package intconv - -import ( - "fmt" - "math" -) - -// UintptrToInt converts v to int, returning an error if it overflows. -func UintptrToInt(v uintptr) (int, error) { - if v > uintptr(math.MaxInt) { - return 0, fmt.Errorf("intconv: uintptr %d overflows int", v) - } - - return int(v), nil -} diff --git a/internal/iolimit/capped_capture_writer.go b/internal/iolimit/capped_capture_writer.go deleted file mode 100644 index 2e69806a..00000000 --- a/internal/iolimit/capped_capture_writer.go +++ /dev/null @@ -1,52 +0,0 @@ -package iolimit - -import "bytes" - -// CappedCaptureWriter captures written bytes up to a fixed limit. -// -// Once the total written bytes would exceed the limit, capture is disabled and -// Bytes() returns nil. Write still reports success for the full input length. -type CappedCaptureWriter struct { - limit int64 - buf bytes.Buffer - full bool -} - -// NewCappedCaptureWriter constructs one capped capture writer. -func NewCappedCaptureWriter(limit int64) *CappedCaptureWriter { - return &CappedCaptureWriter{limit: limit} -} - -// Write captures up to the configured limit and always reports len(src) bytes written. -func (writer *CappedCaptureWriter) Write(src []byte) (int, error) { - if writer.full { - return len(src), nil - } - - room := writer.limit - int64(writer.buf.Len()) - if room <= 0 { - writer.full = true - - return len(src), nil - } - - if int64(len(src)) > room { - _, _ = writer.buf.Write(src[:room]) - writer.full = true - - return len(src), nil - } - - _, _ = writer.buf.Write(src) - - return len(src), nil -} - -// Bytes returns captured bytes, or nil when capture exceeded the limit. -func (writer *CappedCaptureWriter) Bytes() []byte { - if writer.full { - return nil - } - - return writer.buf.Bytes() -} diff --git a/internal/iolimit/capped_capture_writer_test.go b/internal/iolimit/capped_capture_writer_test.go deleted file mode 100644 index e95d06ef..00000000 --- a/internal/iolimit/capped_capture_writer_test.go +++ /dev/null @@ -1,45 +0,0 @@ -package iolimit_test - -import ( - "bytes" - "testing" - - "codeberg.org/lindenii/furgit/internal/iolimit" -) - -func TestCappedCaptureWriterWithinLimit(t *testing.T) { - t.Parallel() - - writer := iolimit.NewCappedCaptureWriter(8) - - _, _ = writer.Write([]byte("hello")) - _, _ = writer.Write([]byte("!")) - - if got := writer.Bytes(); !bytes.Equal(got, []byte("hello!")) { - t.Fatalf("Bytes() = %q, want %q", got, "hello!") - } -} - -func TestCappedCaptureWriterExceededLimit(t *testing.T) { - t.Parallel() - - writer := iolimit.NewCappedCaptureWriter(4) - - _, _ = writer.Write([]byte("abcd")) - _, _ = writer.Write([]byte("x")) - - if got := writer.Bytes(); got != nil { - t.Fatalf("Bytes() = %q, want nil after overflow", got) - } -} - -func TestCappedCaptureWriterZeroLimit(t *testing.T) { - t.Parallel() - - writer := iolimit.NewCappedCaptureWriter(0) - - _, _ = writer.Write([]byte("x")) - if got := writer.Bytes(); got != nil { - t.Fatalf("Bytes() = %q, want nil at zero limit", got) - } -} diff --git a/internal/iolimit/doc.go b/internal/iolimit/doc.go deleted file mode 100644 index b3e81ce2..00000000 --- a/internal/iolimit/doc.go +++ /dev/null @@ -1,5 +0,0 @@ -// Package iolimit provides small internal I/O wrappers with bounded behavior. -// -// It includes helpers for both readers and writers that enforce configured -// limits (length checks, capped capture, etc.). -package iolimit diff --git a/internal/iolimit/expect_length_reader.go b/internal/iolimit/expect_length_reader.go deleted file mode 100644 index a45ad567..00000000 --- a/internal/iolimit/expect_length_reader.go +++ /dev/null @@ -1,79 +0,0 @@ -package iolimit - -import ( - "errors" - "io" -) - -// ErrExpectedLengthExceeded reports that a stream produced bytes beyond the -// expected length. -var ErrExpectedLengthExceeded = errors.New("iolimit: stream exceeded expected length") - -// ExpectLengthReader wraps src and enforces an expected byte length. -// -// It returns io.ErrUnexpectedEOF if src ends before expected bytes are read. -// It returns ErrExpectedLengthExceeded if reads continue beyond the expected -// boundary and src still produces bytes. -// -// This reader does not drain src on close or at the expected boundary. As a -// result, overlength streams are detected only when a caller reads at or past -// the boundary. -func ExpectLengthReader(src io.Reader, expected int64) io.Reader { - return &expectLengthReader{ - src: src, - remaining: expected, - } -} - -type expectLengthReader struct { - src io.Reader - remaining int64 -} - -func (reader *expectLengthReader) Read(dst []byte) (int, error) { - if len(dst) == 0 { - return 0, nil - } - - if reader.remaining == 0 { - var probe [1]byte - - n, err := reader.src.Read(probe[:]) - if n > 0 { - return 0, ErrExpectedLengthExceeded - } - - if err == nil { - return 0, nil - } - - return 0, err - } - - if reader.remaining < 0 { - return 0, ErrExpectedLengthExceeded - } - - if int64(len(dst)) > reader.remaining { - dst = dst[:reader.remaining] - } - - n, err := reader.src.Read(dst) - if n > 0 { - reader.remaining -= int64(n) - } - - if err == io.EOF { - if reader.remaining > 0 { - return n, io.ErrUnexpectedEOF - } - - if n > 0 { - return n, nil - } - - return 0, io.EOF - } - - return n, err -} diff --git a/internal/iolimit/expect_length_reader_test.go b/internal/iolimit/expect_length_reader_test.go deleted file mode 100644 index e2cfeab0..00000000 --- a/internal/iolimit/expect_length_reader_test.go +++ /dev/null @@ -1,78 +0,0 @@ -package iolimit_test - -import ( - "bytes" - "errors" - "io" - "testing" - - "codeberg.org/lindenii/furgit/internal/iolimit" -) - -func TestExpectLengthReaderExact(t *testing.T) { - t.Parallel() - - r := iolimit.ExpectLengthReader(bytes.NewReader([]byte("hello")), 5) - - got, err := io.ReadAll(r) - if err != nil { - t.Fatalf("ReadAll error: %v", err) - } - - if !bytes.Equal(got, []byte("hello")) { - t.Fatalf("ReadAll = %q, want %q", got, "hello") - } - - buf := make([]byte, 1) - - n, err := r.Read(buf) - if n != 0 || !errors.Is(err, io.EOF) { - t.Fatalf("post-boundary Read = (%d,%v), want (0,EOF)", n, err) - } -} - -func TestExpectLengthReaderShort(t *testing.T) { - t.Parallel() - - r := iolimit.ExpectLengthReader(bytes.NewReader([]byte("hey")), 5) - - _, err := io.ReadAll(r) - if !errors.Is(err, io.ErrUnexpectedEOF) { - t.Fatalf("ReadAll error = %v, want ErrUnexpectedEOF", err) - } -} - -func TestExpectLengthReaderLongDetectedOnNextRead(t *testing.T) { - t.Parallel() - - r := iolimit.ExpectLengthReader(bytes.NewReader([]byte("hello!")), 5) - buf := make([]byte, 5) - - n, err := io.ReadFull(r, buf) - if err != nil { - t.Fatalf("ReadFull error: %v", err) - } - - if n != 5 || !bytes.Equal(buf, []byte("hello")) { - t.Fatalf("ReadFull = (%d,%q), want (5,hello)", n, buf) - } - - probe := make([]byte, 1) - - n, err = r.Read(probe) - if n != 0 || !errors.Is(err, iolimit.ErrExpectedLengthExceeded) { - t.Fatalf("overflow Read = (%d,%v), want (0,ErrExpectedLengthExceeded)", n, err) - } -} - -func TestExpectLengthReaderEmptyExpected(t *testing.T) { - t.Parallel() - - r := iolimit.ExpectLengthReader(bytes.NewReader(nil), 0) - buf := make([]byte, 1) - - n, err := r.Read(buf) - if n != 0 || !errors.Is(err, io.EOF) { - t.Fatalf("Read = (%d,%v), want (0,EOF)", n, err) - } -} diff --git a/internal/lru/add.go b/internal/lru/add.go deleted file mode 100644 index 6c055ab5..00000000 --- a/internal/lru/add.go +++ /dev/null @@ -1,35 +0,0 @@ -package lru - -// Add inserts or replaces key and marks it most-recently-used. -// -// Add returns false when the entry's weight exceeds MaxWeight even for an empty -// cache. In that case the cache is unchanged. -// -// Add panics if weightFn returns a negative weight. -func (cache *Cache[K, V]) Add(key K, value V) bool { - w := cache.weightFn(key, value) - if w < 0 { - panic("lru: negative entry weight") - } - - if w > cache.maxWeight { - return false - } - - if elem, ok := cache.items[key]; ok { - cache.removeElem(elem) - } - - ent := &entry[K, V]{ - key: key, - value: value, - weight: w, - } - elem := cache.lru.PushBack(ent) - cache.items[key] = elem - cache.weight += w - - cache.evictOverBudget() - - return true -} diff --git a/internal/lru/cache.go b/internal/lru/cache.go deleted file mode 100644 index 1aa96c52..00000000 --- a/internal/lru/cache.go +++ /dev/null @@ -1,16 +0,0 @@ -package lru - -import "container/list" - -// Cache is a non-concurrent weighted LRU cache. -// -// Methods on Cache are not safe for concurrent use. -type Cache[K comparable, V any] struct { - maxWeight int64 - weightFn WeightFunc[K, V] - onEvict OnEvictFunc[K, V] - - weight int64 - items map[K]*list.Element - lru list.List -} diff --git a/internal/lru/clear.go b/internal/lru/clear.go deleted file mode 100644 index 42f9c50e..00000000 --- a/internal/lru/clear.go +++ /dev/null @@ -1,10 +0,0 @@ -package lru - -// Clear removes all entries from the cache. -func (cache *Cache[K, V]) Clear() { - for elem := cache.lru.Front(); elem != nil; { - next := elem.Next() - cache.removeElem(elem) - elem = next - } -} diff --git a/internal/lru/entries.go b/internal/lru/entries.go deleted file mode 100644 index 132f8f7e..00000000 --- a/internal/lru/entries.go +++ /dev/null @@ -1,7 +0,0 @@ -package lru - -type entry[K comparable, V any] struct { - key K - value V - weight int64 -} diff --git a/internal/lru/evict.go b/internal/lru/evict.go deleted file mode 100644 index 9659dd4f..00000000 --- a/internal/lru/evict.go +++ /dev/null @@ -1,17 +0,0 @@ -package lru - -// OnEvictFunc runs when an entry leaves the cache. -// -// It is called for evictions, explicit removals, Clear, and replacement by Add. -type OnEvictFunc[K comparable, V any] func(key K, value V) - -func (cache *Cache[K, V]) evictOverBudget() { - for cache.weight > cache.maxWeight { - elem := cache.lru.Front() - if elem == nil { - return - } - - cache.removeElem(elem) - } -} diff --git a/internal/lru/get.go b/internal/lru/get.go deleted file mode 100644 index 81383945..00000000 --- a/internal/lru/get.go +++ /dev/null @@ -1,17 +0,0 @@ -package lru - -// Get returns value for key and marks it most-recently-used. -// -//nolint:ireturn -func (cache *Cache[K, V]) Get(key K) (V, bool) { - elem, ok := cache.items[key] - if !ok { - var zero V - - return zero, false - } - - cache.lru.MoveToBack(elem) - //nolint:forcetypeassert - return elem.Value.(*entry[K, V]).value, true -} diff --git a/internal/lru/len.go b/internal/lru/len.go deleted file mode 100644 index bc05e362..00000000 --- a/internal/lru/len.go +++ /dev/null @@ -1,6 +0,0 @@ -package lru - -// Len returns the number of cached entries. -func (cache *Cache[K, V]) Len() int { - return len(cache.items) -} diff --git a/internal/lru/lru.go b/internal/lru/lru.go deleted file mode 100644 index 119f31c1..00000000 --- a/internal/lru/lru.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package lru provides a size-cost bounded LRU cache. -package lru diff --git a/internal/lru/lru_test.go b/internal/lru/lru_test.go deleted file mode 100644 index 006a32b8..00000000 --- a/internal/lru/lru_test.go +++ /dev/null @@ -1,245 +0,0 @@ -package lru_test - -import ( - "slices" - "testing" - - "codeberg.org/lindenii/furgit/internal/lru" -) - -type testValue struct { - weight int64 - label string -} - -func weightFn(key string, value testValue) int64 { - return value.weight -} - -func TestCacheEvictsLRUAndGetUpdatesRecency(t *testing.T) { - t.Parallel() - - cache := lru.New[string, testValue](8, weightFn, nil) - cache.Add("a", testValue{weight: 4, label: "a"}) - cache.Add("b", testValue{weight: 4, label: "b"}) - cache.Add("c", testValue{weight: 4, label: "c"}) - - if _, ok := cache.Peek("a"); ok { - t.Fatalf("expected a to be evicted") - } - - if _, ok := cache.Peek("b"); !ok { - t.Fatalf("expected b to be present") - } - - if _, ok := cache.Peek("c"); !ok { - t.Fatalf("expected c to be present") - } - - if _, ok := cache.Get("b"); !ok { - t.Fatalf("Get(b) should hit") - } - - cache.Add("d", testValue{weight: 4, label: "d"}) - - if _, ok := cache.Peek("c"); ok { - t.Fatalf("expected c to be evicted after b was touched") - } - - if _, ok := cache.Peek("b"); !ok { - t.Fatalf("expected b to remain present") - } - - if _, ok := cache.Peek("d"); !ok { - t.Fatalf("expected d to be present") - } -} - -func TestCachePeekDoesNotUpdateRecency(t *testing.T) { - t.Parallel() - - cache := lru.New[string, testValue](4, weightFn, nil) - cache.Add("a", testValue{weight: 2, label: "a"}) - cache.Add("b", testValue{weight: 2, label: "b"}) - - if _, ok := cache.Peek("a"); !ok { - t.Fatalf("Peek(a) should hit") - } - - cache.Add("c", testValue{weight: 2, label: "c"}) - - if _, ok := cache.Peek("a"); ok { - t.Fatalf("expected a to be evicted; Peek must not update recency") - } - - if _, ok := cache.Peek("b"); !ok { - t.Fatalf("expected b to remain present") - } -} - -func TestCacheReplaceAndResize(t *testing.T) { - t.Parallel() - - var evicted []string - - cache := lru.New[string, testValue](10, weightFn, func(key string, value testValue) { - evicted = append(evicted, key+":"+value.label) - }) - - cache.Add("a", testValue{weight: 4, label: "old"}) - cache.Add("b", testValue{weight: 4, label: "b"}) - cache.Add("a", testValue{weight: 6, label: "new"}) - - if cache.Weight() != 10 { - t.Fatalf("Weight() = %d, want 10", cache.Weight()) - } - - if got, ok := cache.Peek("a"); !ok || got.label != "new" { - t.Fatalf("Peek(a) = (%+v,%v), want new,true", got, ok) - } - - if !slices.Equal(evicted, []string{"a:old"}) { - t.Fatalf("evicted = %v, want [a:old]", evicted) - } - - cache.SetMaxWeight(8) - - if _, ok := cache.Peek("b"); ok { - t.Fatalf("expected b to be evicted after shrinking max weight") - } - - if !slices.Equal(evicted, []string{"a:old", "b:b"}) { - t.Fatalf("evicted = %v, want [a:old b:b]", evicted) - } -} - -func TestCacheRejectsOversizedWithoutMutation(t *testing.T) { - t.Parallel() - - var evicted []string - - cache := lru.New[string, testValue](5, weightFn, func(key string, value testValue) { - evicted = append(evicted, key) - }) - cache.Add("a", testValue{weight: 3, label: "a"}) - - if ok := cache.Add("b", testValue{weight: 6, label: "b"}); ok { - t.Fatalf("Add oversized should return false") - } - - if got, ok := cache.Peek("a"); !ok || got.label != "a" { - t.Fatalf("cache should remain unchanged after oversized add") - } - - if cache.Weight() != 3 { - t.Fatalf("Weight() = %d, want 3", cache.Weight()) - } - - if len(evicted) != 0 { - t.Fatalf("evicted = %v, want none", evicted) - } - - if ok := cache.Add("a", testValue{weight: 6, label: "new"}); ok { - t.Fatalf("oversized replace should return false") - } - - if got, ok := cache.Peek("a"); !ok || got.label != "a" { - t.Fatalf("existing key should remain unchanged after oversized replace") - } - - if len(evicted) != 0 { - t.Fatalf("evicted = %v, want none", evicted) - } -} - -func TestCacheRemoveAndClear(t *testing.T) { - t.Parallel() - - var evicted []string - - cache := lru.New[string, testValue](10, weightFn, func(key string, value testValue) { - evicted = append(evicted, key) - }) - - cache.Add("a", testValue{weight: 2, label: "a"}) - cache.Add("b", testValue{weight: 3, label: "b"}) - cache.Add("c", testValue{weight: 4, label: "c"}) - - removed, ok := cache.Remove("b") - if !ok || removed.label != "b" { - t.Fatalf("Remove(b) = (%+v,%v), want b,true", removed, ok) - } - - if cache.Len() != 2 || cache.Weight() != 6 { - t.Fatalf("post-remove Len/Weight = %d/%d, want 2/6", cache.Len(), cache.Weight()) - } - - cache.Clear() - - if cache.Len() != 0 || cache.Weight() != 0 { - t.Fatalf("post-clear Len/Weight = %d/%d, want 0/0", cache.Len(), cache.Weight()) - } - - // Remove emits b, then Clear emits oldest-to-newest among remaining: a, c. - if !slices.Equal(evicted, []string{"b", "a", "c"}) { - t.Fatalf("evicted = %v, want [b a c]", evicted) - } -} - -func TestCachePanicsForInvalidConfiguration(t *testing.T) { - t.Parallel() - - t.Run("negative max", func(t *testing.T) { - t.Parallel() - - defer func() { - if recover() == nil { - t.Fatalf("expected panic") - } - }() - - _ = lru.New[string, testValue](-1, weightFn, nil) - }) - - t.Run("nil weight function", func(t *testing.T) { - t.Parallel() - - defer func() { - if recover() == nil { - t.Fatalf("expected panic") - } - }() - - _ = lru.New[string, testValue](1, nil, nil) - }) - - t.Run("negative entry weight", func(t *testing.T) { - t.Parallel() - - cache := lru.New[string, testValue](10, func(_ string, _ testValue) int64 { - return -1 - }, nil) - - defer func() { - if recover() == nil { - t.Fatalf("expected panic") - } - }() - - cache.Add("x", testValue{weight: 1, label: "x"}) - }) - - t.Run("set negative max", func(t *testing.T) { - t.Parallel() - - cache := lru.New[string, testValue](10, weightFn, nil) - - defer func() { - if recover() == nil { - t.Fatalf("expected panic") - } - }() - - cache.SetMaxWeight(-1) - }) -} diff --git a/internal/lru/new.go b/internal/lru/new.go deleted file mode 100644 index f683416a..00000000 --- a/internal/lru/new.go +++ /dev/null @@ -1,23 +0,0 @@ -package lru - -import "container/list" - -// New creates a cache with a maximum total weight. -// -// New panics if maxWeight is negative or weightFn is nil. -func New[K comparable, V any](maxWeight int64, weightFn WeightFunc[K, V], onEvict OnEvictFunc[K, V]) *Cache[K, V] { - if maxWeight < 0 { - panic("lru: negative max weight") - } - - if weightFn == nil { - panic("lru: nil weight function") - } - - return &Cache[K, V]{ - maxWeight: maxWeight, - weightFn: weightFn, - onEvict: onEvict, - items: make(map[K]*list.Element), - } -} diff --git a/internal/lru/peek.go b/internal/lru/peek.go deleted file mode 100644 index 8aced931..00000000 --- a/internal/lru/peek.go +++ /dev/null @@ -1,15 +0,0 @@ -package lru - -// Peek returns value for key without changing recency. -// -//nolint:ireturn -func (cache *Cache[K, V]) Peek(key K) (V, bool) { - elem, ok := cache.items[key] - if !ok { - var zero V - - return zero, false - } - //nolint:forcetypeassert - return elem.Value.(*entry[K, V]).value, true -} diff --git a/internal/lru/remove.go b/internal/lru/remove.go deleted file mode 100644 index 3b1f2c93..00000000 --- a/internal/lru/remove.go +++ /dev/null @@ -1,33 +0,0 @@ -package lru - -import "container/list" - -// Remove deletes key from the cache. -// -//nolint:ireturn -func (cache *Cache[K, V]) Remove(key K) (V, bool) { - elem, ok := cache.items[key] - if !ok { - var zero V - - return zero, false - } - - ent := cache.removeElem(elem) - - return ent.value, true -} - -func (cache *Cache[K, V]) removeElem(elem *list.Element) *entry[K, V] { - //nolint:forcetypeassert - ent := elem.Value.(*entry[K, V]) - cache.lru.Remove(elem) - delete(cache.items, ent.key) - - cache.weight -= ent.weight - if cache.onEvict != nil { - cache.onEvict(ent.key, ent.value) - } - - return ent -} diff --git a/internal/lru/weight.go b/internal/lru/weight.go deleted file mode 100644 index 5ef552a1..00000000 --- a/internal/lru/weight.go +++ /dev/null @@ -1,29 +0,0 @@ -package lru - -// WeightFunc reports one entry's weight used for eviction budgeting. -// -// Returned weights MUST be non-negative. -type WeightFunc[K comparable, V any] func(key K, value V) int64 - -// Weight returns the current total weight. -func (cache *Cache[K, V]) Weight() int64 { - return cache.weight -} - -// MaxWeight returns the configured total weight budget. -func (cache *Cache[K, V]) MaxWeight() int64 { - return cache.maxWeight -} - -// SetMaxWeight updates the total weight budget and evicts LRU entries as -// needed. -// -// SetMaxWeight panics if maxWeight is negative. -func (cache *Cache[K, V]) SetMaxWeight(maxWeight int64) { - if maxWeight < 0 { - panic("lru: negative max weight") - } - - cache.maxWeight = maxWeight - cache.evictOverBudget() -} diff --git a/internal/priorityqueue/doc.go b/internal/priorityqueue/doc.go deleted file mode 100644 index 2cdd8522..00000000 --- a/internal/priorityqueue/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package priorityqueue provides a simple priority queue. -package priorityqueue diff --git a/internal/priorityqueue/len.go b/internal/priorityqueue/len.go deleted file mode 100644 index aca8e0ce..00000000 --- a/internal/priorityqueue/len.go +++ /dev/null @@ -1,6 +0,0 @@ -package priorityqueue - -// Len reports the number of queued items. -func (queue *Queue[T]) Len() int { - return len(queue.items) -} diff --git a/internal/priorityqueue/new.go b/internal/priorityqueue/new.go deleted file mode 100644 index bf1c1819..00000000 --- a/internal/priorityqueue/new.go +++ /dev/null @@ -1,6 +0,0 @@ -package priorityqueue - -// New builds one empty priority queue ordered by less. -func New[T any](less func(left, right T) bool) *Queue[T] { - return &Queue[T]{less: less} -} diff --git a/internal/priorityqueue/pop.go b/internal/priorityqueue/pop.go deleted file mode 100644 index 2190b065..00000000 --- a/internal/priorityqueue/pop.go +++ /dev/null @@ -1,21 +0,0 @@ -package priorityqueue - -// Pop removes one highest-priority item. -func (queue *Queue[T]) Pop() (T, bool) { - if len(queue.items) == 0 { - var zero T - - return zero, false - } - - last := len(queue.items) - 1 - top := queue.items[0] - queue.items[0] = queue.items[last] - queue.items = queue.items[:last] - - if len(queue.items) > 0 { - queue.siftDown(0) - } - - return top, true -} diff --git a/internal/priorityqueue/push.go b/internal/priorityqueue/push.go deleted file mode 100644 index d0c6cadd..00000000 --- a/internal/priorityqueue/push.go +++ /dev/null @@ -1,7 +0,0 @@ -package priorityqueue - -// Push inserts one item. -func (queue *Queue[T]) Push(item T) { - queue.items = append(queue.items, item) - queue.siftUp(len(queue.items) - 1) -} diff --git a/internal/priorityqueue/queue.go b/internal/priorityqueue/queue.go deleted file mode 100644 index d57e4791..00000000 --- a/internal/priorityqueue/queue.go +++ /dev/null @@ -1,9 +0,0 @@ -package priorityqueue - -// Queue is one slice-backed priority queue. -// -// Labels: MT-Unsafe. -type Queue[T any] struct { - items []T - less func(left, right T) bool -} diff --git a/internal/priorityqueue/queue_test.go b/internal/priorityqueue/queue_test.go deleted file mode 100644 index f6ab7833..00000000 --- a/internal/priorityqueue/queue_test.go +++ /dev/null @@ -1,36 +0,0 @@ -package priorityqueue_test - -import ( - "slices" - "testing" - - "codeberg.org/lindenii/furgit/internal/priorityqueue" -) - -func TestQueueAscending(t *testing.T) { - t.Parallel() - - queue := priorityqueue.New(func(left, right int) bool { - return left < right - }) - - for _, value := range []int{5, 1, 4, 3, 2} { - queue.Push(value) - } - - var got []int - - for queue.Len() > 0 { - value, ok := queue.Pop() - if !ok { - t.Fatal("Pop should succeed") - } - - got = append(got, value) - } - - want := []int{1, 2, 3, 4, 5} - if !slices.Equal(got, want) { - t.Fatalf("pop order = %v, want %v", got, want) - } -} diff --git a/internal/priorityqueue/sift_down.go b/internal/priorityqueue/sift_down.go deleted file mode 100644 index f14fe93b..00000000 --- a/internal/priorityqueue/sift_down.go +++ /dev/null @@ -1,24 +0,0 @@ -package priorityqueue - -func (queue *Queue[T]) siftDown(idx int) { - for { - left := idx*2 + 1 - if left >= len(queue.items) { - return - } - - best := left - - right := left + 1 - if right < len(queue.items) && queue.less(queue.items[right], queue.items[left]) { - best = right - } - - if !queue.less(queue.items[best], queue.items[idx]) { - return - } - - queue.items[idx], queue.items[best] = queue.items[best], queue.items[idx] - idx = best - } -} diff --git a/internal/priorityqueue/sift_up.go b/internal/priorityqueue/sift_up.go deleted file mode 100644 index 7ff4453f..00000000 --- a/internal/priorityqueue/sift_up.go +++ /dev/null @@ -1,13 +0,0 @@ -package priorityqueue - -func (queue *Queue[T]) siftUp(idx int) { - for idx > 0 { - parent := (idx - 1) / 2 - if !queue.less(queue.items[idx], queue.items[parent]) { - return - } - - queue.items[idx], queue.items[parent] = queue.items[parent], queue.items[idx] - idx = parent - } -} diff --git a/internal/progress/constants.go b/internal/progress/constants.go deleted file mode 100644 index c73adb2e..00000000 --- a/internal/progress/constants.go +++ /dev/null @@ -1,11 +0,0 @@ -package progress - -import "time" - -const ( - // DefaultDelay is the default delayed-progress interval. - DefaultDelay = time.Second - - updateInterval = time.Second - throughputInterval = 500 * time.Millisecond -) diff --git a/internal/progress/consume.go b/internal/progress/consume.go deleted file mode 100644 index fa142f49..00000000 --- a/internal/progress/consume.go +++ /dev/null @@ -1,15 +0,0 @@ -package progress - -import "time" - -func (meter *Meter) consumeUpdateTick(now time.Time) bool { - if now.Before(meter.nextUpdateAt) { - return false - } - - for !now.Before(meter.nextUpdateAt) { - meter.nextUpdateAt = meter.nextUpdateAt.Add(updateInterval) - } - - return true -} diff --git a/internal/progress/counters.go b/internal/progress/counters.go deleted file mode 100644 index 7c7a5085..00000000 --- a/internal/progress/counters.go +++ /dev/null @@ -1,23 +0,0 @@ -package progress - -import ( - "fmt" - - "codeberg.org/lindenii/furgit/internal/intconv" -) - -func (meter *Meter) renderCounters() string { - if meter.total > 0 { - u, err := intconv.Uint64ToInt(meter.lastDone * 100 / meter.total) - if err != nil { - return "overflow" - // TODO - } - - meter.lastPercent = u - - return fmt.Sprintf("%3d%% (%d/%d)%s", meter.lastPercent, meter.lastDone, meter.total, meter.throughputSuffix) - } - - return fmt.Sprintf("%d%s", meter.lastDone, meter.throughputSuffix) -} diff --git a/internal/progress/doc.go b/internal/progress/doc.go deleted file mode 100644 index 964ebdec..00000000 --- a/internal/progress/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package progress supplies meters intended to be used on sideband 2. -package progress diff --git a/internal/progress/humanize.go b/internal/progress/humanize.go deleted file mode 100644 index f13845f7..00000000 --- a/internal/progress/humanize.go +++ /dev/null @@ -1,22 +0,0 @@ -package progress - -import "fmt" - -func humanizeBytes(n uint64) string { - const unit = 1024 - if n < unit { - return fmt.Sprintf("%d B", n) - } - - value := float64(n) - - units := []string{"KiB", "MiB", "GiB", "TiB", "PiB"} - for i := range units { - value /= unit - if value < unit || i == len(units)-1 { - return fmt.Sprintf("%.2f %s", value, units[i]) - } - } - - return fmt.Sprintf("%d B", n) -} diff --git a/internal/progress/meter.go b/internal/progress/meter.go deleted file mode 100644 index bdf0e613..00000000 --- a/internal/progress/meter.go +++ /dev/null @@ -1,30 +0,0 @@ -package progress - -import ( - "time" - - "codeberg.org/lindenii/furgit/common/iowrap" -) - -// Meter renders one in-place progress line. -type Meter struct { - writer iowrap.WriteFlusher - - title string - total uint64 - delay time.Duration - sparse bool - throughput bool - - startedAt time.Time - nextUpdateAt time.Time - nextThroughput time.Time - - lastDone uint64 - lastBytes uint64 - lastPercent int - lastCounterW int - sawValue bool - - throughputSuffix string -} diff --git a/internal/progress/new.go b/internal/progress/new.go deleted file mode 100644 index 2c304279..00000000 --- a/internal/progress/new.go +++ /dev/null @@ -1,21 +0,0 @@ -package progress - -import "time" - -// New creates one progress meter. -func New(opts Options) *Meter { - now := time.Now() - - return &Meter{ - writer: opts.Writer, - title: opts.Title, - total: opts.Total, - delay: max(opts.Delay, time.Duration(0)), - sparse: opts.Sparse, - throughput: opts.Throughput, - startedAt: now, - nextUpdateAt: now.Add(updateInterval), - nextThroughput: now.Add(throughputInterval), - lastPercent: -1, - } -} diff --git a/internal/progress/options.go b/internal/progress/options.go deleted file mode 100644 index 40dd9758..00000000 --- a/internal/progress/options.go +++ /dev/null @@ -1,22 +0,0 @@ -package progress - -import ( - "time" - - "codeberg.org/lindenii/furgit/common/iowrap" -) - -// Options configures one progress meter. -type Options struct { - Writer iowrap.WriteFlusher - - Title string - Total uint64 - - // Delay suppresses progress output until Delay has elapsed since Start. - Delay time.Duration - // Sparse forces one final 100% line at Stop when the caller sampled updates. - Sparse bool - // Throughput appends ", | /s" and refreshes rate every 500ms. - Throughput bool -} diff --git a/internal/progress/refresh.go b/internal/progress/refresh.go deleted file mode 100644 index ed1782db..00000000 --- a/internal/progress/refresh.go +++ /dev/null @@ -1,25 +0,0 @@ -package progress - -import "time" - -func (meter *Meter) refreshThroughput(now time.Time) { - if !meter.throughput { - return - } - - if meter.nextThroughput.After(now) && meter.throughputSuffix != "" { - return - } - - for !now.Before(meter.nextThroughput) { - meter.nextThroughput = meter.nextThroughput.Add(throughputInterval) - } - - elapsed := now.Sub(meter.startedAt) - if elapsed <= 0 { - return - } - - rate := uint64(float64(meter.lastBytes) / elapsed.Seconds()) - meter.throughputSuffix = ", " + humanizeBytes(meter.lastBytes) + " | " + humanizeBytes(rate) + "/s" -} diff --git a/internal/progress/render.go b/internal/progress/render.go deleted file mode 100644 index ae188c0e..00000000 --- a/internal/progress/render.go +++ /dev/null @@ -1,38 +0,0 @@ -package progress - -import ( - "strings" - "time" - - "codeberg.org/lindenii/furgit/internal/utils" -) - -func (meter *Meter) render(now time.Time, eol string) { - if meter.delay > 0 && now.Sub(meter.startedAt) < meter.delay && eol == "\r" { - return - } - - meter.refreshThroughput(now) - - counters := meter.renderCounters() - - clear1 := 0 - if len(counters) < meter.lastCounterW { - clear1 = meter.lastCounterW - len(counters) + 1 - } - - meter.lastCounterW = len(counters) - - line := meter.title + ": " + counters - if clear1 > 0 { - line += strings.Repeat(" ", clear1) - } - - line += eol - - utils.BestEffortFprintf(meter.writer, "%s", line) - - if meter.writer != nil { - _ = meter.writer.Flush() - } -} diff --git a/internal/progress/set.go b/internal/progress/set.go deleted file mode 100644 index 06cf889d..00000000 --- a/internal/progress/set.go +++ /dev/null @@ -1,39 +0,0 @@ -package progress - -import ( - "time" - - "codeberg.org/lindenii/furgit/internal/intconv" -) - -// Set records current progress and renders when percent changed or the 1s tick -// elapsed. -func (meter *Meter) Set(done uint64, bytes uint64) { - meter.lastDone = done - meter.lastBytes = bytes - meter.sawValue = true - - if meter.writer == nil { - return - } - - now := time.Now() - forced := meter.consumeUpdateTick(now) - - percentChanged := false - - if meter.total > 0 { - percent, err := intconv.Uint64ToInt(done * 100 / meter.total) - if err != nil { - return // TODO - } - - percentChanged = percent != meter.lastPercent - } - - if !percentChanged && !forced { - return - } - - meter.render(now, "\r") -} diff --git a/internal/progress/stop.go b/internal/progress/stop.go deleted file mode 100644 index fdc3f9af..00000000 --- a/internal/progress/stop.go +++ /dev/null @@ -1,20 +0,0 @@ -package progress - -import "time" - -// Stop forces the final progress line and appends ", .". -func (meter *Meter) Stop(msg string) { - if !meter.sawValue || meter.writer == nil { - return - } - - if msg == "" { - msg = "done" - } - - if meter.sparse && meter.total > 0 && meter.lastDone != meter.total { - meter.lastDone = meter.total - } - - meter.render(time.Now(), ", "+msg+".\n") -} diff --git a/internal/testgit/algorithms.go b/internal/testgit/algorithms.go deleted file mode 100644 index aea4dc12..00000000 --- a/internal/testgit/algorithms.go +++ /dev/null @@ -1,18 +0,0 @@ -package testgit - -import ( - "testing" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// ForEachAlgorithm runs a subtest for every supported algorithm. -func ForEachAlgorithm(t *testing.T, fn func(t *testing.T, algo objectid.Algorithm)) { - t.Helper() - - for _, algo := range objectid.SupportedAlgorithms() { - t.Run(algo.String(), func(t *testing.T) { - fn(t, algo) - }) - } -} diff --git a/internal/testgit/repo.go b/internal/testgit/repo.go deleted file mode 100644 index 72831bd6..00000000 --- a/internal/testgit/repo.go +++ /dev/null @@ -1,11 +0,0 @@ -// Package testgit provides helpers for integration tests with upstream git(1). -package testgit - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// TestRepo is a temporary git repository harness for integration tests. -type TestRepo struct { - dir string - algo objectid.Algorithm - env []string -} diff --git a/internal/testgit/repo_cat_file.go b/internal/testgit/repo_cat_file.go deleted file mode 100644 index 7dbd2c43..00000000 --- a/internal/testgit/repo_cat_file.go +++ /dev/null @@ -1,14 +0,0 @@ -package testgit - -import ( - "testing" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// CatFile returns raw output from git cat-file. -func (testRepo *TestRepo) CatFile(tb testing.TB, mode string, id objectid.ObjectID) []byte { - tb.Helper() - - return testRepo.RunBytes(tb, "cat-file", mode, id.String()) -} diff --git a/internal/testgit/repo_commit_graph_write.go b/internal/testgit/repo_commit_graph_write.go deleted file mode 100644 index 13221f87..00000000 --- a/internal/testgit/repo_commit_graph_write.go +++ /dev/null @@ -1,13 +0,0 @@ -package testgit - -import "testing" - -// CommitGraphWrite runs "git commit-graph write" with args in the repository. -func (testRepo *TestRepo) CommitGraphWrite(tb testing.TB, args ...string) { - tb.Helper() - - cmdArgs := make([]string, 0, len(args)+3) - cmdArgs = append(cmdArgs, "commit-graph", "write") - cmdArgs = append(cmdArgs, args...) - _ = testRepo.Run(tb, cmdArgs...) -} diff --git a/internal/testgit/repo_commit_tree.go b/internal/testgit/repo_commit_tree.go deleted file mode 100644 index 3a5a75ac..00000000 --- a/internal/testgit/repo_commit_tree.go +++ /dev/null @@ -1,29 +0,0 @@ -package testgit - -import ( - "testing" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// CommitTree creates a commit from a tree and message, optionally with parents. -func (testRepo *TestRepo) CommitTree(tb testing.TB, tree objectid.ObjectID, message string, parents ...objectid.ObjectID) objectid.ObjectID { - tb.Helper() - - args := make([]string, 0, 2+2*len(parents)+2) - - args = append(args, "commit-tree", tree.String()) - for _, p := range parents { - args = append(args, "-p", p.String()) - } - - args = append(args, "-m", message) - hex := testRepo.Run(tb, args...) - - id, err := objectid.ParseHex(testRepo.algo, hex) - if err != nil { - tb.Fatalf("parse commit-tree output %q: %v", hex, err) - } - - return id -} diff --git a/internal/testgit/repo_commit_tree_env.go b/internal/testgit/repo_commit_tree_env.go deleted file mode 100644 index fbddf26f..00000000 --- a/internal/testgit/repo_commit_tree_env.go +++ /dev/null @@ -1,51 +0,0 @@ -package testgit - -import ( - "slices" - "strings" - "testing" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// CommitTreeWithEnv creates one commit from a tree and message, optionally with -// parents, using additional environment variables for the git subprocess. -func (testRepo *TestRepo) CommitTreeWithEnv( - tb testing.TB, - extraEnv []string, - tree objectid.ObjectID, - message string, - parents ...objectid.ObjectID, -) objectid.ObjectID { - tb.Helper() - - args := make([]string, 0, 2+2*len(parents)+2) - - args = append(args, "commit-tree", tree.String()) - for _, parent := range parents { - args = append(args, "-p", parent.String()) - } - - args = append(args, "-m", message) - hex := testRepo.runWithExtraEnv(tb, extraEnv, args...) - - id, err := objectid.ParseHex(testRepo.algo, hex) - if err != nil { - tb.Fatalf("parse commit-tree output %q: %v", hex, err) - } - - return id -} - -func (testRepo *TestRepo) runWithExtraEnv(tb testing.TB, extraEnv []string, args ...string) string { - tb.Helper() - - env := slices.Concat(testRepo.env, extraEnv) - - out, err := testRepo.runBytesWithEnv(tb, nil, testRepo.dir, env, args...) - if err != nil { - tb.Fatalf("git %v failed: %v\n%s", args, err, out) - } - - return strings.TrimSpace(string(out)) -} diff --git a/internal/testgit/repo_from_fixture.go b/internal/testgit/repo_from_fixture.go deleted file mode 100644 index 632de12a..00000000 --- a/internal/testgit/repo_from_fixture.go +++ /dev/null @@ -1,36 +0,0 @@ -package testgit - -import ( - "io/fs" - "os" - "testing" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// NewRepoFromFixture copies one existing repository fixture into a temp dir. -func NewRepoFromFixture(tb testing.TB, algo objectid.Algorithm, fixtureDir string) *TestRepo { - tb.Helper() - - if algo.Size() == 0 { - tb.Fatalf("invalid algorithm: %v", algo) - } - - dst := tb.TempDir() - srcFS := os.DirFS(fixtureDir) - - err := copyFS(dst, srcFS) - if err != nil { - tb.Fatalf("copy fixture %q: %v", fixtureDir, err) - } - - return &TestRepo{ - dir: dst, - algo: algo, - env: defaultEnv(), - } -} - -func copyFS(dst string, src fs.FS) error { - return os.CopyFS(dst, src) -} diff --git a/internal/testgit/repo_fs.go b/internal/testgit/repo_fs.go deleted file mode 100644 index 56acbfcf..00000000 --- a/internal/testgit/repo_fs.go +++ /dev/null @@ -1,86 +0,0 @@ -package testgit - -import ( - "os" - "path/filepath" - "testing" -) - -// OpenFile opens one file relative to the repository root. -func (testRepo *TestRepo) OpenFile(tb testing.TB, name string) *os.File { - tb.Helper() - - root := testRepo.OpenRoot(tb) - - file, err := root.Open(name) - if err != nil { - tb.Fatalf("Open(%q): %v", name, err) - } - - return file -} - -// ReadFile reads one file relative to the repository root. -func (testRepo *TestRepo) ReadFile(tb testing.TB, name string) []byte { - tb.Helper() - - root := testRepo.OpenRoot(tb) - - data, err := root.ReadFile(name) - if err != nil { - tb.Fatalf("ReadFile(%q): %v", name, err) - } - - return data -} - -// WriteFile writes one file relative to the repository root. -func (testRepo *TestRepo) WriteFile(tb testing.TB, name string, data []byte, perm os.FileMode) { - tb.Helper() - - root := testRepo.OpenRoot(tb) - - err := root.WriteFile(name, data, perm) - if err != nil { - tb.Fatalf("WriteFile(%q): %v", name, err) - } -} - -// WriteFileAll writes one file relative to the repository root, creating any -// missing parent directories first. -func (testRepo *TestRepo) WriteFileAll( - tb testing.TB, - name string, - data []byte, - dirPerm os.FileMode, - filePerm os.FileMode, -) { - tb.Helper() - - root := testRepo.OpenRoot(tb) - - dir := filepath.Dir(name) - if dir != "." { - err := root.MkdirAll(dir, dirPerm) - if err != nil { - tb.Fatalf("MkdirAll(%q): %v", dir, err) - } - } - - err := root.WriteFile(name, data, filePerm) - if err != nil { - tb.Fatalf("WriteFile(%q): %v", name, err) - } -} - -// Remove removes one path relative to the repository root. -func (testRepo *TestRepo) Remove(tb testing.TB, name string) { - tb.Helper() - - root := testRepo.OpenRoot(tb) - - err := root.Remove(name) - if err != nil { - tb.Fatalf("Remove(%q): %v", name, err) - } -} diff --git a/internal/testgit/repo_hash_object.go b/internal/testgit/repo_hash_object.go deleted file mode 100644 index 75f1a7ab..00000000 --- a/internal/testgit/repo_hash_object.go +++ /dev/null @@ -1,20 +0,0 @@ -package testgit - -import ( - "testing" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// HashObject hashes and writes an object and returns its object ID. -func (testRepo *TestRepo) HashObject(tb testing.TB, objType string, body []byte) objectid.ObjectID { - tb.Helper() - hex := testRepo.RunInput(tb, body, "hash-object", "-t", objType, "-w", "--stdin") - - id, err := objectid.ParseHex(testRepo.algo, hex) - if err != nil { - tb.Fatalf("parse git hash-object output %q: %v", hex, err) - } - - return id -} diff --git a/internal/testgit/repo_make_commit.go b/internal/testgit/repo_make_commit.go deleted file mode 100644 index 32a063f7..00000000 --- a/internal/testgit/repo_make_commit.go +++ /dev/null @@ -1,16 +0,0 @@ -package testgit - -import ( - "testing" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// MakeCommit creates a commit over a single-file tree and returns (blobID, treeID, commitID). -func (testRepo *TestRepo) MakeCommit(tb testing.TB, message string) (objectid.ObjectID, objectid.ObjectID, objectid.ObjectID) { - tb.Helper() - blobID, treeID := testRepo.MakeSingleFileTree(tb, "file.txt", []byte("commit-body\n")) - commitID := testRepo.CommitTree(tb, treeID, message) - - return blobID, treeID, commitID -} diff --git a/internal/testgit/repo_make_many_objects_history.go b/internal/testgit/repo_make_many_objects_history.go deleted file mode 100644 index f71ead2c..00000000 --- a/internal/testgit/repo_make_many_objects_history.go +++ /dev/null @@ -1,83 +0,0 @@ -package testgit - -import ( - "fmt" - "strings" - "testing" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -const ( - manyObjectsMainCommits = 640 - manyObjectsDevCommits = 220 -) - -// MakeManyObjectsHistory creates a large commit graph. -func (testRepo *TestRepo) MakeManyObjectsHistory(tb testing.TB) { - tb.Helper() - - var ( - mainTip objectid.ObjectID - devTip objectid.ObjectID - hasMain bool - hasDev bool - ) - - for i := range manyObjectsMainCommits { - tree := testRepo.makeManyObjectsTree(tb, "main", i, 3) - - var commit objectid.ObjectID - if hasMain { - commit = testRepo.CommitTree(tb, tree, fmt.Sprintf("main-%04d", i), mainTip) - } else { - commit = testRepo.CommitTree(tb, tree, fmt.Sprintf("main-%04d", i)) - hasMain = true - } - - mainTip = commit - if i%64 == 0 { - testRepo.TagAnnotated(tb, fmt.Sprintf("main-v%04d", i), mainTip, fmt.Sprintf("tag-main-%04d", i)) - } - } - - devTip = mainTip - hasDev = true - - for i := range manyObjectsDevCommits { - tree := testRepo.makeManyObjectsTree(tb, "dev", i, 4) - commit := testRepo.CommitTree(tb, tree, fmt.Sprintf("dev-%04d", i), devTip) - devTip = commit - - if i > 0 && i%55 == 0 { - mergeTree := testRepo.makeManyObjectsTree(tb, "merge", i, 2) - - mainTip = testRepo.CommitTree(tb, mergeTree, fmt.Sprintf("merge-%04d", i), mainTip, devTip) - if i%110 == 0 { - testRepo.TagAnnotated(tb, fmt.Sprintf("merge-v%04d", i), mainTip, fmt.Sprintf("tag-merge-%04d", i)) - } - } - } - - if hasMain { - testRepo.UpdateRef(tb, "refs/heads/main", mainTip) - } - - if hasDev { - testRepo.UpdateRef(tb, "refs/heads/dev", devTip) - } -} - -// makeManyObjectsTree builds one synthetic tree with fanout blobs. -func (testRepo *TestRepo) makeManyObjectsTree(tb testing.TB, prefix string, i int, files int) objectid.ObjectID { - tb.Helper() - - lines := make([]string, 0, files) - for j := range files { - body := fmt.Appendf(nil, "%s-%04d-%02d\n%s\n", prefix, i, j, strings.Repeat("x", 160+(i+j)%96)) - blobID := testRepo.HashObject(tb, "blob", body) - lines = append(lines, fmt.Sprintf("100644 blob %s\t%s_%04d_%02d.txt\n", blobID.String(), prefix, i, j)) - } - - return testRepo.Mktree(tb, strings.Join(lines, "")) -} diff --git a/internal/testgit/repo_make_single_file_tree.go b/internal/testgit/repo_make_single_file_tree.go deleted file mode 100644 index ace98e8a..00000000 --- a/internal/testgit/repo_make_single_file_tree.go +++ /dev/null @@ -1,18 +0,0 @@ -package testgit - -import ( - "fmt" - "testing" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// MakeSingleFileTree writes one blob and one tree entry for it and returns (blobID, treeID). -func (testRepo *TestRepo) MakeSingleFileTree(tb testing.TB, fileName string, fileContent []byte) (objectid.ObjectID, objectid.ObjectID) { - tb.Helper() - blobID := testRepo.HashObject(tb, "blob", fileContent) - treeInput := fmt.Sprintf("100644 blob %s\t%s\n", blobID.String(), fileName) - treeID := testRepo.Mktree(tb, treeInput) - - return blobID, treeID -} diff --git a/internal/testgit/repo_mktree.go b/internal/testgit/repo_mktree.go deleted file mode 100644 index 893d211e..00000000 --- a/internal/testgit/repo_mktree.go +++ /dev/null @@ -1,20 +0,0 @@ -package testgit - -import ( - "testing" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// Mktree creates a tree from textual mktree input and returns its ID. -func (testRepo *TestRepo) Mktree(tb testing.TB, input string) objectid.ObjectID { - tb.Helper() - hex := testRepo.RunInput(tb, []byte(input), "mktree") - - id, err := objectid.ParseHex(testRepo.algo, hex) - if err != nil { - tb.Fatalf("parse mktree output %q: %v", hex, err) - } - - return id -} diff --git a/internal/testgit/repo_new.go b/internal/testgit/repo_new.go deleted file mode 100644 index b7c9968b..00000000 --- a/internal/testgit/repo_new.go +++ /dev/null @@ -1,64 +0,0 @@ -package testgit - -import ( - "os" - "testing" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// RepoOptions controls git-init options for test repositories. -type RepoOptions struct { - // ObjectFormat is the object ID algorithm used for repository objects. - ObjectFormat objectid.Algorithm - // Bare selects whether the repository is initialized as bare. - Bare bool - // RefFormat selects the git ref storage format (for example "files" or - // "reftable"). Empty means git's default format. - RefFormat string -} - -// NewRepo creates a temporary repository initialized with the requested options. -func NewRepo(tb testing.TB, opts RepoOptions) *TestRepo { - tb.Helper() - - algo := opts.ObjectFormat - if algo.Size() == 0 { - tb.Fatalf("invalid algorithm: %v", algo) - } - - dir := tb.TempDir() - - testRepo := &TestRepo{ - dir: dir, - algo: algo, - env: defaultEnv(), - } - - args := []string{"init", "--object-format=" + algo.String()} - if opts.Bare { - args = append(args, "--bare") - } - - if opts.RefFormat != "" { - args = append(args, "--ref-format="+opts.RefFormat) - } - - args = append(args, dir) - testRepo.runBytes(tb, nil, "", args...) - - return testRepo -} - -func defaultEnv() []string { - return append(os.Environ(), - "GIT_CONFIG_GLOBAL=/dev/null", - "GIT_CONFIG_SYSTEM=/dev/null", - "GIT_AUTHOR_NAME=Test Author", - "GIT_AUTHOR_EMAIL=test@example.org", - "GIT_COMMITTER_NAME=Test Committer", - "GIT_COMMITTER_EMAIL=committer@example.org", - "GIT_AUTHOR_DATE=1234567890 +0000", - "GIT_COMMITTER_DATE=1234567890 +0000", - ) -} diff --git a/internal/testgit/repo_open_commit_graph.go b/internal/testgit/repo_open_commit_graph.go deleted file mode 100644 index 4db7261b..00000000 --- a/internal/testgit/repo_open_commit_graph.go +++ /dev/null @@ -1,26 +0,0 @@ -package testgit - -import ( - "testing" - - commitgraphread "codeberg.org/lindenii/furgit/format/commitgraph/read" -) - -// OpenCommitGraph opens the repository commit-graph and registers cleanup on -// the caller. -func (testRepo *TestRepo) OpenCommitGraph(tb testing.TB) *commitgraphread.Reader { - tb.Helper() - - objectsRoot := testRepo.OpenObjectsRoot(tb) - - graph, err := commitgraphread.Open(objectsRoot, testRepo.Algorithm(), commitgraphread.OpenSingle) - if err != nil { - tb.Fatalf("commitgraphread.Open: %v", err) - } - - tb.Cleanup(func() { - _ = graph.Close() - }) - - return graph -} diff --git a/internal/testgit/repo_open_object_store.go b/internal/testgit/repo_open_object_store.go deleted file mode 100644 index 42dc370c..00000000 --- a/internal/testgit/repo_open_object_store.go +++ /dev/null @@ -1,29 +0,0 @@ -package testgit - -import ( - "testing" - - objectstore "codeberg.org/lindenii/furgit/object/store" - "codeberg.org/lindenii/furgit/repository" -) - -// OpenObjectStore opens the repository object store and registers cleanup on -// the caller. -// -//nolint:ireturn -func (testRepo *TestRepo) OpenObjectStore(tb testing.TB) objectstore.Reader { - tb.Helper() - - root := testRepo.OpenGitRoot(tb) - - repo, err := repository.Open(root) - if err != nil { - tb.Fatalf("repository.Open: %v", err) - } - - tb.Cleanup(func() { - _ = repo.Close() - }) - - return repo.Objects() -} diff --git a/internal/testgit/repo_open_repository.go b/internal/testgit/repo_open_repository.go deleted file mode 100644 index fbc98383..00000000 --- a/internal/testgit/repo_open_repository.go +++ /dev/null @@ -1,25 +0,0 @@ -package testgit - -import ( - "testing" - - "codeberg.org/lindenii/furgit/repository" -) - -// OpenRepository opens the repository and registers cleanup on the caller. -func (testRepo *TestRepo) OpenRepository(tb testing.TB) *repository.Repository { - tb.Helper() - - root := testRepo.OpenGitRoot(tb) - - repo, err := repository.Open(root) - if err != nil { - tb.Fatalf("repository.Open: %v", err) - } - - tb.Cleanup(func() { - _ = repo.Close() - }) - - return repo -} diff --git a/internal/testgit/repo_open_root.go b/internal/testgit/repo_open_root.go deleted file mode 100644 index 4530c604..00000000 --- a/internal/testgit/repo_open_root.go +++ /dev/null @@ -1,87 +0,0 @@ -package testgit - -import ( - "errors" - "os" - "testing" -) - -// OpenRoot opens the repository root directory and registers cleanup on the -// caller. -func (testRepo *TestRepo) OpenRoot(tb testing.TB) *os.Root { - tb.Helper() - - root, err := os.OpenRoot(testRepo.dir) - if err != nil { - tb.Fatalf("os.OpenRoot: %v", err) - } - - tb.Cleanup(func() { - _ = root.Close() - }) - - return root -} - -// OpenGitRoot opens the repository gitdir and registers cleanup on the caller. -// -// For bare repositories, this is the repository root itself. For non-bare -// repositories, this is the .git directory under the worktree root. -func (testRepo *TestRepo) OpenGitRoot(tb testing.TB) *os.Root { - tb.Helper() - - repoRoot := testRepo.OpenRoot(tb) - - gitRoot, err := repoRoot.OpenRoot(".git") - if err == nil { - tb.Cleanup(func() { - _ = gitRoot.Close() - }) - - return gitRoot - } - - if !errors.Is(err, os.ErrNotExist) { - tb.Fatalf("OpenRoot(.git): %v", err) - } - - return repoRoot -} - -// OpenObjectsRoot opens the objects directory and registers cleanup on the -// caller. -func (testRepo *TestRepo) OpenObjectsRoot(tb testing.TB) *os.Root { - tb.Helper() - - gitRoot := testRepo.OpenGitRoot(tb) - - objectsRoot, err := gitRoot.OpenRoot("objects") - if err != nil { - tb.Fatalf("OpenRoot(objects): %v", err) - } - - tb.Cleanup(func() { - _ = objectsRoot.Close() - }) - - return objectsRoot -} - -// OpenPackRoot opens the objects/pack directory and registers cleanup on the -// caller. -func (testRepo *TestRepo) OpenPackRoot(tb testing.TB) *os.Root { - tb.Helper() - - objectsRoot := testRepo.OpenObjectsRoot(tb) - - packRoot, err := objectsRoot.OpenRoot("pack") - if err != nil { - tb.Fatalf("OpenRoot(pack): %v", err) - } - - tb.Cleanup(func() { - _ = packRoot.Close() - }) - - return packRoot -} diff --git a/internal/testgit/repo_pack_objects_is_thin.go b/internal/testgit/repo_pack_objects_is_thin.go deleted file mode 100644 index c37b6d27..00000000 --- a/internal/testgit/repo_pack_objects_is_thin.go +++ /dev/null @@ -1,77 +0,0 @@ -package testgit - -import ( - "os/exec" - "strings" - "testing" -) - -// PackObjectsIsThin reports whether git emits one thin pack for the given revs. -// -// It streams `git pack-objects --stdout --revs --thin` into `git index-pack -// --stdin` in one scratch bare repository. A failure in index-pack due to -// unresolved deltas is treated as confirmation that the emitted pack is thin. -func (testRepo *TestRepo) PackObjectsIsThin(tb testing.TB, revs []string) bool { - tb.Helper() - - scratch := NewRepo(tb, RepoOptions{ObjectFormat: testRepo.algo, Bare: true}) - - packArgs := []string{"pack-objects", "--stdout", "--revs", "--thin"} - //nolint:noctx - packCmd := exec.Command("git", packArgs...) //#nosec G204 - packCmd.Dir = testRepo.dir - packCmd.Env = testRepo.env - packCmd.Stdin = strings.NewReader(strings.Join(revs, "\n") + "\n") - packStderr := &strings.Builder{} - packCmd.Stderr = packStderr - - packStdout, err := packCmd.StdoutPipe() - if err != nil { - tb.Fatalf("git %v stdout pipe: %v", packArgs, err) - } - - indexArgs := []string{"index-pack", "--stdin"} - //nolint:noctx - indexCmd := exec.Command("git", indexArgs...) //#nosec G204 - indexCmd.Dir = scratch.dir - indexCmd.Env = scratch.env - indexCmd.Stdin = packStdout - indexStderr := &strings.Builder{} - indexCmd.Stderr = indexStderr - - err = indexCmd.Start() - if err != nil { - tb.Fatalf("git %v start failed: %v", indexArgs, err) - } - - err = packCmd.Start() - if err != nil { - _ = indexCmd.Process.Kill() - _ = indexCmd.Wait() - - tb.Fatalf("git %v start failed: %v", packArgs, err) - } - - packErr := packCmd.Wait() - if packErr != nil { - tb.Fatalf("git %v failed: %v\n%s", packArgs, packErr, packStderr.String()) - } - - indexErr := indexCmd.Wait() - if indexErr == nil { - return false - } - - stderr := strings.ToLower(indexStderr.String()) - if strings.Contains(stderr, "unresolved") && strings.Contains(stderr, "delta") { - return true - } - - if strings.Contains(stderr, "missing") && strings.Contains(stderr, "base") { - return true - } - - tb.Fatalf("git %v failed unexpectedly: %v\n%s", indexArgs, indexErr, indexStderr.String()) - - return false -} diff --git a/internal/testgit/repo_pack_objects_reader.go b/internal/testgit/repo_pack_objects_reader.go deleted file mode 100644 index dc997514..00000000 --- a/internal/testgit/repo_pack_objects_reader.go +++ /dev/null @@ -1,94 +0,0 @@ -package testgit - -import ( - "fmt" - "io" - "os/exec" - "strings" - "sync" - "testing" -) - -// packObjectsReadCloser wraps a pipe reader and process wait fn. -type packObjectsReadCloser struct { - reader io.ReadCloser - wait func() error - once sync.Once -} - -// Read proxies reads to the wrapped reader. -func (reader *packObjectsReadCloser) Read(dst []byte) (int, error) { - return reader.reader.Read(dst) -} - -// Close closes the stream and waits for the underlying process. -func (reader *packObjectsReadCloser) Close() error { - var out error - - reader.once.Do(func() { - errClose := reader.reader.Close() - errWait := reader.wait() - - if errClose != nil { - out = errClose - - return - } - - out = errWait - }) - - return out -} - -// PackObjectsReader streams `git pack-objects --stdout --revs` output. -func (testRepo *TestRepo) PackObjectsReader(tb testing.TB, revs []string, thin bool) io.ReadCloser { - tb.Helper() - - args := []string{"pack-objects", "--stdout", "--revs"} - if thin { - args = append(args, "--thin") - } - - //nolint:noctx - cmd := exec.Command("git", args...) //#nosec G204 - cmd.Dir = testRepo.dir - cmd.Env = testRepo.env - cmd.Stdin = strings.NewReader(strings.Join(revs, "\n") + "\n") - - pr, pw := io.Pipe() - cmd.Stdout = pw - stderr := &strings.Builder{} - cmd.Stderr = stderr - - waitDone := make(chan error, 1) - - go func() { - err := cmd.Start() - if err != nil { - _ = pw.CloseWithError(fmt.Errorf("git %v start failed: %w", args, err)) - - waitDone <- nil - - return - } - - err = cmd.Wait() - if err != nil { - _ = pw.CloseWithError(fmt.Errorf("git %v failed: %w\n%s", args, err, stderr.String())) - } else { - _ = pw.Close() - } - - waitDone <- nil - }() - - return &packObjectsReadCloser{ - reader: pr, - wait: func() error { - <-waitDone - - return nil - }, - } -} diff --git a/internal/testgit/repo_properties.go b/internal/testgit/repo_properties.go deleted file mode 100644 index d46e9030..00000000 --- a/internal/testgit/repo_properties.go +++ /dev/null @@ -1,20 +0,0 @@ -package testgit - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// Algorithm returns the object ID algorithm configured for this repository. -func (testRepo *TestRepo) Algorithm() objectid.Algorithm { - return testRepo.algo -} - -// Env returns a copy of the environment used for git subprocesses. -func (testRepo *TestRepo) Env() []string { - return append([]string(nil), testRepo.env...) -} - -// DirButYouShouldReallyNotUseThisYouShouldReallyConsiderUsingAProperCapabilityInterfaceInsteadAndIAmKeepingThisMethodIntentionallyLongToAnnoyYou returns the git dir of a repo. -// Consider using a properly capability interface such as -// os.Root instead; all uses of ambient path authority must be justified. -func (testRepo *TestRepo) DirButYouShouldReallyNotUseThisYouShouldReallyConsiderUsingAProperCapabilityInterfaceInsteadAndIAmKeepingThisMethodIntentionallyLongToAnnoyYou() string { - return testRepo.dir -} diff --git a/internal/testgit/repo_refs.go b/internal/testgit/repo_refs.go deleted file mode 100644 index a92e1658..00000000 --- a/internal/testgit/repo_refs.go +++ /dev/null @@ -1,48 +0,0 @@ -package testgit - -import ( - "strings" - "testing" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// UpdateRef updates a ref to point at id. -func (testRepo *TestRepo) UpdateRef(tb testing.TB, name string, id objectid.ObjectID) { - tb.Helper() - testRepo.Run(tb, "update-ref", name, id.String()) -} - -// DeleteRef deletes a ref. -func (testRepo *TestRepo) DeleteRef(tb testing.TB, name string) { - tb.Helper() - testRepo.Run(tb, "update-ref", "-d", name) -} - -// SymbolicRef sets a symbolic reference target. -func (testRepo *TestRepo) SymbolicRef(tb testing.TB, name, target string) { - tb.Helper() - testRepo.Run(tb, "symbolic-ref", name, target) -} - -// PackRefs runs git pack-refs with args. -func (testRepo *TestRepo) PackRefs(tb testing.TB, args ...string) { - tb.Helper() - - cmd := append([]string{"pack-refs"}, args...) - testRepo.Run(tb, cmd...) -} - -// ShowRef returns lines from git show-ref output. -func (testRepo *TestRepo) ShowRef(tb testing.TB, args ...string) []string { - tb.Helper() - - cmd := append([]string{"show-ref"}, args...) - - out := testRepo.Run(tb, cmd...) - if strings.TrimSpace(out) == "" { - return nil - } - - return strings.Split(strings.TrimSpace(out), "\n") -} diff --git a/internal/testgit/repo_remove_loose_object.go b/internal/testgit/repo_remove_loose_object.go deleted file mode 100644 index 345d9db7..00000000 --- a/internal/testgit/repo_remove_loose_object.go +++ /dev/null @@ -1,22 +0,0 @@ -package testgit - -import ( - "fmt" - "testing" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// RemoveLooseObject removes one loose object file from the repository. -func (testRepo *TestRepo) RemoveLooseObject(tb testing.TB, id objectid.ObjectID) { - tb.Helper() - - root := testRepo.OpenObjectsRoot(tb) - hex := id.String() - path := fmt.Sprintf("%s/%s", hex[:2], hex[2:]) - - err := root.Remove(path) - if err != nil { - tb.Fatalf("remove loose object %s: %v", id, err) - } -} diff --git a/internal/testgit/repo_repack.go b/internal/testgit/repo_repack.go deleted file mode 100644 index 7773ac13..00000000 --- a/internal/testgit/repo_repack.go +++ /dev/null @@ -1,13 +0,0 @@ -package testgit - -import "testing" - -// Repack runs "git repack" with args in the repository. -func (testRepo *TestRepo) Repack(tb testing.TB, args ...string) { - tb.Helper() - - cmdArgs := make([]string, 0, len(args)+1) - cmdArgs = append(cmdArgs, "repack") - cmdArgs = append(cmdArgs, args...) - _ = testRepo.Run(tb, cmdArgs...) -} diff --git a/internal/testgit/repo_rev_list.go b/internal/testgit/repo_rev_list.go deleted file mode 100644 index 6b0c4f76..00000000 --- a/internal/testgit/repo_rev_list.go +++ /dev/null @@ -1,37 +0,0 @@ -package testgit - -import ( - "strings" - "testing" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// RevList runs "git rev-list" with args and parses one object ID per line. -func (testRepo *TestRepo) RevList(tb testing.TB, args ...string) []objectid.ObjectID { - tb.Helper() - - cmdArgs := make([]string, 0, len(args)+1) - cmdArgs = append(cmdArgs, "rev-list") - cmdArgs = append(cmdArgs, args...) - out := testRepo.Run(tb, cmdArgs...) - - lines := strings.Split(strings.TrimSpace(out), "\n") - - outIDs := make([]objectid.ObjectID, 0, len(lines)) - for _, line := range lines { - line = strings.TrimSpace(line) - if line == "" { - continue - } - - id, err := objectid.ParseHex(testRepo.algo, line) - if err != nil { - tb.Fatalf("parse rev-list oid %q: %v", line, err) - } - - outIDs = append(outIDs, id) - } - - return outIDs -} diff --git a/internal/testgit/repo_rev_parse.go b/internal/testgit/repo_rev_parse.go deleted file mode 100644 index fcdee605..00000000 --- a/internal/testgit/repo_rev_parse.go +++ /dev/null @@ -1,20 +0,0 @@ -package testgit - -import ( - "testing" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// RevParse resolves rev expressions to object IDs. -func (testRepo *TestRepo) RevParse(tb testing.TB, spec string) objectid.ObjectID { - tb.Helper() - hex := testRepo.Run(tb, "rev-parse", spec) - - id, err := objectid.ParseHex(testRepo.algo, hex) - if err != nil { - tb.Fatalf("parse rev-parse output %q: %v", hex, err) - } - - return id -} diff --git a/internal/testgit/repo_run.go b/internal/testgit/repo_run.go deleted file mode 100644 index 448b88f0..00000000 --- a/internal/testgit/repo_run.go +++ /dev/null @@ -1,95 +0,0 @@ -package testgit - -import ( - "bytes" - "os/exec" - "strings" - "testing" -) - -// Run executes git and returns trimmed textual output. -func (testRepo *TestRepo) Run(tb testing.TB, args ...string) string { - tb.Helper() - out := testRepo.runBytes(tb, nil, testRepo.dir, args...) - - return strings.TrimSpace(string(out)) -} - -// RunBytes executes git and returns raw output bytes. -func (testRepo *TestRepo) RunBytes(tb testing.TB, args ...string) []byte { - tb.Helper() - - return testRepo.runBytes(tb, nil, testRepo.dir, args...) -} - -// RunE executes git and returns trimmed textual output plus any command error. -func (testRepo *TestRepo) RunE(tb testing.TB, args ...string) (string, error) { - tb.Helper() - - out, err := testRepo.runBytesE(nil, testRepo.dir, args...) - - return strings.TrimSpace(string(out)), err -} - -// RunInput executes git with stdin and returns trimmed textual output. -func (testRepo *TestRepo) RunInput(tb testing.TB, stdin []byte, args ...string) string { - tb.Helper() - out := testRepo.runBytes(tb, stdin, testRepo.dir, args...) - - return strings.TrimSpace(string(out)) -} - -// RunInputBytes executes git with stdin and returns raw output bytes. -func (testRepo *TestRepo) RunInputBytes(tb testing.TB, stdin []byte, args ...string) []byte { - tb.Helper() - - return testRepo.runBytes(tb, stdin, testRepo.dir, args...) -} - -func (testRepo *TestRepo) runBytes(tb testing.TB, stdin []byte, dir string, args ...string) []byte { - tb.Helper() - - out, err := testRepo.runBytesE(stdin, dir, args...) - if err != nil { - tb.Fatalf("git %v failed: %v\n%s", args, err, out) - } - - return out -} - -func (testRepo *TestRepo) runBytesE(stdin []byte, dir string, args ...string) ([]byte, error) { - return testRepo.runBytesWithEnvNoHelper(stdin, dir, testRepo.env, args...) -} - -// runBytesWithEnv executes git using the supplied environment. -func (testRepo *TestRepo) runBytesWithEnv( - tb testing.TB, - stdin []byte, - dir string, - env []string, - args ...string, -) ([]byte, error) { - tb.Helper() - - return testRepo.runBytesWithEnvNoHelper(stdin, dir, env, args...) -} - -// runBytesWithEnvNoHelper executes git using the supplied environment without -// touching testing helper state. -func (testRepo *TestRepo) runBytesWithEnvNoHelper( - stdin []byte, - dir string, - env []string, - args ...string, -) ([]byte, error) { - //nolint:noctx - cmd := exec.Command("git", args...) //#nosec G204 - cmd.Dir = dir - - cmd.Env = env - if stdin != nil { - cmd.Stdin = bytes.NewReader(stdin) - } - - return cmd.CombinedOutput() -} diff --git a/internal/testgit/repo_run_extra_files.go b/internal/testgit/repo_run_extra_files.go deleted file mode 100644 index 4629c872..00000000 --- a/internal/testgit/repo_run_extra_files.go +++ /dev/null @@ -1,55 +0,0 @@ -package testgit - -import ( - "bytes" - "context" - "os" - "os/exec" - "testing" -) - -// RunWithExtraFilesE executes git with inherited extra files and returns split -// stdout/stderr plus any command error. -func (testRepo *TestRepo) RunWithExtraFilesE( - tb testing.TB, - extraFiles []*os.File, - args ...string, -) ([]byte, []byte, error) { - tb.Helper() - - return testRepo.RunWithExtraFilesEnvContextE( - tb, - context.Background(), - nil, - extraFiles, - args..., - ) -} - -// RunWithExtraFilesEnvContextE executes git with inherited extra files, extra -// environment, and context cancellation, returning split stdout/stderr plus any -// command error. -func (testRepo *TestRepo) RunWithExtraFilesEnvContextE( - tb testing.TB, - ctx context.Context, - extraEnv []string, - extraFiles []*os.File, - args ...string, -) ([]byte, []byte, error) { - tb.Helper() - - cmd := exec.CommandContext(ctx, "git", args...) //#nosec G204 - cmd.Dir = testRepo.dir - cmd.Env = testRepo.env - cmd.Env = append(cmd.Env, extraEnv...) - cmd.ExtraFiles = append([]*os.File(nil), extraFiles...) - - var stdout, stderr bytes.Buffer - - cmd.Stdout = &stdout - cmd.Stderr = &stderr - - err := cmd.Run() - - return stdout.Bytes(), stderr.Bytes(), err -} diff --git a/internal/testgit/repo_tag_annotated.go b/internal/testgit/repo_tag_annotated.go deleted file mode 100644 index cf6e9b3d..00000000 --- a/internal/testgit/repo_tag_annotated.go +++ /dev/null @@ -1,15 +0,0 @@ -package testgit - -import ( - "testing" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// TagAnnotated creates an annotated tag object and returns the resulting tag object ID. -func (testRepo *TestRepo) TagAnnotated(tb testing.TB, name string, target objectid.ObjectID, message string) objectid.ObjectID { - tb.Helper() - testRepo.Run(tb, "tag", "-a", name, target.String(), "-m", message) - - return testRepo.RevParse(tb, "refs/tags/"+name) -} diff --git a/internal/utils/progress.go b/internal/utils/progress.go deleted file mode 100644 index 2adcb26a..00000000 --- a/internal/utils/progress.go +++ /dev/null @@ -1,18 +0,0 @@ -// Package utils provides misc utilities. -package utils - -import ( - "fmt" - "io" -) - -// BestEffortFprintf writes one formatted message to w. -// -// It is nil-safe and ignores write errors by design. -func BestEffortFprintf(w io.Writer, format string, args ...any) { - if w == nil { - return - } - - _, _ = fmt.Fprintf(w, format, args...) -} diff --git a/network/doc.go b/network/doc.go deleted file mode 100644 index d964997b..00000000 --- a/network/doc.go +++ /dev/null @@ -1,5 +0,0 @@ -// Package network encapsulates network-facing Git packages. -// -// These packages implement wire formats, protocol framing, and application -// services built on top of them. -package network diff --git a/network/protocol/doc.go b/network/protocol/doc.go deleted file mode 100644 index d1e00447..00000000 --- a/network/protocol/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package protocol encapsulates network protocol implementations. -package protocol diff --git a/network/protocol/pktline/append.go b/network/protocol/pktline/append.go deleted file mode 100644 index 9425e58e..00000000 --- a/network/protocol/pktline/append.go +++ /dev/null @@ -1,39 +0,0 @@ -package pktline - -import "fmt" - -// AppendData appends one data frame to dst. -// -// Empty payload is encoded as 0004. -func AppendData(dst, payload []byte) ([]byte, error) { - if len(payload) > LargePacketDataMax { - return dst, fmt.Errorf("%w: %d > %d", ErrTooLarge, len(payload), LargePacketDataMax) - } - - var hdr [4]byte - - err := EncodeLengthHeader(&hdr, len(payload)+4) - if err != nil { - return dst, err - } - - dst = append(dst, hdr[:]...) - dst = append(dst, payload...) - - return dst, nil -} - -// AppendFlushPkt appends control frame 0000 (flush-pkt). -func AppendFlushPkt(dst []byte) []byte { - return append(dst, '0', '0', '0', '0') -} - -// AppendDelimPkt appends control frame 0001 (delim-pkt). -func AppendDelimPkt(dst []byte) []byte { - return append(dst, '0', '0', '0', '1') -} - -// AppendResponseEndPkt appends control frame 0002 (response-end-pkt). -func AppendResponseEndPkt(dst []byte) []byte { - return append(dst, '0', '0', '0', '2') -} diff --git a/network/protocol/pktline/append_data_preserves_dst_on_error_test.go b/network/protocol/pktline/append_data_preserves_dst_on_error_test.go deleted file mode 100644 index d127fb39..00000000 --- a/network/protocol/pktline/append_data_preserves_dst_on_error_test.go +++ /dev/null @@ -1,25 +0,0 @@ -package pktline_test - -import ( - "bytes" - "errors" - "testing" - - "codeberg.org/lindenii/furgit/network/protocol/pktline" -) - -func TestAppendDataPreservesDstOnError(t *testing.T) { - t.Parallel() - - orig := []byte("seed") - dst := append([]byte(nil), orig...) - - out, err := pktline.AppendData(dst, bytes.Repeat([]byte{'x'}, pktline.LargePacketDataMax+1)) - if !errors.Is(err, pktline.ErrTooLarge) { - t.Fatalf("got err %v, want ErrTooLarge", err) - } - - if !bytes.Equal(out, orig) { - t.Fatalf("got %q, want %q", string(out), string(orig)) - } -} diff --git a/network/protocol/pktline/append_helpers_test.go b/network/protocol/pktline/append_helpers_test.go deleted file mode 100644 index 259ada16..00000000 --- a/network/protocol/pktline/append_helpers_test.go +++ /dev/null @@ -1,24 +0,0 @@ -package pktline_test - -import ( - "testing" - - "codeberg.org/lindenii/furgit/network/protocol/pktline" -) - -func TestAppendHelpers(t *testing.T) { - t.Parallel() - - out, err := pktline.AppendData(nil, []byte("ok")) - if err != nil { - t.Fatalf("AppendData: %v", err) - } - - out = pktline.AppendFlushPkt(out) - out = pktline.AppendDelimPkt(out) - out = pktline.AppendResponseEndPkt(out) - - if got, want := string(out), "0006ok000000010002"; got != want { - t.Fatalf("got %q, want %q", got, want) - } -} diff --git a/network/protocol/pktline/chunk_writer.go b/network/protocol/pktline/chunk_writer.go deleted file mode 100644 index f009978e..00000000 --- a/network/protocol/pktline/chunk_writer.go +++ /dev/null @@ -1,74 +0,0 @@ -package pktline - -import "io" - -// ChunkWriter packetizes arbitrary stream bytes into data pkt-lines. -// It never writes control packets automatically. -// -// Labels: MT-Unsafe. -type ChunkWriter struct { - enc *Encoder -} - -// NewChunkWriter creates a chunking adapter over enc. -// -// Labels: Deps-Borrowed, Life-Parent. -func NewChunkWriter(enc *Encoder) *ChunkWriter { - return &ChunkWriter{enc: enc} -} - -// Write splits p into data frames not larger than enc's maxData. -// -// It implements io.Writer. -func (cw *ChunkWriter) Write(p []byte) (int, error) { - total := 0 - maxData := cw.enc.effectiveMaxData() - - for len(p) > 0 { - n := min(len(p), maxData) - - err := cw.enc.WriteData(p[:n]) - if err != nil { - return total, err - } - - total += n - p = p[n:] - } - - return total, nil -} - -// ReadFrom reads from r and writes pkt-line data frames to the encoder. -// -// It implements io.ReaderFrom. -func (cw *ChunkWriter) ReadFrom(r io.Reader) (int64, error) { - buf := make([]byte, cw.enc.effectiveMaxData()) - - var total int64 - - for { - n, err := r.Read(buf) - if n > 0 { - werr := cw.enc.WriteData(buf[:n]) - if werr != nil { - return total, werr - } - - total += int64(n) - } - - if err != nil { - if err == io.EOF { - return total, nil - } - - return total, err - } - } -} - -// Flush flushes buffered output in the underlying transport. -func (cw *ChunkWriter) Flush() error { - return cw.enc.Flush() -} diff --git a/network/protocol/pktline/chunk_writer_write_and_read_from_test.go b/network/protocol/pktline/chunk_writer_write_and_read_from_test.go deleted file mode 100644 index efe19e23..00000000 --- a/network/protocol/pktline/chunk_writer_write_and_read_from_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package pktline_test - -import ( - "bufio" - "bytes" - "strings" - "testing" - - "codeberg.org/lindenii/furgit/network/protocol/pktline" -) - -func TestChunkWriterWriteAndReadFrom(t *testing.T) { - t.Parallel() - - var out bytes.Buffer - - bw := bufio.NewWriter(&out) - - enc := pktline.NewEncoder(bw) - enc.SetMaxData(3) - cw := pktline.NewChunkWriter(enc) - - n, err := cw.Write([]byte("abcdefg")) - if err != nil { - t.Fatalf("Write: %v", err) - } - - if n != 7 { - t.Fatalf("Write n=%d, want 7", n) - } - - err = enc.Flush() - if err != nil { - t.Fatalf("Flush: %v", err) - } - - if got, want := out.String(), "0007abc0007def0005g"; got != want { - t.Fatalf("got %q, want %q", got, want) - } - - out.Reset() - - rn, err := cw.ReadFrom(strings.NewReader("wxyz")) - if err != nil { - t.Fatalf("ReadFrom: %v", err) - } - - if rn != 4 { - t.Fatalf("ReadFrom n=%d, want 4", rn) - } - - err = enc.Flush() - if err != nil { - t.Fatalf("Flush: %v", err) - } - - if got, want := out.String(), "0007wxy0005z"; got != want { - t.Fatalf("got %q, want %q", got, want) - } -} diff --git a/network/protocol/pktline/constants.go b/network/protocol/pktline/constants.go deleted file mode 100644 index 811eb3c6..00000000 --- a/network/protocol/pktline/constants.go +++ /dev/null @@ -1,12 +0,0 @@ -package pktline - -const ( - // DefaultPacketMax is a conservative packet size commonly used by - // line-oriented protocol messages. - DefaultPacketMax = 1000 - // LargePacketMax is the maximum on-wire packet size including the - // 4-byte hexadecimal length header. - LargePacketMax = 65520 - // LargePacketDataMax is the maximum payload size in one packet. - LargePacketDataMax = LargePacketMax - 4 -) diff --git a/network/protocol/pktline/decoder.go b/network/protocol/pktline/decoder.go deleted file mode 100644 index 682dd164..00000000 --- a/network/protocol/pktline/decoder.go +++ /dev/null @@ -1,191 +0,0 @@ -package pktline - -import ( - "errors" - "fmt" - "io" -) - -// ReadOptions controls decoding behavior. -type ReadOptions struct { - // ChompLF removes one trailing '\n' from PacketData payloads. - ChompLF bool -} - -// Decoder reads pkt-line frames from an io.Reader. -// -// It is advisable to supply a buffered reader. -// -// It preserves frame boundaries and supports one-frame lookahead via PeekFrame. -// -// Labels: MT-Unsafe. -type Decoder struct { - r io.Reader - maxData int - opts ReadOptions - - peeked bool - peek Frame - peekErr error -} - -// NewDecoder creates a decoder over r. -// -// Labels: Deps-Borrowed, Life-Parent. -func NewDecoder(r io.Reader, opts ReadOptions) *Decoder { - return &Decoder{ - r: r, - maxData: LargePacketDataMax, - opts: opts, - } -} - -// SetMaxData sets maximum payload size accepted for one data packet. -// -// Non-positive n resets to LargePacketDataMax. -func (d *Decoder) SetMaxData(n int) { - if n <= 0 { - d.maxData = LargePacketDataMax - - return - } - - d.maxData = n -} - -func cloneFrame(f Frame) Frame { - if f.Type != PacketData { - return Frame{Type: f.Type} - } - - out := Frame{Type: f.Type} - if f.Payload != nil { - out.Payload = append([]byte(nil), f.Payload...) - } - - return out -} - -// ReadFrame reads one frame. -// -// 0000 is a PacketFlush -// 0001 is a PacketDelim -// 0002 is a PacketResponseEnd -// 0004 is a PacketData with empty payload -// -// 0003 and malformed headers return *ProtocolError. -func (d *Decoder) ReadFrame() (Frame, error) { - if d.peeked { - d.peeked = false - - return cloneFrame(d.peek), d.peekErr - } - - return d.readFrame() -} - -// PeekFrame returns the next frame without consuming it. -// -// A subsequent ReadFrame returns the same frame. -func (d *Decoder) PeekFrame() (Frame, error) { - if !d.peeked { - d.peek, d.peekErr = d.readFrame() - d.peeked = true - } - - return cloneFrame(d.peek), d.peekErr -} - -func (d *Decoder) readFrame() (Frame, error) { - var hdr [4]byte - - _, err := io.ReadFull(d.r, hdr[:]) - if err != nil { - if errors.Is(err, io.EOF) { - return Frame{}, io.EOF - } - - if errors.Is(err, io.ErrUnexpectedEOF) { - return Frame{}, io.ErrUnexpectedEOF - } - - return Frame{}, err - } - - n, err := ParseLengthHeader(hdr) - if err != nil { - return Frame{}, &ProtocolError{Header: hdr, Reason: err.Error()} - } - - switch n { - case 0: - return Frame{Type: PacketFlush}, nil - case 1: - return Frame{Type: PacketDelim}, nil - case 2: - return Frame{Type: PacketResponseEnd}, nil - case 3: - return Frame{}, &ProtocolError{Header: hdr, Reason: "invalid pkt-line length 3"} - } - - if n < 4 { - return Frame{}, &ProtocolError{Header: hdr, Reason: fmt.Sprintf("invalid pkt-line length %d", n)} - } - - if n > LargePacketMax { - perr := &ProtocolError{Header: hdr, Reason: fmt.Sprintf("pkt-line length %d exceeds max %d", n, LargePacketMax)} - - err := d.discardPayload(n - 4) - if err != nil { - return Frame{}, errors.Join(perr, err) - } - - return Frame{}, perr - } - - payloadLen := n - 4 - if payloadLen > d.maxData { - serr := fmt.Errorf("%w: %d > %d", ErrTooLarge, payloadLen, d.maxData) - - err := d.discardPayload(payloadLen) - if err != nil { - return Frame{}, errors.Join(serr, err) - } - - return Frame{}, serr - } - - payload := make([]byte, payloadLen) - - _, err = io.ReadFull(d.r, payload) - if err != nil { - if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) { - return Frame{}, io.ErrUnexpectedEOF - } - - return Frame{}, err - } - - if d.opts.ChompLF && len(payload) > 0 && payload[len(payload)-1] == '\n' { - payload = payload[:len(payload)-1] - } - - return Frame{Type: PacketData, Payload: payload}, nil -} - -func (d *Decoder) discardPayload(n int) error { - if n <= 0 { - return nil - } - - _, err := io.CopyN(io.Discard, d.r, int64(n)) - if err == nil { - return nil - } - - if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) { - return io.ErrUnexpectedEOF - } - - return err -} diff --git a/network/protocol/pktline/decoder_data_control_and_0004_test.go b/network/protocol/pktline/decoder_data_control_and_0004_test.go deleted file mode 100644 index ab92b603..00000000 --- a/network/protocol/pktline/decoder_data_control_and_0004_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package pktline_test - -import ( - "strings" - "testing" - - "codeberg.org/lindenii/furgit/network/protocol/pktline" -) - -func TestDecoderDataControlAnd0004(t *testing.T) { - t.Parallel() - - input := "0006a\n0004000100020000" - dec := pktline.NewDecoder(strings.NewReader(input), pktline.ReadOptions{ChompLF: true}) - - f, err := dec.ReadFrame() - if err != nil { - t.Fatalf("ReadFrame #1: %v", err) - } - - if f.Type != pktline.PacketData || string(f.Payload) != "a" { - t.Fatalf("frame #1 = %#v", f) - } - - f, err = dec.ReadFrame() - if err != nil { - t.Fatalf("ReadFrame #2: %v", err) - } - - if f.Type != pktline.PacketData || len(f.Payload) != 0 { - t.Fatalf("frame #2 = %#v, want empty data", f) - } - - f, err = dec.ReadFrame() - if err != nil { - t.Fatalf("ReadFrame #3: %v", err) - } - - if f.Type != pktline.PacketDelim { - t.Fatalf("frame #3 type = %v, want PacketDelim", f.Type) - } - - f, err = dec.ReadFrame() - if err != nil { - t.Fatalf("ReadFrame #4: %v", err) - } - - if f.Type != pktline.PacketResponseEnd { - t.Fatalf("frame #4 type = %v, want PacketResponseEnd", f.Type) - } - - f, err = dec.ReadFrame() - if err != nil { - t.Fatalf("ReadFrame #5: %v", err) - } - - if f.Type != pktline.PacketFlush { - t.Fatalf("frame #5 type = %v, want PacketFlush", f.Type) - } -} diff --git a/network/protocol/pktline/decoder_invalid_0003_test.go b/network/protocol/pktline/decoder_invalid_0003_test.go deleted file mode 100644 index 716da3f2..00000000 --- a/network/protocol/pktline/decoder_invalid_0003_test.go +++ /dev/null @@ -1,20 +0,0 @@ -package pktline_test - -import ( - "errors" - "strings" - "testing" - - "codeberg.org/lindenii/furgit/network/protocol/pktline" -) - -func TestDecoderInvalid0003(t *testing.T) { - t.Parallel() - - dec := pktline.NewDecoder(strings.NewReader("0003"), pktline.ReadOptions{}) - _, err := dec.ReadFrame() - - if _, ok := errors.AsType[*pktline.ProtocolError](err); !ok { - t.Fatalf("got err %v, want ProtocolError", err) - } -} diff --git a/network/protocol/pktline/decoder_peek_test.go b/network/protocol/pktline/decoder_peek_test.go deleted file mode 100644 index a67da881..00000000 --- a/network/protocol/pktline/decoder_peek_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package pktline_test - -import ( - "strings" - "testing" - - "codeberg.org/lindenii/furgit/network/protocol/pktline" -) - -func TestDecoderPeek(t *testing.T) { - t.Parallel() - - dec := pktline.NewDecoder(strings.NewReader("0005x0000"), pktline.ReadOptions{}) - - f, err := dec.PeekFrame() - if err != nil { - t.Fatalf("PeekFrame: %v", err) - } - - if f.Type != pktline.PacketData || string(f.Payload) != "x" { - t.Fatalf("peek frame = %#v", f) - } - - f, err = dec.ReadFrame() - if err != nil { - t.Fatalf("ReadFrame: %v", err) - } - - if f.Type != pktline.PacketData || string(f.Payload) != "x" { - t.Fatalf("read frame = %#v", f) - } -} diff --git a/network/protocol/pktline/decoder_rejects_over_maximum_length_test.go b/network/protocol/pktline/decoder_rejects_over_maximum_length_test.go deleted file mode 100644 index 357bfc36..00000000 --- a/network/protocol/pktline/decoder_rejects_over_maximum_length_test.go +++ /dev/null @@ -1,22 +0,0 @@ -package pktline_test - -import ( - "errors" - "strings" - "testing" - - "codeberg.org/lindenii/furgit/network/protocol/pktline" -) - -func TestDecoderRejectsOverMaximumLength(t *testing.T) { - t.Parallel() - - dec := pktline.NewDecoder(strings.NewReader("fffe"), pktline.ReadOptions{}) - dec.SetMaxData(70000) - - _, err := dec.ReadFrame() - - if _, ok := errors.AsType[*pktline.ProtocolError](err); !ok { - t.Fatalf("got err %v, want ProtocolError", err) - } -} diff --git a/network/protocol/pktline/decoder_resync_after_over_max_data_test.go b/network/protocol/pktline/decoder_resync_after_over_max_data_test.go deleted file mode 100644 index 42a7572e..00000000 --- a/network/protocol/pktline/decoder_resync_after_over_max_data_test.go +++ /dev/null @@ -1,51 +0,0 @@ -package pktline_test - -import ( - "bufio" - "bytes" - "errors" - "testing" - - "codeberg.org/lindenii/furgit/network/protocol/pktline" -) - -func TestDecoderResyncAfterOverMaxData(t *testing.T) { - t.Parallel() - - var b bytes.Buffer - - bw := bufio.NewWriter(&b) - enc := pktline.NewEncoder(bw) - - err := enc.WriteData([]byte("abcd")) - if err != nil { - t.Fatalf("WriteData #1: %v", err) - } - - err = enc.WriteData([]byte("z")) - if err != nil { - t.Fatalf("WriteData #2: %v", err) - } - - err = enc.Flush() - if err != nil { - t.Fatalf("Flush: %v", err) - } - - dec := pktline.NewDecoder(bytes.NewReader(b.Bytes()), pktline.ReadOptions{}) - dec.SetMaxData(1) - - _, err = dec.ReadFrame() - if !errors.Is(err, pktline.ErrTooLarge) { - t.Fatalf("got err %v, want ErrTooLarge", err) - } - - f, err := dec.ReadFrame() - if err != nil { - t.Fatalf("ReadFrame #2: %v", err) - } - - if f.Type != pktline.PacketData || string(f.Payload) != "z" { - t.Fatalf("got frame %#v, want data z", f) - } -} diff --git a/network/protocol/pktline/decoder_resync_after_over_wire_max_test.go b/network/protocol/pktline/decoder_resync_after_over_wire_max_test.go deleted file mode 100644 index 9413823b..00000000 --- a/network/protocol/pktline/decoder_resync_after_over_wire_max_test.go +++ /dev/null @@ -1,37 +0,0 @@ -package pktline_test - -import ( - "bytes" - "errors" - "testing" - - "codeberg.org/lindenii/furgit/network/protocol/pktline" -) - -func TestDecoderResyncAfterOverWireMax(t *testing.T) { - t.Parallel() - - var b bytes.Buffer - - _, _ = b.WriteString("ffff") - _, _ = b.Write(bytes.Repeat([]byte{'a'}, 65531)) - _, _ = b.WriteString("0005z") - - dec := pktline.NewDecoder(bytes.NewReader(b.Bytes()), pktline.ReadOptions{}) - dec.SetMaxData(70000) - - _, err := dec.ReadFrame() - - if _, ok := errors.AsType[*pktline.ProtocolError](err); !ok { - t.Fatalf("got err %v, want ProtocolError", err) - } - - f, err := dec.ReadFrame() - if err != nil { - t.Fatalf("ReadFrame #2: %v", err) - } - - if f.Type != pktline.PacketData || string(f.Payload) != "z" { - t.Fatalf("got frame %#v, want data z", f) - } -} diff --git a/network/protocol/pktline/decoder_unexpected_eof_test.go b/network/protocol/pktline/decoder_unexpected_eof_test.go deleted file mode 100644 index e1bf4457..00000000 --- a/network/protocol/pktline/decoder_unexpected_eof_test.go +++ /dev/null @@ -1,21 +0,0 @@ -package pktline_test - -import ( - "errors" - "io" - "strings" - "testing" - - "codeberg.org/lindenii/furgit/network/protocol/pktline" -) - -func TestDecoderUnexpectedEOF(t *testing.T) { - t.Parallel() - - dec := pktline.NewDecoder(strings.NewReader("0006a"), pktline.ReadOptions{}) - - _, err := dec.ReadFrame() - if !errors.Is(err, io.ErrUnexpectedEOF) { - t.Fatalf("got err %v, want io.ErrUnexpectedEOF", err) - } -} diff --git a/network/protocol/pktline/doc.go b/network/protocol/pktline/doc.go deleted file mode 100644 index 3f7cca89..00000000 --- a/network/protocol/pktline/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package pktline implements the pkt-line format specified in gitprotocol-common(5). -package pktline diff --git a/network/protocol/pktline/encode_length_header_test.go b/network/protocol/pktline/encode_length_header_test.go deleted file mode 100644 index 38a980f0..00000000 --- a/network/protocol/pktline/encode_length_header_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package pktline_test - -import ( - "errors" - "testing" - - "codeberg.org/lindenii/furgit/network/protocol/pktline" -) - -func TestEncodeLengthHeader(t *testing.T) { - t.Parallel() - - var hdr [4]byte - - err := pktline.EncodeLengthHeader(&hdr, 4) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - - if got := string(hdr[:]); got != "0004" { - t.Fatalf("got %q, want %q", got, "0004") - } - - err = pktline.EncodeLengthHeader(&hdr, pktline.LargePacketMax+1) - if !errors.Is(err, pktline.ErrInvalidLength) { - t.Fatalf("got err %v, want ErrInvalidLength", err) - } -} diff --git a/network/protocol/pktline/encoder.go b/network/protocol/pktline/encoder.go deleted file mode 100644 index a7b17108..00000000 --- a/network/protocol/pktline/encoder.go +++ /dev/null @@ -1,143 +0,0 @@ -package pktline - -import ( - "fmt" - "io" - - "codeberg.org/lindenii/furgit/common/iowrap" -) - -// Encoder writes pkt-line frames to a flush-capable output transport. -// -// It writes exactly one frame per method call and does not auto-chunk data. -// -// Labels: MT-Unsafe. -type Encoder struct { - w iowrap.WriteFlusher - maxData int -} - -// NewEncoder creates an encoder over w. -// -// Labels: Deps-Borrowed, Life-Parent. -func NewEncoder(w iowrap.WriteFlusher) *Encoder { - return &Encoder{ - w: w, - maxData: LargePacketDataMax, - } -} - -// SetMaxData sets the maximum payload size accepted by WriteData. -// -// Non-positive n resets to LargePacketDataMax. -func (e *Encoder) SetMaxData(n int) { - if n <= 0 { - e.maxData = LargePacketDataMax - - return - } - - e.maxData = n -} - -func writeAll(w io.Writer, b []byte) error { - for len(b) > 0 { - n, err := w.Write(b) - if err != nil { - return err - } - - if n <= 0 { - return io.ErrShortWrite - } - - b = b[n:] - } - - return nil -} - -// WriteData writes one data frame. -// -// Empty payload is encoded as 0004. -func (e *Encoder) WriteData(p []byte) error { - maxData := e.effectiveMaxData() - if len(p) > maxData { - return fmt.Errorf("%w: %d > %d", ErrTooLarge, len(p), maxData) - } - - var hdr [4]byte - - err := EncodeLengthHeader(&hdr, len(p)+4) - if err != nil { - return err - } - - err = writeAll(e.w, hdr[:]) - if err != nil { - return err - } - - return writeAll(e.w, p) -} - -// WriteString writes one data frame containing s and returns len(s) on success. -func (e *Encoder) WriteString(s string) (int, error) { - err := e.WriteData([]byte(s)) - if err != nil { - return 0, err - } - - return len(s), nil -} - -// WriteFlushPacket writes control frame 0000 (flush-pkt). -func (e *Encoder) WriteFlushPacket() error { - return e.writeControl(0) -} - -// WriteDelimPacket writes control frame 0001 (delim-pkt). -func (e *Encoder) WriteDelimPacket() error { - return e.writeControl(1) -} - -// WriteResponseEndPacket writes control frame 0002 (response-end-pkt). -func (e *Encoder) WriteResponseEndPacket() error { - return e.writeControl(2) -} - -// Flush flushes buffered output in the underlying transport. -// -// Flush does not emit any pkt-line control frame. -func (e *Encoder) Flush() error { - return e.w.Flush() -} - -// WriteFlushPacketAndFlush writes a flush-pkt (0000) then flushes transport I/O. -func (e *Encoder) WriteFlushPacketAndFlush() error { - err := e.WriteFlushPacket() - if err != nil { - return err - } - - return e.Flush() -} - -func (e *Encoder) writeControl(n int) error { - var hdr [4]byte - - err := EncodeLengthHeader(&hdr, n) - if err != nil { - return err - } - - return writeAll(e.w, hdr[:]) -} - -func (e *Encoder) effectiveMaxData() int { - if e.maxData <= 0 || e.maxData > LargePacketDataMax { - return LargePacketDataMax - } - - return e.maxData -} diff --git a/network/protocol/pktline/encoder_buffered_flush_and_f_flush_test.go b/network/protocol/pktline/encoder_buffered_flush_and_f_flush_test.go deleted file mode 100644 index d0f26878..00000000 --- a/network/protocol/pktline/encoder_buffered_flush_and_f_flush_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package pktline_test - -import ( - "bufio" - "bytes" - "testing" - - "codeberg.org/lindenii/furgit/network/protocol/pktline" -) - -func TestEncoderBufferedFlushAndFFlush(t *testing.T) { - t.Parallel() - - var out bytes.Buffer - - bw := bufio.NewWriter(&out) - enc := pktline.NewEncoder(bw) - - err := enc.WriteData([]byte("x")) - if err != nil { - t.Fatalf("WriteData: %v", err) - } - - if out.Len() != 0 { - t.Fatalf("unexpected immediate output: %q", out.String()) - } - - err = enc.Flush() - if err != nil { - t.Fatalf("Flush: %v", err) - } - - if out.String() != "0005x" { - t.Fatalf("got %q, want %q", out.String(), "0005x") - } - - out.Reset() - bw = bufio.NewWriter(&out) - - enc = pktline.NewEncoder(bw) - - err = enc.WriteFlushPacketAndFlush() - if err != nil { - t.Fatalf("WriteFlushPacketAndFlush: %v", err) - } - - if out.String() != "0000" { - t.Fatalf("got %q, want %q", out.String(), "0000") - } -} diff --git a/network/protocol/pktline/encoder_buffered_flush_behavior_test.go b/network/protocol/pktline/encoder_buffered_flush_behavior_test.go deleted file mode 100644 index b6d14b4b..00000000 --- a/network/protocol/pktline/encoder_buffered_flush_behavior_test.go +++ /dev/null @@ -1,86 +0,0 @@ -package pktline_test - -import ( - "bufio" - "bytes" - "testing" - - "codeberg.org/lindenii/furgit/network/protocol/pktline" -) - -func TestEncoderBufferedFlushBehavior(t *testing.T) { - t.Parallel() - - var out bytes.Buffer - - bw := bufio.NewWriter(&out) - enc := pktline.NewEncoder(bw) - - err := enc.WriteData([]byte("hello")) - if err != nil { - t.Fatalf("WriteData: %v", err) - } - - err = enc.WriteFlushPacket() - if err != nil { - t.Fatalf("WriteFlushPacket: %v", err) - } - - if out.Len() != 0 { - t.Fatalf("WriteFlushPacket should not flush I/O, got %q", out.String()) - } - - err = enc.Flush() - if err != nil { - t.Fatalf("Flush: %v", err) - } - - if got, want := out.String(), "0009hello0000"; got != want { - t.Fatalf("got %q, want %q", got, want) - } - - out.Reset() - bw = bufio.NewWriter(&out) - enc = pktline.NewEncoder(bw) - - err = enc.WriteData([]byte("ok")) - if err != nil { - t.Fatalf("WriteData: %v", err) - } - - err = enc.WriteFlushPacket() - if err != nil { - t.Fatalf("WriteFlushPacket: %v", err) - } - - if out.Len() != 0 { - t.Fatalf("WriteFlushPacket should not flush I/O, got %q", out.String()) - } - - err = enc.Flush() - if err != nil { - t.Fatalf("Flush: %v", err) - } - - if got, want := out.String(), "0006ok0000"; got != want { - t.Fatalf("got %q, want %q", got, want) - } - - out.Reset() - bw = bufio.NewWriter(&out) - enc = pktline.NewEncoder(bw) - - err = enc.WriteData([]byte("yo")) - if err != nil { - t.Fatalf("WriteData: %v", err) - } - - err = enc.WriteFlushPacketAndFlush() - if err != nil { - t.Fatalf("WriteFlushPacketAndFlush: %v", err) - } - - if got, want := out.String(), "0006yo0000"; got != want { - t.Fatalf("got %q, want %q", got, want) - } -} diff --git a/network/protocol/pktline/encoder_set_max_data_cannot_exceed_wire_limit_test.go b/network/protocol/pktline/encoder_set_max_data_cannot_exceed_wire_limit_test.go deleted file mode 100644 index d73baa4f..00000000 --- a/network/protocol/pktline/encoder_set_max_data_cannot_exceed_wire_limit_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package pktline_test - -import ( - "bufio" - "bytes" - "errors" - "testing" - - "codeberg.org/lindenii/furgit/network/protocol/pktline" -) - -func TestEncoderSetMaxDataCannotExceedWireLimit(t *testing.T) { - t.Parallel() - - var out bytes.Buffer - - bw := bufio.NewWriter(&out) - - enc := pktline.NewEncoder(bw) - enc.SetMaxData(pktline.LargePacketDataMax + 100) - - err := enc.WriteData(bytes.Repeat([]byte{'x'}, pktline.LargePacketDataMax+1)) - if !errors.Is(err, pktline.ErrTooLarge) { - t.Fatalf("got err %v, want ErrTooLarge", err) - } -} diff --git a/network/protocol/pktline/encoder_writes_frames_test.go b/network/protocol/pktline/encoder_writes_frames_test.go deleted file mode 100644 index 1922b277..00000000 --- a/network/protocol/pktline/encoder_writes_frames_test.go +++ /dev/null @@ -1,51 +0,0 @@ -package pktline_test - -import ( - "bufio" - "bytes" - "testing" - - "codeberg.org/lindenii/furgit/network/protocol/pktline" -) - -func TestEncoderWritesFrames(t *testing.T) { - t.Parallel() - - var b bytes.Buffer - - bw := bufio.NewWriter(&b) - - enc := pktline.NewEncoder(bw) - - err := enc.WriteData([]byte("hi")) - if err != nil { - t.Fatalf("WriteData: %v", err) - } - - err = enc.WriteFlushPacket() - if err != nil { - t.Fatalf("WriteFlushPacket: %v", err) - } - - err = enc.WriteDelimPacket() - if err != nil { - t.Fatalf("WriteDelimPacket: %v", err) - } - - err = enc.WriteResponseEndPacket() - if err != nil { - t.Fatalf("WriteResponseEndPacket: %v", err) - } - - err = enc.Flush() - if err != nil { - t.Fatalf("Flush: %v", err) - } - - got := b.String() - - want := "0006hi000000010002" - if got != want { - t.Fatalf("got %q, want %q", got, want) - } -} diff --git a/network/protocol/pktline/errors.go b/network/protocol/pktline/errors.go deleted file mode 100644 index 866ff467..00000000 --- a/network/protocol/pktline/errors.go +++ /dev/null @@ -1,31 +0,0 @@ -package pktline - -import "errors" - -var ( - // ErrInvalidLength indicates a malformed 4-byte hexadecimal length header. - ErrInvalidLength = errors.New("pktline: invalid length header") - // ErrTooLarge indicates a payload exceeds configured packet data limits. - ErrTooLarge = errors.New("pktline: payload too large") -) - -// ProtocolError reports invalid pkt-line framing. -// -// It is returned for protocol violations such as invalid control values -// (for example 0003) or non-hex length headers. -type ProtocolError struct { - Header [4]byte - Reason string -} - -func (e *ProtocolError) Error() string { - if e == nil { - return "" - } - - if e.Reason == "" { - return "pktline: protocol error" - } - - return "pktline: protocol error: " + e.Reason -} diff --git a/network/protocol/pktline/frame.go b/network/protocol/pktline/frame.go deleted file mode 100644 index a1cf708c..00000000 --- a/network/protocol/pktline/frame.go +++ /dev/null @@ -1,10 +0,0 @@ -package pktline - -// Frame is one decoded pkt-line frame. -// -// For PacketData, Payload holds frame bytes (possibly empty for 0004). -// For control frames, Payload is nil. -type Frame struct { - Type PacketType - Payload []byte -} diff --git a/network/protocol/pktline/header.go b/network/protocol/pktline/header.go deleted file mode 100644 index 41e50e04..00000000 --- a/network/protocol/pktline/header.go +++ /dev/null @@ -1,57 +0,0 @@ -package pktline - -import "fmt" - -func hexval(b byte) int { - switch { - case b >= '0' && b <= '9': - return int(b - '0') - case b >= 'a' && b <= 'f': - return int(b-'a') + 10 - case b >= 'A' && b <= 'F': - return int(b-'A') + 10 - default: - return -1 - } -} - -// ParseLengthHeader parses a 4-byte hexadecimal pkt-line length header. -// -// The returned value is the full on-wire packet size, including the 4-byte -// header. Semantic interpretation (data/control/error) is done by Decoder. -// -// The 4-byte header is only an actual length when above or equal to 4. -// Otherwise, it indicates some control packet. -func ParseLengthHeader(h [4]byte) (int, error) { - a := hexval(h[0]) - b := hexval(h[1]) - c := hexval(h[2]) - d := hexval(h[3]) - - if a < 0 || b < 0 || c < 0 || d < 0 { - return 0, fmt.Errorf("%w: %q", ErrInvalidLength, string(h[:])) - } - - return (a << 12) | (b << 8) | (c << 4) | d, nil -} - -// EncodeLengthHeader encodes n as a 4-byte hexadecimal pkt-line header. -// -// n is the full on-wire packet size including the 4-byte header. -// -// The 4-byte header is only an actual length when above or equal to 4. -// Otherwise, it indicates some control packet. -func EncodeLengthHeader(dst *[4]byte, n int) error { - if n < 0 || n > LargePacketMax { - return fmt.Errorf("%w: %d", ErrInvalidLength, n) - } - - const hex = "0123456789abcdef" - - dst[0] = hex[(n>>12)&0xf] - dst[1] = hex[(n>>8)&0xf] - dst[2] = hex[(n>>4)&0xf] - dst[3] = hex[n&0xf] - - return nil -} diff --git a/network/protocol/pktline/parse_length_header_test.go b/network/protocol/pktline/parse_length_header_test.go deleted file mode 100644 index b1a4c1e5..00000000 --- a/network/protocol/pktline/parse_length_header_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package pktline_test - -import ( - "errors" - "testing" - - "codeberg.org/lindenii/furgit/network/protocol/pktline" -) - -func TestParseLengthHeader(t *testing.T) { - t.Parallel() - - n, err := pktline.ParseLengthHeader([4]byte{'0', '0', '0', '4'}) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - - if n != 4 { - t.Fatalf("got %d, want 4", n) - } - - _, err = pktline.ParseLengthHeader([4]byte{'0', '0', '0', 'x'}) - if !errors.Is(err, pktline.ErrInvalidLength) { - t.Fatalf("got err %v, want ErrInvalidLength", err) - } -} diff --git a/network/protocol/pktline/type.go b/network/protocol/pktline/type.go deleted file mode 100644 index 641d1c6c..00000000 --- a/network/protocol/pktline/type.go +++ /dev/null @@ -1,15 +0,0 @@ -package pktline - -// PacketType identifies the kind of pkt-line frame. -type PacketType uint8 - -const ( - // PacketData is a regular data frame whose payload is application-defined. - PacketData PacketType = iota - // PacketFlush is control frame 0000 and marks end of a message. - PacketFlush - // PacketDelim is control frame 0001 and separates sections in protocol v2. - PacketDelim - // PacketResponseEnd is control frame 0002 and marks response end on stateless v2 transports. - PacketResponseEnd -) diff --git a/network/protocol/sideband64k/append.go b/network/protocol/sideband64k/append.go deleted file mode 100644 index db6527f8..00000000 --- a/network/protocol/sideband64k/append.go +++ /dev/null @@ -1,40 +0,0 @@ -package sideband64k - -import ( - "fmt" - - "codeberg.org/lindenii/furgit/network/protocol/pktline" -) - -// AppendBand appends one side-band-64k data frame to dst. -func AppendBand(dst []byte, band Band, payload []byte) ([]byte, error) { - if !validBand(band) { - return dst, fmt.Errorf("%w: %d", ErrInvalidBand, band) - } - - maxData := effectiveMaxData(DataMax) - if len(payload) > maxData { - return dst, fmt.Errorf("%w: %d > %d", ErrTooLarge, len(payload), maxData) - } - - framed := make([]byte, len(payload)+1) - framed[0] = byte(band) - copy(framed[1:], payload) - - return pktline.AppendData(dst, framed) -} - -// AppendData appends one band-1 data frame to dst. -func AppendData(dst, payload []byte) ([]byte, error) { - return AppendBand(dst, BandData, payload) -} - -// AppendProgress appends one band-2 progress frame to dst. -func AppendProgress(dst, payload []byte) ([]byte, error) { - return AppendBand(dst, BandProgress, payload) -} - -// AppendError appends one band-3 error frame to dst. -func AppendError(dst, payload []byte) ([]byte, error) { - return AppendBand(dst, BandError, payload) -} diff --git a/network/protocol/sideband64k/append_helpers_test.go b/network/protocol/sideband64k/append_helpers_test.go deleted file mode 100644 index 03196c38..00000000 --- a/network/protocol/sideband64k/append_helpers_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package sideband64k_test - -import ( - "testing" - - "codeberg.org/lindenii/furgit/network/protocol/sideband64k" -) - -func TestAppendHelpers(t *testing.T) { - t.Parallel() - - out, err := sideband64k.AppendData(nil, []byte("a")) - if err != nil { - t.Fatalf("AppendData: %v", err) - } - - out, err = sideband64k.AppendProgress(out, []byte("b")) - if err != nil { - t.Fatalf("AppendProgress: %v", err) - } - - out, err = sideband64k.AppendError(out, []byte("c")) - if err != nil { - t.Fatalf("AppendError: %v", err) - } - - if got, want := string(out), "0006\x01a0006\x02b0006\x03c"; got != want { - t.Fatalf("got %q, want %q", got, want) - } -} diff --git a/network/protocol/sideband64k/append_preserves_dst_on_error_test.go b/network/protocol/sideband64k/append_preserves_dst_on_error_test.go deleted file mode 100644 index 6fed4e4a..00000000 --- a/network/protocol/sideband64k/append_preserves_dst_on_error_test.go +++ /dev/null @@ -1,34 +0,0 @@ -package sideband64k_test - -import ( - "bytes" - "errors" - "testing" - - "codeberg.org/lindenii/furgit/network/protocol/sideband64k" -) - -func TestAppendBandPreservesDstOnError(t *testing.T) { - t.Parallel() - - orig := []byte("seed") - dst := append([]byte(nil), orig...) - - out, err := sideband64k.AppendBand(dst, 4, []byte("x")) - if !errors.Is(err, sideband64k.ErrInvalidBand) { - t.Fatalf("got err %v, want ErrInvalidBand", err) - } - - if !bytes.Equal(out, orig) { - t.Fatalf("got %q, want %q", string(out), string(orig)) - } - - out, err = sideband64k.AppendData(dst, bytes.Repeat([]byte{'x'}, sideband64k.DataMax+1)) - if !errors.Is(err, sideband64k.ErrTooLarge) { - t.Fatalf("got err %v, want ErrTooLarge", err) - } - - if !bytes.Equal(out, orig) { - t.Fatalf("got %q, want %q", string(out), string(orig)) - } -} diff --git a/network/protocol/sideband64k/band.go b/network/protocol/sideband64k/band.go deleted file mode 100644 index 73c61fd8..00000000 --- a/network/protocol/sideband64k/band.go +++ /dev/null @@ -1,13 +0,0 @@ -package sideband64k - -// Band identifies the sideband stream within a pkt-line data frame. -type Band uint8 - -const ( - // BandData carries primary payload bytes. - BandData Band = 1 - // BandProgress carries progress or informational messages. - BandProgress Band = 2 - // BandError carries fatal error messages. - BandError Band = 3 -) diff --git a/network/protocol/sideband64k/chunk_writer.go b/network/protocol/sideband64k/chunk_writer.go deleted file mode 100644 index 78c66edf..00000000 --- a/network/protocol/sideband64k/chunk_writer.go +++ /dev/null @@ -1,73 +0,0 @@ -package sideband64k - -import "io" - -// ChunkWriter packetizes arbitrary stream bytes into side-band-64k data frames -// for one fixed band. -// -// It never writes control packets automatically. -// -// Labels: MT-Unsafe. -type ChunkWriter struct { - enc *Encoder - band Band -} - -// NewChunkWriter creates a chunking adapter over enc for one band. -// -// Labels: Deps-Borrowed, Life-Parent. -func NewChunkWriter(enc *Encoder, band Band) *ChunkWriter { - return &ChunkWriter{enc: enc, band: band} -} - -// Write splits p into sideband frames not larger than enc's maxData. -func (cw *ChunkWriter) Write(p []byte) (int, error) { - total := 0 - maxData := cw.enc.effectiveMaxData() - - for len(p) > 0 { - n := min(len(p), maxData) - - err := cw.enc.WriteBand(cw.band, p[:n]) - if err != nil { - return total, err - } - - total += n - p = p[n:] - } - - return total, nil -} - -// ReadFrom reads from r and writes sideband frames to the encoder. -func (cw *ChunkWriter) ReadFrom(r io.Reader) (int64, error) { - buf := make([]byte, cw.enc.effectiveMaxData()) - - var total int64 - - for { - n, err := r.Read(buf) - if n > 0 { - werr := cw.enc.WriteBand(cw.band, buf[:n]) - if werr != nil { - return total, werr - } - - total += int64(n) - } - - if err != nil { - if err == io.EOF { - return total, nil - } - - return total, err - } - } -} - -// Flush flushes buffered output in the underlying transport. -func (cw *ChunkWriter) Flush() error { - return cw.enc.Flush() -} diff --git a/network/protocol/sideband64k/chunk_writer_write_and_read_from_test.go b/network/protocol/sideband64k/chunk_writer_write_and_read_from_test.go deleted file mode 100644 index ef2b0fff..00000000 --- a/network/protocol/sideband64k/chunk_writer_write_and_read_from_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package sideband64k_test - -import ( - "bufio" - "bytes" - "strings" - "testing" - - "codeberg.org/lindenii/furgit/network/protocol/sideband64k" -) - -func TestChunkWriterWriteAndReadFrom(t *testing.T) { - t.Parallel() - - var out bytes.Buffer - - bw := bufio.NewWriter(&out) - enc := sideband64k.NewEncoder(bw) - enc.SetMaxData(3) - - cw := sideband64k.NewChunkWriter(enc, sideband64k.BandProgress) - - n, err := cw.Write([]byte("abcdefg")) - if err != nil { - t.Fatalf("Write: %v", err) - } - - if n != 7 { - t.Fatalf("Write n=%d, want 7", n) - } - - err = enc.Flush() - if err != nil { - t.Fatalf("Flush: %v", err) - } - - if got, want := out.String(), "0008\x02abc0008\x02def0006\x02g"; got != want { - t.Fatalf("got %q, want %q", got, want) - } - - out.Reset() - - rn, err := cw.ReadFrom(strings.NewReader("wxyz")) - if err != nil { - t.Fatalf("ReadFrom: %v", err) - } - - if rn != 4 { - t.Fatalf("ReadFrom n=%d, want 4", rn) - } - - err = enc.Flush() - if err != nil { - t.Fatalf("Flush: %v", err) - } - - if got, want := out.String(), "0008\x02wxy0006\x02z"; got != want { - t.Fatalf("got %q, want %q", got, want) - } -} diff --git a/network/protocol/sideband64k/constants.go b/network/protocol/sideband64k/constants.go deleted file mode 100644 index 2a6a2e47..00000000 --- a/network/protocol/sideband64k/constants.go +++ /dev/null @@ -1,10 +0,0 @@ -package sideband64k - -import "codeberg.org/lindenii/furgit/network/protocol/pktline" - -const ( - // PacketMax is the maximum on-wire pkt-line size used by side-band-64k. - PacketMax = pktline.LargePacketMax - // DataMax is the maximum sideband payload size excluding the 1-byte band designator. - DataMax = pktline.LargePacketDataMax - 1 -) diff --git a/network/protocol/sideband64k/decoder.go b/network/protocol/sideband64k/decoder.go deleted file mode 100644 index e34f5d12..00000000 --- a/network/protocol/sideband64k/decoder.go +++ /dev/null @@ -1,162 +0,0 @@ -package sideband64k - -import ( - "fmt" - "io" - - "codeberg.org/lindenii/furgit/network/protocol/pktline" -) - -// ReadOptions controls sideband decoding behavior. -type ReadOptions struct { - // ChompLF removes one trailing '\n' from FrameData payloads only. - ChompLF bool -} - -// Decoder reads side-band-64k frames from an io.Reader. -// -// It preserves frame boundaries and supports one-frame lookahead via -// PeekFrame. -// -// Labels: MT-Unsafe. -type Decoder struct { - dec *pktline.Decoder - maxData int - opts ReadOptions - - peeked bool - peek Frame - peekErr error -} - -// NewDecoder creates a decoder over r. -// -// Labels: Deps-Borrowed, Life-Parent. -func NewDecoder(r io.Reader, opts ReadOptions) *Decoder { - d := &Decoder{ - dec: pktline.NewDecoder(r, pktline.ReadOptions{}), - maxData: DataMax, - opts: opts, - } - d.dec.SetMaxData(pktline.LargePacketDataMax) - - return d -} - -// SetMaxData sets maximum payload size accepted for one sideband data packet. -// -// Non-positive n resets to DataMax. -func (d *Decoder) SetMaxData(n int) { - if n <= 0 { - d.maxData = DataMax - - return - } - - d.maxData = n -} - -// ReadFrame reads one frame. -func (d *Decoder) ReadFrame() (Frame, error) { - if d.peeked { - d.peeked = false - - return cloneFrame(d.peek), d.peekErr - } - - return d.readFrame() -} - -// PeekFrame returns the next frame without consuming it. -func (d *Decoder) PeekFrame() (Frame, error) { - if !d.peeked { - d.peek, d.peekErr = d.readFrame() - d.peeked = true - } - - return cloneFrame(d.peek), d.peekErr -} - -func (d *Decoder) readFrame() (Frame, error) { - f, err := d.dec.ReadFrame() - if err != nil { - return Frame{}, err - } - - switch f.Type { - case pktline.PacketFlush: - return Frame{Type: FrameFlush}, nil - case pktline.PacketDelim: - return Frame{Type: FrameDelim}, nil - case pktline.PacketResponseEnd: - return Frame{Type: FrameResponseEnd}, nil - case pktline.PacketData: - if len(f.Payload) == 0 { - return Frame{}, &ProtocolError{Reason: "missing sideband designator"} - } - - payload := f.Payload[1:] - if len(payload) > d.effectiveMaxData() { - return Frame{}, fmt.Errorf("%w: %d > %d", ErrTooLarge, len(payload), d.effectiveMaxData()) - } - - band := Band(f.Payload[0]) - if !validBand(band) { - return Frame{}, &ProtocolError{Reason: fmt.Sprintf("%v: %d", ErrInvalidBand, band)} - } - - payload = append([]byte(nil), payload...) - if d.opts.ChompLF && band == BandData && len(payload) > 0 && payload[len(payload)-1] == '\n' { - payload = payload[:len(payload)-1] - } - - return Frame{ - Type: frameTypeForBand(band), - Payload: payload, - }, nil - default: - return Frame{}, &ProtocolError{Reason: "unknown pkt-line frame type"} - } -} - -func (d *Decoder) effectiveMaxData() int { - return effectiveMaxData(d.maxData) -} - -func cloneFrame(f Frame) Frame { - if f.Type == FrameFlush || f.Type == FrameDelim || f.Type == FrameResponseEnd { - return Frame{Type: f.Type} - } - - out := Frame{Type: f.Type} - if f.Payload != nil { - out.Payload = append([]byte(nil), f.Payload...) - } - - return out -} - -func validBand(band Band) bool { - return band == BandData || band == BandProgress || band == BandError -} - -func frameTypeForBand(band Band) FrameType { - switch band { - case BandData: - return FrameData - case BandProgress: - return FrameProgress - case BandError: - return FrameError - default: - panic("invalid sideband64k band") - } -} - -func effectiveMaxData(n int) int { - if n <= 0 || n > DataMax { - return DataMax - } - - return n -} diff --git a/network/protocol/sideband64k/decoder_data_control_and_keepalive_test.go b/network/protocol/sideband64k/decoder_data_control_and_keepalive_test.go deleted file mode 100644 index 9103c492..00000000 --- a/network/protocol/sideband64k/decoder_data_control_and_keepalive_test.go +++ /dev/null @@ -1,78 +0,0 @@ -package sideband64k_test - -import ( - "strings" - "testing" - - "codeberg.org/lindenii/furgit/network/protocol/sideband64k" -) - -func TestDecoderDataControlAndKeepalive(t *testing.T) { - t.Parallel() - - input := "0007\x01a\n0005\x010007\x02p\n0007\x03e\n000100020000" - dec := sideband64k.NewDecoder(strings.NewReader(input), sideband64k.ReadOptions{ChompLF: true}) - - f, err := dec.ReadFrame() - if err != nil { - t.Fatalf("ReadFrame #1: %v", err) - } - - if f.Type != sideband64k.FrameData || string(f.Payload) != "a" { - t.Fatalf("frame #1 = %#v", f) - } - - f, err = dec.ReadFrame() - if err != nil { - t.Fatalf("ReadFrame #2: %v", err) - } - - if f.Type != sideband64k.FrameData || len(f.Payload) != 0 { - t.Fatalf("frame #2 = %#v, want empty data", f) - } - - f, err = dec.ReadFrame() - if err != nil { - t.Fatalf("ReadFrame #3: %v", err) - } - - if f.Type != sideband64k.FrameProgress || string(f.Payload) != "p\n" { - t.Fatalf("frame #3 = %#v", f) - } - - f, err = dec.ReadFrame() - if err != nil { - t.Fatalf("ReadFrame #4: %v", err) - } - - if f.Type != sideband64k.FrameError || string(f.Payload) != "e\n" { - t.Fatalf("frame #4 = %#v", f) - } - - f, err = dec.ReadFrame() - if err != nil { - t.Fatalf("ReadFrame #5: %v", err) - } - - if f.Type != sideband64k.FrameDelim { - t.Fatalf("frame #5 type = %v, want FrameDelim", f.Type) - } - - f, err = dec.ReadFrame() - if err != nil { - t.Fatalf("ReadFrame #6: %v", err) - } - - if f.Type != sideband64k.FrameResponseEnd { - t.Fatalf("frame #6 type = %v, want FrameResponseEnd", f.Type) - } - - f, err = dec.ReadFrame() - if err != nil { - t.Fatalf("ReadFrame #7: %v", err) - } - - if f.Type != sideband64k.FrameFlush { - t.Fatalf("frame #7 type = %v, want FrameFlush", f.Type) - } -} diff --git a/network/protocol/sideband64k/decoder_invalid_band_test.go b/network/protocol/sideband64k/decoder_invalid_band_test.go deleted file mode 100644 index a4bc11a9..00000000 --- a/network/protocol/sideband64k/decoder_invalid_band_test.go +++ /dev/null @@ -1,20 +0,0 @@ -package sideband64k_test - -import ( - "errors" - "strings" - "testing" - - "codeberg.org/lindenii/furgit/network/protocol/sideband64k" -) - -func TestDecoderInvalidBand(t *testing.T) { - t.Parallel() - - dec := sideband64k.NewDecoder(strings.NewReader("0005\x04"), sideband64k.ReadOptions{}) - _, err := dec.ReadFrame() - - if _, ok := errors.AsType[*sideband64k.ProtocolError](err); !ok { - t.Fatalf("got err %v, want ProtocolError", err) - } -} diff --git a/network/protocol/sideband64k/decoder_invalid_empty_payload_test.go b/network/protocol/sideband64k/decoder_invalid_empty_payload_test.go deleted file mode 100644 index df9faa71..00000000 --- a/network/protocol/sideband64k/decoder_invalid_empty_payload_test.go +++ /dev/null @@ -1,20 +0,0 @@ -package sideband64k_test - -import ( - "errors" - "strings" - "testing" - - "codeberg.org/lindenii/furgit/network/protocol/sideband64k" -) - -func TestDecoderInvalidEmptyPayload(t *testing.T) { - t.Parallel() - - dec := sideband64k.NewDecoder(strings.NewReader("0004"), sideband64k.ReadOptions{}) - _, err := dec.ReadFrame() - - if _, ok := errors.AsType[*sideband64k.ProtocolError](err); !ok { - t.Fatalf("got err %v, want ProtocolError", err) - } -} diff --git a/network/protocol/sideband64k/decoder_malformed_pktline_test.go b/network/protocol/sideband64k/decoder_malformed_pktline_test.go deleted file mode 100644 index 5e4e4551..00000000 --- a/network/protocol/sideband64k/decoder_malformed_pktline_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package sideband64k_test - -import ( - "errors" - "strings" - "testing" - - "codeberg.org/lindenii/furgit/network/protocol/pktline" - "codeberg.org/lindenii/furgit/network/protocol/sideband64k" -) - -func TestDecoderInvalid0003(t *testing.T) { - t.Parallel() - - dec := sideband64k.NewDecoder(strings.NewReader("0003"), sideband64k.ReadOptions{}) - _, err := dec.ReadFrame() - - if _, ok := errors.AsType[*pktline.ProtocolError](err); !ok { - t.Fatalf("got err %v, want pktline.ProtocolError", err) - } -} - -func TestDecoderRejectsOverMaximumLength(t *testing.T) { - t.Parallel() - - dec := sideband64k.NewDecoder(strings.NewReader("fffe"), sideband64k.ReadOptions{}) - _, err := dec.ReadFrame() - - if _, ok := errors.AsType[*pktline.ProtocolError](err); !ok { - t.Fatalf("got err %v, want pktline.ProtocolError", err) - } -} diff --git a/network/protocol/sideband64k/decoder_partial_read_test.go b/network/protocol/sideband64k/decoder_partial_read_test.go deleted file mode 100644 index 3f103787..00000000 --- a/network/protocol/sideband64k/decoder_partial_read_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package sideband64k_test - -import ( - "testing" - - "codeberg.org/lindenii/furgit/network/protocol/sideband64k" -) - -func TestDecoderHandlesPartialReads(t *testing.T) { - t.Parallel() - - r := &byteReader{data: []byte("0007\x02ok0000")} - dec := sideband64k.NewDecoder(r, sideband64k.ReadOptions{}) - - f, err := dec.ReadFrame() - if err != nil { - t.Fatalf("ReadFrame #1: %v", err) - } - - if f.Type != sideband64k.FrameProgress || string(f.Payload) != "ok" { - t.Fatalf("frame #1 = %#v", f) - } - - f, err = dec.ReadFrame() - if err != nil { - t.Fatalf("ReadFrame #2: %v", err) - } - - if f.Type != sideband64k.FrameFlush { - t.Fatalf("frame #2 = %#v", f) - } -} diff --git a/network/protocol/sideband64k/decoder_peek_test.go b/network/protocol/sideband64k/decoder_peek_test.go deleted file mode 100644 index 31397762..00000000 --- a/network/protocol/sideband64k/decoder_peek_test.go +++ /dev/null @@ -1,34 +0,0 @@ -package sideband64k_test - -import ( - "strings" - "testing" - - "codeberg.org/lindenii/furgit/network/protocol/sideband64k" -) - -func TestDecoderPeek(t *testing.T) { - t.Parallel() - - dec := sideband64k.NewDecoder(strings.NewReader("0006\x01x0000"), sideband64k.ReadOptions{}) - - f, err := dec.PeekFrame() - if err != nil { - t.Fatalf("PeekFrame: %v", err) - } - - if f.Type != sideband64k.FrameData || string(f.Payload) != "x" { - t.Fatalf("peek frame = %#v", f) - } - - f.Payload[0] = 'y' - - f, err = dec.ReadFrame() - if err != nil { - t.Fatalf("ReadFrame: %v", err) - } - - if f.Type != sideband64k.FrameData || string(f.Payload) != "x" { - t.Fatalf("read frame = %#v", f) - } -} diff --git a/network/protocol/sideband64k/decoder_resync_after_over_max_data_test.go b/network/protocol/sideband64k/decoder_resync_after_over_max_data_test.go deleted file mode 100644 index b0ae600a..00000000 --- a/network/protocol/sideband64k/decoder_resync_after_over_max_data_test.go +++ /dev/null @@ -1,51 +0,0 @@ -package sideband64k_test - -import ( - "bufio" - "bytes" - "errors" - "testing" - - "codeberg.org/lindenii/furgit/network/protocol/sideband64k" -) - -func TestDecoderResyncAfterOverMaxData(t *testing.T) { - t.Parallel() - - var b bytes.Buffer - - bw := bufio.NewWriter(&b) - enc := sideband64k.NewEncoder(bw) - - err := enc.WriteData([]byte("abcd")) - if err != nil { - t.Fatalf("WriteData #1: %v", err) - } - - err = enc.WriteData([]byte("z")) - if err != nil { - t.Fatalf("WriteData #2: %v", err) - } - - err = enc.Flush() - if err != nil { - t.Fatalf("Flush: %v", err) - } - - dec := sideband64k.NewDecoder(bytes.NewReader(b.Bytes()), sideband64k.ReadOptions{}) - dec.SetMaxData(1) - - _, err = dec.ReadFrame() - if !errors.Is(err, sideband64k.ErrTooLarge) { - t.Fatalf("got err %v, want ErrTooLarge", err) - } - - f, err := dec.ReadFrame() - if err != nil { - t.Fatalf("ReadFrame #2: %v", err) - } - - if f.Type != sideband64k.FrameData || string(f.Payload) != "z" { - t.Fatalf("got frame %#v, want data z", f) - } -} diff --git a/network/protocol/sideband64k/decoder_resync_after_over_wire_max_test.go b/network/protocol/sideband64k/decoder_resync_after_over_wire_max_test.go deleted file mode 100644 index 73966925..00000000 --- a/network/protocol/sideband64k/decoder_resync_after_over_wire_max_test.go +++ /dev/null @@ -1,37 +0,0 @@ -package sideband64k_test - -import ( - "bytes" - "errors" - "testing" - - "codeberg.org/lindenii/furgit/network/protocol/pktline" - "codeberg.org/lindenii/furgit/network/protocol/sideband64k" -) - -func TestDecoderResyncAfterOverWireMax(t *testing.T) { - t.Parallel() - - var b bytes.Buffer - - _, _ = b.WriteString("ffff") - _, _ = b.Write(bytes.Repeat([]byte{'a'}, 65531)) - _, _ = b.WriteString("0006\x01z") - - dec := sideband64k.NewDecoder(bytes.NewReader(b.Bytes()), sideband64k.ReadOptions{}) - - _, err := dec.ReadFrame() - - if _, ok := errors.AsType[*pktline.ProtocolError](err); !ok { - t.Fatalf("got err %v, want pktline.ProtocolError", err) - } - - f, err := dec.ReadFrame() - if err != nil { - t.Fatalf("ReadFrame #2: %v", err) - } - - if f.Type != sideband64k.FrameData || string(f.Payload) != "z" { - t.Fatalf("got frame %#v, want data z", f) - } -} diff --git a/network/protocol/sideband64k/decoder_unexpected_eof_test.go b/network/protocol/sideband64k/decoder_unexpected_eof_test.go deleted file mode 100644 index d9d71fb9..00000000 --- a/network/protocol/sideband64k/decoder_unexpected_eof_test.go +++ /dev/null @@ -1,21 +0,0 @@ -package sideband64k_test - -import ( - "errors" - "io" - "strings" - "testing" - - "codeberg.org/lindenii/furgit/network/protocol/sideband64k" -) - -func TestDecoderUnexpectedEOF(t *testing.T) { - t.Parallel() - - dec := sideband64k.NewDecoder(strings.NewReader("0006\x01"), sideband64k.ReadOptions{}) - - _, err := dec.ReadFrame() - if !errors.Is(err, io.ErrUnexpectedEOF) { - t.Fatalf("got err %v, want io.ErrUnexpectedEOF", err) - } -} diff --git a/network/protocol/sideband64k/doc.go b/network/protocol/sideband64k/doc.go deleted file mode 100644 index 55c33650..00000000 --- a/network/protocol/sideband64k/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package sideband64k implements Git side-band-64k multiplexing over pkt-line. -package sideband64k diff --git a/network/protocol/sideband64k/encoder.go b/network/protocol/sideband64k/encoder.go deleted file mode 100644 index 6cdef38a..00000000 --- a/network/protocol/sideband64k/encoder.go +++ /dev/null @@ -1,103 +0,0 @@ -package sideband64k - -import ( - "fmt" - - "codeberg.org/lindenii/furgit/common/iowrap" - "codeberg.org/lindenii/furgit/network/protocol/pktline" -) - -// Encoder writes side-band-64k frames to a flush-capable output transport. -// -// It writes exactly one frame per method call and does not auto-chunk data. -// -// Labels: MT-Unsafe. -type Encoder struct { - enc *pktline.Encoder - maxData int -} - -// NewEncoder creates an encoder over w. -// -// Labels: Deps-Borrowed, Life-Parent. -func NewEncoder(w iowrap.WriteFlusher) *Encoder { - return &Encoder{ - enc: pktline.NewEncoder(w), - maxData: DataMax, - } -} - -// SetMaxData sets the maximum payload size accepted by WriteBand. -// -// Non-positive n resets to DataMax. -func (e *Encoder) SetMaxData(n int) { - if n <= 0 { - e.maxData = DataMax - - return - } - - e.maxData = n -} - -// WriteBand writes one side-band-64k data frame for the given band. -func (e *Encoder) WriteBand(band Band, p []byte) error { - if !validBand(band) { - return fmt.Errorf("%w: %d", ErrInvalidBand, band) - } - - maxData := e.effectiveMaxData() - if len(p) > maxData { - return fmt.Errorf("%w: %d > %d", ErrTooLarge, len(p), maxData) - } - - framed := make([]byte, len(p)+1) - framed[0] = byte(band) - copy(framed[1:], p) - - return e.enc.WriteData(framed) -} - -// WriteData writes one band-1 data frame. -func (e *Encoder) WriteData(p []byte) error { - return e.WriteBand(BandData, p) -} - -// WriteProgress writes one band-2 progress frame. -func (e *Encoder) WriteProgress(p []byte) error { - return e.WriteBand(BandProgress, p) -} - -// WriteError writes one band-3 error frame. -func (e *Encoder) WriteError(p []byte) error { - return e.WriteBand(BandError, p) -} - -// WriteFlushPacket writes control frame 0000 (flush-pkt). -func (e *Encoder) WriteFlushPacket() error { - return e.enc.WriteFlushPacket() -} - -// WriteDelimPacket writes control frame 0001 (delim-pkt). -func (e *Encoder) WriteDelimPacket() error { - return e.enc.WriteDelimPacket() -} - -// WriteResponseEndPacket writes control frame 0002 (response-end-pkt). -func (e *Encoder) WriteResponseEndPacket() error { - return e.enc.WriteResponseEndPacket() -} - -// Flush flushes buffered output in the underlying transport. -func (e *Encoder) Flush() error { - return e.enc.Flush() -} - -// WriteFlushPacketAndFlush writes a flush-pkt (0000) then flushes transport I/O. -func (e *Encoder) WriteFlushPacketAndFlush() error { - return e.enc.WriteFlushPacketAndFlush() -} - -func (e *Encoder) effectiveMaxData() int { - return effectiveMaxData(e.maxData) -} diff --git a/network/protocol/sideband64k/encoder_buffered_flush_behavior_test.go b/network/protocol/sideband64k/encoder_buffered_flush_behavior_test.go deleted file mode 100644 index 83103ea3..00000000 --- a/network/protocol/sideband64k/encoder_buffered_flush_behavior_test.go +++ /dev/null @@ -1,59 +0,0 @@ -package sideband64k_test - -import ( - "bufio" - "bytes" - "testing" - - "codeberg.org/lindenii/furgit/network/protocol/sideband64k" -) - -func TestEncoderBufferedFlushBehavior(t *testing.T) { - t.Parallel() - - var out bytes.Buffer - - bw := bufio.NewWriter(&out) - enc := sideband64k.NewEncoder(bw) - - err := enc.WriteData([]byte("hello")) - if err != nil { - t.Fatalf("WriteData: %v", err) - } - - err = enc.WriteFlushPacket() - if err != nil { - t.Fatalf("WriteFlushPacket: %v", err) - } - - if out.Len() != 0 { - t.Fatalf("WriteFlushPacket should not flush I/O, got %q", out.String()) - } - - err = enc.Flush() - if err != nil { - t.Fatalf("Flush: %v", err) - } - - if got, want := out.String(), "000a\x01hello0000"; got != want { - t.Fatalf("got %q, want %q", got, want) - } - - out.Reset() - bw = bufio.NewWriter(&out) - enc = sideband64k.NewEncoder(bw) - - err = enc.WriteData([]byte("yo")) - if err != nil { - t.Fatalf("WriteData: %v", err) - } - - err = enc.WriteFlushPacketAndFlush() - if err != nil { - t.Fatalf("WriteFlushPacketAndFlush: %v", err) - } - - if got, want := out.String(), "0007\x01yo0000"; got != want { - t.Fatalf("got %q, want %q", got, want) - } -} diff --git a/network/protocol/sideband64k/encoder_partial_write_test.go b/network/protocol/sideband64k/encoder_partial_write_test.go deleted file mode 100644 index 97c8f762..00000000 --- a/network/protocol/sideband64k/encoder_partial_write_test.go +++ /dev/null @@ -1,46 +0,0 @@ -package sideband64k_test - -import ( - "errors" - "io" - "testing" - - "codeberg.org/lindenii/furgit/network/protocol/sideband64k" -) - -func TestEncoderHandlesPartialWrites(t *testing.T) { - t.Parallel() - - dst := &limitWriter{maxPerWrite: 2} - enc := sideband64k.NewEncoder(dst) - - err := enc.WriteProgress([]byte("abc")) - if err != nil { - t.Fatalf("WriteProgress: %v", err) - } - - err = enc.WriteFlushPacketAndFlush() - if err != nil { - t.Fatalf("WriteFlushPacketAndFlush: %v", err) - } - - if got, want := dst.buf.String(), "0008\x02abc0000"; got != want { - t.Fatalf("got %q, want %q", got, want) - } - - if dst.flushes != 1 { - t.Fatalf("flushes=%d, want 1", dst.flushes) - } -} - -func TestEncoderReturnsShortWrite(t *testing.T) { - t.Parallel() - - dst := &limitWriter{shortWrite: true} - enc := sideband64k.NewEncoder(dst) - - err := enc.WriteData([]byte("x")) - if !errors.Is(err, io.ErrShortWrite) { - t.Fatalf("got err %v, want io.ErrShortWrite", err) - } -} diff --git a/network/protocol/sideband64k/encoder_set_max_data_cannot_exceed_wire_limit_test.go b/network/protocol/sideband64k/encoder_set_max_data_cannot_exceed_wire_limit_test.go deleted file mode 100644 index 2bfcf073..00000000 --- a/network/protocol/sideband64k/encoder_set_max_data_cannot_exceed_wire_limit_test.go +++ /dev/null @@ -1,23 +0,0 @@ -package sideband64k_test - -import ( - "bytes" - "errors" - "testing" - - "codeberg.org/lindenii/furgit/network/protocol/sideband64k" -) - -func TestEncoderSetMaxDataCannotExceedWireLimit(t *testing.T) { - t.Parallel() - - var dst limitWriter - - enc := sideband64k.NewEncoder(&dst) - enc.SetMaxData(sideband64k.DataMax + 100) - - err := enc.WriteData(bytes.Repeat([]byte{'x'}, sideband64k.DataMax+1)) - if !errors.Is(err, sideband64k.ErrTooLarge) { - t.Fatalf("got err %v, want ErrTooLarge", err) - } -} diff --git a/network/protocol/sideband64k/encoder_writes_frames_test.go b/network/protocol/sideband64k/encoder_writes_frames_test.go deleted file mode 100644 index 85fe5845..00000000 --- a/network/protocol/sideband64k/encoder_writes_frames_test.go +++ /dev/null @@ -1,58 +0,0 @@ -package sideband64k_test - -import ( - "bufio" - "bytes" - "testing" - - "codeberg.org/lindenii/furgit/network/protocol/sideband64k" -) - -func TestEncoderWritesFrames(t *testing.T) { - t.Parallel() - - var b bytes.Buffer - - bw := bufio.NewWriter(&b) - enc := sideband64k.NewEncoder(bw) - - err := enc.WriteData([]byte("hi")) - if err != nil { - t.Fatalf("WriteData: %v", err) - } - - err = enc.WriteProgress([]byte("ok")) - if err != nil { - t.Fatalf("WriteProgress: %v", err) - } - - err = enc.WriteError([]byte("no")) - if err != nil { - t.Fatalf("WriteError: %v", err) - } - - err = enc.WriteFlushPacket() - if err != nil { - t.Fatalf("WriteFlushPacket: %v", err) - } - - err = enc.WriteDelimPacket() - if err != nil { - t.Fatalf("WriteDelimPacket: %v", err) - } - - err = enc.WriteResponseEndPacket() - if err != nil { - t.Fatalf("WriteResponseEndPacket: %v", err) - } - - err = enc.Flush() - if err != nil { - t.Fatalf("Flush: %v", err) - } - - want := "0007\x01hi0007\x02ok0007\x03no000000010002" - if got := b.String(); got != want { - t.Fatalf("got %q, want %q", got, want) - } -} diff --git a/network/protocol/sideband64k/errors.go b/network/protocol/sideband64k/errors.go deleted file mode 100644 index 44e7c165..00000000 --- a/network/protocol/sideband64k/errors.go +++ /dev/null @@ -1,27 +0,0 @@ -package sideband64k - -import "errors" - -var ( - // ErrTooLarge indicates a payload exceeds configured sideband data limits. - ErrTooLarge = errors.New("sideband64k: payload too large") - // ErrInvalidBand indicates a data frame has an invalid sideband designator. - ErrInvalidBand = errors.New("sideband64k: invalid band designator") -) - -// ProtocolError reports invalid side-band-64k framing. -type ProtocolError struct { - Reason string -} - -func (e *ProtocolError) Error() string { - if e == nil { - return "" - } - - if e.Reason == "" { - return "sideband64k: protocol error" - } - - return "sideband64k: protocol error: " + e.Reason -} diff --git a/network/protocol/sideband64k/frame.go b/network/protocol/sideband64k/frame.go deleted file mode 100644 index 1335a8e3..00000000 --- a/network/protocol/sideband64k/frame.go +++ /dev/null @@ -1,12 +0,0 @@ -package sideband64k - -// Frame is one decoded side-band-64k frame. -// -// For FrameData, FrameProgress, and FrameError, Payload holds frame bytes and -// may be empty. -// -// For control frames, Payload is nil. -type Frame struct { - Type FrameType - Payload []byte -} diff --git a/network/protocol/sideband64k/frame_type.go b/network/protocol/sideband64k/frame_type.go deleted file mode 100644 index 052d8b10..00000000 --- a/network/protocol/sideband64k/frame_type.go +++ /dev/null @@ -1,19 +0,0 @@ -package sideband64k - -// FrameType identifies the kind of decoded sideband frame. -type FrameType uint8 - -const ( - // FrameData carries primary payload bytes from band 1. - FrameData FrameType = iota - // FrameProgress carries progress bytes from band 2. - FrameProgress - // FrameError carries fatal error bytes from band 3. - FrameError - // FrameFlush is pkt-line control frame 0000. - FrameFlush - // FrameDelim is pkt-line control frame 0001. - FrameDelim - // FrameResponseEnd is pkt-line control frame 0002. - FrameResponseEnd -) diff --git a/network/protocol/sideband64k/helpers_test.go b/network/protocol/sideband64k/helpers_test.go deleted file mode 100644 index f9b2608f..00000000 --- a/network/protocol/sideband64k/helpers_test.go +++ /dev/null @@ -1,46 +0,0 @@ -package sideband64k_test - -import ( - "bytes" - "io" -) - -type limitWriter struct { - buf bytes.Buffer - maxPerWrite int - flushes int - shortWrite bool -} - -func (w *limitWriter) Write(p []byte) (int, error) { - if w.shortWrite { - return 0, nil - } - - if w.maxPerWrite > 0 && len(p) > w.maxPerWrite { - p = p[:w.maxPerWrite] - } - - return w.buf.Write(p) -} - -func (w *limitWriter) Flush() error { - w.flushes++ - - return nil -} - -type byteReader struct { - data []byte -} - -func (r *byteReader) Read(p []byte) (int, error) { - if len(r.data) == 0 { - return 0, io.EOF - } - - p[0] = r.data[0] - r.data = r.data[1:] - - return 1, nil -} diff --git a/network/protocol/v0v1/doc.go b/network/protocol/v0v1/doc.go deleted file mode 100644 index 2c96ea23..00000000 --- a/network/protocol/v0v1/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package v0v1 provides common constants and routines for the V0 and V1 protocols. -package v0v1 diff --git a/network/protocol/v0v1/server/advertise.go b/network/protocol/v0v1/server/advertise.go deleted file mode 100644 index 8ec7dfd9..00000000 --- a/network/protocol/v0v1/server/advertise.go +++ /dev/null @@ -1,53 +0,0 @@ -package server - -import ( - "fmt" - "strings" -) - -// AdvertiseRefs writes one server ref advertisement. -func (session *Session) AdvertiseRefs(ad Advertisement, capabilityTokens []string) error { - if session.opts.Version == Version1 { - err := session.enc.WriteData([]byte("version 1\n")) - if err != nil { - return err - } - } - - capList := strings.Join(capabilityTokens, " ") - - refs := sortAdvertisedRefs(ad.Refs) - if len(refs) == 0 { - line := fmt.Sprintf("%s capabilities^{}\x00%s\n", session.opts.Algorithm.Zero(), capList) - - err := session.enc.WriteData([]byte(line)) - if err != nil { - return err - } - - return session.WriteFlushPacket() - } - - for i, entry := range refs { - line := fmt.Sprintf("%s %s", entry.ID, entry.Name) - if i == 0 { - line += "\x00" + capList - } - - err := session.enc.WriteData([]byte(line + "\n")) - if err != nil { - return err - } - - if entry.Peeled != nil { - peeled := fmt.Sprintf("%s %s^{}\n", *entry.Peeled, entry.Name) - - err = session.enc.WriteData([]byte(peeled)) - if err != nil { - return err - } - } - } - - return session.WriteFlushPacket() -} diff --git a/network/protocol/v0v1/server/advertise_test.go b/network/protocol/v0v1/server/advertise_test.go deleted file mode 100644 index 3ad7a725..00000000 --- a/network/protocol/v0v1/server/advertise_test.go +++ /dev/null @@ -1,101 +0,0 @@ -package server_test - -import ( - "strings" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - server "codeberg.org/lindenii/furgit/network/protocol/v0v1/server" - objectid "codeberg.org/lindenii/furgit/object/id" -) - -func TestAdvertiseRefsWritesVersionOneHeadCapsAndPeeledTag(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - headID := mustHexID(t, algo, "1") - tagID := mustHexID(t, algo, "2") - peeledID := mustHexID(t, algo, "3") - mainID := mustHexID(t, algo, "4") - - var out bufferWriteFlusher - - session := server.NewSession( - strings.NewReader(""), - &out, - server.Options{ - Version: server.Version1, - Algorithm: algo, - }, - ) - - err := session.AdvertiseRefs(server.Advertisement{ - Refs: []server.AdvertisedRef{ - {Name: "refs/tags/v1", ID: tagID, Peeled: &peeledID}, - {Name: "HEAD", ID: headID}, - {Name: "refs/heads/main", ID: mainID}, - }, - }, []string{ - "report-status", - "delete-refs", - "object-format=" + algo.String(), - "agent=furgit-test/1", - }) - if err != nil { - t.Fatalf("AdvertiseRefs: %v", err) - } - - got := out.String() - wantParts := []string{ - "000eversion 1\n", - headID.String() + " HEAD\x00report-status delete-refs object-format=" + algo.String() + " agent=furgit-test/1\n", - mainID.String() + " refs/heads/main\n", - tagID.String() + " refs/tags/v1\n", - peeledID.String() + " refs/tags/v1^{}\n", - "0000", - } - - for _, part := range wantParts { - if !strings.Contains(got, part) { - t.Fatalf("advertisement missing %q in %q", part, got) - } - } - }) -} - -func TestAdvertiseRefsWritesNoRefsCapabilitiesLine(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - var out bufferWriteFlusher - - session := server.NewSession( - strings.NewReader(""), - &out, - server.Options{ - Algorithm: algo, - }, - ) - - err := session.AdvertiseRefs(server.Advertisement{}, []string{ - "report-status", - "object-format=" + algo.String(), - }) - if err != nil { - t.Fatalf("AdvertiseRefs: %v", err) - } - - got := out.String() - - want := algo.Zero().String() + " capabilities^{}\x00report-status object-format=" + algo.String() + "\n" - if !strings.Contains(got, want) { - t.Fatalf("unexpected no-refs advertisement %q", got) - } - }) -} diff --git a/network/protocol/v0v1/server/advertised_ref.go b/network/protocol/v0v1/server/advertised_ref.go deleted file mode 100644 index cf6ddcc8..00000000 --- a/network/protocol/v0v1/server/advertised_ref.go +++ /dev/null @@ -1,22 +0,0 @@ -package server - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// AdvertisedRef is one ref entry in one v0/v1 server advertisement. -type AdvertisedRef struct { - // Name is the advertised reference name. It may be HEAD or one full - // reference name. - Name string - // ID is the object ID currently advertised for Name. - ID objectid.ObjectID - // Peeled is the peeled annotated-tag target when available. - // - // If set, advertisement writes one immediate "^{}" line after the - // main entry, matching Git's advertisement rules. - Peeled *objectid.ObjectID -} - -// Advertisement is one server-side ref advertisement. -type Advertisement struct { - Refs []AdvertisedRef -} diff --git a/network/protocol/v0v1/server/doc.go b/network/protocol/v0v1/server/doc.go deleted file mode 100644 index ea0b3f18..00000000 --- a/network/protocol/v0v1/server/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package server implements shared server-side Git protocol v0/v1 framing. -package server diff --git a/network/protocol/v0v1/server/errors.go b/network/protocol/v0v1/server/errors.go deleted file mode 100644 index 6a456234..00000000 --- a/network/protocol/v0v1/server/errors.go +++ /dev/null @@ -1,18 +0,0 @@ -package server - -// ProtocolError reports one malformed or unsupported protocol input. -type ProtocolError struct { - Reason string -} - -// Error returns the formatted error string. -func (err *ProtocolError) Error() string { - return "protocol/v0v1/server: protocol error: " + err.Reason -} - -// ErrUnexpectedPacket reports one unexpected pkt-line control packet. -var ErrUnexpectedPacket = &ProtocolError{Reason: "unexpected control packet"} - -// ErrSideBandNotEnabled reports one attempt to write sideband frames without a -// negotiated side-band-64k session. -var ErrSideBandNotEnabled = &ProtocolError{Reason: "side-band-64k not enabled"} diff --git a/network/protocol/v0v1/server/frame.go b/network/protocol/v0v1/server/frame.go deleted file mode 100644 index ad2a0801..00000000 --- a/network/protocol/v0v1/server/frame.go +++ /dev/null @@ -1,20 +0,0 @@ -package server - -import "codeberg.org/lindenii/furgit/network/protocol/pktline" - -// FrameType identifies one low-level v0/v1 server pkt-line frame type. -type FrameType = pktline.PacketType - -const ( - // FrameData is one data pkt-line. - FrameData = pktline.PacketData - // FrameFlush is one flush-pkt. - FrameFlush = pktline.PacketFlush - // FrameDelim is one delim-pkt. - FrameDelim = pktline.PacketDelim - // FrameResponseEnd is one response-end-pkt. - FrameResponseEnd = pktline.PacketResponseEnd -) - -// Frame is one decoded low-level pkt-line frame. -type Frame = pktline.Frame diff --git a/network/protocol/v0v1/server/helpers.go b/network/protocol/v0v1/server/helpers.go deleted file mode 100644 index 9a62f714..00000000 --- a/network/protocol/v0v1/server/helpers.go +++ /dev/null @@ -1,29 +0,0 @@ -package server - -import ( - "slices" -) - -func sortAdvertisedRefs(refs []AdvertisedRef) []AdvertisedRef { - out := append([]AdvertisedRef(nil), refs...) - slices.SortFunc(out, func(left, right AdvertisedRef) int { - if left.Name == "HEAD" && right.Name != "HEAD" { - return -1 - } - - if left.Name != "HEAD" && right.Name == "HEAD" { - return 1 - } - - switch { - case left.Name < right.Name: - return -1 - case left.Name > right.Name: - return 1 - default: - return 0 - } - }) - - return out -} diff --git a/network/protocol/v0v1/server/helpers_test.go b/network/protocol/v0v1/server/helpers_test.go deleted file mode 100644 index 261bbdc5..00000000 --- a/network/protocol/v0v1/server/helpers_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package server_test - -import ( - "bytes" - "strings" - "testing" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -type bufferWriteFlusher struct { - bytes.Buffer -} - -func (bufferWriteFlusher) Flush() error { - return nil -} - -func mustHexID(tb testing.TB, algo objectid.Algorithm, digit string) objectid.ObjectID { - tb.Helper() - - id, err := objectid.ParseHex(algo, strings.Repeat(digit, algo.HexLen())) - if err != nil { - tb.Fatalf("objectid.ParseHex(%q): %v", strings.Repeat(digit, algo.HexLen()), err) - } - - return id -} diff --git a/network/protocol/v0v1/server/receivepack/capabilities.go b/network/protocol/v0v1/server/receivepack/capabilities.go deleted file mode 100644 index e0ff51a3..00000000 --- a/network/protocol/v0v1/server/receivepack/capabilities.go +++ /dev/null @@ -1,192 +0,0 @@ -package receivepack - -import ( - "fmt" - "slices" - "strings" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// Capabilities describes one receive-pack capability set. -type Capabilities struct { - ReportStatus bool - ReportStatusV2 bool - DeleteRefs bool - SideBand64K bool - Quiet bool - Atomic bool - OfsDelta bool - PushOptions bool - PushCertNonce string - ObjectFormat objectid.Algorithm - SessionID string - Agent string -} - -// Normalize returns one normalized copy of caps. -func (caps Capabilities) Normalize(defaultAlgorithm objectid.Algorithm) Capabilities { - if caps.ObjectFormat == objectid.AlgorithmUnknown { - caps.ObjectFormat = defaultAlgorithm - } - - return caps -} - -// Tokens returns capabilities in Git advertisement order. -func (caps Capabilities) Tokens(defaultAlgorithm objectid.Algorithm) []string { - caps = caps.Normalize(defaultAlgorithm) - - tokens := make([]string, 0, 11) - if caps.ReportStatus { - tokens = append(tokens, "report-status") - } - - if caps.ReportStatusV2 { - tokens = append(tokens, "report-status-v2") - } - - if caps.DeleteRefs { - tokens = append(tokens, "delete-refs") - } - - if caps.SideBand64K { - tokens = append(tokens, "side-band-64k") - } - - if caps.Quiet { - tokens = append(tokens, "quiet") - } - - if caps.Atomic { - tokens = append(tokens, "atomic") - } - - if caps.OfsDelta { - tokens = append(tokens, "ofs-delta") - } - - if caps.PushCertNonce != "" { - tokens = append(tokens, "push-cert="+caps.PushCertNonce) - } - - if caps.PushOptions { - tokens = append(tokens, "push-options") - } - - if caps.SessionID != "" { - tokens = append(tokens, "session-id="+caps.SessionID) - } - - if caps.ObjectFormat != objectid.AlgorithmUnknown { - tokens = append(tokens, "object-format="+caps.ObjectFormat.String()) - } - - if caps.Agent != "" { - tokens = append(tokens, "agent="+caps.Agent) - } - - return tokens -} - -func (caps Capabilities) supportsToken(token string, defaultAlgorithm objectid.Algorithm) bool { - name, value, _ := strings.Cut(token, "=") - - switch name { - case "report-status": - return caps.ReportStatus && value == "" - case "report-status-v2": - return caps.ReportStatusV2 && value == "" - case "delete-refs": - return caps.DeleteRefs && value == "" - case "side-band-64k": - return caps.SideBand64K && value == "" - case "quiet": - return caps.Quiet && value == "" - case "atomic": - return caps.Atomic && value == "" - case "ofs-delta": - return caps.OfsDelta && value == "" - case "push-options": - return caps.PushOptions && value == "" - case "push-cert": - return caps.PushCertNonce != "" && value != "" - case "object-format": - if value == "" { - return false - } - - algo, ok := objectid.ParseAlgorithm(value) - - return ok && algo == caps.Normalize(defaultAlgorithm).ObjectFormat - case "session-id": - return caps.SessionID != "" && value != "" - case "agent": - return caps.Agent != "" && value != "" - default: - return false - } -} - -func parseCapabilityList(s string) ([]string, error) { - s = strings.TrimSuffix(s, "\n") - if s == "" { - return nil, nil - } - - tokens := strings.Fields(s) - if slices.Contains(tokens, "") { - return nil, &ProtocolError{Reason: "empty capability token"} - } - - return tokens, nil -} - -func parseRequestedCapabilities( - tokens []string, - supported Capabilities, - defaultAlgorithm objectid.Algorithm, -) (Capabilities, error) { - var requested Capabilities - - requested.ObjectFormat = defaultAlgorithm - - for _, token := range tokens { - if !supported.supportsToken(token, defaultAlgorithm) { - return Capabilities{}, &ProtocolError{ - Reason: fmt.Sprintf("unsupported capability %q", token), - } - } - - name, value, _ := strings.Cut(token, "=") - switch name { - case "report-status": - requested.ReportStatus = true - case "report-status-v2": - requested.ReportStatusV2 = true - case "delete-refs": - requested.DeleteRefs = true - case "side-band-64k": - requested.SideBand64K = true - case "quiet": - requested.Quiet = true - case "atomic": - requested.Atomic = true - case "ofs-delta": - requested.OfsDelta = true - case "push-options": - requested.PushOptions = true - case "push-cert": - requested.PushCertNonce = value - case "object-format": - algo, _ := objectid.ParseAlgorithm(value) - requested.ObjectFormat = algo - case "session-id": - requested.SessionID = value - case "agent": - requested.Agent = value - } - } - - return requested, nil -} diff --git a/network/protocol/v0v1/server/receivepack/doc.go b/network/protocol/v0v1/server/receivepack/doc.go deleted file mode 100644 index 65793831..00000000 --- a/network/protocol/v0v1/server/receivepack/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package receivepack implements the receive-pack-specific server side of Git protocol v0/v1. -package receivepack diff --git a/network/protocol/v0v1/server/receivepack/errors.go b/network/protocol/v0v1/server/receivepack/errors.go deleted file mode 100644 index d89f8959..00000000 --- a/network/protocol/v0v1/server/receivepack/errors.go +++ /dev/null @@ -1,11 +0,0 @@ -package receivepack - -// ProtocolError reports one malformed or unsupported receive-pack protocol input. -type ProtocolError struct { - Reason string -} - -// Error returns the formatted error string. -func (err *ProtocolError) Error() string { - return "protocol/v0v1/server/receivepack: protocol error: " + err.Reason -} diff --git a/network/protocol/v0v1/server/receivepack/helpers_test.go b/network/protocol/v0v1/server/receivepack/helpers_test.go deleted file mode 100644 index 5db8e6a6..00000000 --- a/network/protocol/v0v1/server/receivepack/helpers_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package receivepack_test - -import ( - "bytes" - "strings" - "testing" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -type bufferWriteFlusher struct { - bytes.Buffer -} - -func (bufferWriteFlusher) Flush() error { - return nil -} - -func mustHexID(tb testing.TB, algo objectid.Algorithm, digit string) objectid.ObjectID { - tb.Helper() - - id, err := objectid.ParseHex(algo, strings.Repeat(digit, algo.HexLen())) - if err != nil { - tb.Fatalf("objectid.ParseHex(%q): %v", strings.Repeat(digit, algo.HexLen()), err) - } - - return id -} diff --git a/network/protocol/v0v1/server/receivepack/parse_test.go b/network/protocol/v0v1/server/receivepack/parse_test.go deleted file mode 100644 index d54d8f8d..00000000 --- a/network/protocol/v0v1/server/receivepack/parse_test.go +++ /dev/null @@ -1,255 +0,0 @@ -package receivepack_test - -import ( - "errors" - "strings" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - "codeberg.org/lindenii/furgit/network/protocol/pktline" - common "codeberg.org/lindenii/furgit/network/protocol/v0v1/server" - receivepack "codeberg.org/lindenii/furgit/network/protocol/v0v1/server/receivepack" - objectid "codeberg.org/lindenii/furgit/object/id" -) - -func TestReadRequestParsesCommandsAndPushOptions(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - oldZero := algo.Zero().String() - oneID := mustHexID(t, algo, "1") - - var wire bufferWriteFlusher - - enc := pktline.NewEncoder(&wire) - - err := enc.WriteData([]byte( - oldZero + " " + oneID.String() + " refs/heads/main\x00report-status push-options object-format=" + algo.String() + "\n", - )) - if err != nil { - t.Fatalf("WriteData(first): %v", err) - } - - err = enc.WriteData([]byte( - oneID.String() + " " + oldZero + " refs/heads/old\n", - )) - if err != nil { - t.Fatalf("WriteData(second): %v", err) - } - - err = enc.WriteFlushPacket() - if err != nil { - t.Fatalf("WriteFlushPacket(commands): %v", err) - } - - err = enc.WriteData([]byte("ci.skip\n")) - if err != nil { - t.Fatalf("WriteData(push-option): %v", err) - } - - err = enc.WriteFlushPacket() - if err != nil { - t.Fatalf("WriteFlushPacket(push-options): %v", err) - } - - base := common.NewSession(strings.NewReader(wire.String()), &bufferWriteFlusher{}, common.Options{ - Algorithm: algo, - }) - session := receivepack.NewSession(base, receivepack.Capabilities{ - ReportStatus: true, - PushOptions: true, - ObjectFormat: algo, - }) - - req, err := session.ReadRequest() - if err != nil { - t.Fatalf("ReadRequest: %v", err) - } - - if len(req.Commands) != 2 { - t.Fatalf("len(req.Commands) = %d, want 2", len(req.Commands)) - } - - if !req.Capabilities.ReportStatus || !req.Capabilities.PushOptions { - t.Fatalf("capabilities = %#v", req.Capabilities) - } - - if len(req.PushOptions) != 1 || req.PushOptions[0] != "ci.skip" { - t.Fatalf("push options = %#v", req.PushOptions) - } - - if !req.PackExpected { - t.Fatalf("PackExpected = false, want true") - } - - if req.DeleteOnly { - t.Fatalf("DeleteOnly = true, want false") - } - }) -} - -func TestReadRequestDeleteOnlyDoesNotExpectPack(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - oneID := mustHexID(t, algo, "1") - - var wire bufferWriteFlusher - - enc := pktline.NewEncoder(&wire) - - err := enc.WriteData([]byte( - oneID.String() + " " + algo.Zero().String() + " refs/heads/old\x00delete-refs object-format=" + algo.String() + "\n", - )) - if err != nil { - t.Fatalf("WriteData: %v", err) - } - - err = enc.WriteFlushPacket() - if err != nil { - t.Fatalf("WriteFlushPacket: %v", err) - } - - base := common.NewSession(strings.NewReader(wire.String()), &bufferWriteFlusher{}, common.Options{ - Algorithm: algo, - }) - session := receivepack.NewSession(base, receivepack.Capabilities{ - DeleteRefs: true, - ObjectFormat: algo, - }) - - req, err := session.ReadRequest() - if err != nil { - t.Fatalf("ReadRequest: %v", err) - } - - if req.PackExpected { - t.Fatalf("PackExpected = true, want false") - } - - if !req.DeleteOnly { - t.Fatalf("DeleteOnly = false, want true") - } - }) -} - -func TestReadRequestRejectsUnsupportedCapability(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - oneID := mustHexID(t, algo, "1") - - var wire bufferWriteFlusher - - enc := pktline.NewEncoder(&wire) - - err := enc.WriteData([]byte( - algo.Zero().String() + " " + oneID.String() + " refs/heads/main\x00atomic object-format=" + algo.String() + "\n", - )) - if err != nil { - t.Fatalf("WriteData: %v", err) - } - - err = enc.WriteFlushPacket() - if err != nil { - t.Fatalf("WriteFlushPacket: %v", err) - } - - base := common.NewSession(strings.NewReader(wire.String()), &bufferWriteFlusher{}, common.Options{ - Algorithm: algo, - }) - session := receivepack.NewSession(base, receivepack.Capabilities{ObjectFormat: algo}) - - _, err = session.ReadRequest() - if err == nil { - t.Fatalf("ReadRequest error = nil, want error") - } - - protocolErr, ok := errors.AsType[*receivepack.ProtocolError](err) - if !ok { - t.Fatalf("errors.AsType[*receivepack.ProtocolError](%T) = false", err) - } - - if !strings.Contains(protocolErr.Reason, "unsupported capability") { - t.Fatalf("ProtocolError.Reason = %q", protocolErr.Reason) - } - }) -} - -func TestReadRequestParsesPushCertificate(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - oneID := mustHexID(t, algo, "1") - - var wire bufferWriteFlusher - - enc := pktline.NewEncoder(&wire) - - err := enc.WriteData([]byte("push-cert\x00push-cert=nonce object-format=" + algo.String() + "\n")) - if err != nil { - t.Fatalf("WriteData(push-cert): %v", err) - } - - lines := []string{ - "certificate version 0.1\n", - "pusher Example \n", - "nonce nonce\n", - "push-option ci.skip\n", - "\n", - algo.Zero().String() + " " + oneID.String() + " refs/heads/main\n", - "-----BEGIN PGP SIGNATURE-----\n", - "abcdef\n", - "push-cert-end\n", - } - - for _, line := range lines { - err = enc.WriteData([]byte(line)) - if err != nil { - t.Fatalf("WriteData(%q): %v", line, err) - } - } - - err = enc.WriteFlushPacket() - if err != nil { - t.Fatalf("WriteFlushPacket: %v", err) - } - - base := common.NewSession(strings.NewReader(wire.String()), &bufferWriteFlusher{}, common.Options{ - Algorithm: algo, - }) - session := receivepack.NewSession(base, receivepack.Capabilities{ - PushCertNonce: "server-nonce", - ObjectFormat: algo, - }) - - req, err := session.ReadRequest() - if err != nil { - t.Fatalf("ReadRequest: %v", err) - } - - if req.PushCert == nil { - t.Fatalf("PushCert = nil, want parsed certificate") - } - - if len(req.Commands) != 1 { - t.Fatalf("len(req.Commands) = %d, want 1", len(req.Commands)) - } - - if len(req.PushCert.EmbeddedOption) != 1 || req.PushCert.EmbeddedOption[0] != "ci.skip" { - t.Fatalf("embedded options = %#v", req.PushCert.EmbeddedOption) - } - }) -} diff --git a/network/protocol/v0v1/server/receivepack/report_status.go b/network/protocol/v0v1/server/receivepack/report_status.go deleted file mode 100644 index d852a161..00000000 --- a/network/protocol/v0v1/server/receivepack/report_status.go +++ /dev/null @@ -1,185 +0,0 @@ -package receivepack - -import ( - "fmt" - - "codeberg.org/lindenii/furgit/network/protocol/pktline" -) - -// WriteReportStatus writes one classic report-status response. -func (session *Session) WriteReportStatus(result ReportStatusResult) error { - unpackResult := "ok" - if result.UnpackError != "" { - unpackResult = result.UnpackError - } - - if !session.negotiated.SideBand64K { - err := session.base.WriteData(fmt.Appendf(nil, "unpack %s\n", unpackResult)) - if err != nil { - return err - } - - for _, command := range result.Commands { - line := fmt.Sprintf("ok %s\n", command.Name) - if command.Error != "" { - line = fmt.Sprintf("ng %s %s\n", command.Name, command.Error) - } - - err = session.base.WriteData([]byte(line)) - if err != nil { - return err - } - } - - return session.base.WriteFlushPacket() - } - - buf, err := pktline.AppendData(nil, fmt.Appendf(nil, "unpack %s\n", unpackResult)) - if err != nil { - return err - } - - for _, command := range result.Commands { - line := fmt.Sprintf("ok %s\n", command.Name) - if command.Error != "" { - line = fmt.Sprintf("ng %s %s\n", command.Name, command.Error) - } - - buf, err = pktline.AppendData(buf, []byte(line)) - if err != nil { - return err - } - } - - buf = pktline.AppendFlushPkt(buf) - - w := session.base.PrimaryDataWriter() - - _, err = w.Write(buf) - if err != nil { - return err - } - - return session.base.WriteFlushPacket() -} - -// WriteReportStatusV2 writes one report-status-v2 response. -func (session *Session) WriteReportStatusV2(result ReportStatusResult) error { - unpackResult := "ok" - if result.UnpackError != "" { - unpackResult = result.UnpackError - } - - if !session.negotiated.SideBand64K { //nolint:nestif - err := session.base.WriteData(fmt.Appendf(nil, "unpack %s\n", unpackResult)) - if err != nil { - return err - } - - for _, command := range result.Commands { - if command.Error != "" { - err = session.base.WriteData(fmt.Appendf(nil, "ng %s %s\n", command.Name, command.Error)) - if err != nil { - return err - } - - continue - } - - err = session.base.WriteData(fmt.Appendf(nil, "ok %s\n", command.Name)) - if err != nil { - return err - } - - if command.RefName != "" { - err = session.base.WriteData(fmt.Appendf(nil, "option refname %s\n", command.RefName)) - if err != nil { - return err - } - } - - if command.OldID != nil { - err = session.base.WriteData(fmt.Appendf(nil, "option old-oid %s\n", *command.OldID)) - if err != nil { - return err - } - } - - if command.NewID != nil { - err = session.base.WriteData(fmt.Appendf(nil, "option new-oid %s\n", *command.NewID)) - if err != nil { - return err - } - } - - if command.ForcedUpdate { - err = session.base.WriteData([]byte("option forced-update\n")) - if err != nil { - return err - } - } - } - - return session.base.WriteFlushPacket() - } - - buf, err := pktline.AppendData(nil, fmt.Appendf(nil, "unpack %s\n", unpackResult)) - if err != nil { - return err - } - - for _, command := range result.Commands { - if command.Error != "" { - buf, err = pktline.AppendData(buf, fmt.Appendf(nil, "ng %s %s\n", command.Name, command.Error)) - if err != nil { - return err - } - - continue - } - - buf, err = pktline.AppendData(buf, fmt.Appendf(nil, "ok %s\n", command.Name)) - if err != nil { - return err - } - - if command.RefName != "" { - buf, err = pktline.AppendData(buf, fmt.Appendf(nil, "option refname %s\n", command.RefName)) - if err != nil { - return err - } - } - - if command.OldID != nil { - buf, err = pktline.AppendData(buf, fmt.Appendf(nil, "option old-oid %s\n", *command.OldID)) - if err != nil { - return err - } - } - - if command.NewID != nil { - buf, err = pktline.AppendData(buf, fmt.Appendf(nil, "option new-oid %s\n", *command.NewID)) - if err != nil { - return err - } - } - - if command.ForcedUpdate { - buf, err = pktline.AppendData(buf, []byte("option forced-update\n")) - if err != nil { - return err - } - } - } - - buf = pktline.AppendFlushPkt(buf) - - w := session.base.PrimaryDataWriter() - - _, err = w.Write(buf) - if err != nil { - return err - } - - return session.base.WriteFlushPacket() -} diff --git a/network/protocol/v0v1/server/receivepack/report_status_test.go b/network/protocol/v0v1/server/receivepack/report_status_test.go deleted file mode 100644 index 3cde5103..00000000 --- a/network/protocol/v0v1/server/receivepack/report_status_test.go +++ /dev/null @@ -1,293 +0,0 @@ -package receivepack_test - -import ( - "errors" - "io" - "strings" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - "codeberg.org/lindenii/furgit/network/protocol/pktline" - "codeberg.org/lindenii/furgit/network/protocol/sideband64k" - common "codeberg.org/lindenii/furgit/network/protocol/v0v1/server" - receivepack "codeberg.org/lindenii/furgit/network/protocol/v0v1/server/receivepack" - objectid "codeberg.org/lindenii/furgit/object/id" -) - -func TestWriteReportStatusWritesClassicStatus(t *testing.T) { - t.Parallel() - - var out bufferWriteFlusher - - base := common.NewSession(strings.NewReader(""), &out, common.Options{}) - session := receivepack.NewSession(base, receivepack.Capabilities{}) - - err := session.WriteReportStatus(receivepack.ReportStatusResult{ - Commands: []receivepack.CommandResult{ - {Name: "refs/heads/main"}, - {Name: "refs/heads/dev", Error: "non-fast-forward"}, - }, - }) - if err != nil { - t.Fatalf("WriteReportStatus: %v", err) - } - - got := out.String() - wantParts := []string{ - "unpack ok\n", - "ok refs/heads/main\n", - "ng refs/heads/dev non-fast-forward\n", - "0000", - } - - for _, part := range wantParts { - if !strings.Contains(got, part) { - t.Fatalf("report-status missing %q in %q", part, got) - } - } -} - -func TestWriteReportStatusUsesSideBand64KWhenNegotiated(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - var requestWire bufferWriteFlusher - - requestEnc := pktline.NewEncoder(&requestWire) - - err := requestEnc.WriteData([]byte( - algo.Zero().String() + " " + mustHexID(t, algo, "1").String() + " refs/heads/main\x00report-status side-band-64k object-format=" + algo.String() + "\n", - )) - if err != nil { - t.Fatalf("WriteData(request): %v", err) - } - - err = requestEnc.WriteFlushPacket() - if err != nil { - t.Fatalf("WriteFlushPacket(request): %v", err) - } - - var out bufferWriteFlusher - - base := common.NewSession(strings.NewReader(requestWire.String()), &out, common.Options{ - Algorithm: algo, - }) - session := receivepack.NewSession(base, receivepack.Capabilities{ - ReportStatus: true, - SideBand64K: true, - ObjectFormat: algo, - }) - - _, err = session.ReadRequest() - if err != nil { - t.Fatalf("ReadRequest: %v", err) - } - - err = session.WriteReportStatus(receivepack.ReportStatusResult{ - Commands: []receivepack.CommandResult{ - {Name: "refs/heads/main"}, - }, - }) - if err != nil { - t.Fatalf("WriteReportStatus: %v", err) - } - - dec := sideband64k.NewDecoder(strings.NewReader(out.String()), sideband64k.ReadOptions{}) - - frame, err := dec.ReadFrame() - if err != nil { - t.Fatalf("ReadFrame(unpack): %v", err) - } - - if frame.Type != sideband64k.FrameData { - t.Fatalf("first frame = %#v", frame) - } - - statusDec := pktline.NewDecoder(strings.NewReader(string(frame.Payload)), pktline.ReadOptions{}) - - statusFrame, err := statusDec.ReadFrame() - if err != nil { - t.Fatalf("ReadFrame(unpack status): %v", err) - } - - if statusFrame.Type != pktline.PacketData || string(statusFrame.Payload) != "unpack ok\n" { - t.Fatalf("first status frame = %#v", statusFrame) - } - - statusFrame, err = statusDec.ReadFrame() - if err != nil { - t.Fatalf("ReadFrame(ok status): %v", err) - } - - if statusFrame.Type != pktline.PacketData || string(statusFrame.Payload) != "ok refs/heads/main\n" { - t.Fatalf("second status frame = %#v", statusFrame) - } - - statusFrame, err = statusDec.ReadFrame() - if err != nil { - t.Fatalf("ReadFrame(status flush): %v", err) - } - - if statusFrame.Type != pktline.PacketFlush { - t.Fatalf("status flush frame.Type = %v, want FrameFlush", statusFrame.Type) - } - - frame, err = dec.ReadFrame() - if err != nil { - t.Fatalf("ReadFrame(outer flush): %v", err) - } - - if frame.Type != sideband64k.FrameFlush { - t.Fatalf("outer flush frame.Type = %v, want FrameFlush", frame.Type) - } - }) -} - -func TestWriteReportStatusV2WritesOptionLines(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - oldID := mustHexID(t, algo, "1") - newID := mustHexID(t, algo, "2") - - var out bufferWriteFlusher - - base := common.NewSession(strings.NewReader(""), &out, common.Options{}) - session := receivepack.NewSession(base, receivepack.Capabilities{}) - - err := session.WriteReportStatusV2(receivepack.ReportStatusResult{ - Commands: []receivepack.CommandResult{ - { - Name: "refs/pseudo/proc", - RefName: "refs/heads/main", - OldID: &oldID, - NewID: &newID, - ForcedUpdate: true, - }, - {Name: "refs/heads/dev", Error: "rejected"}, - }, - }) - if err != nil { - t.Fatalf("WriteReportStatusV2: %v", err) - } - - got := out.String() - wantParts := []string{ - "unpack ok\n", - "ok refs/pseudo/proc\n", - "option refname refs/heads/main\n", - "option old-oid " + oldID.String() + "\n", - "option new-oid " + newID.String() + "\n", - "option forced-update\n", - "ng refs/heads/dev rejected\n", - "0000", - } - - for _, part := range wantParts { - if !strings.Contains(got, part) { - t.Fatalf("report-status-v2 missing %q in %q", part, got) - } - } - }) -} - -func TestWriteProgressRequiresSideBand64K(t *testing.T) { - t.Parallel() - - base := common.NewSession(strings.NewReader(""), &bufferWriteFlusher{}, common.Options{}) - session := receivepack.NewSession(base, receivepack.Capabilities{}) - - err := session.WriteProgress([]byte("progress\n")) - if !errors.Is(err, common.ErrSideBandNotEnabled) { - t.Fatalf("WriteProgress error = %v, want %v", err, common.ErrSideBandNotEnabled) - } -} - -func TestProgressWriterDiscardsWithoutSideBand64K(t *testing.T) { - t.Parallel() - - var out bufferWriteFlusher - - base := common.NewSession(strings.NewReader(""), &out, common.Options{}) - session := receivepack.NewSession(base, receivepack.Capabilities{}) - - n, err := io.WriteString(session.ProgressWriter(), "progress line\n") - if err != nil { - t.Fatalf("ProgressWriter.Write: %v", err) - } - - if n != len("progress line\n") { - t.Fatalf("ProgressWriter.Write n = %d, want %d", n, len("progress line\n")) - } - - if out.String() != "" { - t.Fatalf("unexpected wire output without side-band-64k: %q", out.String()) - } -} - -func TestProgressWriterUsesSideBand64KWhenNegotiated(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - var requestWire bufferWriteFlusher - - requestEnc := pktline.NewEncoder(&requestWire) - - err := requestEnc.WriteData([]byte( - algo.Zero().String() + " " + mustHexID(t, algo, "1").String() + " refs/heads/main\x00report-status side-band-64k object-format=" + algo.String() + "\n", - )) - if err != nil { - t.Fatalf("WriteData(request): %v", err) - } - - err = requestEnc.WriteFlushPacket() - if err != nil { - t.Fatalf("WriteFlushPacket(request): %v", err) - } - - var out bufferWriteFlusher - - base := common.NewSession(strings.NewReader(requestWire.String()), &out, common.Options{ - Algorithm: algo, - }) - session := receivepack.NewSession(base, receivepack.Capabilities{ - ReportStatus: true, - SideBand64K: true, - ObjectFormat: algo, - }) - - _, err = session.ReadRequest() - if err != nil { - t.Fatalf("ReadRequest: %v", err) - } - - _, err = io.WriteString(session.ProgressWriter(), "remote: stage 1\r") - if err != nil { - t.Fatalf("ProgressWriter.Write: %v", err) - } - - dec := sideband64k.NewDecoder(strings.NewReader(out.String()), sideband64k.ReadOptions{}) - - frame, err := dec.ReadFrame() - if err != nil { - t.Fatalf("ReadFrame(progress): %v", err) - } - - if frame.Type != sideband64k.FrameProgress { - t.Fatalf("frame.Type = %v, want FrameProgress", frame.Type) - } - - if string(frame.Payload) != "remote: stage 1\r" { - t.Fatalf("frame.Payload = %q, want %q", frame.Payload, "remote: stage 1\r") - } - }) -} diff --git a/network/protocol/v0v1/server/receivepack/session.go b/network/protocol/v0v1/server/receivepack/session.go deleted file mode 100644 index 5299b42d..00000000 --- a/network/protocol/v0v1/server/receivepack/session.go +++ /dev/null @@ -1,303 +0,0 @@ -package receivepack - -import ( - "fmt" - "strings" - - "codeberg.org/lindenii/furgit/common/iowrap" - common "codeberg.org/lindenii/furgit/network/protocol/v0v1/server" - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// Session is one stateful server-side receive-pack protocol session. -// -// Labels: MT-Unsafe. -type Session struct { - base *common.Session - supported Capabilities - negotiated Capabilities -} - -// NewSession creates one receive-pack session over one common server session. -// -// Labels: Deps-Borrowed, Life-Parent. -func NewSession(base *common.Session, supported Capabilities) *Session { - return &Session{ - base: base, - supported: supported, - } -} - -// AdvertiseRefs writes one receive-pack ref advertisement. -func (session *Session) AdvertiseRefs(ad common.Advertisement) error { - return session.base.AdvertiseRefs(ad, session.supported.Tokens(session.base.Algorithm())) -} - -// ReadRequest reads one receive-pack request through optional push-options. -func (session *Session) ReadRequest() (*Request, error) { - req := &Request{} - - var sawCommands bool - - for { - frame, err := session.base.ReadFrame() - if err != nil { - return nil, err - } - - switch frame.Type { - case common.FrameFlush: - goto afterCommands - case common.FrameData: - case common.FrameDelim, common.FrameResponseEnd: - return nil, &ProtocolError{Reason: fmt.Sprintf("unexpected packet type %v", frame.Type)} - } - - payload := string(frame.Payload) - if strings.HasPrefix(payload, "shallow ") { - line := trimOneLF(payload) - - shallowID, err := parseObjectID(session.base.Algorithm(), line[len("shallow "):]) - if err != nil { - return nil, err - } - - req.Shallow = append(req.Shallow, shallowID) - - continue - } - - if strings.HasPrefix(payload, "push-cert\x00") { - if sawCommands { - return nil, &ProtocolError{Reason: "got both push certificate and unsigned commands"} - } - - capabilityTokens, err := parseCapabilityList(payload[len("push-cert\x00"):]) - if err != nil { - return nil, err - } - - requested, err := parseRequestedCapabilities( - capabilityTokens, - session.supported, - session.base.Algorithm(), - ) - if err != nil { - return nil, err - } - - req.Capabilities = requested - - cert, err := session.readPushCertificate() - if err != nil { - return nil, err - } - - req.PushCert = cert - req.Commands = append(req.Commands, cert.Commands...) - sawCommands = true - - continue - } - - line := trimOneLF(payload) - if !sawCommands && strings.Contains(line, "\x00") { - commandPart, capPart, _ := strings.Cut(line, "\x00") - - capabilityTokens, err := parseCapabilityList(capPart) - if err != nil { - return nil, err - } - - requested, err := parseRequestedCapabilities( - capabilityTokens, - session.supported, - session.base.Algorithm(), - ) - if err != nil { - return nil, err - } - - req.Capabilities = requested - line = commandPart - } - - cmd, err := parseCommand(session.base.Algorithm(), line) - if err != nil { - return nil, err - } - - req.Commands = append(req.Commands, cmd) - sawCommands = true - } - -afterCommands: - if req.Capabilities.PushOptions { - for { - frame, err := session.base.ReadFrame() - if err != nil { - return nil, err - } - - switch frame.Type { - case common.FrameFlush: - goto afterPushOptions - case common.FrameData: - req.PushOptions = append(req.PushOptions, trimOneLF(string(frame.Payload))) - case common.FrameDelim, common.FrameResponseEnd: - return nil, &ProtocolError{Reason: fmt.Sprintf("unexpected packet type %v", frame.Type)} - } - } - } - -afterPushOptions: - req.DeleteOnly = deleteOnly(req.Commands) - - req.PackExpected = len(req.Commands) > 0 && !req.DeleteOnly - - session.negotiated = req.Capabilities - - if req.Capabilities.SideBand64K { - session.base.EnableSideBand64K() - } - - return req, nil -} - -// WriteProgress writes one progress packet. -func (session *Session) WriteProgress(p []byte) error { - return session.base.WriteProgress(p) -} - -// ProgressWriter returns one chunking writer for sideband progress output. -// -// When side-band-64k was not negotiated, writes are discarded. -// -// Labels: Life-Parent. -func (session *Session) ProgressWriter() iowrap.WriteFlusher { - return session.base.ProgressWriter() -} - -// WriteError writes one fatal error packet. -func (session *Session) WriteError(p []byte) error { - return session.base.WriteError(p) -} - -// ErrorWriter returns one chunking writer for sideband error output. -// -// When side-band-64k was not negotiated, writes are discarded. -// -// Labels: Life-Parent. -func (session *Session) ErrorWriter() iowrap.WriteFlusher { - return session.base.ErrorWriter() -} - -func trimOneLF(s string) string { - return strings.TrimSuffix(s, "\n") -} - -func parseObjectID(algo objectid.Algorithm, s string) (objectid.ObjectID, error) { - id, err := objectid.ParseHex(algo, s) - if err != nil { - return objectid.ObjectID{}, &ProtocolError{ - Reason: fmt.Sprintf("invalid object id %q", s), - } - } - - return id, nil -} - -func commandIsDelete(cmd Command) bool { - return cmd.NewID == cmd.NewID.Algorithm().Zero() -} - -func deleteOnly(commands []Command) bool { - if len(commands) == 0 { - return false - } - - for _, cmd := range commands { - if !commandIsDelete(cmd) { - return false - } - } - - return true -} - -func parseCommand(algo objectid.Algorithm, line string) (Command, error) { - fields := strings.Fields(line) - if len(fields) != 3 { - return Command{}, &ProtocolError{Reason: fmt.Sprintf("malformed command %q", line)} - } - - oldID, err := parseObjectID(algo, fields[0]) - if err != nil { - return Command{}, err - } - - newID, err := parseObjectID(algo, fields[1]) - if err != nil { - return Command{}, err - } - - return Command{OldID: oldID, NewID: newID, Name: fields[2]}, nil -} - -func (session *Session) readPushCertificate() (*PushCertificate, error) { - cert := &PushCertificate{} - inCommands := false - inSignature := false - - for { - frame, err := session.base.ReadFrame() - if err != nil { - return nil, err - } - - switch frame.Type { - case common.FrameFlush: - return nil, &ProtocolError{Reason: "unexpected flush inside push certificate"} - case common.FrameData: - case common.FrameDelim, common.FrameResponseEnd: - return nil, &ProtocolError{Reason: fmt.Sprintf("unexpected packet type %v", frame.Type)} - } - - line := string(frame.Payload) - if line == "push-cert-end\n" { - return cert, nil - } - - if !inCommands { - if line == "\n" { - inCommands = true - - continue - } - - trimmed := trimOneLF(line) - cert.HeaderLines = append(cert.HeaderLines, trimmed) - - if strings.HasPrefix(trimmed, "push-option ") { - cert.EmbeddedOption = append(cert.EmbeddedOption, trimmed[len("push-option "):]) - } - - continue - } - - if !inSignature { - trimmed := trimOneLF(line) - - cmd, err := parseCommand(session.base.Algorithm(), trimmed) - if err == nil { - cert.Commands = append(cert.Commands, cmd) - - continue - } - - inSignature = true - } - - cert.SignatureLines = append(cert.SignatureLines, trimOneLF(line)) - } -} diff --git a/network/protocol/v0v1/server/receivepack/types.go b/network/protocol/v0v1/server/receivepack/types.go deleted file mode 100644 index b281a86b..00000000 --- a/network/protocol/v0v1/server/receivepack/types.go +++ /dev/null @@ -1,45 +0,0 @@ -package receivepack - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// Command is one requested reference update. -type Command struct { - OldID objectid.ObjectID - NewID objectid.ObjectID - Name string -} - -// PushCertificate is one parsed push certificate block. -type PushCertificate struct { - HeaderLines []string - EmbeddedOption []string - Commands []Command - SignatureLines []string -} - -// Request is one parsed receive-pack request. -type Request struct { - Capabilities Capabilities - Shallow []objectid.ObjectID - Commands []Command - PushCert *PushCertificate - PushOptions []string - PackExpected bool - DeleteOnly bool -} - -// CommandResult is one per-command report-status result. -type CommandResult struct { - Name string - Error string - RefName string - OldID *objectid.ObjectID - NewID *objectid.ObjectID - ForcedUpdate bool -} - -// ReportStatusResult is one report-status payload. -type ReportStatusResult struct { - UnpackError string - Commands []CommandResult -} diff --git a/network/protocol/v0v1/server/session.go b/network/protocol/v0v1/server/session.go deleted file mode 100644 index a66cc37a..00000000 --- a/network/protocol/v0v1/server/session.go +++ /dev/null @@ -1,142 +0,0 @@ -package server - -import ( - "io" - - "codeberg.org/lindenii/furgit/common/iowrap" - "codeberg.org/lindenii/furgit/network/protocol/pktline" - "codeberg.org/lindenii/furgit/network/protocol/sideband64k" - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// Options configures one server-side v0/v1 session. -type Options struct { - // Version selects protocol v0 or v1 framing. - Version Version - // Algorithm is the repository object ID algorithm for this session. - Algorithm objectid.Algorithm -} - -// Session is one stateful server-side v0/v1 server protocol session. -// -// Labels: MT-Unsafe. -type Session struct { - dec *pktline.Decoder - enc *pktline.Encoder - sideband *sideband64k.Encoder - opts Options - useSideBand bool -} - -// NewSession creates one v0/v1 server session over r and w. -// -// Labels: Deps-Borrowed, Life-Parent. -func NewSession(r io.Reader, w iowrap.WriteFlusher, opts Options) *Session { - return &Session{ - dec: pktline.NewDecoder(r, pktline.ReadOptions{}), - enc: pktline.NewEncoder(w), - sideband: sideband64k.NewEncoder(w), - opts: opts, - } -} - -// Algorithm returns the session object ID algorithm. -func (session *Session) Algorithm() objectid.Algorithm { - return session.opts.Algorithm -} - -// ReadFrame reads one low-level pkt-line frame from the session input. -func (session *Session) ReadFrame() (Frame, error) { - return session.dec.ReadFrame() -} - -// EnableSideBand64K enables side-band-64k output framing for subsequent data, -// progress, error, and flush writes. -func (session *Session) EnableSideBand64K() { - session.useSideBand = true -} - -// WriteData writes one primary output packet. -func (session *Session) WriteData(p []byte) error { - if session.useSideBand { - return session.sideband.WriteData(p) - } - - return session.enc.WriteData(p) -} - -// WriteProgress writes one progress packet. -func (session *Session) WriteProgress(p []byte) error { - if !session.useSideBand { - return ErrSideBandNotEnabled - } - - return session.sideband.WriteProgress(p) -} - -// WriteError writes one fatal error packet. -func (session *Session) WriteError(p []byte) error { - if !session.useSideBand { - return ErrSideBandNotEnabled - } - - return session.sideband.WriteError(p) -} - -// WriteFlushPacket writes one trailing flush packet. -func (session *Session) WriteFlushPacket() error { - if session.useSideBand { - return session.sideband.WriteFlushPacket() - } - - return session.enc.WriteFlushPacket() -} - -// Flush flushes buffered transport output without emitting pkt-line frames. -func (session *Session) Flush() error { - if session.useSideBand { - return session.sideband.Flush() - } - - return session.enc.Flush() -} - -// ProgressWriter returns one chunking writer for sideband progress output. -// -// When side-band-64k was not negotiated, writes are discarded. -// -// Labels: Life-Parent. -func (session *Session) ProgressWriter() iowrap.WriteFlusher { - if !session.useSideBand { - return iowrap.NopFlush(io.Discard) - } - - return sideband64k.NewChunkWriter(session.sideband, sideband64k.BandProgress) -} - -// ErrorWriter returns one chunking writer for sideband error output. -// -// When side-band-64k was not negotiated, writes are discarded. -// -// Labels: Life-Parent. -func (session *Session) ErrorWriter() iowrap.WriteFlusher { - if !session.useSideBand { - return iowrap.NopFlush(io.Discard) - } - - return sideband64k.NewChunkWriter(session.sideband, sideband64k.BandError) -} - -// PrimaryDataWriter returns one chunking writer for primary output bytes. -// -// When side-band-64k is enabled, writes are chunked into band-1 sideband -// frames. Otherwise writes are chunked into direct pkt-line data frames. -// -// Labels: Life-Parent. -func (session *Session) PrimaryDataWriter() iowrap.WriteFlusher { - if session.useSideBand { - return sideband64k.NewChunkWriter(session.sideband, sideband64k.BandData) - } - - return pktline.NewChunkWriter(session.enc) -} diff --git a/network/protocol/v0v1/server/version.go b/network/protocol/v0v1/server/version.go deleted file mode 100644 index 23ae9466..00000000 --- a/network/protocol/v0v1/server/version.go +++ /dev/null @@ -1,12 +0,0 @@ -package server - -// Version identifies the protocol version used on one v0/v1 server session. -type Version uint8 - -const ( - // Version0 is the original protocol framing with no leading version line. - Version0 Version = iota - // Version1 is protocol v1, which is v0 plus one leading "version 1\n" - // pkt-line before ref advertisement. - Version1 -) diff --git a/network/receivepack/advertise.go b/network/receivepack/advertise.go deleted file mode 100644 index 0fa010bf..00000000 --- a/network/receivepack/advertise.go +++ /dev/null @@ -1,57 +0,0 @@ -package receivepack - -import ( - "errors" - - common "codeberg.org/lindenii/furgit/network/protocol/v0v1/server" - "codeberg.org/lindenii/furgit/ref" - refstore "codeberg.org/lindenii/furgit/ref/store" -) - -func advertisedRefs(opts Options) ([]common.AdvertisedRef, error) { - listed, err := opts.Refs.List("") - if err != nil { - return nil, err - } - - return buildAdvertisedRefs(opts, listed) -} - -func buildAdvertisedRefs(opts Options, listed []ref.Ref) ([]common.AdvertisedRef, error) { - refs := make([]common.AdvertisedRef, 0, len(listed)) - for _, entry := range listed { - switch resolved := entry.(type) { - case ref.Detached: - advertised := common.AdvertisedRef{ - Name: resolved.Name(), - ID: resolved.ID, - } - - if resolved.Peeled != nil { - advertised.Peeled = resolved.Peeled - } - - refs = append(refs, advertised) - case ref.Symbolic: - if resolved.Name() != "HEAD" { - continue - } - - head, err := opts.Refs.ResolveToDetached("HEAD") - if err != nil { - if errors.Is(err, refstore.ErrReferenceNotFound) { - continue - } - - return nil, err - } - - refs = append(refs, common.AdvertisedRef{ - Name: "HEAD", - ID: head.ID, - }) - } - } - - return refs, nil -} diff --git a/network/receivepack/capabilities_defaults.go b/network/receivepack/capabilities_defaults.go deleted file mode 100644 index 72c36c30..00000000 --- a/network/receivepack/capabilities_defaults.go +++ /dev/null @@ -1,17 +0,0 @@ -package receivepack - -import ( - "crypto/rand" -) - -func defaultAgent() string { - return "furgit" -} - -func defaultSessionID() string { - return "furgit-" + rand.Text() -} - -func defaultPushCertNonce() string { - return "furgit-" + rand.Text() -} diff --git a/network/receivepack/commands.go b/network/receivepack/commands.go deleted file mode 100644 index a9edec1a..00000000 --- a/network/receivepack/commands.go +++ /dev/null @@ -1,19 +0,0 @@ -package receivepack - -import ( - protoreceive "codeberg.org/lindenii/furgit/network/protocol/v0v1/server/receivepack" - "codeberg.org/lindenii/furgit/network/receivepack/service" -) - -func translateCommands(commands []protoreceive.Command) []service.Command { - out := make([]service.Command, 0, len(commands)) - for _, command := range commands { - out = append(out, service.Command{ - OldID: command.OldID, - NewID: command.NewID, - Name: command.Name, - }) - } - - return out -} diff --git a/network/receivepack/doc.go b/network/receivepack/doc.go deleted file mode 100644 index b63f49d5..00000000 --- a/network/receivepack/doc.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package receivepack provides the application-facing server-side push entry -// point. -package receivepack diff --git a/network/receivepack/errors.go b/network/receivepack/errors.go deleted file mode 100644 index 18e7a135..00000000 --- a/network/receivepack/errors.go +++ /dev/null @@ -1,15 +0,0 @@ -package receivepack - -import "errors" - -var ( - // ErrMissingAlgorithm reports one missing repository hash algorithm. - ErrMissingAlgorithm = errors.New("receivepack: missing object id algorithm") - // ErrMissingRefs reports one missing reference store dependency. - ErrMissingRefs = errors.New("receivepack: missing refs store") - // ErrMissingObjects reports one missing object store dependency. - ErrMissingObjects = errors.New("receivepack: missing objects store") - // ErrUnsupportedProtocol reports one unsupported requested Git protocol - // version. - ErrUnsupportedProtocol = errors.New("receivepack: unsupported protocol version") -) diff --git a/network/receivepack/hook.go b/network/receivepack/hook.go deleted file mode 100644 index 9c323bcc..00000000 --- a/network/receivepack/hook.go +++ /dev/null @@ -1,97 +0,0 @@ -package receivepack - -import ( - "context" - - "codeberg.org/lindenii/furgit/common/iowrap" - commitgraphread "codeberg.org/lindenii/furgit/format/commitgraph/read" - "codeberg.org/lindenii/furgit/network/receivepack/service" - objectid "codeberg.org/lindenii/furgit/object/id" - objectstore "codeberg.org/lindenii/furgit/object/store" - refstore "codeberg.org/lindenii/furgit/ref/store" -) - -type HookIO struct { - Progress iowrap.WriteFlusher - Error iowrap.WriteFlusher -} - -// RefUpdate is one requested reference update presented to a receive-pack hook. -type RefUpdate struct { - Name string - OldID objectid.ObjectID - NewID objectid.ObjectID -} - -// UpdateDecision is one hook decision for a requested reference update. -type UpdateDecision struct { - Accept bool - Message string -} - -// HookRequest is the input presented to a receive-pack hook before quarantine -// promotion and ref updates. -// -// Labels: Life-Call. -type HookRequest struct { - Refs refstore.Reader - ExistingObjects objectstore.Reader - // QuarantinedObjects exposes quarantined objects for this push. - // - // When the push did not create a quarantine, QuarantinedObjects is nil. - QuarantinedObjects objectstore.Reader - CommitGraph *commitgraphread.Reader - Updates []RefUpdate - PushOptions []string - IO HookIO -} - -// Hook decides whether each requested update should proceed. -// -// The hook runs after pack ingestion into quarantine and before quarantine -// promotion or ref updates. The returned decisions must have the same length as -// HookRequest.Updates. -type Hook func(context.Context, HookRequest) ([]UpdateDecision, error) - -func translateHook(hook Hook) service.Hook { - if hook == nil { - return nil - } - - return func(ctx context.Context, req service.HookRequest) ([]service.UpdateDecision, error) { - translatedUpdates := make([]RefUpdate, 0, len(req.Updates)) - for _, update := range req.Updates { - translatedUpdates = append(translatedUpdates, RefUpdate{ - Name: update.Name, - OldID: update.OldID, - NewID: update.NewID, - }) - } - - decisions, err := hook(ctx, HookRequest{ - Refs: req.Refs, - ExistingObjects: req.ExistingObjects, - QuarantinedObjects: req.QuarantinedObjects, - CommitGraph: req.CommitGraph, - Updates: translatedUpdates, - PushOptions: append([]string(nil), req.PushOptions...), - IO: HookIO{ - Progress: req.IO.Progress, - Error: req.IO.Error, - }, - }) - if err != nil { - return nil, err - } - - out := make([]service.UpdateDecision, 0, len(decisions)) - for _, decision := range decisions { - out = append(out, service.UpdateDecision{ - Accept: decision.Accept, - Message: decision.Message, - }) - } - - return out, nil - } -} diff --git a/network/receivepack/hooks/chain.go b/network/receivepack/hooks/chain.go deleted file mode 100644 index f98c06f8..00000000 --- a/network/receivepack/hooks/chain.go +++ /dev/null @@ -1,51 +0,0 @@ -package hooks - -import ( - "context" - "fmt" - - receivepack "codeberg.org/lindenii/furgit/network/receivepack" -) - -// Chain combines hooks by running them in order and intersecting their -// decisions. The first rejecting message for each update is preserved. -func Chain(hooks ...receivepack.Hook) receivepack.Hook { - return func( - ctx context.Context, - req receivepack.HookRequest, - ) ([]receivepack.UpdateDecision, error) { - decisions := make([]receivepack.UpdateDecision, len(req.Updates)) - for i := range decisions { - decisions[i].Accept = true - } - - for _, hook := range hooks { - if hook == nil { - continue - } - - hookDecisions, err := hook(ctx, req) - if err != nil { - return nil, err - } - - if len(hookDecisions) != len(req.Updates) { - return nil, fmt.Errorf("hook returned %d decisions for %d updates", len(hookDecisions), len(req.Updates)) - } - - for i, decision := range hookDecisions { - if decision.Accept { - continue - } - - if decisions[i].Accept { - decisions[i].Message = decision.Message - } - - decisions[i].Accept = false - } - } - - return decisions, nil - } -} diff --git a/network/receivepack/hooks/doc.go b/network/receivepack/hooks/doc.go deleted file mode 100644 index bef2baf9..00000000 --- a/network/receivepack/hooks/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package hooks provides a few pre-defined hooks that callers might find useful. -package hooks diff --git a/network/receivepack/hooks/reject_force_push.go b/network/receivepack/hooks/reject_force_push.go deleted file mode 100644 index 5840a031..00000000 --- a/network/receivepack/hooks/reject_force_push.go +++ /dev/null @@ -1,69 +0,0 @@ -package hooks - -import ( - "context" - "errors" - "fmt" - - "codeberg.org/lindenii/furgit/commitquery" - receivepack "codeberg.org/lindenii/furgit/network/receivepack" - "codeberg.org/lindenii/furgit/object/fetch" - objectmix "codeberg.org/lindenii/furgit/object/store/mix" - refstore "codeberg.org/lindenii/furgit/ref/store" -) - -// RejectForcePush rejects updates whose new value is not a fast-forward of the -// currently resolved reference. -func RejectForcePush() receivepack.Hook { - return func( - ctx context.Context, - req receivepack.HookRequest, - ) ([]receivepack.UpdateDecision, error) { - _ = ctx - - objects := req.ExistingObjects - if req.QuarantinedObjects != nil { - objects = objectmix.New(req.QuarantinedObjects, req.ExistingObjects) - } - - queries := commitquery.New(fetch.New(objects), req.CommitGraph) - - decisions := make([]receivepack.UpdateDecision, len(req.Updates)) - for i := range decisions { - decisions[i].Accept = true - } - - for i, update := range req.Updates { - if update.OldID == update.OldID.Algorithm().Zero() || update.NewID == update.NewID.Algorithm().Zero() { - continue - } - - current, err := req.Refs.ResolveToDetached(update.Name) - switch { - case err == nil: - case errors.Is(err, refstore.ErrReferenceNotFound): - continue - default: - return nil, fmt.Errorf("resolve %s: %w", update.Name, err) - } - - if current.ID == update.NewID { - continue - } - - ok, err := queries.IsAncestor(current.ID, update.NewID) - if err != nil { - return nil, fmt.Errorf("check fast-forward %s: %w", update.Name, err) - } - - if !ok { - decisions[i] = receivepack.UpdateDecision{ - Accept: false, - Message: "non-fast-forward", - } - } - } - - return decisions, nil - } -} diff --git a/network/receivepack/int_test.go b/network/receivepack/int_test.go deleted file mode 100644 index 352bbe7b..00000000 --- a/network/receivepack/int_test.go +++ /dev/null @@ -1,1095 +0,0 @@ -package receivepack_test - -import ( - "context" - "fmt" - "io" - "os" - "strings" - "testing" - "time" - - "codeberg.org/lindenii/furgit/internal/testgit" - "codeberg.org/lindenii/furgit/network/protocol/pktline" - "codeberg.org/lindenii/furgit/network/protocol/sideband64k" - receivepack "codeberg.org/lindenii/furgit/network/receivepack" - receivepackhooks "codeberg.org/lindenii/furgit/network/receivepack/hooks" - objectid "codeberg.org/lindenii/furgit/object/id" - objectstore "codeberg.org/lindenii/furgit/object/store" - objectdual "codeberg.org/lindenii/furgit/object/store/dual" - objectloose "codeberg.org/lindenii/furgit/object/store/loose" - objectpacked "codeberg.org/lindenii/furgit/object/store/packed" -) - -func TestReceivePackDeleteOnlyAtomicDeleteSucceeds(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - _, _, commitID := testRepo.MakeCommit(t, "base") - testRepo.UpdateRef(t, "refs/heads/main", commitID) - - repo := testRepo.OpenRepository(t) - - var ( - input strings.Builder - output bufferWriteFlusher - ) - - input.WriteString(pktlineData( - commitID.String() + " " + algo.Zero().String() + " refs/heads/main\x00report-status atomic delete-refs object-format=" + algo.String() + "\n", - )) - input.WriteString("0000") - - err := receivepack.ReceivePack(context.Background(), &output, strings.NewReader(input.String()), receivepack.Options{ - GitProtocol: "", - Algorithm: algo, - Refs: repo.Refs(), - ExistingObjects: repo.Objects(), - }) - if err != nil { - t.Fatalf("ReceivePack: %v", err) - } - - got := output.String() - if !strings.Contains(got, "ok refs/heads/main\n") { - t.Fatalf("unexpected receive-pack output %q", got) - } - - _, err = repo.Refs().Resolve("refs/heads/main") - if err == nil { - t.Fatal("refs/heads/main still exists after delete push") - } - }) -} - -func TestReceivePackDeleteOnlyNonAtomicAppliesIndependentDeletes(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - _, _, commitID := testRepo.MakeCommit(t, "base") - _, _, staleID := testRepo.MakeCommit(t, "stale") - testRepo.UpdateRef(t, "refs/heads/main", commitID) - testRepo.UpdateRef(t, "refs/heads/topic", commitID) - - repo := testRepo.OpenRepository(t) - - var ( - input strings.Builder - output bufferWriteFlusher - ) - - input.WriteString(pktlineData( - staleID.String() + " " + algo.Zero().String() + " refs/heads/main\x00report-status delete-refs object-format=" + algo.String() + "\n", - )) - input.WriteString(pktlineData( - commitID.String() + " " + algo.Zero().String() + " refs/heads/topic\n", - )) - input.WriteString("0000") - - err := receivepack.ReceivePack(context.Background(), &output, strings.NewReader(input.String()), receivepack.Options{ - GitProtocol: "", - Algorithm: algo, - Refs: repo.Refs(), - ExistingObjects: repo.Objects(), - }) - if err != nil { - t.Fatalf("ReceivePack: %v", err) - } - - got := output.String() - if !strings.Contains(got, "ng refs/heads/main ") || !strings.Contains(got, "ok refs/heads/topic\n") { - t.Fatalf("unexpected receive-pack output %q", got) - } - - _, err = repo.Refs().Resolve("refs/heads/main") - if err != nil { - t.Fatalf("Resolve(main): %v", err) - } - - _, err = repo.Refs().Resolve("refs/heads/topic") - if err == nil { - t.Fatal("refs/heads/topic still exists after successful delete") - } - }) -} - -func TestReceivePackDeleteOnlyAtomicFailureLeavesAllRefsUntouched(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - _, _, commitID := testRepo.MakeCommit(t, "base") - _, _, staleID := testRepo.MakeCommit(t, "stale") - testRepo.UpdateRef(t, "refs/heads/main", commitID) - testRepo.UpdateRef(t, "refs/heads/topic", commitID) - - repo := testRepo.OpenRepository(t) - - var ( - input strings.Builder - output bufferWriteFlusher - ) - - input.WriteString(pktlineData( - staleID.String() + " " + algo.Zero().String() + " refs/heads/main\x00report-status atomic delete-refs object-format=" + algo.String() + "\n", - )) - input.WriteString(pktlineData( - commitID.String() + " " + algo.Zero().String() + " refs/heads/topic\n", - )) - input.WriteString("0000") - - err := receivepack.ReceivePack(context.Background(), &output, strings.NewReader(input.String()), receivepack.Options{ - GitProtocol: "", - Algorithm: algo, - Refs: repo.Refs(), - ExistingObjects: repo.Objects(), - }) - if err != nil { - t.Fatalf("ReceivePack: %v", err) - } - - got := output.String() - if !strings.Contains(got, "ng refs/heads/main ") || !strings.Contains(got, "ng refs/heads/topic ") { - t.Fatalf("unexpected receive-pack output %q", got) - } - - _, err = repo.Refs().Resolve("refs/heads/main") - if err != nil { - t.Fatalf("Resolve(main): %v", err) - } - - _, err = repo.Refs().Resolve("refs/heads/topic") - if err != nil { - t.Fatalf("Resolve(topic): %v", err) - } - }) -} - -func TestReceivePackAdvertisesResolvedHEAD(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - _, _, commitID := testRepo.MakeCommit(t, "base") - testRepo.UpdateRef(t, "refs/heads/main", commitID) - testRepo.SymbolicRef(t, "HEAD", "refs/heads/main") - - repo := testRepo.OpenRepository(t) - - var ( - input strings.Builder - output bufferWriteFlusher - ) - - input.WriteString("0000") - - err := receivepack.ReceivePack(context.Background(), &output, strings.NewReader(input.String()), receivepack.Options{ - Algorithm: algo, - Refs: repo.Refs(), - ExistingObjects: repo.Objects(), - }) - if err != nil { - t.Fatalf("ReceivePack: %v", err) - } - - got := output.String() - - want := commitID.String() + " HEAD" - if !strings.Contains(got, want) { - t.Fatalf("HEAD advertisement missing %q in %q", want, got) - } - }) -} - -func TestReceivePackVersion2FallsBackToV0(t *testing.T) { - t.Parallel() - - testReceivePackProtocolFallback(t, "version=2") -} - -func TestReceivePackHighestRequestedVersionFallsBackToV0ForV2(t *testing.T) { - t.Parallel() - - testReceivePackProtocolFallback(t, "version=1:version=2") -} - -func TestReceivePackWithoutReportStatusWritesNoStatusPayload(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - _, _, commitID := testRepo.MakeCommit(t, "base") - testRepo.UpdateRef(t, "refs/heads/main", commitID) - - repo := testRepo.OpenRepository(t) - - var ( - input strings.Builder - output bufferWriteFlusher - ) - - input.WriteString(pktlineData( - commitID.String() + " " + algo.Zero().String() + " refs/heads/main\x00delete-refs atomic object-format=" + algo.String() + "\n", - )) - input.WriteString("0000") - - err := receivepack.ReceivePack(context.Background(), &output, strings.NewReader(input.String()), receivepack.Options{ - Algorithm: algo, - Refs: repo.Refs(), - ExistingObjects: repo.Objects(), - }) - if err != nil { - t.Fatalf("ReceivePack: %v", err) - } - - got := output.String() - if strings.Contains(got, "unpack ") || strings.Contains(got, "ng refs/heads/main ") || strings.Contains(got, "ok refs/heads/main\n") { - t.Fatalf("unexpected status payload %q", got) - } - }) -} - -func testReceivePackProtocolFallback(t *testing.T, gitProtocol string) { - t.Helper() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - _, _, commitID := testRepo.MakeCommit(t, "base") - testRepo.UpdateRef(t, "refs/heads/main", commitID) - - repo := testRepo.OpenRepository(t) - - var ( - input strings.Builder - output bufferWriteFlusher - ) - - input.WriteString(pktlineData( - commitID.String() + " " + algo.Zero().String() + " refs/heads/main\x00report-status atomic delete-refs object-format=" + algo.String() + "\n", - )) - input.WriteString("0000") - - err := receivepack.ReceivePack(context.Background(), &output, strings.NewReader(input.String()), receivepack.Options{ - GitProtocol: gitProtocol, - Algorithm: algo, - Refs: repo.Refs(), - ExistingObjects: repo.Objects(), - }) - if err != nil { - t.Fatalf("ReceivePack: %v", err) - } - - if strings.HasPrefix(output.String(), pktlineData("version 1\n")) { - t.Fatalf("receive-pack output started with protocol v1 preface for %q: %q", gitProtocol, output.String()) - } - }) -} - -func TestReceivePackPackRequestWithoutObjectIngressReportsNotConfigured(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - _, _, commitID := testRepo.MakeCommit(t, "base") - testRepo.UpdateRef(t, "refs/heads/main", commitID) - - repo := testRepo.OpenRepository(t) - - var ( - input strings.Builder - output bufferWriteFlusher - ) - - input.WriteString(pktlineData( - commitID.String() + " " + commitID.String() + " refs/heads/main\x00report-status object-format=" + algo.String() + "\n", - )) - input.WriteString("0000") - - err := receivepack.ReceivePack(context.Background(), &output, strings.NewReader(input.String()), receivepack.Options{ - Algorithm: algo, - Refs: repo.Refs(), - ExistingObjects: repo.Objects(), - }) - if err != nil { - t.Fatalf("ReceivePack: %v", err) - } - - got := output.String() - if !strings.Contains(got, "unpack object ingress not configured\n") { - t.Fatalf("unexpected receive-pack output %q", got) - } - }) -} - -func TestReceivePackPackCreatePromotesObjectsAndUpdatesRef(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - sender := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - _, _, commitID := sender.MakeCommit(t, "pushed commit") - - receiver := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - repo := receiver.OpenRepository(t) - objectIngress := openReceivePackIngress(t, receiver, algo) - - packStream := sender.PackObjectsReader(t, []string{commitID.String()}, false) - t.Cleanup(func() { - _ = packStream.Close() - }) - - var ( - input strings.Builder - output bufferWriteFlusher - ) - - input.WriteString(pktlineData( - algo.Zero().String() + " " + commitID.String() + " refs/heads/main\x00report-status-v2 atomic object-format=" + algo.String() + "\n", - )) - input.WriteString("0000") - - err := receivepack.ReceivePack( - context.Background(), - &output, - io.MultiReader(strings.NewReader(input.String()), packStream), - receivepack.Options{ - Algorithm: algo, - Refs: repo.Refs(), - ExistingObjects: repo.Objects(), - ObjectIngress: objectIngress, - }, - ) - if err != nil { - t.Fatalf("ReceivePack: %v", err) - } - - got := output.String() - if !strings.Contains(got, "unpack ok\n") || !strings.Contains(got, "ok refs/heads/main\n") { - t.Fatalf("unexpected receive-pack output %q", got) - } - - reopened := receiver.OpenRepository(t) - - resolved, err := reopened.Refs().ResolveToDetached("refs/heads/main") - if err != nil { - t.Fatalf("ResolveToDetached(main): %v", err) - } - - if resolved.ID != commitID { - t.Fatalf("refs/heads/main = %s, want %s", resolved.ID, commitID) - } - - if gotType := receiver.Run(t, "cat-file", "-t", commitID.String()); gotType != "commit" { - t.Fatalf("cat-file -t = %q, want commit", gotType) - } - - packs := receiver.Run(t, "count-objects", "-v") - if !strings.Contains(packs, "packs: 1") { - t.Fatalf("count-objects output missing promoted pack: %q", packs) - } - }) -} - -func TestReceivePackHookSeesQuarantinedObjectsAndCanRejectBeforePromotion(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - sender := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - _, _, commitID := sender.MakeCommit(t, "pushed commit") - - receiver := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - repo := receiver.OpenRepository(t) - objectIngress := openReceivePackIngress(t, receiver, algo) - - packStream := sender.PackObjectsReader(t, []string{commitID.String()}, false) - t.Cleanup(func() { - _ = packStream.Close() - }) - - var ( - input strings.Builder - output bufferWriteFlusher - hookCalled bool - ) - - input.WriteString(pktlineData( - algo.Zero().String() + " " + commitID.String() + " refs/heads/main\x00report-status-v2 atomic object-format=" + algo.String() + "\n", - )) - input.WriteString("0000") - - err := receivepack.ReceivePack( - context.Background(), - &output, - io.MultiReader(strings.NewReader(input.String()), packStream), - receivepack.Options{ - Algorithm: algo, - Refs: repo.Refs(), - ExistingObjects: repo.Objects(), - ObjectIngress: objectIngress, - Hook: func(ctx context.Context, req receivepack.HookRequest) ([]receivepack.UpdateDecision, error) { - hookCalled = true - - if len(req.Updates) != 1 || req.Updates[0].NewID != commitID { - t.Fatalf("unexpected hook updates: %+v", req.Updates) - } - - _, _, err := req.ExistingObjects.ReadHeader(commitID) - if err == nil { - t.Fatalf("existing objects unexpectedly contained quarantined commit %s", commitID) - } - - _, _, err = req.QuarantinedObjects.ReadHeader(commitID) - if err != nil { - t.Fatalf("quarantined objects missing commit %s: %v", commitID, err) - } - - return []receivepack.UpdateDecision{{ - Accept: false, - Message: "blocked by hook", - }}, nil - }, - }, - ) - if err != nil { - t.Fatalf("ReceivePack: %v", err) - } - - if !hookCalled { - t.Fatal("hook was not called") - } - - got := output.String() - if !strings.Contains(got, "unpack ok\n") || !strings.Contains(got, "ng refs/heads/main blocked by hook\n") { - t.Fatalf("unexpected receive-pack output %q", got) - } - - _, err = repo.Refs().Resolve("refs/heads/main") - if err == nil { - t.Fatal("refs/heads/main exists after hook rejection") - } - - packs := receiver.Run(t, "count-objects", "-v") - if !strings.Contains(packs, "packs: 0") { - t.Fatalf("count-objects output shows unexpected promoted pack: %q", packs) - } - }) -} - -func TestReceivePackHookCanRejectSubsetOfNonAtomicDeleteOnlyPush(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - _, _, commitID := testRepo.MakeCommit(t, "base") - testRepo.UpdateRef(t, "refs/heads/main", commitID) - testRepo.UpdateRef(t, "refs/heads/topic", commitID) - - repo := testRepo.OpenRepository(t) - - var ( - input strings.Builder - output bufferWriteFlusher - ) - - input.WriteString(pktlineData( - commitID.String() + " " + algo.Zero().String() + " refs/heads/main\x00report-status delete-refs object-format=" + algo.String() + "\n", - )) - input.WriteString(pktlineData( - commitID.String() + " " + algo.Zero().String() + " refs/heads/topic\n", - )) - input.WriteString("0000") - - err := receivepack.ReceivePack(context.Background(), &output, strings.NewReader(input.String()), receivepack.Options{ - Algorithm: algo, - Refs: repo.Refs(), - ExistingObjects: repo.Objects(), - Hook: func(ctx context.Context, req receivepack.HookRequest) ([]receivepack.UpdateDecision, error) { - return []receivepack.UpdateDecision{ - {Accept: false, Message: "leave main alone"}, - {Accept: true}, - }, nil - }, - }) - if err != nil { - t.Fatalf("ReceivePack: %v", err) - } - - got := output.String() - if !strings.Contains(got, "ng refs/heads/main leave main alone\n") || !strings.Contains(got, "ok refs/heads/topic\n") { - t.Fatalf("unexpected receive-pack output %q", got) - } - - _, err = repo.Refs().Resolve("refs/heads/main") - if err != nil { - t.Fatalf("Resolve(main): %v", err) - } - - _, err = repo.Refs().Resolve("refs/heads/topic") - if err == nil { - t.Fatal("refs/heads/topic still exists after successful delete") - } - }) -} - -func TestReceivePackHookProgressUsesSideBand64K(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - _, _, commitID := testRepo.MakeCommit(t, "base") - testRepo.UpdateRef(t, "refs/heads/main", commitID) - - repo := testRepo.OpenRepository(t) - - var ( - input strings.Builder - output bufferWriteFlusher - ) - - input.WriteString(pktlineData( - commitID.String() + " " + algo.Zero().String() + " refs/heads/main\x00report-status side-band-64k atomic delete-refs object-format=" + algo.String() + "\n", - )) - input.WriteString("0000") - - err := receivepack.ReceivePack(context.Background(), &output, strings.NewReader(input.String()), receivepack.Options{ - Algorithm: algo, - Refs: repo.Refs(), - ExistingObjects: repo.Objects(), - Hook: func(ctx context.Context, req receivepack.HookRequest) ([]receivepack.UpdateDecision, error) { - _, err := io.WriteString(req.IO.Progress, "hook says hello\n") - if err != nil { - return nil, err - } - - return []receivepack.UpdateDecision{{Accept: true}}, nil - }, - }) - if err != nil { - t.Fatalf("ReceivePack: %v", err) - } - - _, sidebandWire, ok := strings.Cut(output.String(), "0000") - if !ok { - t.Fatalf("output missing advertisement flush: %q", output.String()) - } - - dec := sideband64k.NewDecoder(strings.NewReader(sidebandWire), sideband64k.ReadOptions{}) - - sawHookProgress := false - - var frame sideband64k.Frame - - for { - var err error - - frame, err = dec.ReadFrame() - if err != nil { - t.Fatalf("ReadFrame: %v", err) - } - - if frame.Type == sideband64k.FrameProgress && string(frame.Payload) == "hook says hello\n" { - sawHookProgress = true - } - - if frame.Type == sideband64k.FrameData { - break - } - } - - if !sawHookProgress { - t.Fatal("missing hook progress frame") - } - - statusDec := pktline.NewDecoder(strings.NewReader(string(frame.Payload)), pktline.ReadOptions{}) - - statusFrame, err := statusDec.ReadFrame() - if err != nil { - t.Fatalf("ReadFrame(status unpack): %v", err) - } - - if statusFrame.Type != pktline.PacketData || string(statusFrame.Payload) != "unpack ok\n" { - t.Fatalf("status frame = %#v", statusFrame) - } - }) -} - -func TestReceivePackPredefinedRejectForcePushHookRejectsNonFastForward(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - _, treeID := testRepo.MakeSingleFileTree(t, "base.txt", []byte("base\n")) - baseID := testRepo.CommitTree(t, treeID, "base") - currentID := testRepo.CommitTree(t, treeID, "current", baseID) - forcedID := testRepo.CommitTree(t, treeID, "forced", baseID) - testRepo.UpdateRef(t, "refs/heads/main", currentID) - - repo := testRepo.OpenRepository(t) - objectIngress := openReceivePackIngress(t, testRepo, algo) - packStream := testRepo.PackObjectsReader(t, []string{forcedID.String(), "^" + currentID.String()}, false) - t.Cleanup(func() { - _ = packStream.Close() - }) - - var ( - input strings.Builder - output bufferWriteFlusher - ) - - input.WriteString(pktlineData( - currentID.String() + " " + forcedID.String() + " refs/heads/main\x00report-status atomic object-format=" + algo.String() + "\n", - )) - input.WriteString("0000") - - err := receivepack.ReceivePack( - context.Background(), - &output, - io.MultiReader(strings.NewReader(input.String()), packStream), - receivepack.Options{ - Algorithm: algo, - Refs: repo.Refs(), - ExistingObjects: repo.Objects(), - ObjectIngress: objectIngress, - Hook: receivepackhooks.RejectForcePush(), - }, - ) - if err != nil { - t.Fatalf("ReceivePack: %v", err) - } - - got := output.String() - if !strings.Contains(got, "ng refs/heads/main non-fast-forward\n") { - t.Fatalf("unexpected receive-pack output %q", got) - } - - resolved, err := repo.Refs().ResolveToDetached("refs/heads/main") - if err != nil { - t.Fatalf("ResolveToDetached(main): %v", err) - } - - if resolved.ID != currentID { - t.Fatalf("refs/heads/main = %s, want %s", resolved.ID, currentID) - } - }) -} - -func TestReceivePackReportStatusV2IncludesRefDetails(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - _, _, commitID := testRepo.MakeCommit(t, "base") - testRepo.UpdateRef(t, "refs/heads/main", commitID) - - repo := testRepo.OpenRepository(t) - - var ( - input strings.Builder - output bufferWriteFlusher - ) - - input.WriteString(pktlineData( - commitID.String() + " " + algo.Zero().String() + " refs/heads/main\x00report-status-v2 atomic delete-refs object-format=" + algo.String() + "\n", - )) - input.WriteString("0000") - - err := receivepack.ReceivePack(context.Background(), &output, strings.NewReader(input.String()), receivepack.Options{ - Algorithm: algo, - Refs: repo.Refs(), - ExistingObjects: repo.Objects(), - }) - if err != nil { - t.Fatalf("ReceivePack: %v", err) - } - - got := output.String() - if !strings.Contains(got, "option refname refs/heads/main\n") { - t.Fatalf("missing option refname in %q", got) - } - - if !strings.Contains(got, "option old-oid "+commitID.String()+"\n") { - t.Fatalf("missing option old-oid in %q", got) - } - - if !strings.Contains(got, "option new-oid "+algo.Zero().String()+"\n") { - t.Fatalf("missing option new-oid in %q", got) - } - }) -} - -func TestReceivePackGitPushCreatesBranch(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - t.Parallel() - - sender := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - _, _, commitID := sender.MakeCommit(t, "pushed commit") - sender.UpdateRef(t, "refs/heads/main", commitID) - - receiver := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - repo := receiver.OpenRepository(t) - objectIngress := openReceivePackIngress(t, receiver, algo) - - stdout, stderr, clientErr, serverErr := runGitPushFD( - t, - sender, - receivepack.Options{ - Algorithm: algo, - Refs: repo.Refs(), - ExistingObjects: repo.Objects(), - ObjectIngress: objectIngress, - }, - "push", "--porcelain", "fd::3,4/test", "refs/heads/main:refs/heads/main", - ) - if clientErr != nil { - t.Fatalf("git push failed: %v\nstdout=%s\nstderr=%s", clientErr, stdout, stderr) - } - - if serverErr != nil { - t.Fatalf("ReceivePack: %v", serverErr) - } - - resolved, err := receiver.OpenRepository(t).Refs().ResolveToDetached("refs/heads/main") - if err != nil { - t.Fatalf("ResolveToDetached(main): %v", err) - } - - if resolved.ID != commitID { - t.Fatalf("refs/heads/main = %s, want %s", resolved.ID, commitID) - } - }) -} - -func TestReceivePackGitPushRefUpdateWithoutNewObjectsSucceeds(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - t.Parallel() - - sender := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - blobID, treeID := sender.MakeSingleFileTree(t, "base.txt", []byte("base\n")) - commitID := sender.CommitTree(t, treeID, "base") - sender.UpdateRef(t, "refs/heads/main", commitID) - - receiver := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - receiver.HashObject(t, "blob", sender.RunBytes(t, "cat-file", "blob", blobID.String())) - receiver.HashObject(t, "tree", sender.RunBytes(t, "cat-file", "tree", treeID.String())) - receiver.HashObject(t, "commit", sender.RunBytes(t, "cat-file", "commit", commitID.String())) - receiver.UpdateRef(t, "refs/heads/main", commitID) - - repo := receiver.OpenRepository(t) - objectIngress := openReceivePackIngress(t, receiver, algo) - - stdout, stderr, clientErr, serverErr := runGitPushFD( - t, - sender, - receivepack.Options{ - Algorithm: algo, - Refs: repo.Refs(), - ExistingObjects: repo.Objects(), - ObjectIngress: objectIngress, - }, - "push", "--porcelain", "fd::3,4/test", "refs/heads/main:refs/heads/topic", - ) - if clientErr != nil { - t.Fatalf("git push failed: %v\nstdout=%s\nstderr=%s", clientErr, stdout, stderr) - } - - if serverErr != nil { - t.Fatalf("ReceivePack: %v", serverErr) - } - - resolved, err := receiver.OpenRepository(t).Refs().ResolveToDetached("refs/heads/topic") - if err != nil { - t.Fatalf("ResolveToDetached(topic): %v", err) - } - - if resolved.ID != commitID { - t.Fatalf("refs/heads/topic = %s, want %s", resolved.ID, commitID) - } - - packs := receiver.Run(t, "count-objects", "-v") - if !strings.Contains(packs, "packs: 0") { - t.Fatalf("count-objects output shows unexpected promoted pack: %q", packs) - } - }) -} - -func TestReceivePackGitPushAtomicDelete(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - t.Parallel() - - sender := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - receiver := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - _, _, commitID := receiver.MakeCommit(t, "base") - receiver.UpdateRef(t, "refs/heads/main", commitID) - - repo := receiver.OpenRepository(t) - - stdout, stderr, clientErr, serverErr := runGitPushFD( - t, - sender, - receivepack.Options{ - Algorithm: algo, - Refs: repo.Refs(), - ExistingObjects: repo.Objects(), - }, - "push", "--porcelain", "--atomic", "fd::3,4/test", ":refs/heads/main", - ) - if clientErr != nil { - t.Fatalf("git push failed: %v\nstdout=%s\nstderr=%s", clientErr, stdout, stderr) - } - - if serverErr != nil { - t.Fatalf("ReceivePack: %v", serverErr) - } - - _, err := receiver.OpenRepository(t).Refs().Resolve("refs/heads/main") - if err == nil { - t.Fatal("refs/heads/main still exists after delete push") - } - }) -} - -func TestReceivePackGitPushRejectsForcedUpdateViaHook(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - t.Parallel() - - sender := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - blobID, treeID := sender.MakeSingleFileTree(t, "base.txt", []byte("base\n")) - baseID := sender.CommitTree(t, treeID, "base") - currentID := sender.CommitTree(t, treeID, "current", baseID) - forcedID := sender.CommitTree(t, treeID, "forced", baseID) - sender.UpdateRef(t, "refs/heads/main", forcedID) - - receiver := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - receiver.HashObject(t, "blob", sender.RunBytes(t, "cat-file", "blob", blobID.String())) - receiver.HashObject(t, "tree", sender.RunBytes(t, "cat-file", "tree", treeID.String())) - receiver.HashObject(t, "commit", sender.RunBytes(t, "cat-file", "commit", baseID.String())) - receiver.HashObject(t, "commit", sender.RunBytes(t, "cat-file", "commit", currentID.String())) - receiver.UpdateRef(t, "refs/heads/main", currentID) - - repo := receiver.OpenRepository(t) - objectIngress := openReceivePackIngress(t, receiver, algo) - - stdout, stderr, clientErr, serverErr := runGitPushFD( - t, - sender, - receivepack.Options{ - Algorithm: algo, - Refs: repo.Refs(), - ExistingObjects: repo.Objects(), - ObjectIngress: objectIngress, - Hook: receivepackhooks.RejectForcePush(), - }, - "push", "--porcelain", "--force", "fd::3,4/test", "refs/heads/main:refs/heads/main", - ) - if clientErr == nil { - t.Fatalf("git push unexpectedly succeeded\nstdout=%s\nstderr=%s", stdout, stderr) - } - - if serverErr != nil { - t.Fatalf("ReceivePack: %v", serverErr) - } - - if !strings.Contains(stdout, "non-fast-forward") && !strings.Contains(stderr, "non-fast-forward") { - t.Fatalf("git push output missing non-fast-forward message\nstdout=%s\nstderr=%s", stdout, stderr) - } - - resolved, err := receiver.OpenRepository(t).Refs().ResolveToDetached("refs/heads/main") - if err != nil { - t.Fatalf("ResolveToDetached(main): %v", err) - } - - if resolved.ID != currentID { - t.Fatalf("refs/heads/main = %s, want %s", resolved.ID, currentID) - } - }) -} - -type bufferWriteFlusher struct { - strings.Builder -} - -func (bufferWriteFlusher) Flush() error { - return nil -} - -func pktlineData(payload string) string { - return fmt.Sprintf("%04x%s", len(payload)+4, payload) -} - -func openReceivePackIngress( - tb testing.TB, - testRepo *testgit.TestRepo, - algo objectid.Algorithm, -) objectstore.Quarantiner { - tb.Helper() - - objectsRoot := testRepo.OpenObjectsRoot(tb) - - err := objectsRoot.Mkdir("pack", 0o755) - if err != nil && !os.IsExist(err) { - tb.Fatalf("Mkdir(pack): %v", err) - } - - packRoot, err := objectsRoot.OpenRoot("pack") - if err != nil { - tb.Fatalf("OpenRoot(pack): %v", err) - } - - tb.Cleanup(func() { - _ = packRoot.Close() - }) - - looseStore, err := objectloose.New(objectsRoot, algo) - if err != nil { - tb.Fatalf("loose.New: %v", err) - } - - tb.Cleanup(func() { - _ = looseStore.Close() - }) - - packedStore, err := objectpacked.New(packRoot, algo, objectpacked.Options{WriteRev: true}) - if err != nil { - tb.Fatalf("packed.New: %v", err) - } - - tb.Cleanup(func() { - _ = packedStore.Close() - }) - - return objectdual.New(looseStore, packedStore) -} - -type fileWriteFlusher struct { - *os.File -} - -func (fileWriteFlusher) Flush() error { - return nil -} - -func runGitPushFD( - tb testing.TB, - sender *testgit.TestRepo, - opts receivepack.Options, - gitArgs ...string, -) (stdout string, stderr string, clientErr error, serverErr error) { - tb.Helper() - - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - - serverRead, clientWrite, err := os.Pipe() - if err != nil { - tb.Fatalf("os.Pipe(serverRead/clientWrite): %v", err) - } - - clientRead, serverWrite, err := os.Pipe() - if err != nil { - tb.Fatalf("os.Pipe(clientRead/serverWrite): %v", err) - } - - tb.Cleanup(func() { - _ = serverRead.Close() - _ = clientWrite.Close() - _ = clientRead.Close() - _ = serverWrite.Close() - }) - - go func() { - <-ctx.Done() - - _ = serverRead.Close() - _ = clientWrite.Close() - _ = clientRead.Close() - _ = serverWrite.Close() - }() - - serverErrCh := make(chan error, 1) - - go func() { - defer func() { - _ = serverRead.Close() - _ = serverWrite.Close() - }() - - serverErrCh <- receivepack.ReceivePack( - ctx, - fileWriteFlusher{serverWrite}, - serverRead, - opts, - ) - }() - - stdoutBytes, stderrBytes, clientErr := sender.RunWithExtraFilesEnvContextE( - tb, - ctx, - nil, - []*os.File{clientRead, clientWrite}, - gitArgs..., - ) - _ = clientRead.Close() - _ = clientWrite.Close() - - serverErr = <-serverErrCh - - if ctx.Err() != nil { - tb.Fatalf( - "git push fd:: timed out\nstdout=%s\nstderr=%s\nclientErr=%v\nserverErr=%v", - stdoutBytes, - stderrBytes, - clientErr, - serverErr, - ) - } - - return string(stdoutBytes), string(stderrBytes), clientErr, serverErr -} diff --git a/network/receivepack/options.go b/network/receivepack/options.go deleted file mode 100644 index 30792765..00000000 --- a/network/receivepack/options.go +++ /dev/null @@ -1,71 +0,0 @@ -package receivepack - -import ( - commitgraphread "codeberg.org/lindenii/furgit/format/commitgraph/read" - objectid "codeberg.org/lindenii/furgit/object/id" - objectstore "codeberg.org/lindenii/furgit/object/store" - refstore "codeberg.org/lindenii/furgit/ref/store" -) - -// Options configures one receive-pack invocation. -// -// ReceivePack borrows all configured dependencies. -// -// Refs and ExistingObjects are required and must be non-nil. -// ObjectIngress is required if the invocation may need to ingest or quarantine a -// pack. -type Options struct { - // GitProtocol is the raw Git protocol version string from the transport, - // such as "version=1". - GitProtocol string - // Algorithm is the repository object ID algorithm used by the push session. - Algorithm objectid.Algorithm - // Refs is the reference store visible to the push. - Refs interface { - refstore.Reader - refstore.Transactioner - refstore.Batcher - } - // ExistingObjects is the object store visible to the push before any newly - // uploaded quarantined objects are promoted. - ExistingObjects objectstore.Reader - // ObjectIngress creates coordinated quarantines for quarantined object and - // pack ingestion during the push. - ObjectIngress objectstore.Quarantiner - // CommitGraph is an optional commit-graph snapshot corresponding to - // ExistingObjects. - CommitGraph *commitgraphread.Reader - // Hook, when non-nil, runs after pack ingestion into quarantine and before - // quarantine promotion or ref updates. Hook is borrowed for the duration of - // ReceivePack. - Hook Hook - // Agent is the receive-pack agent string advertised via capability. - // - // When empty, ReceivePack derives one from build info and falls back to - // "furgit". - Agent string - // SessionID is the advertised receive-pack session-id capability value. - // - // When empty, ReceivePack generates one random value per invocation. - SessionID string - // PushCertNonce is the advertised push-cert nonce capability value. - // - // When empty, ReceivePack generates one random value per invocation. - PushCertNonce string -} - -func validateOptions(opts Options) error { - if opts.Algorithm == 0 { - return ErrMissingAlgorithm - } - - if opts.Refs == nil { - return ErrMissingRefs - } - - if opts.ExistingObjects == nil { - return ErrMissingObjects - } - - return nil -} diff --git a/network/receivepack/receivepack.go b/network/receivepack/receivepack.go deleted file mode 100644 index d58e9fa0..00000000 --- a/network/receivepack/receivepack.go +++ /dev/null @@ -1,139 +0,0 @@ -package receivepack - -import ( - "context" - "io" - - "codeberg.org/lindenii/furgit/common/iowrap" - common "codeberg.org/lindenii/furgit/network/protocol/v0v1/server" - protoreceive "codeberg.org/lindenii/furgit/network/protocol/v0v1/server/receivepack" - "codeberg.org/lindenii/furgit/network/receivepack/service" -) - -// TODO: Some more designing to do. In particular, we'd like to have access to -// commit graphs and stored object abstractions and such here, especially because -// hooks might want to access full repos, but we risk creating -// circular dependencies if we import repository/ here. Might need an interface-ish -// design, but that risks being over-complicated. -// Theoretically we could also just give the hooks an os.Root but that -// feels a bit ugly. - -// ReceivePack serves one receive-pack session over r/w. -// -// Labels: Deps-Borrowed. -func ReceivePack( - ctx context.Context, - w iowrap.WriteFlusher, - r io.Reader, - opts Options, -) error { - err := validateOptions(opts) - if err != nil { - return err - } - - version := parseVersion(opts.GitProtocol) - - base := common.NewSession(r, w, common.Options{ - Version: version, - Algorithm: opts.Algorithm, - }) - - agent := opts.Agent - if agent == "" { - agent = defaultAgent() - } - - sessionID := opts.SessionID - if sessionID == "" { - sessionID = defaultSessionID() - } - - pushCertNonce := opts.PushCertNonce - if pushCertNonce == "" { - pushCertNonce = defaultPushCertNonce() - } - - protoSession := protoreceive.NewSession(base, protoreceive.Capabilities{ - ReportStatus: true, - ReportStatusV2: true, - DeleteRefs: true, - SideBand64K: true, - Quiet: true, - Atomic: true, - OfsDelta: true, - PushOptions: true, - PushCertNonce: pushCertNonce, - SessionID: sessionID, - ObjectFormat: opts.Algorithm, - Agent: agent, - }) - - refs, err := advertisedRefs(opts) - if err != nil { - return err - } - - err = protoSession.AdvertiseRefs(common.Advertisement{Refs: refs}) - if err != nil { - return err - } - - err = base.Flush() - if err != nil { - return err - } - - req, err := protoSession.ReadRequest() - if err != nil { - return err - } - - progress := protoSession.ProgressWriter() - - if req.Capabilities.Quiet { - progress = iowrap.NopFlush(io.Discard) - } - - serviceReq := &service.Request{ - Commands: translateCommands(req.Commands), - PushOptions: append([]string(nil), req.PushOptions...), - Atomic: req.Capabilities.Atomic, - PackExpected: req.PackExpected, - Pack: r, - } - - svc := service.New(service.Options{ - Refs: opts.Refs, - ExistingObjects: opts.ExistingObjects, - ObjectIngress: opts.ObjectIngress, - CommitGraph: opts.CommitGraph, - Progress: progress, - Hook: translateHook(opts.Hook), - HookIO: service.HookIO{ - Progress: progress, - Error: protoSession.ErrorWriter(), - }, - }) - - result, err := svc.Execute(ctx, serviceReq) - if err != nil { - return err - } - - protoResult := translateResult(result) - - if req.Capabilities.ReportStatusV2 { - err = protoSession.WriteReportStatusV2(protoResult) - if err != nil { - return err - } - } else if req.Capabilities.ReportStatus { - err = protoSession.WriteReportStatus(protoResult) - if err != nil { - return err - } - } - - return base.Flush() -} diff --git a/network/receivepack/results.go b/network/receivepack/results.go deleted file mode 100644 index d43bee73..00000000 --- a/network/receivepack/results.go +++ /dev/null @@ -1,26 +0,0 @@ -package receivepack - -import ( - protoreceive "codeberg.org/lindenii/furgit/network/protocol/v0v1/server/receivepack" - "codeberg.org/lindenii/furgit/network/receivepack/service" -) - -func translateResult(result *service.Result) protoreceive.ReportStatusResult { - out := protoreceive.ReportStatusResult{ - UnpackError: result.UnpackError, - Commands: make([]protoreceive.CommandResult, 0, len(result.Commands)), - } - - for _, command := range result.Commands { - out.Commands = append(out.Commands, protoreceive.CommandResult{ - Name: command.Name, - Error: command.Error, - RefName: command.RefName, - OldID: command.OldID, - NewID: command.NewID, - ForcedUpdate: command.ForcedUpdate, - }) - } - - return out -} diff --git a/network/receivepack/service/apply.go b/network/receivepack/service/apply.go deleted file mode 100644 index fdf3eef6..00000000 --- a/network/receivepack/service/apply.go +++ /dev/null @@ -1,137 +0,0 @@ -package service - -import ( - "codeberg.org/lindenii/furgit/internal/utils" - refstore "codeberg.org/lindenii/furgit/ref/store" -) - -func (service *Service) applyAtomic(result *Result, commands []Command) error { - total := len(commands) - utils.BestEffortFprintf(service.opts.Progress, "updating refs: 0/%d\r", total) - - tx, err := service.opts.Refs.BeginTransaction() - if err != nil { - return err - } - - for i, command := range commands { - err = queueWriteTransaction(tx, command) - if err != nil { - _ = tx.Abort() - - fillCommandErrors(result, commands, err.Error()) - utils.BestEffortFprintf(service.opts.Progress, "updating refs: failed at %d/%d.\n", i+1, total) - - return nil - } - - utils.BestEffortFprintf(service.opts.Progress, "updating refs: %d/%d\r", i+1, total) - } - - err = tx.Commit() - if err != nil { - fillCommandErrors(result, commands, err.Error()) - utils.BestEffortFprintf(service.opts.Progress, "updating refs: failed at commit.\n") - - return nil - } - - result.Applied = true - for _, command := range commands { - result.Commands = append(result.Commands, successCommandResult(command)) - } - - utils.BestEffortFprintf(service.opts.Progress, "updating refs: done.\n") - - return nil -} - -func (service *Service) applyBatch(result *Result, commands []Command) error { - total := len(commands) - - utils.BestEffortFprintf(service.opts.Progress, "updating refs...\r") - - batch, err := service.opts.Refs.BeginBatch() - if err != nil { - return err - } - - for _, command := range commands { - err = queueWriteBatch(batch, command) - if err != nil { - _ = batch.Abort() - - fillCommandErrors(result, commands, err.Error()) - utils.BestEffortFprintf(service.opts.Progress, "updating refs: failed while queueing batch.\n") - - return nil - } - } - - batchResults, err := batch.Apply() - if err != nil && len(batchResults) == 0 { - utils.BestEffortFprintf(service.opts.Progress, "updating refs: failed at apply.\n") - - return err - } - - appliedAny := false - failedCount := 0 - - for i, command := range commands { - item := successCommandResult(command) - if i < len(batchResults) && batchResults[i].Error != nil { - item.Error = batchResults[i].Error.Error() - failedCount++ - } else { - appliedAny = true - } - - result.Commands = append(result.Commands, item) - - utils.BestEffortFprintf(service.opts.Progress, "updating refs: %d/%d\r", i+1, total) - } - - result.Applied = appliedAny - - if failedCount == 0 { - utils.BestEffortFprintf(service.opts.Progress, "updating refs: done.\n") - } else { - utils.BestEffortFprintf(service.opts.Progress, "updating refs: failed (%d/%d).\n", failedCount, total) - } - - return nil -} - -func queueWriteTransaction(tx refstore.Transaction, command Command) error { - if isDelete(command) { - return tx.Delete(command.Name, command.OldID) - } - - if command.OldID == command.OldID.Algorithm().Zero() { - return tx.Create(command.Name, command.NewID) - } - - return tx.Update(command.Name, command.NewID, command.OldID) -} - -func queueWriteBatch(batch refstore.Batch, command Command) error { - if isDelete(command) { - return batch.Delete(command.Name, command.OldID) - } - - if command.OldID == command.OldID.Algorithm().Zero() { - return batch.Create(command.Name, command.NewID) - } - - return batch.Update(command.Name, command.NewID, command.OldID) -} - -func successCommandResult(command Command) CommandResult { - return CommandResult{ - Name: command.Name, - RefName: command.Name, - OldID: new(command.OldID), - NewID: new(command.NewID), - } -} diff --git a/network/receivepack/service/command.go b/network/receivepack/service/command.go deleted file mode 100644 index 9ad50c4f..00000000 --- a/network/receivepack/service/command.go +++ /dev/null @@ -1,26 +0,0 @@ -package service - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// Command is one protocol-independent requested ref update. -type Command struct { - OldID objectid.ObjectID - NewID objectid.ObjectID - Name string -} - -func fillCommandErrors(result *Result, commands []Command, errText string) { - for _, command := range commands { - result.Commands = append(result.Commands, CommandResult{ - Name: command.Name, - Error: errText, - RefName: command.Name, - OldID: new(command.OldID), - NewID: new(command.NewID), - }) - } -} - -func isDelete(command Command) bool { - return command.NewID == command.NewID.Algorithm().Zero() -} diff --git a/network/receivepack/service/command_result.go b/network/receivepack/service/command_result.go deleted file mode 100644 index 37549f08..00000000 --- a/network/receivepack/service/command_result.go +++ /dev/null @@ -1,13 +0,0 @@ -package service - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// CommandResult is one per-command execution result. -type CommandResult struct { - Name string - Error string - RefName string - OldID *objectid.ObjectID - NewID *objectid.ObjectID - ForcedUpdate bool -} diff --git a/network/receivepack/service/doc.go b/network/receivepack/service/doc.go deleted file mode 100644 index c3fa3041..00000000 --- a/network/receivepack/service/doc.go +++ /dev/null @@ -1,6 +0,0 @@ -// Package service implements the protocol-independent receive-pack service. -// -// A Service borrows the stores, hooks, and I/O endpoints supplied in -// Options. Callers retain ownership of those dependencies and must keep them -// valid for each Execute call that uses them. -package service diff --git a/network/receivepack/service/execute.go b/network/receivepack/service/execute.go deleted file mode 100644 index 92d34a63..00000000 --- a/network/receivepack/service/execute.go +++ /dev/null @@ -1,120 +0,0 @@ -package service - -import ( - "context" - - "codeberg.org/lindenii/furgit/internal/utils" - objectstore "codeberg.org/lindenii/furgit/object/store" -) - -// Execute validates one receive-pack request, optionally ingests its pack into -// quarantine, runs the optional hook, and applies allowed ref updates. -// -// Labels: Deps-Borrowed. -func (service *Service) Execute(ctx context.Context, req *Request) (*Result, error) { - result := &Result{ - Commands: make([]CommandResult, 0, len(req.Commands)), - } - - var err error - - quarantine, ok := service.ingestQuarantine(result, req.Commands, req) - if !ok { - return result, nil - } - - if quarantine != nil { - defer func(q objectstore.Quarantine) { - _ = q.Discard() - }(quarantine) - } - - for _, command := range req.Commands { - result.Planned = append(result.Planned, PlannedUpdate{ - Name: command.Name, - OldID: command.OldID, - NewID: command.NewID, - Delete: isDelete(command), - }) - } - - if len(req.Commands) == 0 { - return result, nil - } - - allowedCommands, allowedIndices, rejected, ok, errText := service.runHook( - ctx, - req, - req.Commands, - quarantine, - ) - if !ok { - fillCommandErrors(result, req.Commands, errText) - - return result, nil - } - - if req.Atomic && len(rejected) != 0 { - result.Commands = make([]CommandResult, 0, len(req.Commands)) - for index, command := range req.Commands { - message := rejected[index] - if message == "" { - message = "atomic push rejected by hook" - } - - result.Commands = append(result.Commands, resultForHookRejection(command, message)) - } - - return result, nil - } - - if len(allowedCommands) == 0 { - result.Commands = mergeCommandResults(req.Commands, rejected, nil, nil) - - return result, nil - } - - if req.PackExpected && quarantine != nil { - // Git migrates quarantined objects into permanent storage immediately - // before starting ref updates. - utils.BestEffortFprintf(service.opts.Progress, "promoting quarantine...\r") - - err := quarantine.Promote() - if err != nil { - utils.BestEffortFprintf(service.opts.Progress, "promoting quarantine: failed: %v.\n", err) - - result.UnpackError = err.Error() - fillCommandErrors(result, req.Commands, err.Error()) - - return result, nil - } - - utils.BestEffortFprintf(service.opts.Progress, "promoting quarantine: done.\n") - } - - if req.Atomic { - subresult := &Result{} - - err := service.applyAtomic(subresult, allowedCommands) - if err != nil { - return result, err - } - - result.Commands = mergeCommandResults(req.Commands, rejected, subresult.Commands, allowedIndices) - result.Applied = subresult.Applied - - return result, nil - } - - subresult := &Result{} - - err = service.applyBatch(subresult, allowedCommands) - if err != nil { - return result, err - } - - result.Commands = mergeCommandResults(req.Commands, rejected, subresult.Commands, allowedIndices) - result.Applied = subresult.Applied - - return result, nil -} diff --git a/network/receivepack/service/hook.go b/network/receivepack/service/hook.go deleted file mode 100644 index 3826e6fb..00000000 --- a/network/receivepack/service/hook.go +++ /dev/null @@ -1,48 +0,0 @@ -package service - -import ( - "context" - - "codeberg.org/lindenii/furgit/common/iowrap" - commitgraphread "codeberg.org/lindenii/furgit/format/commitgraph/read" - objectid "codeberg.org/lindenii/furgit/object/id" - objectstore "codeberg.org/lindenii/furgit/object/store" - refstore "codeberg.org/lindenii/furgit/ref/store" -) - -type HookIO struct { - Progress iowrap.WriteFlusher - Error iowrap.WriteFlusher -} - -type RefUpdate struct { - Name string - OldID objectid.ObjectID - NewID objectid.ObjectID -} - -type UpdateDecision struct { - Accept bool - Message string -} - -// HookRequest is the view passed to one Hook invocation. -// -// Labels: Life-Call. -type HookRequest struct { - Refs refstore.Reader - ExistingObjects objectstore.Reader - // QuarantinedObjects exposes quarantined objects for this push. - // - // When the push did not create a quarantine, QuarantinedObjects is nil. - QuarantinedObjects objectstore.Reader - CommitGraph *commitgraphread.Reader - Updates []RefUpdate - PushOptions []string - IO HookIO -} - -// Hook is an optional per-request validation hook. -// -// The returned decisions must have the same length as HookRequest.Updates. -type Hook func(context.Context, HookRequest) ([]UpdateDecision, error) diff --git a/network/receivepack/service/hook_apply.go b/network/receivepack/service/hook_apply.go deleted file mode 100644 index 97d25009..00000000 --- a/network/receivepack/service/hook_apply.go +++ /dev/null @@ -1,31 +0,0 @@ -package service - -func resultForHookRejection(command Command, message string) CommandResult { - result := successCommandResult(command) - result.Error = message - - return result -} - -func mergeCommandResults( - commands []Command, - rejected map[int]string, - applied []CommandResult, - appliedIndices []int, -) []CommandResult { - out := make([]CommandResult, len(commands)) - - for index, message := range rejected { - out[index] = resultForHookRejection(commands[index], message) - } - - for i, appliedResult := range applied { - if i >= len(appliedIndices) { - break - } - - out[appliedIndices[i]] = appliedResult - } - - return out -} diff --git a/network/receivepack/service/ingest_quarantine.go b/network/receivepack/service/ingest_quarantine.go deleted file mode 100644 index 75f3a790..00000000 --- a/network/receivepack/service/ingest_quarantine.go +++ /dev/null @@ -1,81 +0,0 @@ -package service - -import ( - "codeberg.org/lindenii/furgit/internal/utils" - objectstore "codeberg.org/lindenii/furgit/object/store" -) - -func (service *Service) ingestQuarantine( - result *Result, - commands []Command, - req *Request, -) (objectstore.Quarantine, bool) { - if !req.PackExpected { - return nil, true - } - - if req.Pack == nil { - utils.BestEffortFprintf(service.opts.Progress, "unpack failed: missing pack stream.\n") - - result.UnpackError = "missing pack stream" - fillCommandErrors(result, commands, "missing pack stream") - - return nil, false - } - - if service.opts.ObjectIngress == nil { - utils.BestEffortFprintf(service.opts.Progress, "unpack failed: object ingress not configured.\n") - - result.UnpackError = "object ingress not configured" - fillCommandErrors(result, commands, "object ingress not configured") - - return nil, false - } - - var err error - - err = service.opts.ExistingObjects.Refresh() - if err != nil { - utils.BestEffortFprintf(service.opts.Progress, "unpack failed: refresh existing objects: %v.\n", err) - - result.UnpackError = err.Error() - fillCommandErrors(result, commands, err.Error()) - - return nil, false - } - - utils.BestEffortFprintf(service.opts.Progress, "creating quarantine...\r") - - quarantine, err := service.opts.ObjectIngress.BeginQuarantine(objectstore.QuarantineOptions{}) - if err != nil { - utils.BestEffortFprintf(service.opts.Progress, "unpack failed: %v.\n", err) - - result.UnpackError = err.Error() - fillCommandErrors(result, commands, err.Error()) - - return nil, false - } - - utils.BestEffortFprintf(service.opts.Progress, "creating quarantine: done.\n") - utils.BestEffortFprintf(service.opts.Progress, "unpacking...\r") - - err = quarantine.WritePack(req.Pack, objectstore.PackWriteOptions{ - ThinBase: service.opts.ExistingObjects, - Progress: service.opts.Progress, - RequireTrailingEOF: false, - }) - if err != nil { - utils.BestEffortFprintf(service.opts.Progress, "unpack failed: %v.\n", err) - - result.UnpackError = err.Error() - fillCommandErrors(result, commands, err.Error()) - - _ = quarantine.Discard() - - return nil, false - } - - utils.BestEffortFprintf(service.opts.Progress, "unpacking: done.\n") - - return quarantine, true -} diff --git a/network/receivepack/service/options.go b/network/receivepack/service/options.go deleted file mode 100644 index b6e71d64..00000000 --- a/network/receivepack/service/options.go +++ /dev/null @@ -1,31 +0,0 @@ -package service - -import ( - "codeberg.org/lindenii/furgit/common/iowrap" - commitgraphread "codeberg.org/lindenii/furgit/format/commitgraph/read" - objectstore "codeberg.org/lindenii/furgit/object/store" - refstore "codeberg.org/lindenii/furgit/ref/store" -) - -// Options configures one protocol-independent receive-pack service. -// -// Service borrows all configured dependencies. -// -// Refs and ExistingObjects are required and must be non-nil. -// ObjectIngress is required if Execute may need to ingest or quarantine a -// pack. -// CommitGraph, Progress, Hook, and HookIO are optional; when provided they are also -// borrowed for the duration of Execute. -type Options struct { - Refs interface { - refstore.Reader - refstore.Transactioner - refstore.Batcher - } - ExistingObjects objectstore.Reader - ObjectIngress objectstore.Quarantiner - CommitGraph *commitgraphread.Reader - Progress iowrap.WriteFlusher - Hook Hook - HookIO HookIO -} diff --git a/network/receivepack/service/request.go b/network/receivepack/service/request.go deleted file mode 100644 index 33e3796f..00000000 --- a/network/receivepack/service/request.go +++ /dev/null @@ -1,15 +0,0 @@ -package service - -import "io" - -// Request is one protocol-independent receive-pack execution request. -// -// If PackExpected is true, Pack must be non-nil and remain valid until -// Execute finishes consuming it. -type Request struct { - Commands []Command - PushOptions []string - Atomic bool - PackExpected bool - Pack io.Reader -} diff --git a/network/receivepack/service/result.go b/network/receivepack/service/result.go deleted file mode 100644 index 7a75be11..00000000 --- a/network/receivepack/service/result.go +++ /dev/null @@ -1,9 +0,0 @@ -package service - -// Result is one receive-pack execution result. -type Result struct { - UnpackError string - Commands []CommandResult - Planned []PlannedUpdate - Applied bool -} diff --git a/network/receivepack/service/run_hook.go b/network/receivepack/service/run_hook.go deleted file mode 100644 index c8b1b76c..00000000 --- a/network/receivepack/service/run_hook.go +++ /dev/null @@ -1,93 +0,0 @@ -package service - -import ( - "context" - - "codeberg.org/lindenii/furgit/internal/utils" - objectstore "codeberg.org/lindenii/furgit/object/store" -) - -func (service *Service) runHook( - ctx context.Context, - req *Request, - commands []Command, - quarantinedObjects objectstore.Reader, -) ( - allowedCommands []Command, - allowedIndices []int, - rejected map[int]string, - ok bool, - errText string, -) { - allowedCommands = append([]Command(nil), commands...) - - allowedIndices = make([]int, 0, len(commands)) - for index := range commands { - allowedIndices = append(allowedIndices, index) - } - - rejected = make(map[int]string) - if service.opts.Hook == nil { - return allowedCommands, allowedIndices, rejected, true, "" - } - - utils.BestEffortFprintf(service.opts.Progress, "running hooks...\r") - - updates := make([]RefUpdate, 0, len(commands)) - for _, command := range commands { - updates = append(updates, RefUpdate{ - Name: command.Name, - OldID: command.OldID, - NewID: command.NewID, - }) - } - - decisions, err := service.opts.Hook(ctx, HookRequest{ - Refs: service.opts.Refs, - ExistingObjects: service.opts.ExistingObjects, - QuarantinedObjects: quarantinedObjects, - CommitGraph: service.opts.CommitGraph, - Updates: updates, - PushOptions: append([]string(nil), req.PushOptions...), - IO: service.opts.HookIO, - }) - if err != nil { - utils.BestEffortFprintf(service.opts.Progress, "running hooks: failed: %v.\n", err) - - return nil, nil, nil, false, err.Error() - } - - if len(decisions) != len(commands) { - utils.BestEffortFprintf(service.opts.Progress, "running hooks: failed: wrong decision count.\n") - - return nil, nil, nil, false, "hook returned wrong number of update decisions" - } - - allowedCommands = allowedCommands[:0] - allowedIndices = allowedIndices[:0] - - for index, decision := range decisions { - if decision.Accept { - allowedCommands = append(allowedCommands, commands[index]) - allowedIndices = append(allowedIndices, index) - - continue - } - - message := decision.Message - if message == "" { - message = "rejected by hook" - } - - rejected[index] = message - } - - utils.BestEffortFprintf( - service.opts.Progress, - "running hooks: done (%d/%d accepted).\n", - len(allowedCommands), - len(commands), - ) - - return allowedCommands, allowedIndices, rejected, true, "" -} diff --git a/network/receivepack/service/service.go b/network/receivepack/service/service.go deleted file mode 100644 index 0d931b64..00000000 --- a/network/receivepack/service/service.go +++ /dev/null @@ -1,13 +0,0 @@ -package service - -// Service executes protocol-independent receive-pack requests. -type Service struct { - opts Options -} - -// New creates one receive-pack service. -// -// Labels: Deps-Borrowed, Life-Parent. -func New(opts Options) *Service { - return &Service{opts: opts} -} diff --git a/network/receivepack/service/service_test.go b/network/receivepack/service/service_test.go deleted file mode 100644 index 94e105da..00000000 --- a/network/receivepack/service/service_test.go +++ /dev/null @@ -1,129 +0,0 @@ -package service_test - -import ( - "context" - "os" - "strings" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - "codeberg.org/lindenii/furgit/network/receivepack/service" - objectid "codeberg.org/lindenii/furgit/object/id" - objectstore "codeberg.org/lindenii/furgit/object/store" - objectdual "codeberg.org/lindenii/furgit/object/store/dual" - objectloose "codeberg.org/lindenii/furgit/object/store/loose" - "codeberg.org/lindenii/furgit/object/store/memory" - objectpacked "codeberg.org/lindenii/furgit/object/store/packed" -) - -func TestExecutePackExpectedWithoutObjectIngress(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - store := memory.New(algo) - svc := service.New(service.Options{ - ExistingObjects: store, - }) - - result, err := svc.Execute(context.Background(), &service.Request{ - Commands: []service.Command{{ - Name: "refs/heads/main", - OldID: algo.Zero(), - NewID: algo.Zero(), - }}, - PackExpected: true, - Pack: strings.NewReader("not a pack"), - }) - if err != nil { - t.Fatalf("Execute: %v", err) - } - - if result.UnpackError != "object ingress not configured" { - t.Fatalf("unexpected unpack error %q", result.UnpackError) - } - }) -} - -func TestExecuteDiscardedQuarantineAfterIngestFailure(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - store := memory.New(algo) - objectIngress := newDualIngress(t, algo) - - svc := service.New(service.Options{ - ExistingObjects: store, - ObjectIngress: objectIngress, - }) - - result, err := svc.Execute(context.Background(), &service.Request{ - Commands: []service.Command{{ - Name: "refs/heads/main", - OldID: algo.Zero(), - NewID: algo.Zero(), - }}, - PackExpected: true, - Pack: strings.NewReader("not a pack"), - }) - if err != nil { - t.Fatalf("Execute: %v", err) - } - - if result.UnpackError == "" { - t.Fatal("Execute returned empty unpack error for invalid pack") - } - }) -} - -func newDualIngress(tb testing.TB, algo objectid.Algorithm) objectstore.Quarantiner { - tb.Helper() - - objectsRoot, err := os.OpenRoot(tb.TempDir()) - if err != nil { - tb.Fatalf("os.OpenRoot: %v", err) - } - - tb.Cleanup(func() { - _ = objectsRoot.Close() - }) - - err = objectsRoot.Mkdir("pack", 0o755) - if err != nil { - tb.Fatalf("Mkdir(pack): %v", err) - } - - packRoot, err := objectsRoot.OpenRoot("pack") - if err != nil { - tb.Fatalf("OpenRoot(pack): %v", err) - } - - tb.Cleanup(func() { - _ = packRoot.Close() - }) - - looseStore, err := objectloose.New(objectsRoot, algo) - if err != nil { - tb.Fatalf("loose.New: %v", err) - } - - tb.Cleanup(func() { - _ = looseStore.Close() - }) - - packedStore, err := objectpacked.New(packRoot, algo, objectpacked.Options{WriteRev: true}) - if err != nil { - tb.Fatalf("packed.New: %v", err) - } - - tb.Cleanup(func() { - _ = packedStore.Close() - }) - - return objectdual.New(looseStore, packedStore) -} diff --git a/network/receivepack/service/update.go b/network/receivepack/service/update.go deleted file mode 100644 index 753e3b02..00000000 --- a/network/receivepack/service/update.go +++ /dev/null @@ -1,11 +0,0 @@ -package service - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// PlannedUpdate is one requested ref update planned for this execution. -type PlannedUpdate struct { - Name string - OldID objectid.ObjectID - NewID objectid.ObjectID - Delete bool -} diff --git a/network/receivepack/version.go b/network/receivepack/version.go deleted file mode 100644 index 9a4544dc..00000000 --- a/network/receivepack/version.go +++ /dev/null @@ -1,35 +0,0 @@ -package receivepack - -import ( - "strings" - - common "codeberg.org/lindenii/furgit/network/protocol/v0v1/server" -) - -func parseVersion(gitProtocol string) common.Version { - if gitProtocol == "" { - return common.Version0 - } - - var highestRequested uint8 - - for field := range strings.SplitSeq(gitProtocol, ":") { - switch field { - case "version=0": - case "version=1": - if highestRequested < 1 { - highestRequested = 1 - } - case "version=2": - if highestRequested < 2 { - highestRequested = 2 - } - } - } - - if highestRequested == 1 { - return common.Version1 - } - - return common.Version0 -} diff --git a/object/blob/blob.go b/object/blob/blob.go deleted file mode 100644 index 93856c51..00000000 --- a/object/blob/blob.go +++ /dev/null @@ -1,14 +0,0 @@ -// Package blob provides representations, parsers, and serializers for blob objects. -package blob - -// Blob represents a Git blob object. -// -// Blob is fully materialized in memory. -// -// Consider using objectstore.Reader.ReadReaderContent, -// or appropriate streaming write APIs. -// -// Labels: MT-Unsafe. -type Blob struct { - Data []byte -} diff --git a/object/blob/parse.go b/object/blob/parse.go deleted file mode 100644 index faee9e46..00000000 --- a/object/blob/parse.go +++ /dev/null @@ -1,6 +0,0 @@ -package blob - -// Parse decodes a blob object body. -func Parse(body []byte) (*Blob, error) { - return &Blob{Data: append([]byte(nil), body...)}, nil -} diff --git a/object/blob/parse_test.go b/object/blob/parse_test.go deleted file mode 100644 index 09d5d5d0..00000000 --- a/object/blob/parse_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package blob_test - -import ( - "bytes" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - "codeberg.org/lindenii/furgit/object/blob" - objectid "codeberg.org/lindenii/furgit/object/id" -) - -func TestBlobParseFromGit(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - body := []byte("hello\nblob\n") - blobID := testRepo.HashObject(t, "blob", body) - - rawBody := testRepo.CatFile(t, "blob", blobID) - - parsed, err := blob.Parse(rawBody) - if err != nil { - t.Fatalf("ParseBlob: %v", err) - } - - if !bytes.Equal(parsed.Data, body) { - t.Fatalf("blob body mismatch") - } - }) -} diff --git a/object/blob/serialize.go b/object/blob/serialize.go deleted file mode 100644 index 80cce8dc..00000000 --- a/object/blob/serialize.go +++ /dev/null @@ -1,32 +0,0 @@ -package blob - -import ( - "errors" - - objectheader "codeberg.org/lindenii/furgit/object/header" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// SerializeWithoutHeader renders the raw blob body bytes. -func (blob *Blob) SerializeWithoutHeader() ([]byte, error) { - return append([]byte(nil), blob.Data...), nil -} - -// SerializeWithHeader renders the raw object (header + body). -func (blob *Blob) SerializeWithHeader() ([]byte, error) { - body, err := blob.SerializeWithoutHeader() - if err != nil { - return nil, err - } - - header, ok := objectheader.Encode(objecttype.TypeBlob, int64(len(body))) - if !ok { - return nil, errors.New("object: blob: failed to encode object header") - } - - raw := make([]byte, len(header)+len(body)) - copy(raw, header) - copy(raw[len(header):], body) - - return raw, nil -} diff --git a/object/blob/serialize_test.go b/object/blob/serialize_test.go deleted file mode 100644 index 4292abad..00000000 --- a/object/blob/serialize_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package blob_test - -import ( - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - "codeberg.org/lindenii/furgit/object/blob" - objectid "codeberg.org/lindenii/furgit/object/id" -) - -func TestBlobSerialize(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - body := []byte("hello\nblob\n") - wantID := testRepo.HashObject(t, "blob", body) - - obj := &blob.Blob{Data: body} - - rawObj, err := obj.SerializeWithHeader() - if err != nil { - t.Fatalf("SerializeWithHeader: %v", err) - } - - gotID := algo.Sum(rawObj) - if gotID != wantID { - t.Fatalf("object id mismatch: got %s want %s", gotID, wantID) - } - }) -} diff --git a/object/blob/test.go b/object/blob/test.go deleted file mode 100644 index 9e538219..00000000 --- a/object/blob/test.go +++ /dev/null @@ -1,10 +0,0 @@ -package blob - -import objecttype "codeberg.org/lindenii/furgit/object/type" - -// ObjectType returns TypeBlob. -func (blob *Blob) ObjectType() objecttype.Type { - _ = blob - - return objecttype.TypeBlob -} diff --git a/object/commit/commit.go b/object/commit/commit.go deleted file mode 100644 index 0f7649e1..00000000 --- a/object/commit/commit.go +++ /dev/null @@ -1,25 +0,0 @@ -// Package commit provides parsed commit objects and commit serialization. -// -// It parses commits into ordinary Go values for reading and construction. It -// does not preserve the exact original byte layout needed for signature -// verification; callers that need signature-verification payload fidelity -// should use [codeberg.org/lindenii/furgit/object/signed/commit]. -package commit - -import ( - objectid "codeberg.org/lindenii/furgit/object/id" - objectsignature "codeberg.org/lindenii/furgit/object/signature" -) - -// Commit represents a fully materialized Git commit object. -// -// Labels: MT-Unsafe. -type Commit struct { - Tree objectid.ObjectID - Parents []objectid.ObjectID - Author objectsignature.Signature - Committer objectsignature.Signature - Message []byte - ChangeID string - ExtraHeaders []ExtraHeader -} diff --git a/object/commit/extraheader.go b/object/commit/extraheader.go deleted file mode 100644 index 79d4f9cc..00000000 --- a/object/commit/extraheader.go +++ /dev/null @@ -1,7 +0,0 @@ -package commit - -// ExtraHeader represents an extra header in a Git object. -type ExtraHeader struct { - Key string - Value []byte -} diff --git a/object/commit/parse.go b/object/commit/parse.go deleted file mode 100644 index 9dcc930d..00000000 --- a/object/commit/parse.go +++ /dev/null @@ -1,94 +0,0 @@ -package commit - -import ( - "bytes" - "errors" - "fmt" - - objectid "codeberg.org/lindenii/furgit/object/id" - objectsignature "codeberg.org/lindenii/furgit/object/signature" -) - -// Parse decodes a commit object body. -func Parse(body []byte, algo objectid.Algorithm) (*Commit, error) { - c := new(Commit) - - i := 0 - for i < len(body) { - rel := bytes.IndexByte(body[i:], '\n') - if rel < 0 { - return nil, errors.New("object: commit: missing newline") - } - - line := body[i : i+rel] - i += rel + 1 - - if len(line) == 0 { - break - } - - key, value, found := bytes.Cut(line, []byte{' '}) - if !found { - return nil, errors.New("object: commit: malformed header") - } - - switch string(key) { - case "tree": - id, err := objectid.ParseHex(algo, string(value)) - if err != nil { - return nil, fmt.Errorf("object: commit: tree: %w", err) - } - - c.Tree = id - case "parent": - id, err := objectid.ParseHex(algo, string(value)) - if err != nil { - return nil, fmt.Errorf("object: commit: parent: %w", err) - } - - c.Parents = append(c.Parents, id) - case "author": - idt, err := objectsignature.Parse(value) - if err != nil { - return nil, fmt.Errorf("object: commit: author: %w", err) - } - - c.Author = *idt - case "committer": - idt, err := objectsignature.Parse(value) - if err != nil { - return nil, fmt.Errorf("object: commit: committer: %w", err) - } - - c.Committer = *idt - case "change-id": - c.ChangeID = string(value) - case "gpgsig", "gpgsig-sha256": - for i < len(body) { - nextRel := bytes.IndexByte(body[i:], '\n') - if nextRel < 0 { - return nil, errors.New("object: commit: unterminated gpgsig") - } - - if body[i] != ' ' { - break - } - - i += nextRel + 1 - } - default: - c.ExtraHeaders = append(c.ExtraHeaders, ExtraHeader{ - Key: string(key), - Value: append([]byte(nil), value...), - }) - } - } - - if i > len(body) { - return nil, errors.New("object: commit: parser position out of bounds") - } - - c.Message = append([]byte(nil), body[i:]...) - - return c, nil -} diff --git a/object/commit/parse_test.go b/object/commit/parse_test.go deleted file mode 100644 index ad2c7aed..00000000 --- a/object/commit/parse_test.go +++ /dev/null @@ -1,91 +0,0 @@ -package commit_test - -import ( - "bytes" - "fmt" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - "codeberg.org/lindenii/furgit/object/commit" - objectid "codeberg.org/lindenii/furgit/object/id" -) - -func TestCommitParseFromGit(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - _, treeID, commitID := testRepo.MakeCommit(t, "subject\n\nbody") - - rawBody := testRepo.CatFile(t, "commit", commitID) - - parsed, err := commit.Parse(rawBody, algo) - if err != nil { - t.Fatalf("ParseCommit: %v", err) - } - - if parsed.Tree != treeID { - t.Fatalf("tree id mismatch: got %s want %s", parsed.Tree, treeID) - } - - if len(parsed.Parents) != 0 { - t.Fatalf("parent count = %d, want 0", len(parsed.Parents)) - } - - if !bytes.Equal(parsed.Author.Name, []byte("Test Author")) { - t.Fatalf("author name = %q, want %q", parsed.Author.Name, "Test Author") - } - - if !bytes.Equal(parsed.Committer.Name, []byte("Test Committer")) { - t.Fatalf("committer name = %q, want %q", parsed.Committer.Name, "Test Committer") - } - - if !bytes.Contains(parsed.Message, []byte("subject")) { - t.Fatalf("commit message missing subject: %q", parsed.Message) - } - }) -} - -func TestCommitParseMultipleParents(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - - _, treeID := testRepo.MakeSingleFileTree(t, "file.txt", []byte("merge-content\n")) - parent1 := testRepo.CommitTree(t, treeID, "parent-one") - parent2 := testRepo.CommitTree(t, treeID, "parent-two", parent1) - - rawCommit := fmt.Sprintf( - "tree %s\nparent %s\nparent %s\nauthor Test Author 1234567890 +0000\ncommitter Test Committer 1234567890 +0000\n\nMerge commit\n", - treeID, - parent1, - parent2, - ) - mergeID := testRepo.HashObject(t, "commit", []byte(rawCommit)) - rawBody := testRepo.CatFile(t, "commit", mergeID) - - parsed, err := commit.Parse(rawBody, algo) - if err != nil { - t.Fatalf("ParseCommit(merge): %v", err) - } - - if parsed.Tree != treeID { - t.Fatalf("merge tree = %s, want %s", parsed.Tree, treeID) - } - - if len(parsed.Parents) != 2 { - t.Fatalf("merge parent count = %d, want 2", len(parsed.Parents)) - } - - if parsed.Parents[0] != parent1 { - t.Fatalf("merge parent[0] = %s, want %s", parsed.Parents[0], parent1) - } - - if parsed.Parents[1] != parent2 { - t.Fatalf("merge parent[1] = %s, want %s", parsed.Parents[1], parent2) - } - - if !bytes.Equal(parsed.Message, []byte("Merge commit\n")) { - t.Fatalf("merge message = %q, want %q", parsed.Message, "Merge commit\n") - } - }) -} diff --git a/object/commit/serialize.go b/object/commit/serialize.go deleted file mode 100644 index 3f141550..00000000 --- a/object/commit/serialize.go +++ /dev/null @@ -1,84 +0,0 @@ -package commit - -import ( - "bytes" - "errors" - "fmt" - - objectheader "codeberg.org/lindenii/furgit/object/header" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// SerializeWithoutHeader renders the raw commit body bytes. -func (commit *Commit) SerializeWithoutHeader() ([]byte, error) { - var buf bytes.Buffer - - if commit.Tree.Algorithm().Size() == 0 { - return nil, errors.New("object: commit: missing tree id") - } - - fmt.Fprintf(&buf, "tree %s\n", commit.Tree.String()) - - for _, parent := range commit.Parents { - fmt.Fprintf(&buf, "parent %s\n", parent.String()) - } - - authorBytes, err := commit.Author.Serialize() - if err != nil { - return nil, err - } - - buf.WriteString("author ") - buf.Write(authorBytes) - buf.WriteByte('\n') - - committerBytes, err := commit.Committer.Serialize() - if err != nil { - return nil, err - } - - buf.WriteString("committer ") - buf.Write(committerBytes) - buf.WriteByte('\n') - - if commit.ChangeID != "" { - buf.WriteString("change-id ") - buf.WriteString(commit.ChangeID) - buf.WriteByte('\n') - } - - for _, h := range commit.ExtraHeaders { - if h.Key == "" { - return nil, errors.New("object: commit: extra header has empty key") - } - - buf.WriteString(h.Key) - buf.WriteByte(' ') - buf.Write(h.Value) - buf.WriteByte('\n') - } - - buf.WriteByte('\n') - buf.Write(commit.Message) - - return buf.Bytes(), nil -} - -// SerializeWithHeader renders the raw object (header + body). -func (commit *Commit) SerializeWithHeader() ([]byte, error) { - body, err := commit.SerializeWithoutHeader() - if err != nil { - return nil, err - } - - header, ok := objectheader.Encode(objecttype.TypeCommit, int64(len(body))) - if !ok { - return nil, errors.New("object: commit: failed to encode object header") - } - - raw := make([]byte, len(header)+len(body)) - copy(raw, header) - copy(raw[len(header):], body) - - return raw, nil -} diff --git a/object/commit/serialize_test.go b/object/commit/serialize_test.go deleted file mode 100644 index e58a8078..00000000 --- a/object/commit/serialize_test.go +++ /dev/null @@ -1,34 +0,0 @@ -package commit_test - -import ( - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - "codeberg.org/lindenii/furgit/object/commit" - objectid "codeberg.org/lindenii/furgit/object/id" -) - -func TestCommitSerialize(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - _, _, commitID := testRepo.MakeCommit(t, "subject\n\nbody") - - rawBody := testRepo.CatFile(t, "commit", commitID) - - parsed, err := commit.Parse(rawBody, algo) - if err != nil { - t.Fatalf("ParseCommit: %v", err) - } - - rawObj, err := parsed.SerializeWithHeader() - if err != nil { - t.Fatalf("SerializeWithHeader: %v", err) - } - - gotID := algo.Sum(rawObj) - if gotID != commitID { - t.Fatalf("commit id mismatch: got %s want %s", gotID, commitID) - } - }) -} diff --git a/object/commit/type.go b/object/commit/type.go deleted file mode 100644 index b8aa11e8..00000000 --- a/object/commit/type.go +++ /dev/null @@ -1,10 +0,0 @@ -package commit - -import objecttype "codeberg.org/lindenii/furgit/object/type" - -// ObjectType returns TypeCommit. -func (commit *Commit) ObjectType() objecttype.Type { - _ = commit - - return objecttype.TypeCommit -} diff --git a/object/doc.go b/object/doc.go deleted file mode 100644 index f675b963..00000000 --- a/object/doc.go +++ /dev/null @@ -1,7 +0,0 @@ -// Package object provides the shared [Object] interface and parsing functions -// for Git object values. -// -// Concrete object forms such as [blob], [tree], [commit], and [tag] live in -// subpackages. Use [codeberg.org/lindenii/furgit/object/stored] when object -// values need to be paired with the object IDs they were loaded under. -package object diff --git a/object/fetch/doc.go b/object/fetch/doc.go deleted file mode 100644 index 89bf9a98..00000000 --- a/object/fetch/doc.go +++ /dev/null @@ -1,8 +0,0 @@ -// Package fetch loads typed Git objects from object storage and provides -// higher-level object queries. -// -// Fetching is above [objectstore]: it parses stored objects into blobs, trees, -// commits, and tags, exposes object metadata, peels tree-ish or commit-ish -// objects, resolves paths within trees, and can expose one tree as an [io/fs] -// view. -package fetch diff --git a/object/fetch/exact_blob.go b/object/fetch/exact_blob.go deleted file mode 100644 index ef4b84fe..00000000 --- a/object/fetch/exact_blob.go +++ /dev/null @@ -1,26 +0,0 @@ -package fetch - -import ( - giterrors "codeberg.org/lindenii/furgit/errors" - "codeberg.org/lindenii/furgit/object/blob" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/stored" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// ExactBlob reads, parses, and wraps the blob at id. -// -// Labels: Life-Parent. -func (r *Fetcher) ExactBlob(id objectid.ObjectID) (*stored.Stored[*blob.Blob], error) { - parsed, err := r.parseObject(id) - if err != nil { - return nil, err - } - - blob, ok := parsed.(*blob.Blob) - if !ok { - return nil, &giterrors.ObjectTypeError{OID: id, Got: parsed.ObjectType(), Want: objecttype.TypeBlob} - } - - return stored.New(id, blob), nil -} diff --git a/object/fetch/exact_blob_reader.go b/object/fetch/exact_blob_reader.go deleted file mode 100644 index 4a313d3e..00000000 --- a/object/fetch/exact_blob_reader.go +++ /dev/null @@ -1,16 +0,0 @@ -package fetch - -import ( - "io" - - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// ExactBlobReader returns a reader for the content of the blob at id, -// together with its content size in bytes. -// -// Labels: Life-Parent, Close-Caller. -func (r *Fetcher) ExactBlobReader(id objectid.ObjectID) (io.ReadCloser, int64, error) { - return r.exactReader(id, objecttype.TypeBlob) -} diff --git a/object/fetch/exact_commit.go b/object/fetch/exact_commit.go deleted file mode 100644 index 9483b2b1..00000000 --- a/object/fetch/exact_commit.go +++ /dev/null @@ -1,26 +0,0 @@ -package fetch - -import ( - giterrors "codeberg.org/lindenii/furgit/errors" - "codeberg.org/lindenii/furgit/object/commit" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/stored" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// ExactCommit reads, parses, and wraps the commit at id. -// -// Labels: Life-Parent. -func (r *Fetcher) ExactCommit(id objectid.ObjectID) (*stored.Stored[*commit.Commit], error) { - parsed, err := r.parseObject(id) - if err != nil { - return nil, err - } - - commit, ok := parsed.(*commit.Commit) - if !ok { - return nil, &giterrors.ObjectTypeError{OID: id, Got: parsed.ObjectType(), Want: objecttype.TypeCommit} - } - - return stored.New(id, commit), nil -} diff --git a/object/fetch/exact_object.go b/object/fetch/exact_object.go deleted file mode 100644 index 2e4a8217..00000000 --- a/object/fetch/exact_object.go +++ /dev/null @@ -1,20 +0,0 @@ -package fetch - -import ( - "codeberg.org/lindenii/furgit/object" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/stored" -) - -// ExactObject reads, parses, and wraps the object at id without constraining -// its concrete object kind. -// -// Labels: Life-Parent. -func (r *Fetcher) ExactObject(id objectid.ObjectID) (*stored.Stored[object.Object], error) { - parsed, err := r.parseObject(id) - if err != nil { - return nil, err - } - - return stored.New(id, parsed), nil -} diff --git a/object/fetch/exact_reader.go b/object/fetch/exact_reader.go deleted file mode 100644 index d588480d..00000000 --- a/object/fetch/exact_reader.go +++ /dev/null @@ -1,26 +0,0 @@ -package fetch - -import ( - "io" - - giterrors "codeberg.org/lindenii/furgit/errors" - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// exactReader reads one object's content stream and verifies that its header -// type matches wantType. -func (r *Fetcher) exactReader(id objectid.ObjectID, wantType objecttype.Type) (io.ReadCloser, int64, error) { - gotType, size, rc, err := r.store.ReadReaderContent(id) - if err != nil { - return nil, 0, wrapObjectReadError(id, err) - } - - if gotType != wantType { - _ = rc.Close() - - return nil, 0, &giterrors.ObjectTypeError{OID: id, Got: gotType, Want: wantType} - } - - return rc, size, nil -} diff --git a/object/fetch/exact_tag.go b/object/fetch/exact_tag.go deleted file mode 100644 index 230e7d57..00000000 --- a/object/fetch/exact_tag.go +++ /dev/null @@ -1,26 +0,0 @@ -package fetch - -import ( - giterrors "codeberg.org/lindenii/furgit/errors" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/stored" - "codeberg.org/lindenii/furgit/object/tag" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// ExactTag reads, parses, and wraps the tag at id. -// -// Labels: Life-Parent. -func (r *Fetcher) ExactTag(id objectid.ObjectID) (*stored.Stored[*tag.Tag], error) { - parsed, err := r.parseObject(id) - if err != nil { - return nil, err - } - - tag, ok := parsed.(*tag.Tag) - if !ok { - return nil, &giterrors.ObjectTypeError{OID: id, Got: parsed.ObjectType(), Want: objecttype.TypeTag} - } - - return stored.New(id, tag), nil -} diff --git a/object/fetch/exact_tree.go b/object/fetch/exact_tree.go deleted file mode 100644 index 8bfc87ea..00000000 --- a/object/fetch/exact_tree.go +++ /dev/null @@ -1,26 +0,0 @@ -package fetch - -import ( - giterrors "codeberg.org/lindenii/furgit/errors" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/stored" - "codeberg.org/lindenii/furgit/object/tree" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// ExactTree reads, parses, and wraps the tree at id. -// -// Labels: Life-Parent. -func (r *Fetcher) ExactTree(id objectid.ObjectID) (*stored.Stored[*tree.Tree], error) { - parsed, err := r.parseObject(id) - if err != nil { - return nil, err - } - - tree, ok := parsed.(*tree.Tree) - if !ok { - return nil, &giterrors.ObjectTypeError{OID: id, Got: parsed.ObjectType(), Want: objecttype.TypeTree} - } - - return stored.New(id, tree), nil -} diff --git a/object/fetch/fetcher.go b/object/fetch/fetcher.go deleted file mode 100644 index fcd64d88..00000000 --- a/object/fetch/fetcher.go +++ /dev/null @@ -1,20 +0,0 @@ -package fetch - -import objectstore "codeberg.org/lindenii/furgit/object/store" - -// Fetcher provides ordinary object access above an object store. -// -// It exposes object metadata, typed object loading, tree-ish and commit-ish -// peeling, path resolution, one-tree fs views, and blob content streaming. -// -// Labels: MT-Safe. -type Fetcher struct { - store objectstore.Reader -} - -// New returns a Fetcher that reads objects from store. -// -// Labels: Deps-Borrowed, Life-Parent. -func New(store objectstore.Reader) *Fetcher { - return &Fetcher{store: store} -} diff --git a/object/fetch/header.go b/object/fetch/header.go deleted file mode 100644 index 0a535dd9..00000000 --- a/object/fetch/header.go +++ /dev/null @@ -1,18 +0,0 @@ -package fetch - -import ( - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// Header returns the object type and content size at id. -// -// Labels: Life-Parent. -func (r *Fetcher) Header(id objectid.ObjectID) (objecttype.Type, int64, error) { - ty, size, err := r.store.ReadHeader(id) - if err != nil { - return objecttype.TypeInvalid, 0, wrapObjectReadError(id, err) - } - - return ty, size, nil -} diff --git a/object/fetch/object_errors.go b/object/fetch/object_errors.go deleted file mode 100644 index 08de6f75..00000000 --- a/object/fetch/object_errors.go +++ /dev/null @@ -1,19 +0,0 @@ -package fetch - -import ( - stderrors "errors" - - giterrors "codeberg.org/lindenii/furgit/errors" - objectid "codeberg.org/lindenii/furgit/object/id" - objectstore "codeberg.org/lindenii/furgit/object/store" -) - -// wrapObjectReadError maps raw object-store lookup failures to fetcher-level -// object lookup errors. -func wrapObjectReadError(id objectid.ObjectID, err error) error { - if stderrors.Is(err, objectstore.ErrObjectNotFound) { - return &giterrors.ObjectMissingError{OID: id} - } - - return err -} diff --git a/object/fetch/object_parse.go b/object/fetch/object_parse.go deleted file mode 100644 index 0a61bb3d..00000000 --- a/object/fetch/object_parse.go +++ /dev/null @@ -1,27 +0,0 @@ -package fetch - -import ( - "fmt" - - "codeberg.org/lindenii/furgit/object" - objectid "codeberg.org/lindenii/furgit/object/id" -) - -func (r *Fetcher) parseObject(id objectid.ObjectID) (object.Object, error) { - ty, content, err := r.store.ReadBytesContent(id) - if err != nil { - return nil, wrapObjectReadError(id, err) - } - - parsed, err := object.ParseWithoutHeader(ty, content, id.Algorithm()) - if err != nil { - tyName, ok := ty.Name() - if !ok { - tyName = fmt.Sprintf("type %d", ty) - } - - return nil, fmt.Errorf("object/fetch: parse object %s (%s): %w", id, tyName, err) - } - - return parsed, nil -} diff --git a/object/fetch/path.go b/object/fetch/path.go deleted file mode 100644 index e3c468db..00000000 --- a/object/fetch/path.go +++ /dev/null @@ -1,105 +0,0 @@ -package fetch - -import ( - "fmt" - - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/tree" -) - -// PathEmptyError indicates that Path received no segments. -type PathEmptyError struct{} - -func (err *PathEmptyError) Error() string { - return "object/fetch: empty tree path" -} - -// PathSegmentEmptyError indicates that one path segment is empty. -type PathSegmentEmptyError struct { - Index int -} - -func (err *PathSegmentEmptyError) Error() string { - return fmt.Sprintf("object/fetch: empty tree path segment at index %d", err.Index) -} - -// PathNotFoundError indicates that one tree path segment was not found. -type PathNotFoundError struct { - Index int - Name []byte -} - -func (err *PathNotFoundError) Error() string { - return fmt.Sprintf("object/fetch: tree entry %q not found at index %d", err.Name, err.Index) -} - -// PathNotTreeError indicates that one intermediate path segment was not a tree. -type PathNotTreeError struct { - Index int - Name []byte -} - -func (err *PathNotTreeError) Error() string { - return fmt.Sprintf("object/fetch: path segment %q at index %d is not a tree", err.Name, err.Index) -} - -// Path resolves parts within the tree identified by root and returns the final -// tree entry. -// -// The root object may be any tree-ish object accepted by PeelToTree. -// -// parts must contain at least one path segment. Intermediate path segments -// must resolve to tree entries. The final entry is returned without loading -// its object. Path segments may not contain \x00. -// -// The path cannot be accurately represented as a string or a single []byte -// because Git tree entry names may include slashes. While []string is -// technically possible (since Go strings are not necessarily UTF-8), they -// do often imply UTF-8 in practice, which would be undesirable. -// -// If your entry names are valid UTF-8 and uses / solely as segment separators, -// it may be convenient to use TreeFS for an io/fs.FS-like interface. -// -// Labels: Life-Parent. -func (r *Fetcher) Path(root objectid.ObjectID, parts [][]byte) (tree.TreeEntry, error) { - if len(parts) == 0 { - return tree.TreeEntry{}, &PathEmptyError{} - } - - current, err := r.PeelToTree(root) - if err != nil { - return tree.TreeEntry{}, err - } - - for i, part := range parts { - if len(part) == 0 { - return tree.TreeEntry{}, &PathSegmentEmptyError{Index: i} - } - - entry := current.Object().Entry(part) - if entry == nil { - return tree.TreeEntry{}, &PathNotFoundError{ - Index: i, - Name: append([]byte(nil), part...), - } - } - - if i == len(parts)-1 { - return *entry, nil - } - - if entry.Mode != tree.FileModeDir { - return tree.TreeEntry{}, &PathNotTreeError{ - Index: i, - Name: append([]byte(nil), part...), - } - } - - current, err = r.ExactTree(entry.ID) - if err != nil { - return tree.TreeEntry{}, err - } - } - - return tree.TreeEntry{}, &PathNotFoundError{Index: len(parts) - 1} -} diff --git a/object/fetch/peel_to_blob.go b/object/fetch/peel_to_blob.go deleted file mode 100644 index adf86495..00000000 --- a/object/fetch/peel_to_blob.go +++ /dev/null @@ -1,31 +0,0 @@ -package fetch - -import ( - giterrors "codeberg.org/lindenii/furgit/errors" - "codeberg.org/lindenii/furgit/object/blob" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/stored" - "codeberg.org/lindenii/furgit/object/tag" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// PeelToBlob peels tags until it reaches a blob. -// -// Labels: Life-Parent. -func (r *Fetcher) PeelToBlob(id objectid.ObjectID) (*stored.Stored[*blob.Blob], error) { - for { - obj, err := r.ExactObject(id) - if err != nil { - return nil, err - } - - switch parsed := obj.Object().(type) { - case *blob.Blob: - return stored.New(id, parsed), nil - case *tag.Tag: - id = parsed.Target - default: - return nil, &giterrors.ObjectTypeError{OID: id, Got: parsed.ObjectType(), Want: objecttype.TypeBlob} - } - } -} diff --git a/object/fetch/peel_to_blob_id.go b/object/fetch/peel_to_blob_id.go deleted file mode 100644 index 7a43b4cc..00000000 --- a/object/fetch/peel_to_blob_id.go +++ /dev/null @@ -1,38 +0,0 @@ -package fetch - -import ( - giterrors "codeberg.org/lindenii/furgit/errors" - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// PeelToBlobID peels tags until it reaches a blob object ID. -func (r *Fetcher) PeelToBlobID(id objectid.ObjectID) (objectid.ObjectID, error) { - for { - ty, _, err := r.Header(id) - if err != nil { - return objectid.ObjectID{}, err - } - - switch ty { - case objecttype.TypeBlob: - return id, nil - case objecttype.TypeTag: - tag, err := r.ExactTag(id) - if err != nil { - return objectid.ObjectID{}, err - } - - id = tag.Object().Target - case objecttype.TypeInvalid, - objecttype.TypeCommit, - objecttype.TypeTree, - objecttype.TypeFuture, - objecttype.TypeOfsDelta, - objecttype.TypeRefDelta: - return objectid.ObjectID{}, &giterrors.ObjectTypeError{OID: id, Got: ty, Want: objecttype.TypeBlob} - default: - return objectid.ObjectID{}, &giterrors.ObjectTypeError{OID: id, Got: ty, Want: objecttype.TypeBlob} - } - } -} diff --git a/object/fetch/peel_to_blob_reader.go b/object/fetch/peel_to_blob_reader.go deleted file mode 100644 index dedffd01..00000000 --- a/object/fetch/peel_to_blob_reader.go +++ /dev/null @@ -1,20 +0,0 @@ -package fetch - -import ( - "io" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// PeelToBlobReader returns a reader for the content of the peeled blob at id, -// together with its content size in bytes. -// -// Labels: Life-Parent, Close-Caller. -func (r *Fetcher) PeelToBlobReader(id objectid.ObjectID) (io.ReadCloser, int64, error) { - blobID, err := r.PeelToBlobID(id) - if err != nil { - return nil, 0, err - } - - return r.ExactBlobReader(blobID) -} diff --git a/object/fetch/peel_to_commit.go b/object/fetch/peel_to_commit.go deleted file mode 100644 index e5fdce2b..00000000 --- a/object/fetch/peel_to_commit.go +++ /dev/null @@ -1,31 +0,0 @@ -package fetch - -import ( - giterrors "codeberg.org/lindenii/furgit/errors" - "codeberg.org/lindenii/furgit/object/commit" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/stored" - "codeberg.org/lindenii/furgit/object/tag" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// PeelToCommit peels tags until it reaches a commit. -// -// Labels: Life-Parent. -func (r *Fetcher) PeelToCommit(id objectid.ObjectID) (*stored.Stored[*commit.Commit], error) { - for { - obj, err := r.ExactObject(id) - if err != nil { - return nil, err - } - - switch parsed := obj.Object().(type) { - case *commit.Commit: - return stored.New(id, parsed), nil - case *tag.Tag: - id = parsed.Target - default: - return nil, &giterrors.ObjectTypeError{OID: id, Got: parsed.ObjectType(), Want: objecttype.TypeCommit} - } - } -} diff --git a/object/fetch/peel_to_commit_id.go b/object/fetch/peel_to_commit_id.go deleted file mode 100644 index 7b58bdea..00000000 --- a/object/fetch/peel_to_commit_id.go +++ /dev/null @@ -1,38 +0,0 @@ -package fetch - -import ( - giterrors "codeberg.org/lindenii/furgit/errors" - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// PeelToCommitID peels tags until it reaches a commit object ID. -func (r *Fetcher) PeelToCommitID(id objectid.ObjectID) (objectid.ObjectID, error) { - for { - ty, _, err := r.Header(id) - if err != nil { - return objectid.ObjectID{}, err - } - - switch ty { - case objecttype.TypeCommit: - return id, nil - case objecttype.TypeTag: - tag, err := r.ExactTag(id) - if err != nil { - return objectid.ObjectID{}, err - } - - id = tag.Object().Target - case objecttype.TypeInvalid, - objecttype.TypeTree, - objecttype.TypeBlob, - objecttype.TypeFuture, - objecttype.TypeOfsDelta, - objecttype.TypeRefDelta: - return objectid.ObjectID{}, &giterrors.ObjectTypeError{OID: id, Got: ty, Want: objecttype.TypeCommit} - default: - return objectid.ObjectID{}, &giterrors.ObjectTypeError{OID: id, Got: ty, Want: objecttype.TypeCommit} - } - } -} diff --git a/object/fetch/peel_to_tree.go b/object/fetch/peel_to_tree.go deleted file mode 100644 index adc87e6b..00000000 --- a/object/fetch/peel_to_tree.go +++ /dev/null @@ -1,35 +0,0 @@ -package fetch - -import ( - giterrors "codeberg.org/lindenii/furgit/errors" - "codeberg.org/lindenii/furgit/object/commit" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/stored" - "codeberg.org/lindenii/furgit/object/tag" - "codeberg.org/lindenii/furgit/object/tree" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// PeelToTree peels tags until it reaches a tree or commit. If it reaches a -// commit, it returns the commit's root tree. -// -// Labels: Life-Parent. -func (r *Fetcher) PeelToTree(id objectid.ObjectID) (*stored.Stored[*tree.Tree], error) { - for { - obj, err := r.ExactObject(id) - if err != nil { - return nil, err - } - - switch parsed := obj.Object().(type) { - case *tree.Tree: - return stored.New(id, parsed), nil - case *commit.Commit: - return r.ExactTree(parsed.Tree) - case *tag.Tag: - id = parsed.Target - default: - return nil, &giterrors.ObjectTypeError{OID: id, Got: parsed.ObjectType(), Want: objecttype.TypeTree} - } - } -} diff --git a/object/fetch/peel_to_tree_id.go b/object/fetch/peel_to_tree_id.go deleted file mode 100644 index 4c9bdac9..00000000 --- a/object/fetch/peel_to_tree_id.go +++ /dev/null @@ -1,45 +0,0 @@ -package fetch - -import ( - giterrors "codeberg.org/lindenii/furgit/errors" - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// PeelToTreeID peels tags until it reaches a tree object ID, or a commit whose -// root tree object ID is then returned. -func (r *Fetcher) PeelToTreeID(id objectid.ObjectID) (objectid.ObjectID, error) { - for { - ty, _, err := r.Header(id) - if err != nil { - return objectid.ObjectID{}, err - } - - switch ty { - case objecttype.TypeTree: - return id, nil - case objecttype.TypeCommit: - commit, err := r.ExactCommit(id) - if err != nil { - return objectid.ObjectID{}, err - } - - return commit.Object().Tree, nil - case objecttype.TypeTag: - tag, err := r.ExactTag(id) - if err != nil { - return objectid.ObjectID{}, err - } - - id = tag.Object().Target - case objecttype.TypeInvalid, - objecttype.TypeBlob, - objecttype.TypeFuture, - objecttype.TypeOfsDelta, - objecttype.TypeRefDelta: - return objectid.ObjectID{}, &giterrors.ObjectTypeError{OID: id, Got: ty, Want: objecttype.TypeTree} - default: - return objectid.ObjectID{}, &giterrors.ObjectTypeError{OID: id, Got: ty, Want: objecttype.TypeTree} - } - } -} diff --git a/object/fetch/size.go b/object/fetch/size.go deleted file mode 100644 index da59e12a..00000000 --- a/object/fetch/size.go +++ /dev/null @@ -1,15 +0,0 @@ -package fetch - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// Size returns the object content size at id. -// -// Labels: Life-Parent. -func (r *Fetcher) Size(id objectid.ObjectID) (int64, error) { - size, err := r.store.ReadSize(id) - if err != nil { - return 0, wrapObjectReadError(id, err) - } - - return size, nil -} diff --git a/object/fetch/treefs.go b/object/fetch/treefs.go deleted file mode 100644 index 39ea7ad5..00000000 --- a/object/fetch/treefs.go +++ /dev/null @@ -1,30 +0,0 @@ -package fetch - -import ( - "io/fs" - - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/tree" -) - -// TreeFS exposes one Git tree as an fs.FS view backed by a Fetcher. -// -// TreeFS interprets names using io/fs path rules. Those rules do not match raw -// Git tree entry naming exactly: names are UTF-8, slash-separated, and must be -// valid fs.FS paths. Tree entries that cannot be represented under those rules -// are not addressable through this API. -// -// Labels: MT-Safe. -type TreeFS struct { - fetcher *Fetcher - rootTree objectid.ObjectID - rootEntry *tree.TreeEntry -} - -var ( - _ fs.FS = (*TreeFS)(nil) - _ fs.ReadFileFS = (*TreeFS)(nil) - _ fs.ReadDirFS = (*TreeFS)(nil) - _ fs.StatFS = (*TreeFS)(nil) - _ fs.SubFS = (*TreeFS)(nil) -) diff --git a/object/fetch/treefs_entry.go b/object/fetch/treefs_entry.go deleted file mode 100644 index e577d86c..00000000 --- a/object/fetch/treefs_entry.go +++ /dev/null @@ -1,85 +0,0 @@ -package fetch - -import ( - "errors" - "fmt" - "io/fs" - - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/tree" -) - -func (treeFS *TreeFS) resolvePath(op treeFSOp, name string) (treeEntryValue, error) { - if !treeFSValidPath(name) { - return treeEntryValue{}, treeFSPathError(op, name, fs.ErrInvalid) - } - - if name == "." { - return treeEntryValue{ - name: ".", - mode: tree.FileModeDir, - treeID: treeFS.rootTree, - treeEntry: treeFS.rootEntry, - }, nil - } - - entry, err := treeFS.fetcher.Path(treeFS.rootTree, tree.SplitPath([]byte(name))) - if err != nil { - return treeEntryValue{}, treeFS.pathResolveError(op, name, err) - } - - return treeEntryValue{ - name: string(entry.Name), - mode: entry.Mode, - objectID: entry.ID, - treeEntry: &entry, - }, nil -} - -func (treeFS *TreeFS) pathResolveError(op treeFSOp, name string, err error) error { - if _, ok := errors.AsType[*PathNotFoundError](err); ok { - return treeFSPathError(op, name, fs.ErrNotExist) - } - - if _, ok := errors.AsType[*PathNotTreeError](err); ok { - return treeFSPathError(op, name, fs.ErrInvalid) - } - - if _, ok := errors.AsType[*PathEmptyError](err); ok { - return treeFSPathError(op, name, fs.ErrInvalid) - } - - if _, ok := errors.AsType[*PathSegmentEmptyError](err); ok { - return treeFSPathError(op, name, fs.ErrInvalid) - } - - return treeFSPathError(op, name, err) -} - -type treeEntryValue struct { - name string - mode tree.FileMode - objectID objectid.ObjectID - treeID objectid.ObjectID - treeEntry *tree.TreeEntry -} - -func (entry treeEntryValue) isDir() bool { - return entry.mode == tree.FileModeDir -} - -func (entry treeEntryValue) blobSize(fetcher *Fetcher) (int64, error) { - return fetcher.Size(entry.objectID) -} - -func (entry treeEntryValue) subtreeID() (objectid.ObjectID, error) { - if entry.name == "." { - return entry.treeID, nil - } - - if entry.mode != tree.FileModeDir { - return objectid.ObjectID{}, fmt.Errorf("object/fetch: path %q is not a tree", entry.name) - } - - return entry.objectID, nil -} diff --git a/object/fetch/treefs_info.go b/object/fetch/treefs_info.go deleted file mode 100644 index f1db7e9a..00000000 --- a/object/fetch/treefs_info.go +++ /dev/null @@ -1,75 +0,0 @@ -package fetch - -import ( - "io/fs" - "time" - - "codeberg.org/lindenii/furgit/object/tree" -) - -type treeFSInfo struct { - name string - mode fs.FileMode - size int64 - sys any - isDir bool -} - -var ( - _ fs.FileInfo = (*treeFSInfo)(nil) - _ fs.DirEntry = (*treeFSInfo)(nil) -) - -func (info *treeFSInfo) Name() string { return info.name } -func (info *treeFSInfo) Size() int64 { return info.size } -func (info *treeFSInfo) Mode() fs.FileMode { return info.mode } -func (info *treeFSInfo) Type() fs.FileMode { return info.mode.Type() } -func (info *treeFSInfo) IsDir() bool { return info.isDir } -func (info *treeFSInfo) ModTime() time.Time { return time.Time{} } -func (info *treeFSInfo) Sys() any { return info.sys } -func (info *treeFSInfo) Info() (fs.FileInfo, error) { - return info, nil -} - -func treeFSEntryMode(mode tree.FileMode) fs.FileMode { - switch mode { - case tree.FileModeDir: - return fs.ModeDir | 0o555 - case tree.FileModeRegular: - return 0o444 - case tree.FileModeExecutable: - return 0o555 - case tree.FileModeSymlink: - return fs.ModeSymlink | 0o444 - case tree.FileModeGitlink: - return fs.ModeIrregular - default: - return fs.ModeIrregular - } -} - -func (treeFS *TreeFS) statEntry(entry treeEntryValue) (*treeFSInfo, error) { - size := int64(0) - - if entry.mode == tree.FileModeRegular || entry.mode == tree.FileModeExecutable || entry.mode == tree.FileModeSymlink { - var err error - - size, err = entry.blobSize(treeFS.fetcher) - if err != nil { - return nil, err - } - } - - var sys any - if entry.treeEntry != nil { - sys = *entry.treeEntry - } - - return &treeFSInfo{ - name: entry.name, - mode: treeFSEntryMode(entry.mode), - size: size, - sys: sys, - isDir: entry.isDir(), - }, nil -} diff --git a/object/fetch/treefs_new.go b/object/fetch/treefs_new.go deleted file mode 100644 index f1096a3c..00000000 --- a/object/fetch/treefs_new.go +++ /dev/null @@ -1,19 +0,0 @@ -package fetch - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// TreeFS returns a new filesystem view rooted at root, which may be any -// tree-ish object accepted by PeelToTreeID. -// -// Labels: Deps-Borrowed, Life-Parent. -func (r *Fetcher) TreeFS(root objectid.ObjectID) (*TreeFS, error) { - rootTree, err := r.PeelToTreeID(root) - if err != nil { - return nil, err - } - - return &TreeFS{ - fetcher: r, - rootTree: rootTree, - }, nil -} diff --git a/object/fetch/treefs_op.go b/object/fetch/treefs_op.go deleted file mode 100644 index f0472923..00000000 --- a/object/fetch/treefs_op.go +++ /dev/null @@ -1,28 +0,0 @@ -package fetch - -type treeFSOp uint8 - -const ( - treeFSOpOpen treeFSOp = iota - treeFSOpReadFile - treeFSOpReadDir - treeFSOpStat - treeFSOpSub -) - -func (op treeFSOp) pathErrorOp() string { - switch op { - case treeFSOpOpen: - return "open" - case treeFSOpReadFile: - return "readfile" - case treeFSOpReadDir: - return "readdir" - case treeFSOpStat: - return "stat" - case treeFSOpSub: - return "sub" - default: - return "treefs" - } -} diff --git a/object/fetch/treefs_open.go b/object/fetch/treefs_open.go deleted file mode 100644 index fc0f7635..00000000 --- a/object/fetch/treefs_open.go +++ /dev/null @@ -1,122 +0,0 @@ -package fetch - -import ( - "fmt" - "io" - "io/fs" - - "codeberg.org/lindenii/furgit/object/tree" -) - -// Open opens name for reading. -// -// Directories are returned as fs.ReadDirFile values. Gitlink entries are not -// readable through TreeFS. -func (treeFS *TreeFS) Open(name string) (fs.File, error) { - entry, err := treeFS.resolvePath(treeFSOpOpen, name) - if err != nil { - return nil, err - } - - info, err := treeFS.statEntry(entry) - if err != nil { - return nil, treeFSPathError(treeFSOpOpen, name, err) - } - - if entry.isDir() { - treeID, err := entry.subtreeID() - if err != nil { - return nil, treeFSPathError(treeFSOpOpen, name, err) - } - - tree, err := treeFS.fetcher.ExactTree(treeID) - if err != nil { - return nil, treeFSPathError(treeFSOpOpen, name, err) - } - - entries := make([]fs.DirEntry, 0, len(tree.Object().Entries)) - for _, child := range tree.Object().Entries { - childEntry := treeEntryValue{ - name: string(child.Name), - mode: child.Mode, - objectID: child.ID, - treeEntry: &child, - } - - childInfo, err := treeFS.statEntry(childEntry) - if err != nil { - return nil, treeFSPathError(treeFSOpOpen, name, err) - } - - entries = append(entries, childInfo) - } - - return &treeFSDir{ - info: info, - entries: entries, - }, nil - } - - if entry.mode == tree.FileModeGitlink { - return nil, treeFSPathError(treeFSOpOpen, name, fmt.Errorf("object/fetch: gitlink entries are not readable as files")) - } - - reader, _, err := treeFS.fetcher.ExactBlobReader(entry.objectID) - if err != nil { - return nil, treeFSPathError(treeFSOpOpen, name, err) - } - - return &treeFSBlob{ - info: info, - reader: reader, - }, nil -} - -type treeFSBlob struct { - info *treeFSInfo - reader io.ReadCloser -} - -var _ fs.File = (*treeFSBlob)(nil) - -func (file *treeFSBlob) Stat() (fs.FileInfo, error) { return file.info, nil } -func (file *treeFSBlob) Read(p []byte) (int, error) { return file.reader.Read(p) } -func (file *treeFSBlob) Close() error { return file.reader.Close() } - -type treeFSDir struct { - info *treeFSInfo - entries []fs.DirEntry - offset int -} - -var ( - _ fs.File = (*treeFSDir)(nil) - _ fs.ReadDirFile = (*treeFSDir)(nil) -) - -func (dir *treeFSDir) Stat() (fs.FileInfo, error) { return dir.info, nil } -func (dir *treeFSDir) Close() error { return nil } - -func (dir *treeFSDir) Read(_ []byte) (int, error) { - return 0, fs.ErrInvalid -} - -func (dir *treeFSDir) ReadDir(n int) ([]fs.DirEntry, error) { - if dir.offset >= len(dir.entries) && n > 0 { - return nil, io.EOF - } - - if n <= 0 { - out := append([]fs.DirEntry(nil), dir.entries[dir.offset:]...) - dir.offset = len(dir.entries) - - return out, nil - } - - end := min(dir.offset+n, len(dir.entries)) - - out := append([]fs.DirEntry(nil), dir.entries[dir.offset:end]...) - dir.offset = end - - return out, nil -} diff --git a/object/fetch/treefs_path.go b/object/fetch/treefs_path.go deleted file mode 100644 index a2dc3155..00000000 --- a/object/fetch/treefs_path.go +++ /dev/null @@ -1,11 +0,0 @@ -package fetch - -import "io/fs" - -func treeFSValidPath(name string) bool { - return name == "." || fs.ValidPath(name) -} - -func treeFSPathError(op treeFSOp, path string, err error) error { - return &fs.PathError{Op: op.pathErrorOp(), Path: path, Err: err} -} diff --git a/object/fetch/treefs_readdir.go b/object/fetch/treefs_readdir.go deleted file mode 100644 index 7518c607..00000000 --- a/object/fetch/treefs_readdir.go +++ /dev/null @@ -1,20 +0,0 @@ -package fetch - -import "io/fs" - -// ReadDir reads and returns all directory entries for name. -func (treeFS *TreeFS) ReadDir(name string) ([]fs.DirEntry, error) { - file, err := treeFS.Open(name) - if err != nil { - return nil, err - } - - defer func() { _ = file.Close() }() - - readDirFile, ok := file.(fs.ReadDirFile) - if !ok { - return nil, treeFSPathError(treeFSOpReadDir, name, fs.ErrInvalid) - } - - return readDirFile.ReadDir(-1) -} diff --git a/object/fetch/treefs_readfile.go b/object/fetch/treefs_readfile.go deleted file mode 100644 index b248135f..00000000 --- a/object/fetch/treefs_readfile.go +++ /dev/null @@ -1,40 +0,0 @@ -package fetch - -import ( - "fmt" - "io" - - "codeberg.org/lindenii/furgit/object/tree" -) - -// ReadFile reads the blob contents at name. -// -// Directories and gitlink entries are not readable through TreeFS. -func (treeFS *TreeFS) ReadFile(name string) ([]byte, error) { - entry, err := treeFS.resolvePath(treeFSOpReadFile, name) - if err != nil { - return nil, err - } - - if entry.isDir() { - return nil, treeFSPathError(treeFSOpReadFile, name, fmt.Errorf("is a directory")) - } - - if entry.mode == tree.FileModeGitlink { - return nil, treeFSPathError(treeFSOpReadFile, name, fmt.Errorf("object/fetch: gitlink entries are not readable as files")) - } - - reader, _, err := treeFS.fetcher.ExactBlobReader(entry.objectID) - if err != nil { - return nil, treeFSPathError(treeFSOpReadFile, name, err) - } - - defer func() { _ = reader.Close() }() - - data, err := io.ReadAll(reader) - if err != nil { - return nil, treeFSPathError(treeFSOpReadFile, name, err) - } - - return data, nil -} diff --git a/object/fetch/treefs_stat.go b/object/fetch/treefs_stat.go deleted file mode 100644 index 7d7a6418..00000000 --- a/object/fetch/treefs_stat.go +++ /dev/null @@ -1,22 +0,0 @@ -package fetch - -import "io/fs" - -// Stat returns synthetic file metadata for name. -// -// TreeFS metadata reflects Git tree entry mode and blob size where applicable. -// It does not represent filesystem stat metadata: ModTime is zero, ownership is -// unavailable, and Sys returns the underlying tree.TreeEntry when one exists. -func (treeFS *TreeFS) Stat(name string) (fs.FileInfo, error) { - entry, err := treeFS.resolvePath(treeFSOpStat, name) - if err != nil { - return nil, err - } - - info, err := treeFS.statEntry(entry) - if err != nil { - return nil, treeFSPathError(treeFSOpStat, name, err) - } - - return info, nil -} diff --git a/object/fetch/treefs_sub.go b/object/fetch/treefs_sub.go deleted file mode 100644 index c303d16d..00000000 --- a/object/fetch/treefs_sub.go +++ /dev/null @@ -1,22 +0,0 @@ -package fetch - -import "io/fs" - -// Sub returns a new TreeFS rooted at dir. -func (treeFS *TreeFS) Sub(dir string) (fs.FS, error) { - entry, err := treeFS.resolvePath(treeFSOpSub, dir) - if err != nil { - return nil, err - } - - treeID, err := entry.subtreeID() - if err != nil { - return nil, treeFSPathError(treeFSOpSub, dir, fs.ErrInvalid) - } - - return &TreeFS{ - fetcher: treeFS.fetcher, - rootTree: treeID, - rootEntry: entry.treeEntry, - }, nil -} diff --git a/object/fetch/treefs_test.go b/object/fetch/treefs_test.go deleted file mode 100644 index ba5d4127..00000000 --- a/object/fetch/treefs_test.go +++ /dev/null @@ -1,111 +0,0 @@ -package fetch_test - -import ( - "errors" - "io/fs" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - "codeberg.org/lindenii/furgit/object/fetch" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/tree" - "codeberg.org/lindenii/furgit/repository" -) - -func TestTreeFS(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - t.Parallel() - - repoData := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - repoData.WriteFile(t, "plain.txt", []byte("plain\n"), 0o644) - repoData.WriteFileAll(t, "dir/exec.sh", []byte("#!/bin/sh\nexit 0\n"), 0o755, 0o755) - repoData.SymbolicRef(t, "HEAD", "refs/heads/main") - _ = repoData.Run(t, "add", ".") - treeHex := repoData.Run(t, "write-tree") - - treeID, err := objectid.ParseHex(algo, treeHex) - if err != nil { - t.Fatalf("ParseHex(write-tree): %v", err) - } - - commitID := repoData.CommitTree(t, treeID, "treefs") - - root := repoData.OpenGitRoot(t) - - repo, err := repository.Open(root) - if err != nil { - t.Fatalf("repository.Open: %v", err) - } - - defer func() { _ = repo.Close() }() - - fetcher := fetch.New(repo.Objects()) - - treeFS, err := fetcher.TreeFS(commitID) - if err != nil { - t.Fatalf("fetcher.TreeFS: %v", err) - } - - content, err := treeFS.ReadFile("plain.txt") - if err != nil { - t.Fatalf("ReadFile(plain.txt): %v", err) - } - - if string(content) != "plain\n" { - t.Fatalf("ReadFile(plain.txt) = %q, want %q", string(content), "plain\n") - } - - entries, err := treeFS.ReadDir(".") - if err != nil { - t.Fatalf("ReadDir(.): %v", err) - } - - if len(entries) != 2 { - t.Fatalf("len(ReadDir(.)) = %d, want 2", len(entries)) - } - - info, err := treeFS.Stat("plain.txt") - if err != nil { - t.Fatalf("Stat(plain.txt): %v", err) - } - - entry, ok := info.Sys().(tree.TreeEntry) - if !ok { - t.Fatalf("Stat(plain.txt).Sys() type = %T, want tree.TreeEntry", info.Sys()) - } - - if entry.Mode != tree.FileModeRegular { - t.Fatalf("Stat(plain.txt).Sys().Mode = %o, want %o", entry.Mode, tree.FileModeRegular) - } - - subFS, err := treeFS.Sub("dir") - if err != nil { - t.Fatalf("Sub(dir): %v", err) - } - - subReadFileFS, ok := subFS.(fs.ReadFileFS) - if !ok { - t.Fatalf("Sub(dir) type does not implement fs.ReadFileFS") - } - - subContent, err := subReadFileFS.ReadFile("exec.sh") - if err != nil { - t.Fatalf("Sub(dir).ReadFile(exec.sh): %v", err) - } - - if string(subContent) != "#!/bin/sh\nexit 0\n" { - t.Fatalf("Sub(dir).ReadFile(exec.sh) = %q", string(subContent)) - } - - _, err = treeFS.ReadFile("dir") - if err == nil { - t.Fatal("ReadFile(dir) unexpectedly succeeded") - } - - if _, ok := errors.AsType[*fs.PathError](err); !ok { - t.Fatalf("ReadFile(dir) err type = %T, want *fs.PathError", err) - } - }) -} diff --git a/object/header/append.go b/object/header/append.go deleted file mode 100644 index 6d824740..00000000 --- a/object/header/append.go +++ /dev/null @@ -1,29 +0,0 @@ -package objectheader - -import ( - "strconv" - - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// Append appends a canonical loose-object header ("type size\\x00") to dst. -func Append(dst []byte, ty objecttype.Type, size int64) ([]byte, bool) { - if size < 0 { - return nil, false - } - - tyName, ok := ty.Name() - if !ok { - return nil, false - } - - sizeStr := strconv.FormatInt(size, 10) - out := make([]byte, 0, len(dst)+len(tyName)+len(sizeStr)+2) - out = append(out, dst...) - out = append(out, tyName...) - out = append(out, ' ') - out = append(out, sizeStr...) - out = append(out, 0) - - return out, true -} diff --git a/object/header/doc.go b/object/header/doc.go deleted file mode 100644 index 9c953ebb..00000000 --- a/object/header/doc.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package objectheader parses and serializes loose-object headers -// ("type size\x00"). -package objectheader diff --git a/object/header/encode.go b/object/header/encode.go deleted file mode 100644 index a03c1f05..00000000 --- a/object/header/encode.go +++ /dev/null @@ -1,8 +0,0 @@ -package objectheader - -import objecttype "codeberg.org/lindenii/furgit/object/type" - -// Encode returns a canonical loose-object header ("type size\\x00"). -func Encode(ty objecttype.Type, size int64) ([]byte, bool) { - return Append(nil, ty, size) -} diff --git a/object/header/parse.go b/object/header/parse.go deleted file mode 100644 index cad521e5..00000000 --- a/object/header/parse.go +++ /dev/null @@ -1,42 +0,0 @@ -package objectheader - -import ( - "bytes" - "strconv" - - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// Parse parses a canonical loose-object header ("type size\\x00"). -// It returns the parsed type, size, bytes consumed (including trailing NUL), -// and whether parsing succeeded. -func Parse(data []byte) (objecttype.Type, int64, int, bool) { - space := bytes.IndexByte(data, ' ') - if space <= 0 { - return objecttype.TypeInvalid, 0, 0, false - } - - nulRel := bytes.IndexByte(data[space+1:], 0) - if nulRel < 0 { - return objecttype.TypeInvalid, 0, 0, false - } - - nul := space + 1 + nulRel - - ty, ok := objecttype.Parse(string(data[:space])) - if !ok { - return objecttype.TypeInvalid, 0, 0, false - } - - sizeBytes := data[space+1 : nul] - if len(sizeBytes) == 0 { - return objecttype.TypeInvalid, 0, 0, false - } - - size, err := strconv.ParseInt(string(sizeBytes), 10, 64) - if err != nil || size < 0 { - return objecttype.TypeInvalid, 0, 0, false - } - - return ty, size, nul + 1, true -} diff --git a/object/id/algorithm.go b/object/id/algorithm.go deleted file mode 100644 index a695889c..00000000 --- a/object/id/algorithm.go +++ /dev/null @@ -1,12 +0,0 @@ -package objectid - -//#nosec gosec - -// Algorithm identifies the hash algorithm used for Git object IDs. -type Algorithm uint8 - -const ( - AlgorithmUnknown Algorithm = iota - AlgorithmSHA1 - AlgorithmSHA256 -) diff --git a/object/id/algorithm_details.go b/object/id/algorithm_details.go deleted file mode 100644 index 15e96292..00000000 --- a/object/id/algorithm_details.go +++ /dev/null @@ -1,17 +0,0 @@ -package objectid - -import "hash" - -type algorithmDetails struct { - name string - size int - packHashID uint32 - signatureHeaderName string - sum func([]byte) ObjectID - new func() hash.Hash - emptyTree ObjectID -} - -func (algo Algorithm) info() algorithmDetails { - return algorithmTable[algo] -} diff --git a/object/id/algorithm_emptytree.go b/object/id/algorithm_emptytree.go deleted file mode 100644 index 32f57385..00000000 --- a/object/id/algorithm_emptytree.go +++ /dev/null @@ -1,7 +0,0 @@ -package objectid - -// EmptyTree returns the object ID of an empty tree ("tree 0\x00") for this -// algorithm. -func (algo Algorithm) EmptyTree() ObjectID { - return algo.info().emptyTree -} diff --git a/object/id/algorithm_hexlen.go b/object/id/algorithm_hexlen.go deleted file mode 100644 index 2b7fa0fa..00000000 --- a/object/id/algorithm_hexlen.go +++ /dev/null @@ -1,6 +0,0 @@ -package objectid - -// HexLen returns the encoded hexadecimal length. -func (algo Algorithm) HexLen() int { - return algo.Size() * 2 -} diff --git a/object/id/algorithm_new.go b/object/id/algorithm_new.go deleted file mode 100644 index 8abbaeda..00000000 --- a/object/id/algorithm_new.go +++ /dev/null @@ -1,13 +0,0 @@ -package objectid - -import "hash" - -// New returns a new hash.Hash for this algorithm. -func (algo Algorithm) New() (hash.Hash, error) { - newFn := algo.info().new - if newFn == nil { - return nil, ErrInvalidAlgorithm - } - - return newFn(), nil -} diff --git a/object/id/algorithm_packhashid.go b/object/id/algorithm_packhashid.go deleted file mode 100644 index 93c0f61b..00000000 --- a/object/id/algorithm_packhashid.go +++ /dev/null @@ -1,8 +0,0 @@ -package objectid - -// PackHashID returns the Git pack/rev hash-id encoding for this algorithm. -// -// Unknown algorithms return 0. -func (algo Algorithm) PackHashID() uint32 { - return algo.info().packHashID -} diff --git a/object/id/algorithm_parse.go b/object/id/algorithm_parse.go deleted file mode 100644 index d5fb0c64..00000000 --- a/object/id/algorithm_parse.go +++ /dev/null @@ -1,8 +0,0 @@ -package objectid - -// ParseAlgorithm parses a canonical algorithm name (e.g. "sha1", "sha256"). -func ParseAlgorithm(s string) (Algorithm, bool) { - algo, ok := algorithmByName[s] - - return algo, ok -} diff --git a/object/id/algorithm_signatureheadername.go b/object/id/algorithm_signatureheadername.go deleted file mode 100644 index 34fa41ce..00000000 --- a/object/id/algorithm_signatureheadername.go +++ /dev/null @@ -1,6 +0,0 @@ -package objectid - -// SignatureHeaderName returns the signature header name for this algorithm. -func (algo Algorithm) SignatureHeaderName() string { - return algo.info().signatureHeaderName -} diff --git a/object/id/algorithm_size.go b/object/id/algorithm_size.go deleted file mode 100644 index 104bfeb2..00000000 --- a/object/id/algorithm_size.go +++ /dev/null @@ -1,6 +0,0 @@ -package objectid - -// Size returns the hash size in bytes. -func (algo Algorithm) Size() int { - return algo.info().size -} diff --git a/object/id/algorithm_string.go b/object/id/algorithm_string.go deleted file mode 100644 index 410ee8a3..00000000 --- a/object/id/algorithm_string.go +++ /dev/null @@ -1,11 +0,0 @@ -package objectid - -// String returns the canonical algorithm name. -func (algo Algorithm) String() string { - inf := algo.info() - if inf.name == "" { - return "unknown" - } - - return inf.name -} diff --git a/object/id/algorithm_sum.go b/object/id/algorithm_sum.go deleted file mode 100644 index 26ad2ff6..00000000 --- a/object/id/algorithm_sum.go +++ /dev/null @@ -1,6 +0,0 @@ -package objectid - -// Sum computes an object ID from raw data using the selected algorithm. -func (algo Algorithm) Sum(data []byte) ObjectID { - return algo.info().sum(data) -} diff --git a/object/id/algorithm_supported.go b/object/id/algorithm_supported.go deleted file mode 100644 index 1f61e771..00000000 --- a/object/id/algorithm_supported.go +++ /dev/null @@ -1,7 +0,0 @@ -package objectid - -// SupportedAlgorithms returns all object ID algorithms supported by furgit. -// Do not mutate. -func SupportedAlgorithms() []Algorithm { - return supportedAlgorithms -} diff --git a/object/id/algorithm_tables.go b/object/id/algorithm_tables.go deleted file mode 100644 index e4ec3257..00000000 --- a/object/id/algorithm_tables.go +++ /dev/null @@ -1,72 +0,0 @@ -package objectid - -import ( - "crypto/sha1" //#nosec:G505 - "crypto/sha256" -) - -//nolint:gochecknoglobals -var algorithmTable = [...]algorithmDetails{ - AlgorithmUnknown: {}, - AlgorithmSHA1: { - name: "sha1", - size: sha1.Size, - packHashID: 1, - signatureHeaderName: "gpgsig", - sum: func(data []byte) ObjectID { - sum := sha1.Sum(data) //#nosec G401 - - var id ObjectID - copy(id.data[:], sum[:]) - id.algo = AlgorithmSHA1 - - return id - }, - new: sha1.New, - }, - AlgorithmSHA256: { - name: "sha256", - size: sha256.Size, - packHashID: 2, - signatureHeaderName: "gpgsig-sha256", - sum: func(data []byte) ObjectID { - sum := sha256.Sum256(data) - - var id ObjectID - copy(id.data[:], sum[:]) - id.algo = AlgorithmSHA256 - - return id - }, - new: sha256.New, - }, -} - -var ( - //nolint:gochecknoglobals - algorithmByName = map[string]Algorithm{} - //nolint:gochecknoglobals - algorithmBySignatureHeaderName = map[string]Algorithm{} - //nolint:gochecknoglobals - supportedAlgorithms []Algorithm -) - -func init() { //nolint:gochecknoinits - emptyTreeInput := []byte("tree 0\x00") - - for algo := Algorithm(0); int(algo) < len(algorithmTable); algo++ { - info := &algorithmTable[algo] - if info.name == "" { - continue - } - - info.emptyTree = info.sum(emptyTreeInput) - - algorithmByName[info.name] = algo - if info.signatureHeaderName != "" { - algorithmBySignatureHeaderName[info.signatureHeaderName] = algo - } - - supportedAlgorithms = append(supportedAlgorithms, algo) - } -} diff --git a/object/id/algorithm_zero.go b/object/id/algorithm_zero.go deleted file mode 100644 index e8c0abf2..00000000 --- a/object/id/algorithm_zero.go +++ /dev/null @@ -1,11 +0,0 @@ -package objectid - -// Zero returns the all-zero object ID for this algorithm. -func (algo Algorithm) Zero() ObjectID { - id, err := FromBytes(algo, make([]byte, algo.Size())) - if err != nil { - panic(err) - } - - return id -} diff --git a/object/id/doc.go b/object/id/doc.go deleted file mode 100644 index 1436535d..00000000 --- a/object/id/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package objectid provides Git object IDs and object-ID hash algorithms. -package objectid diff --git a/object/id/errors.go b/object/id/errors.go deleted file mode 100644 index 8e604c44..00000000 --- a/object/id/errors.go +++ /dev/null @@ -1,10 +0,0 @@ -package objectid - -import "errors" - -var ( - // ErrInvalidAlgorithm indicates an unsupported object ID algorithm. - ErrInvalidAlgorithm = errors.New("objectid: invalid algorithm") - // ErrInvalidObjectID indicates malformed object ID data. - ErrInvalidObjectID = errors.New("objectid: invalid object id") -) diff --git a/object/id/max_size.go b/object/id/max_size.go deleted file mode 100644 index d2a64a10..00000000 --- a/object/id/max_size.go +++ /dev/null @@ -1,6 +0,0 @@ -package objectid - -import "crypto/sha256" - -// maxObjectIDSize MUST be >= the largest supported algorithm size. -const maxObjectIDSize = sha256.Size diff --git a/object/id/objectid.go b/object/id/objectid.go deleted file mode 100644 index 33a54225..00000000 --- a/object/id/objectid.go +++ /dev/null @@ -1,11 +0,0 @@ -package objectid - -//#nosec G505 - -// ObjectID represents a Git object ID. -// -//nolint:recvcheck -type ObjectID struct { - algo Algorithm - data [maxObjectIDSize]byte -} diff --git a/object/id/objectid_algorithm.go b/object/id/objectid_algorithm.go deleted file mode 100644 index cb694b7c..00000000 --- a/object/id/objectid_algorithm.go +++ /dev/null @@ -1,6 +0,0 @@ -package objectid - -// Algorithm returns the object ID's hash algorithm. -func (id ObjectID) Algorithm() Algorithm { - return id.algo -} diff --git a/object/id/objectid_byte.go b/object/id/objectid_byte.go deleted file mode 100644 index 8bd8ab82..00000000 --- a/object/id/objectid_byte.go +++ /dev/null @@ -1,19 +0,0 @@ -package objectid - -// Bytes returns a copy of the object ID bytes. -func (id ObjectID) Bytes() []byte { - size := id.Algorithm().Size() - - return append([]byte(nil), id.data[:size]...) -} - -// RawBytes returns a direct byte slice view of the object ID bytes. -// -// Use Bytes when an independent copy is required. -// -// Labels: Mut-Never. -func (id *ObjectID) RawBytes() []byte { - size := id.Algorithm().Size() - - return id.data[:size:size] -} diff --git a/object/id/objectid_compare.go b/object/id/objectid_compare.go deleted file mode 100644 index a40bcc89..00000000 --- a/object/id/objectid_compare.go +++ /dev/null @@ -1,9 +0,0 @@ -package objectid - -import "bytes" - -// Compare lexicographically compares two object IDs by their canonical byte -// representation. -func Compare(left, right ObjectID) int { - return bytes.Compare(left.RawBytes(), right.RawBytes()) -} diff --git a/object/id/objectid_frombytes.go b/object/id/objectid_frombytes.go deleted file mode 100644 index ea8dacfe..00000000 --- a/object/id/objectid_frombytes.go +++ /dev/null @@ -1,20 +0,0 @@ -package objectid - -import "fmt" - -// FromBytes builds an object ID from raw bytes for the specified algorithm. -func FromBytes(algo Algorithm, b []byte) (ObjectID, error) { - var id ObjectID - if algo.Size() == 0 { - return id, ErrInvalidAlgorithm - } - - if len(b) != algo.Size() { - return id, fmt.Errorf("%w: got %d bytes, expected %d", ErrInvalidObjectID, len(b), algo.Size()) - } - - copy(id.data[:], b) - id.algo = algo - - return id, nil -} diff --git a/object/id/objectid_parse.go b/object/id/objectid_parse.go deleted file mode 100644 index e6cbb641..00000000 --- a/object/id/objectid_parse.go +++ /dev/null @@ -1,32 +0,0 @@ -package objectid - -import ( - "encoding/hex" - "fmt" -) - -// ParseHex parses an object ID from hex for the specified algorithm. -func ParseHex(algo Algorithm, s string) (ObjectID, error) { - var id ObjectID - if algo.Size() == 0 { - return id, ErrInvalidAlgorithm - } - - if len(s)%2 != 0 { - return id, fmt.Errorf("%w: odd hex length %d", ErrInvalidObjectID, len(s)) - } - - if len(s) != algo.HexLen() { - return id, fmt.Errorf("%w: got %d chars, expected %d", ErrInvalidObjectID, len(s), algo.HexLen()) - } - - decoded, err := hex.DecodeString(s) - if err != nil { - return id, fmt.Errorf("%w: decode: %w", ErrInvalidObjectID, err) - } - - copy(id.data[:], decoded) - id.algo = algo - - return id, nil -} diff --git a/object/id/objectid_string.go b/object/id/objectid_string.go deleted file mode 100644 index 36a7177d..00000000 --- a/object/id/objectid_string.go +++ /dev/null @@ -1,10 +0,0 @@ -package objectid - -import "encoding/hex" - -// String returns the canonical hex representation. -func (id ObjectID) String() string { - size := id.Algorithm().Size() - - return hex.EncodeToString(id.data[:size]) -} diff --git a/object/id/objectid_test.go b/object/id/objectid_test.go deleted file mode 100644 index 9d179fb5..00000000 --- a/object/id/objectid_test.go +++ /dev/null @@ -1,229 +0,0 @@ -package objectid_test - -import ( - "bytes" - "strings" - "testing" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -func TestParseAlgorithm(t *testing.T) { - t.Parallel() - - algo, ok := objectid.ParseAlgorithm("sha1") - if !ok || algo != objectid.AlgorithmSHA1 { - t.Fatalf("ParseAlgorithm(sha1) = (%v,%v)", algo, ok) - } - - algo, ok = objectid.ParseAlgorithm("sha256") - if !ok || algo != objectid.AlgorithmSHA256 { - t.Fatalf("ParseAlgorithm(sha256) = (%v,%v)", algo, ok) - } - - if _, ok := objectid.ParseAlgorithm("md5"); ok { - t.Fatalf("ParseAlgorithm(md5) should fail") - } -} - -func TestParseHexRoundtrip(t *testing.T) { - t.Parallel() - - for _, algo := range objectid.SupportedAlgorithms() { - t.Run(algo.String(), func(t *testing.T) { - t.Parallel() - - hex := strings.Repeat("01", algo.Size()) - - id, err := objectid.ParseHex(algo, hex) - if err != nil { - t.Fatalf("ParseHex failed: %v", err) - } - - if got := id.String(); got != hex { - t.Fatalf("String() = %q, want %q", got, hex) - } - - if got := id.Algorithm().Size(); got != algo.Size() { - t.Fatalf("Size() = %d, want %d", got, algo.Size()) - } - - raw := id.Bytes() - if len(raw) != algo.Size() { - t.Fatalf("Bytes len = %d, want %d", len(raw), algo.Size()) - } - - id2, err := objectid.FromBytes(algo, raw) - if err != nil { - t.Fatalf("FromBytes failed: %v", err) - } - - if id2.String() != hex { - t.Fatalf("FromBytes roundtrip = %q, want %q", id2.String(), hex) - } - }) - } -} - -func TestParseHexErrors(t *testing.T) { - t.Parallel() - - t.Run("unknown algo", func(t *testing.T) { - t.Parallel() - - _, err := objectid.ParseHex(objectid.AlgorithmUnknown, "00") - if err == nil { - t.Fatalf("expected ParseHex error") - } - }) - - for _, algo := range objectid.SupportedAlgorithms() { - t.Run(algo.String(), func(t *testing.T) { - t.Parallel() - - _, err := objectid.ParseHex(algo, strings.Repeat("0", algo.HexLen()-1)) - if err == nil { - t.Fatalf("expected ParseHex odd-len error") - } - - _, err = objectid.ParseHex(algo, strings.Repeat("0", algo.HexLen()-2)) - if err == nil { - t.Fatalf("expected ParseHex wrong-len error") - } - - _, err = objectid.ParseHex(algo, "z"+strings.Repeat("0", algo.HexLen()-1)) - if err == nil { - t.Fatalf("expected ParseHex invalid-hex error") - } - }) - } -} - -func TestFromBytesErrors(t *testing.T) { - t.Parallel() - - _, err := objectid.FromBytes(objectid.AlgorithmUnknown, []byte{1, 2}) - if err == nil { - t.Fatalf("expected FromBytes unknown algo error") - } - - for _, algo := range objectid.SupportedAlgorithms() { - _, err = objectid.FromBytes(algo, []byte{1, 2}) - if err == nil { - t.Fatalf("expected FromBytes wrong size error") - } - } -} - -func TestBytesReturnsCopy(t *testing.T) { - t.Parallel() - - for _, algo := range objectid.SupportedAlgorithms() { - id, err := objectid.ParseHex(algo, strings.Repeat("01", algo.Size())) - if err != nil { - t.Fatalf("ParseHex failed: %v", err) - } - - b1 := id.Bytes() - - b2 := id.Bytes() - if !bytes.Equal(b1, b2) { - t.Fatalf("Bytes mismatch") - } - - b1[0] ^= 0xff - if bytes.Equal(b1, b2) { - t.Fatalf("Bytes should return independent copies") - } - } -} - -func TestRawBytesAliasesStorage(t *testing.T) { - t.Parallel() - - for _, algo := range objectid.SupportedAlgorithms() { - id, err := objectid.ParseHex(algo, strings.Repeat("01", algo.Size())) - if err != nil { - t.Fatalf("ParseHex failed: %v", err) - } - - b := id.RawBytes() - if len(b) != id.Algorithm().Size() { - t.Fatalf("RawBytes len = %d, want %d", len(b), id.Algorithm().Size()) - } - - if cap(b) != len(b) { - t.Fatalf("RawBytes cap = %d, want %d", cap(b), len(b)) - } - - orig := id.String() - b[0] ^= 0xff - - if id.String() == orig { - t.Fatalf("RawBytes should alias object ID storage") - } - } -} - -func TestAlgorithmSum(t *testing.T) { - t.Parallel() - - id1 := objectid.AlgorithmSHA1.Sum([]byte("hello")) - if id1.Algorithm() != objectid.AlgorithmSHA1 || id1.Algorithm().Size() != objectid.AlgorithmSHA1.Size() { - t.Fatalf("sha1 sum produced invalid object id") - } - - id2 := objectid.AlgorithmSHA256.Sum([]byte("hello")) - if id2.Algorithm() != objectid.AlgorithmSHA256 || id2.Algorithm().Size() != objectid.AlgorithmSHA256.Size() { - t.Fatalf("sha256 sum produced invalid object id") - } - - if id1.String() == id2.String() { - t.Fatalf("sha1 and sha256 should differ") - } -} - -func TestAlgorithmEmptyTree(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - algo objectid.Algorithm - want string - }{ - { - name: "sha1", - algo: objectid.AlgorithmSHA1, - want: "4b825dc642cb6eb9a060e54bf8d69288fbee4904", - }, - { - name: "sha256", - algo: objectid.AlgorithmSHA256, - want: "6ef19b41225c5369f1c104d45d8d85efa9b057b53b14b4b9b939dd74decc5321", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - got := tt.algo.EmptyTree() - if got.Algorithm() != tt.algo { - t.Fatalf("EmptyTree() algorithm = %v, want %v", got.Algorithm(), tt.algo) - } - - if got.String() != tt.want { - t.Fatalf("EmptyTree() = %q, want %q", got.String(), tt.want) - } - }) - } -} - -func TestUnknownAlgorithmEmptyTree(t *testing.T) { - t.Parallel() - - got := objectid.AlgorithmUnknown.EmptyTree() - if got != (objectid.ObjectID{}) { - t.Fatalf("EmptyTree() for unknown algorithm = %#v, want zero value", got) - } -} diff --git a/object/id/signatureheadername_parse.go b/object/id/signatureheadername_parse.go deleted file mode 100644 index dbe0636a..00000000 --- a/object/id/signatureheadername_parse.go +++ /dev/null @@ -1,9 +0,0 @@ -package objectid - -// ParseSignatureHeaderName parses one canonical signature header name such as -// "gpgsig" or "gpgsig-sha256". -func ParseSignatureHeaderName(s string) (Algorithm, bool) { - algo, ok := algorithmBySignatureHeaderName[s] - - return algo, ok -} diff --git a/object/object.go b/object/object.go deleted file mode 100644 index d1b1bc4f..00000000 --- a/object/object.go +++ /dev/null @@ -1,10 +0,0 @@ -package object - -import objecttype "codeberg.org/lindenii/furgit/object/type" - -// Object is a Git object. -type Object interface { - ObjectType() objecttype.Type - SerializeWithoutHeader() ([]byte, error) - SerializeWithHeader() ([]byte, error) -} diff --git a/object/parse_with_header.go b/object/parse_with_header.go deleted file mode 100644 index 9bcf5a4c..00000000 --- a/object/parse_with_header.go +++ /dev/null @@ -1,25 +0,0 @@ -package object - -import ( - "fmt" - - objectheader "codeberg.org/lindenii/furgit/object/header" - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// ParseWithHeader parses a loose object in "type size\x00body" format. -// -//nolint:ireturn -func ParseWithHeader(raw []byte, algo objectid.Algorithm) (Object, error) { - ty, size, headerLen, ok := objectheader.Parse(raw) - if !ok { - return nil, fmt.Errorf("object: malformed object header") - } - - body := raw[headerLen:] - if int64(len(body)) != size { - return nil, fmt.Errorf("object: size mismatch: header says %d bytes, body has %d", size, len(body)) - } - - return ParseWithoutHeader(ty, body, algo) -} diff --git a/object/parse_without_header.go b/object/parse_without_header.go deleted file mode 100644 index c889cb40..00000000 --- a/object/parse_without_header.go +++ /dev/null @@ -1,32 +0,0 @@ -package object - -import ( - "fmt" - - "codeberg.org/lindenii/furgit/object/blob" - "codeberg.org/lindenii/furgit/object/commit" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/tag" - "codeberg.org/lindenii/furgit/object/tree" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// ParseWithoutHeader parses a typed object body. -// -//nolint:ireturn -func ParseWithoutHeader(ty objecttype.Type, body []byte, algo objectid.Algorithm) (Object, error) { - switch ty { - case objecttype.TypeBlob: - return blob.Parse(body) - case objecttype.TypeTree: - return tree.Parse(body, algo) - case objecttype.TypeCommit: - return commit.Parse(body, algo) - case objecttype.TypeTag: - return tag.Parse(body, algo) - case objecttype.TypeInvalid, objecttype.TypeFuture, objecttype.TypeOfsDelta, objecttype.TypeRefDelta: - return nil, fmt.Errorf("object: unsupported object type %d", ty) - default: - return nil, fmt.Errorf("object: unsupported object type %d", ty) - } -} diff --git a/object/signature/parse.go b/object/signature/parse.go deleted file mode 100644 index a6880eee..00000000 --- a/object/signature/parse.go +++ /dev/null @@ -1,97 +0,0 @@ -package signature - -import ( - "bytes" - "errors" - "fmt" - "strconv" - - "codeberg.org/lindenii/furgit/internal/intconv" -) - -// Parse parses a canonical Git signature line: -// "Name 123456789 +0000". -func Parse(line []byte) (*Signature, error) { - lt := bytes.IndexByte(line, '<') - if lt < 0 { - return nil, errors.New("object: signature: missing opening <") - } - - gtRel := bytes.IndexByte(line[lt+1:], '>') - if gtRel < 0 { - return nil, errors.New("object: signature: missing closing >") - } - - gt := lt + 1 + gtRel - - nameBytes := append([]byte(nil), bytes.TrimRight(line[:lt], " ")...) - emailBytes := append([]byte(nil), line[lt+1:gt]...) - - rest := line[gt+1:] - if len(rest) == 0 || rest[0] != ' ' { - return nil, errors.New("object: signature: missing timestamp separator") - } - - rest = rest[1:] - - before, after, ok := bytes.Cut(rest, []byte{' '}) - if !ok { - return nil, errors.New("object: signature: missing timezone separator") - } - - when, err := strconv.ParseInt(string(before), 10, 64) - if err != nil { - return nil, fmt.Errorf("object: signature: invalid timestamp: %w", err) - } - - tz := after - if len(tz) < 5 { - return nil, errors.New("object: signature: invalid timezone encoding") - } - - sign := 1 - - switch tz[0] { - case '-': - sign = -1 - case '+': - default: - return nil, errors.New("object: signature: invalid timezone sign") - } - - hh, err := strconv.Atoi(string(tz[1:3])) - if err != nil { - return nil, fmt.Errorf("object: signature: invalid timezone hours: %w", err) - } - - mm, err := strconv.Atoi(string(tz[3:5])) - if err != nil { - return nil, fmt.Errorf("object: signature: invalid timezone minutes: %w", err) - } - - if hh < 0 || hh > 23 { - return nil, errors.New("object: signature: invalid timezone hours range") - } - - if mm < 0 || mm > 59 { - return nil, errors.New("object: signature: invalid timezone minutes range") - } - - total := int64(hh)*60 + int64(mm) - - offset, err := intconv.Int64ToInt32(total) - if err != nil { - return nil, errors.New("object: signature: timezone overflow") - } - - if sign < 0 { - offset = -offset - } - - return &Signature{ - Name: nameBytes, - Email: emailBytes, - WhenUnix: when, - OffsetMinutes: offset, - }, nil -} diff --git a/object/signature/serialize.go b/object/signature/serialize.go deleted file mode 100644 index 3f60d20d..00000000 --- a/object/signature/serialize.go +++ /dev/null @@ -1,33 +0,0 @@ -package signature - -import ( - "fmt" - "strconv" - "strings" -) - -// Serialize renders the signature in canonical Git format. -func (signature Signature) Serialize() ([]byte, error) { - var b strings.Builder - b.Grow(len(signature.Name) + len(signature.Email) + 32) - b.Write(signature.Name) - b.WriteString(" <") - b.Write(signature.Email) - b.WriteString("> ") - b.WriteString(strconv.FormatInt(signature.WhenUnix, 10)) - b.WriteByte(' ') - - offset := signature.OffsetMinutes - - sign := '+' - if offset < 0 { - sign = '-' - offset = -offset - } - - hh := offset / 60 - mm := offset % 60 - fmt.Fprintf(&b, "%c%02d%02d", sign, hh, mm) - - return []byte(b.String()), nil -} diff --git a/object/signature/signature.go b/object/signature/signature.go deleted file mode 100644 index bd8b8d87..00000000 --- a/object/signature/signature.go +++ /dev/null @@ -1,10 +0,0 @@ -// Package signature provides Git author, committer, and tagger signatures. -package signature - -// Signature represents a Git signature (author/committer/tagger). -type Signature struct { - Name []byte - Email []byte - WhenUnix int64 - OffsetMinutes int32 -} diff --git a/object/signature/when.go b/object/signature/when.go deleted file mode 100644 index 0a252f68..00000000 --- a/object/signature/when.go +++ /dev/null @@ -1,10 +0,0 @@ -package signature - -import "time" - -// When returns a time.Time with the signature's timezone offset. -func (signature Signature) When() time.Time { - loc := time.FixedZone("git", int(signature.OffsetMinutes)*60) - - return time.Unix(signature.WhenUnix, 0).In(loc) -} diff --git a/object/signed/commit/commit.go b/object/signed/commit/commit.go deleted file mode 100644 index cd0ff197..00000000 --- a/object/signed/commit/commit.go +++ /dev/null @@ -1,15 +0,0 @@ -package signedcommit - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// Commit represents the payload and signatures parsed from a raw comit object. -type Commit struct { - body []byte - payload []byteRange - signatures map[objectid.Algorithm][]byteRange -} - -type byteRange struct { - start int - end int -} diff --git a/object/signed/commit/doc.go b/object/signed/commit/doc.go deleted file mode 100644 index 91da6fa8..00000000 --- a/object/signed/commit/doc.go +++ /dev/null @@ -1,6 +0,0 @@ -// Package signedcommit extracts commit signing payloads and signatures from raw -// commit object bodies. -package signedcommit - -// TODO: Consider whether we want to fully copy the bytes into here. -// The Append functions are a bit weird ergonomically. diff --git a/object/signed/commit/integration_test.go b/object/signed/commit/integration_test.go deleted file mode 100644 index 82b34b14..00000000 --- a/object/signed/commit/integration_test.go +++ /dev/null @@ -1,138 +0,0 @@ -package signedcommit_test - -import ( - "bytes" - "os" - "os/exec" - "path/filepath" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - objectid "codeberg.org/lindenii/furgit/object/id" - signedcommit "codeberg.org/lindenii/furgit/object/signed/commit" -) - -func setupSSHSignedCommit( - t *testing.T, - algo objectid.Algorithm, -) (payload []byte, allowedSignersPath string, signaturePath string) { - t.Helper() - - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - - signDir := t.TempDir() - - signRoot, err := os.OpenRoot(signDir) - if err != nil { - t.Fatalf("os.OpenRoot(%q): %v", signDir, err) - } - - t.Cleanup(func() { _ = signRoot.Close() }) - - privateKeyPath := filepath.Join(signDir, "signing_key") - allowedSignersPath = filepath.Join(signDir, "allowed_signers") - signaturePath = filepath.Join(signDir, "commit.sig") - - cmd := exec.Command( //nolint:noctx - "ssh-keygen", - "-q", - "-t", "ed25519", - "-N", "", - "-C", "runxiyu@umich.edu", - "-f", privateKeyPath, - ) //#nosec G204 - - out, err := cmd.CombinedOutput() - if err != nil { - t.Fatalf("ssh-keygen generate failed: %v\n%s", err, out) - } - - publicKey, err := signRoot.ReadFile("signing_key.pub") - if err != nil { - t.Fatalf("ReadFile(signing_key.pub): %v", err) - } - - err = signRoot.WriteFile( - "allowed_signers", - append([]byte("runxiyu@umich.edu "), publicKey...), - 0o600, - ) - if err != nil { - t.Fatalf("WriteFile(allowed_signers): %v", err) - } - - testRepo.Run(t, "config", "gpg.format", "ssh") - testRepo.Run(t, "config", "user.signingkey", privateKeyPath) - - testRepo.WriteFile(t, "file.txt", []byte("signed\n"), 0o644) - testRepo.Run(t, "add", "file.txt") - testRepo.Run(t, "commit", "-S", "-m", "signed commit") - - commitID := testRepo.RevParse(t, "HEAD^{commit}") - body := testRepo.CatFile(t, "commit", commitID) - - commit, err := signedcommit.Parse(body) - if err != nil { - t.Fatalf("Parse: %v", err) - } - - signature, ok := commit.AppendSignature(nil, algo) - if !ok { - t.Fatalf("missing %s signature", algo) - } - - err = signRoot.WriteFile("commit.sig", signature, 0o600) - if err != nil { - t.Fatalf("WriteFile(commit.sig): %v", err) - } - - return commit.AppendPayload(nil), allowedSignersPath, signaturePath -} - -func TestSSHSignedCommitIntegration(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - payload, allowedSignersPath, signaturePath := setupSSHSignedCommit(t, algo) - - cmd := exec.Command( //nolint:noctx - "ssh-keygen", - "-Y", "verify", - "-n", "git", - "-f", allowedSignersPath, - "-I", "runxiyu@umich.edu", - "-s", signaturePath, - ) //#nosec G204 - cmd.Stdin = bytes.NewReader(payload) - - out, err := cmd.CombinedOutput() - if err != nil { - t.Fatalf("ssh-keygen verify failed: %v\n%s", err, out) - } - }) -} - -func TestSSHSignedCommitIntegrationRejectsTamperedPayload(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - payload, allowedSignersPath, signaturePath := setupSSHSignedCommit(t, algo) - payload = append([]byte(nil), payload...) - payload[len(payload)-2] ^= 1 - - cmd := exec.Command( //nolint:noctx - "ssh-keygen", - "-Y", "verify", - "-n", "git", - "-f", allowedSignersPath, - "-I", "runxiyu@umich.edu", - "-s", signaturePath, - ) //#nosec G204 - cmd.Stdin = bytes.NewReader(payload) - - out, err := cmd.CombinedOutput() - if err == nil { - t.Fatalf("ssh-keygen verify unexpectedly succeeded:\n%s", out) - } - }) -} diff --git a/object/signed/commit/parse.go b/object/signed/commit/parse.go deleted file mode 100644 index fa498093..00000000 --- a/object/signed/commit/parse.go +++ /dev/null @@ -1,107 +0,0 @@ -package signedcommit - -import ( - "bytes" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// Parse parses one raw commit object body for signature extraction. -// -// The returned Commit remains valid only while body remains unchanged. -// -// Labels: Deps-Borrowed, Life-Parent. -func Parse(body []byte) (*Commit, error) { - commit := &Commit{ - body: body, - signatures: make(map[objectid.Algorithm][]byteRange), - } - - payloadStart := 0 - i := 0 - - for i < len(body) { - lineStart := i - - rel := bytes.IndexByte(body[i:], '\n') - next := len(body) - - lineEnd := len(body) - if rel >= 0 { - lineEnd = i + rel - next = lineEnd + 1 - } - - line := body[lineStart:lineEnd] - i = next - - if len(line) == 0 { - commit.appendPayloadRange(payloadStart, len(body)) - - return commit, nil - } - - if line[0] == ' ' { - continue - } - - if !bytes.HasPrefix(line, []byte("gpgsig")) { - continue - } - - commit.appendPayloadRange(payloadStart, lineStart) - - key, valueStart, found := bytes.Cut(line, []byte{' '}) - if found { - if algo, ok := objectid.ParseSignatureHeaderName(string(key)); ok { - commit.signatures[algo] = append(commit.signatures[algo], byteRange{ - start: lineEnd - len(valueStart), - end: next, - }) - } - } - - for i < len(body) { - rel := bytes.IndexByte(body[i:], '\n') - next = len(body) - - lineEnd = len(body) - if rel >= 0 { - lineEnd = i + rel - next = lineEnd + 1 - } - - contStart := i - - cont := body[contStart:lineEnd] - if len(cont) == 0 || cont[0] != ' ' { - break - } - - if found { - if algo, ok := objectid.ParseSignatureHeaderName(string(key)); ok { - commit.signatures[algo] = append(commit.signatures[algo], byteRange{ - start: contStart + 1, - end: next, - }) - } - } - - i = next - } - - payloadStart = i - } - - commit.appendPayloadRange(payloadStart, len(body)) - - return commit, nil -} - -func (commit *Commit) appendPayloadRange(start, end int) { - if start >= end { - return - } - - commit.payload = append(commit.payload, byteRange{start: start, end: end}) -} diff --git a/object/signed/commit/payload_append.go b/object/signed/commit/payload_append.go deleted file mode 100644 index c261910a..00000000 --- a/object/signed/commit/payload_append.go +++ /dev/null @@ -1,11 +0,0 @@ -package signedcommit - -// AppendPayload appends the commit verification payload to dst, omitting all -// embedded signature headers. -func (commit *Commit) AppendPayload(dst []byte) []byte { - for _, part := range commit.payload { - dst = append(dst, commit.body[part.start:part.end]...) - } - - return dst -} diff --git a/object/signed/commit/signature_algorithms.go b/object/signed/commit/signature_algorithms.go deleted file mode 100644 index ac763706..00000000 --- a/object/signed/commit/signature_algorithms.go +++ /dev/null @@ -1,16 +0,0 @@ -package signedcommit - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// Algorithms returns the algorithms for which the commit carries signatures. -func (commit *Commit) Algorithms() []objectid.Algorithm { - var algorithms []objectid.Algorithm - - for _, algo := range objectid.SupportedAlgorithms() { - if _, ok := commit.signatures[algo]; ok { - algorithms = append(algorithms, algo) - } - } - - return algorithms -} diff --git a/object/signed/commit/signature_append.go b/object/signed/commit/signature_append.go deleted file mode 100644 index 7f9144b7..00000000 --- a/object/signed/commit/signature_append.go +++ /dev/null @@ -1,17 +0,0 @@ -package signedcommit - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// AppendSignature appends the unfolded signature for algo to dst. -func (commit *Commit) AppendSignature(dst []byte, algo objectid.Algorithm) ([]byte, bool) { - signature, ok := commit.signatures[algo] - if !ok { - return dst, false - } - - for _, part := range signature { - dst = append(dst, commit.body[part.start:part.end]...) - } - - return dst, true -} diff --git a/object/signed/commit/unit_test.go b/object/signed/commit/unit_test.go deleted file mode 100644 index 88d4fa3b..00000000 --- a/object/signed/commit/unit_test.go +++ /dev/null @@ -1,170 +0,0 @@ -package signedcommit_test - -import ( - "slices" - "testing" - - objectid "codeberg.org/lindenii/furgit/object/id" - signedcommit "codeberg.org/lindenii/furgit/object/signed/commit" -) - -func TestParseUpstreamMultiplySignedCommit(t *testing.T) { - t.Parallel() - - // t/t7510-signed-commit.sh - body := []byte("" + - "tree 0cfbf08886fca9a91cb753ec8734c84fcbe52c9f\n" + - "parent 9da738312d24ef0a29be2c8c2b6fc5cf7085a293\n" + - "author A U Thor 1112912653 -0700\n" + - "committer C O Mitter 1112912653 -0700\n" + - "gpgsig -----BEGIN PGP SIGNATURE-----\n" + - " \n" + - " iHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBDRYcY29tbWl0dGVy\n" + - " QGV4YW1wbGUuY29tAAoJEBO29R7N3kMNd+8AoK1I8mhLHviPH+q2I5fIVgPsEtYC\n" + - " AKCTqBh+VabJceXcGIZuF0Ry+udbBQ==\n" + - " =tQ0N\n" + - " -----END PGP SIGNATURE-----\n" + - "gpgsig-sha256 -----BEGIN PGP SIGNATURE-----\n" + - " \n" + - " iHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBIBYcY29tbWl0dGVy\n" + - " QGV4YW1wbGUuY29tAAoJEBO29R7N3kMN/NEAn0XO9RYSBj2dFyozi0JKSbssYMtO\n" + - " AJwKCQ1BQOtuwz//IjU8TiS+6S4iUw==\n" + - " =pIwP\n" + - " -----END PGP SIGNATURE-----\n" + - "\n" + - "second\n") - - commit, err := signedcommit.Parse(body) - if err != nil { - t.Fatalf("Parse: %v", err) - } - - gotPayload := string(commit.AppendPayload(nil)) - - wantPayload := "" + - "tree 0cfbf08886fca9a91cb753ec8734c84fcbe52c9f\n" + - "parent 9da738312d24ef0a29be2c8c2b6fc5cf7085a293\n" + - "author A U Thor 1112912653 -0700\n" + - "committer C O Mitter 1112912653 -0700\n" + - "\n" + - "second\n" - if gotPayload != wantPayload { - t.Fatalf("payload mismatch:\n got: %q\nwant: %q", gotPayload, wantPayload) - } - - gotSHA1, ok := commit.AppendSignature(nil, objectid.AlgorithmSHA1) - if !ok { - t.Fatal("missing sha1 signature") - } - - wantSHA1 := "" + - "-----BEGIN PGP SIGNATURE-----\n" + - "\n" + - "iHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBDRYcY29tbWl0dGVy\n" + - "QGV4YW1wbGUuY29tAAoJEBO29R7N3kMNd+8AoK1I8mhLHviPH+q2I5fIVgPsEtYC\n" + - "AKCTqBh+VabJceXcGIZuF0Ry+udbBQ==\n" + - "=tQ0N\n" + - "-----END PGP SIGNATURE-----\n" - if string(gotSHA1) != wantSHA1 { - t.Fatalf("sha1 signature mismatch:\n got: %q\nwant: %q", string(gotSHA1), wantSHA1) - } - - gotSHA256, ok := commit.AppendSignature(nil, objectid.AlgorithmSHA256) - if !ok { - t.Fatal("missing sha256 signature") - } - - wantSHA256 := "" + - "-----BEGIN PGP SIGNATURE-----\n" + - "\n" + - "iHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBIBYcY29tbWl0dGVy\n" + - "QGV4YW1wbGUuY29tAAoJEBO29R7N3kMN/NEAn0XO9RYSBj2dFyozi0JKSbssYMtO\n" + - "AJwKCQ1BQOtuwz//IjU8TiS+6S4iUw==\n" + - "=pIwP\n" + - "-----END PGP SIGNATURE-----\n" - if string(gotSHA256) != wantSHA256 { - t.Fatalf("sha256 signature mismatch:\n got: %q\nwant: %q", string(gotSHA256), wantSHA256) - } - - gotAlgorithms := commit.Algorithms() - - wantAlgorithms := []objectid.Algorithm{ - objectid.AlgorithmSHA1, - objectid.AlgorithmSHA256, - } - if !slices.Equal(gotAlgorithms, wantAlgorithms) { - t.Fatalf("Algorithms() = %v, want %v", gotAlgorithms, wantAlgorithms) - } -} - -func TestParseStripsUnknownGpgsigHeadersFromPayload(t *testing.T) { - t.Parallel() - - body := []byte("" + - "tree deadbeef\n" + - "gpgsig-future header\n" + - " continued\n" + - "\n" + - "message\n") - - commit, err := signedcommit.Parse(body) - if err != nil { - t.Fatalf("Parse: %v", err) - } - - gotPayload := string(commit.AppendPayload(nil)) - - wantPayload := "" + - "tree deadbeef\n" + - "\n" + - "message\n" - if gotPayload != wantPayload { - t.Fatalf("payload mismatch:\n got: %q\nwant: %q", gotPayload, wantPayload) - } - - if gotAlgorithms := commit.Algorithms(); len(gotAlgorithms) != 0 { - t.Fatalf("Algorithms() = %v, want none", gotAlgorithms) - } -} - -func TestParseAllowsDuplicateSignatureHeaders(t *testing.T) { - t.Parallel() - - body := []byte("" + - "tree deadbeef\n" + - "gpgsig one\n" + - " two\n" + - "gpgsig three\n" + - " four\n" + - "\n" + - "message\n") - - commit, err := signedcommit.Parse(body) - if err != nil { - t.Fatalf("Parse: %v", err) - } - - gotPayload := string(commit.AppendPayload(nil)) - - wantPayload := "" + - "tree deadbeef\n" + - "\n" + - "message\n" - if gotPayload != wantPayload { - t.Fatalf("payload mismatch:\n got: %q\nwant: %q", gotPayload, wantPayload) - } - - gotSignature, ok := commit.AppendSignature(nil, objectid.AlgorithmSHA1) - if !ok { - t.Fatal("missing sha1 signature") - } - - wantSignature := "" + - "one\n" + - "two\n" + - "three\n" + - "four\n" - if string(gotSignature) != wantSignature { - t.Fatalf("signature mismatch:\n got: %q\nwant: %q", string(gotSignature), wantSignature) - } -} diff --git a/object/signed/doc.go b/object/signed/doc.go deleted file mode 100644 index fb6fc3f8..00000000 --- a/object/signed/doc.go +++ /dev/null @@ -1,7 +0,0 @@ -// Package signed encapsulates raw signed-object processing. -// -// Its subpackages extract verification payloads and embedded signatures from -// raw commit and tag object bodies, without depending on the parsed -// object models in [codeberg.org/lindenii/furgit/object/commit] and -// [codeberg.org/lindenii/furgit/object/tag]. -package signed diff --git a/object/signed/tag/doc.go b/object/signed/tag/doc.go deleted file mode 100644 index 22b1098a..00000000 --- a/object/signed/tag/doc.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package signedtag extracts tag signing payloads and signatures from raw tag -// object bodies. -package signedtag diff --git a/object/signed/tag/integration_test.go b/object/signed/tag/integration_test.go deleted file mode 100644 index af32aa02..00000000 --- a/object/signed/tag/integration_test.go +++ /dev/null @@ -1,139 +0,0 @@ -package signedtag_test - -import ( - "bytes" - "os" - "os/exec" - "path/filepath" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - objectid "codeberg.org/lindenii/furgit/object/id" - signedtag "codeberg.org/lindenii/furgit/object/signed/tag" -) - -func setupSSHSignedTag( - t *testing.T, - algo objectid.Algorithm, -) (payload []byte, allowedSignersPath string, signaturePath string) { - t.Helper() - - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - - signDir := t.TempDir() - - signRoot, err := os.OpenRoot(signDir) - if err != nil { - t.Fatalf("os.OpenRoot(%q): %v", signDir, err) - } - - t.Cleanup(func() { _ = signRoot.Close() }) - - privateKeyPath := filepath.Join(signDir, "signing_key") - allowedSignersPath = filepath.Join(signDir, "allowed_signers") - signaturePath = filepath.Join(signDir, "tag.sig") - - cmd := exec.Command( //nolint:noctx - "ssh-keygen", - "-q", - "-t", "ed25519", - "-N", "", - "-C", "runxiyu@umich.edu", - "-f", privateKeyPath, - ) //#nosec G204 - - out, err := cmd.CombinedOutput() - if err != nil { - t.Fatalf("ssh-keygen generate failed: %v\n%s", err, out) - } - - publicKey, err := signRoot.ReadFile("signing_key.pub") - if err != nil { - t.Fatalf("ReadFile(signing_key.pub): %v", err) - } - - err = signRoot.WriteFile( - "allowed_signers", - append([]byte("runxiyu@umich.edu "), publicKey...), - 0o600, - ) - if err != nil { - t.Fatalf("WriteFile(allowed_signers): %v", err) - } - - testRepo.Run(t, "config", "gpg.format", "ssh") - testRepo.Run(t, "config", "user.signingkey", privateKeyPath) - - testRepo.WriteFile(t, "file.txt", []byte("signed\n"), 0o644) - testRepo.Run(t, "add", "file.txt") - testRepo.Run(t, "commit", "-m", "base commit") - testRepo.Run(t, "tag", "-s", "-m", "signed tag", "signed-tag") - - tagID := testRepo.RevParse(t, "signed-tag^{tag}") - body := testRepo.CatFile(t, "tag", tagID) - - tag, err := signedtag.Parse(body, algo) - if err != nil { - t.Fatalf("Parse: %v", err) - } - - signature, ok := tag.AppendSignature(nil, algo) - if !ok { - t.Fatal("missing signature") - } - - err = signRoot.WriteFile("tag.sig", signature, 0o600) - if err != nil { - t.Fatalf("WriteFile(tag.sig): %v", err) - } - - return tag.AppendPayload(nil), allowedSignersPath, signaturePath -} - -func TestSSHSignedTagIntegration(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - payload, allowedSignersPath, signaturePath := setupSSHSignedTag(t, algo) - - cmd := exec.Command( //nolint:noctx - "ssh-keygen", - "-Y", "verify", - "-n", "git", - "-f", allowedSignersPath, - "-I", "runxiyu@umich.edu", - "-s", signaturePath, - ) //#nosec G204 - cmd.Stdin = bytes.NewReader(payload) - - out, err := cmd.CombinedOutput() - if err != nil { - t.Fatalf("ssh-keygen verify failed: %v\n%s", err, out) - } - }) -} - -func TestSSHSignedTagIntegrationRejectsTamperedPayload(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - payload, allowedSignersPath, signaturePath := setupSSHSignedTag(t, algo) - payload = append([]byte(nil), payload...) - payload[len(payload)-2] ^= 1 - - cmd := exec.Command( //nolint:noctx - "ssh-keygen", - "-Y", "verify", - "-n", "git", - "-f", allowedSignersPath, - "-I", "runxiyu@umich.edu", - "-s", signaturePath, - ) //#nosec G204 - cmd.Stdin = bytes.NewReader(payload) - - out, err := cmd.CombinedOutput() - if err == nil { - t.Fatalf("ssh-keygen verify unexpectedly succeeded:\n%s", out) - } - }) -} diff --git a/object/signed/tag/parse.go b/object/signed/tag/parse.go deleted file mode 100644 index b2061d3f..00000000 --- a/object/signed/tag/parse.go +++ /dev/null @@ -1,143 +0,0 @@ -package signedtag - -import ( - "bytes" - "slices" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -var signatureBeginLines = [][]byte{ //nolint:gochecknoglobals - []byte("-----BEGIN PGP SIGNATURE-----"), - []byte("-----BEGIN PGP MESSAGE-----"), - []byte("-----BEGIN SSH SIGNATURE-----"), - []byte("-----BEGIN SIGNED MESSAGE-----"), -} - -// Parse parses one raw tag object body for signature extraction. -// -// Git stores the signature for storageAlgo as an in-body ASCII-armored -// trailer, and may store additional signatures for other algorithms in -// gpgsig* headers. -// -// The returned Tag remains valid only while body remains unchanged. -// -// Labels: Deps-Borrowed, Life-Parent. -func Parse(body []byte, storageAlgo objectid.Algorithm) (*Tag, error) { - tag := &Tag{ - body: body, - signatures: make(map[objectid.Algorithm][]byteRange), - } - - signatureStart := len(body) - for i := 0; i < len(body); { - lineStart := i - rel := bytes.IndexByte(body[i:], '\n') - next := len(body) - - lineEnd := len(body) - if rel >= 0 { - lineEnd = i + rel - next = lineEnd + 1 - } - - line := body[lineStart:lineEnd] - if slices.ContainsFunc(signatureBeginLines, func(begin []byte) bool { - return bytes.HasPrefix(line, begin) - }) { - signatureStart = lineStart - } - - i = next - } - - payloadStart := 0 - - payloadEnd := signatureStart - if signatureStart == len(body) { - payloadEnd = len(body) - } - - for i := 0; i < payloadEnd; { - lineStart := i - rel := bytes.IndexByte(body[i:payloadEnd], '\n') - next := payloadEnd - - lineEnd := payloadEnd - if rel >= 0 { - lineEnd = i + rel - next = lineEnd + 1 - } - - line := body[lineStart:lineEnd] - i = next - - if len(line) == 0 { - break - } - - if line[0] == ' ' { - continue - } - - key, valueStart, found := bytes.Cut(line, []byte{' '}) - if !found { - continue - } - - algo, ok := objectid.ParseSignatureHeaderName(string(key)) - if !ok { - continue - } - - tag.appendPayloadRange(payloadStart, lineStart) - tag.signatures[algo] = append(tag.signatures[algo], byteRange{ - start: lineEnd - len(valueStart), - end: next, - }) - - for i < payloadEnd { - rel := bytes.IndexByte(body[i:payloadEnd], '\n') - next = payloadEnd - - lineEnd = payloadEnd - if rel >= 0 { - lineEnd = i + rel - next = lineEnd + 1 - } - - cont := body[i:lineEnd] - if len(cont) == 0 || cont[0] != ' ' { - break - } - - tag.signatures[algo] = append(tag.signatures[algo], byteRange{ - start: i + 1, - end: next, - }) - - i = next - } - - payloadStart = i - } - - tag.appendPayloadRange(payloadStart, payloadEnd) - - if signatureStart != len(body) { - tag.signatures[storageAlgo] = append(tag.signatures[storageAlgo], byteRange{ - start: signatureStart, - end: len(body), - }) - } - - return tag, nil -} - -func (tag *Tag) appendPayloadRange(start, end int) { - if start >= end { - return - } - - tag.payload = append(tag.payload, byteRange{start: start, end: end}) -} diff --git a/object/signed/tag/payload_append.go b/object/signed/tag/payload_append.go deleted file mode 100644 index dae29dd8..00000000 --- a/object/signed/tag/payload_append.go +++ /dev/null @@ -1,11 +0,0 @@ -package signedtag - -// AppendPayload appends the tag verification payload to dst, omitting all -// embedded signatures. -func (tag *Tag) AppendPayload(dst []byte) []byte { - for _, part := range tag.payload { - dst = append(dst, tag.body[part.start:part.end]...) - } - - return dst -} diff --git a/object/signed/tag/signature_algorithms.go b/object/signed/tag/signature_algorithms.go deleted file mode 100644 index bc178bce..00000000 --- a/object/signed/tag/signature_algorithms.go +++ /dev/null @@ -1,16 +0,0 @@ -package signedtag - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// Algorithms returns the algorithms for which the tag carries signatures. -func (tag *Tag) Algorithms() []objectid.Algorithm { - var algorithms []objectid.Algorithm - - for _, algo := range objectid.SupportedAlgorithms() { - if _, ok := tag.signatures[algo]; ok { - algorithms = append(algorithms, algo) - } - } - - return algorithms -} diff --git a/object/signed/tag/signature_append.go b/object/signed/tag/signature_append.go deleted file mode 100644 index 101816eb..00000000 --- a/object/signed/tag/signature_append.go +++ /dev/null @@ -1,17 +0,0 @@ -package signedtag - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// AppendSignature appends the signature for algo to dst. -func (tag *Tag) AppendSignature(dst []byte, algo objectid.Algorithm) ([]byte, bool) { - signature, ok := tag.signatures[algo] - if !ok { - return dst, false - } - - for _, part := range signature { - dst = append(dst, tag.body[part.start:part.end]...) - } - - return dst, true -} diff --git a/object/signed/tag/tag.go b/object/signed/tag/tag.go deleted file mode 100644 index 2ebf9369..00000000 --- a/object/signed/tag/tag.go +++ /dev/null @@ -1,15 +0,0 @@ -package signedtag - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// Tag represents the payload and signatures parsed from a raw tag object. -type Tag struct { - body []byte - payload []byteRange - signatures map[objectid.Algorithm][]byteRange -} - -type byteRange struct { - start int - end int -} diff --git a/object/signed/tag/unit_test.go b/object/signed/tag/unit_test.go deleted file mode 100644 index dd4ae66f..00000000 --- a/object/signed/tag/unit_test.go +++ /dev/null @@ -1,257 +0,0 @@ -package signedtag_test - -import ( - "testing" - - objectid "codeberg.org/lindenii/furgit/object/id" - signedtag "codeberg.org/lindenii/furgit/object/signed/tag" -) - -func TestParseSignedTag(t *testing.T) { - t.Parallel() - - body := []byte("" + - "object 04b871796dc0420f8e7561a895b52484b701d51a\n" + - "type commit\n" + - "tag signedtag\n" + - "tagger C O Mitter 1465981006 +0000\n" + - "gpgsig-sha256 -----BEGIN PGP SIGNATURE-----\n" + - " Version: GnuPG v1\n" + - " \n" + - " header-signature\n" + - " -----END PGP SIGNATURE-----\n" + - "\n" + - "signed tag\n" + - "\n" + - "signed tag message body\n" + - "-----BEGIN PGP SIGNATURE-----\n" + - "Version: GnuPG v1\n" + - "\n" + - "body-signature\n" + - "-----END PGP SIGNATURE-----\n") - - tag, err := signedtag.Parse(body, objectid.AlgorithmSHA1) - if err != nil { - t.Fatalf("Parse: %v", err) - } - - gotPayload := string(tag.AppendPayload(nil)) - - wantPayload := "" + - "object 04b871796dc0420f8e7561a895b52484b701d51a\n" + - "type commit\n" + - "tag signedtag\n" + - "tagger C O Mitter 1465981006 +0000\n" + - "\n" + - "signed tag\n" + - "\n" + - "signed tag message body\n" - if gotPayload != wantPayload { - t.Fatalf("payload mismatch:\n got: %q\nwant: %q", gotPayload, wantPayload) - } - - gotAlgorithms := tag.Algorithms() - if len(gotAlgorithms) != 2 || gotAlgorithms[0] != objectid.AlgorithmSHA1 || gotAlgorithms[1] != objectid.AlgorithmSHA256 { - t.Fatalf("algorithms mismatch: got %v", gotAlgorithms) - } - - gotSignature, ok := tag.AppendSignature(nil, objectid.AlgorithmSHA1) - if !ok { - t.Fatal("missing sha1 signature") - } - - wantSignature := "" + - "-----BEGIN PGP SIGNATURE-----\n" + - "Version: GnuPG v1\n" + - "\n" + - "body-signature\n" + - "-----END PGP SIGNATURE-----\n" - if string(gotSignature) != wantSignature { - t.Fatalf("signature mismatch:\n got: %q\nwant: %q", string(gotSignature), wantSignature) - } - - gotHeaderSignature, ok := tag.AppendSignature(nil, objectid.AlgorithmSHA256) - if !ok { - t.Fatal("missing sha256 signature") - } - - wantHeaderSignature := "" + - "-----BEGIN PGP SIGNATURE-----\n" + - "Version: GnuPG v1\n" + - "\n" + - "header-signature\n" + - "-----END PGP SIGNATURE-----\n" - if string(gotHeaderSignature) != wantHeaderSignature { - t.Fatalf("header signature mismatch:\n got: %q\nwant: %q", string(gotHeaderSignature), wantHeaderSignature) - } -} - -func TestParseHeaderOnlyTagStripsHeaderAndKeepsHeaderSignature(t *testing.T) { - t.Parallel() - - body := []byte("" + - "object deadbeef\n" + - "type commit\n" + - "tag signedtag\n" + - "tagger T A Gger 1465981006 +0000\n" + - "gpgsig-sha256 header\n" + - " continued\n" + - "\n" + - "message\n") - - tag, err := signedtag.Parse(body, objectid.AlgorithmSHA1) - if err != nil { - t.Fatalf("Parse: %v", err) - } - - gotPayload := string(tag.AppendPayload(nil)) - - wantPayload := "" + - "object deadbeef\n" + - "type commit\n" + - "tag signedtag\n" + - "tagger T A Gger 1465981006 +0000\n" + - "\n" + - "message\n" - if gotPayload != wantPayload { - t.Fatalf("payload mismatch:\n got: %q\nwant: %q", gotPayload, wantPayload) - } - - gotSignature, ok := tag.AppendSignature(nil, objectid.AlgorithmSHA256) - if !ok { - t.Fatal("missing sha256 signature") - } - - wantSignature := "" + - "header\n" + - "continued\n" - if string(gotSignature) != wantSignature { - t.Fatalf("signature mismatch:\n got: %q\nwant: %q", string(gotSignature), wantSignature) - } - - if _, ok := tag.AppendSignature(nil, objectid.AlgorithmSHA1); ok { - t.Fatal("unexpected sha1 signature") - } -} - -func TestParseKeepsUnknownHeaderSignatureTextInPayload(t *testing.T) { - t.Parallel() - - body := []byte("" + - "object deadbeef\n" + - "type commit\n" + - "tag signedtag\n" + - "tagger T A Gger 1465981006 +0000\n" + - "gpgsig-future header\n" + - " continued\n" + - "\n" + - "message line\n" + - "-----BEGIN PGP SIGNATURE-----\n" + - "body-signature\n" + - "-----END PGP SIGNATURE-----\n") - - tag, err := signedtag.Parse(body, objectid.AlgorithmSHA1) - if err != nil { - t.Fatalf("Parse: %v", err) - } - - gotPayload := string(tag.AppendPayload(nil)) - - wantPayload := "" + - "object deadbeef\n" + - "type commit\n" + - "tag signedtag\n" + - "tagger T A Gger 1465981006 +0000\n" + - "gpgsig-future header\n" + - " continued\n" + - "\n" + - "message line\n" - if gotPayload != wantPayload { - t.Fatalf("payload mismatch:\n got: %q\nwant: %q", gotPayload, wantPayload) - } -} - -func TestParseKeepsMessageGpgsigTextInPayload(t *testing.T) { - t.Parallel() - - body := []byte("" + - "object deadbeef\n" + - "type commit\n" + - "tag signedtag\n" + - "tagger T A Gger 1465981006 +0000\n" + - "\n" + - "message line\n" + - "gpgsig-future header\n" + - " continued\n" + - "-----BEGIN PGP SIGNATURE-----\n" + - "body-signature\n" + - "-----END PGP SIGNATURE-----\n") - - tag, err := signedtag.Parse(body, objectid.AlgorithmSHA1) - if err != nil { - t.Fatalf("Parse: %v", err) - } - - gotPayload := string(tag.AppendPayload(nil)) - - wantPayload := "" + - "object deadbeef\n" + - "type commit\n" + - "tag signedtag\n" + - "tagger T A Gger 1465981006 +0000\n" + - "\n" + - "message line\n" + - "gpgsig-future header\n" + - " continued\n" - if gotPayload != wantPayload { - t.Fatalf("payload mismatch:\n got: %q\nwant: %q", gotPayload, wantPayload) - } -} - -func TestParseUsesLastSignatureBeginByPrefix(t *testing.T) { - t.Parallel() - - body := []byte("" + - "object deadbeef\n" + - "type commit\n" + - "tag signedtag\n" + - "tagger T A Gger 1465981006 +0000\n" + - "\n" + - "message\n" + - "-----BEGIN PGP SIGNATURE----- stray\n" + - "still message\n" + - "-----BEGIN PGP SIGNATURE----- trailing\n" + - "body-signature\n") - - tag, err := signedtag.Parse(body, objectid.AlgorithmSHA1) - if err != nil { - t.Fatalf("Parse: %v", err) - } - - gotPayload := string(tag.AppendPayload(nil)) - - wantPayload := "" + - "object deadbeef\n" + - "type commit\n" + - "tag signedtag\n" + - "tagger T A Gger 1465981006 +0000\n" + - "\n" + - "message\n" + - "-----BEGIN PGP SIGNATURE----- stray\n" + - "still message\n" - if gotPayload != wantPayload { - t.Fatalf("payload mismatch:\n got: %q\nwant: %q", gotPayload, wantPayload) - } - - gotSignature, ok := tag.AppendSignature(nil, objectid.AlgorithmSHA1) - if !ok { - t.Fatal("missing signature") - } - - wantSignature := "" + - "-----BEGIN PGP SIGNATURE----- trailing\n" + - "body-signature\n" - if string(gotSignature) != wantSignature { - t.Fatalf("signature mismatch:\n got: %q\nwant: %q", string(gotSignature), wantSignature) - } -} diff --git a/object/store/base_quarantine.go b/object/store/base_quarantine.go deleted file mode 100644 index 754fb3ee..00000000 --- a/object/store/base_quarantine.go +++ /dev/null @@ -1,17 +0,0 @@ -package objectstore - -// BaseQuarantine is one quarantined write. It is intended to be embedded. -type BaseQuarantine interface { - // Reader exposes the objects written into this quarantine. - Reader - - // Promote publishes quarantined writes into their final destination. - // - // Promote invalidates the receiver. - Promote() error - - // Discard abandons quarantined writes. - // - // Discard invalidates the receiver. - Discard() error -} diff --git a/object/store/chain/bytes.go b/object/store/chain/bytes.go deleted file mode 100644 index dc9b7906..00000000 --- a/object/store/chain/bytes.go +++ /dev/null @@ -1,46 +0,0 @@ -package chain - -import ( - "errors" - "fmt" - - objectid "codeberg.org/lindenii/furgit/object/id" - objectstore "codeberg.org/lindenii/furgit/object/store" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// ReadBytesFull reads a full serialized object from the first backend that has it. -func (chain *Chain) ReadBytesFull(id objectid.ObjectID) ([]byte, error) { - for i, backend := range chain.backends { - full, err := backend.ReadBytesFull(id) - if err == nil { - return full, nil - } - - if errors.Is(err, objectstore.ErrObjectNotFound) { - continue - } - - return nil, fmt.Errorf("objectstore: backend %d read bytes full: %w", i, err) - } - - return nil, objectstore.ErrObjectNotFound -} - -// ReadBytesContent reads an object's type and content bytes from the first backend that has it. -func (chain *Chain) ReadBytesContent(id objectid.ObjectID) (objecttype.Type, []byte, error) { - for i, backend := range chain.backends { - ty, content, err := backend.ReadBytesContent(id) - if err == nil { - return ty, content, nil - } - - if errors.Is(err, objectstore.ErrObjectNotFound) { - continue - } - - return objecttype.TypeInvalid, nil, fmt.Errorf("objectstore: backend %d read bytes content: %w", i, err) - } - - return objecttype.TypeInvalid, nil, objectstore.ErrObjectNotFound -} diff --git a/object/store/chain/chain.go b/object/store/chain/chain.go deleted file mode 100644 index 218c8abd..00000000 --- a/object/store/chain/chain.go +++ /dev/null @@ -1,12 +0,0 @@ -// Package chain provides a wrapper object storage backend to query a chain of -// backends. -package chain - -import objectstore "codeberg.org/lindenii/furgit/object/store" - -// Chain queries multiple object databases in order. -// -// Labels: Close-Caller. -type Chain struct { - backends []objectstore.Reader -} diff --git a/object/store/chain/header.go b/object/store/chain/header.go deleted file mode 100644 index f6c92459..00000000 --- a/object/store/chain/header.go +++ /dev/null @@ -1,28 +0,0 @@ -package chain - -import ( - "errors" - "fmt" - - objectid "codeberg.org/lindenii/furgit/object/id" - objectstore "codeberg.org/lindenii/furgit/object/store" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// ReadHeader reads object header data from the first backend that has it. -func (chain *Chain) ReadHeader(id objectid.ObjectID) (objecttype.Type, int64, error) { - for i, backend := range chain.backends { - ty, size, err := backend.ReadHeader(id) - if err == nil { - return ty, size, nil - } - - if errors.Is(err, objectstore.ErrObjectNotFound) { - continue - } - - return objecttype.TypeInvalid, 0, fmt.Errorf("objectstore: backend %d read header: %w", i, err) - } - - return objecttype.TypeInvalid, 0, objectstore.ErrObjectNotFound -} diff --git a/object/store/chain/new.go b/object/store/chain/new.go deleted file mode 100644 index dd499d38..00000000 --- a/object/store/chain/new.go +++ /dev/null @@ -1,14 +0,0 @@ -package chain - -import objectstore "codeberg.org/lindenii/furgit/object/store" - -// New creates an ordered object database chain. -// -// The provided backends must be non-nil and distinct. -// -// Labels: Deps-Borrowed, Life-Parent. -func New(backends ...objectstore.Reader) *Chain { - return &Chain{ - backends: append([]objectstore.Reader(nil), backends...), - } -} diff --git a/object/store/chain/reader.go b/object/store/chain/reader.go deleted file mode 100644 index 3991ee9a..00000000 --- a/object/store/chain/reader.go +++ /dev/null @@ -1,47 +0,0 @@ -package chain - -import ( - "errors" - "fmt" - "io" - - objectid "codeberg.org/lindenii/furgit/object/id" - objectstore "codeberg.org/lindenii/furgit/object/store" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// ReadReaderFull reads a full serialized object stream from the first backend that has it. -func (chain *Chain) ReadReaderFull(id objectid.ObjectID) (io.ReadCloser, error) { - for i, backend := range chain.backends { - reader, err := backend.ReadReaderFull(id) - if err == nil { - return reader, nil - } - - if errors.Is(err, objectstore.ErrObjectNotFound) { - continue - } - - return nil, fmt.Errorf("objectstore: backend %d read reader full: %w", i, err) - } - - return nil, objectstore.ErrObjectNotFound -} - -// ReadReaderContent reads an object's type, declared content length, and content stream from the first backend that has it. -func (chain *Chain) ReadReaderContent(id objectid.ObjectID) (objecttype.Type, int64, io.ReadCloser, error) { - for i, backend := range chain.backends { - ty, size, reader, err := backend.ReadReaderContent(id) - if err == nil { - return ty, size, reader, nil - } - - if errors.Is(err, objectstore.ErrObjectNotFound) { - continue - } - - return objecttype.TypeInvalid, 0, nil, fmt.Errorf("objectstore: backend %d read reader content: %w", i, err) - } - - return objecttype.TypeInvalid, 0, nil, objectstore.ErrObjectNotFound -} diff --git a/object/store/chain/refresh.go b/object/store/chain/refresh.go deleted file mode 100644 index c47352dc..00000000 --- a/object/store/chain/refresh.go +++ /dev/null @@ -1,17 +0,0 @@ -package chain - -import "errors" - -// Refresh forwards refresh calls to all backends. -func (chain *Chain) Refresh() error { - var errs []error - - for _, backend := range chain.backends { - err := backend.Refresh() - if err != nil { - errs = append(errs, err) - } - } - - return errors.Join(errs...) -} diff --git a/object/store/chain/size.go b/object/store/chain/size.go deleted file mode 100644 index f0099028..00000000 --- a/object/store/chain/size.go +++ /dev/null @@ -1,27 +0,0 @@ -package chain - -import ( - "errors" - "fmt" - - objectid "codeberg.org/lindenii/furgit/object/id" - objectstore "codeberg.org/lindenii/furgit/object/store" -) - -// ReadSize reads object content length from the first backend that has it. -func (chain *Chain) ReadSize(id objectid.ObjectID) (int64, error) { - for i, backend := range chain.backends { - size, err := backend.ReadSize(id) - if err == nil { - return size, nil - } - - if errors.Is(err, objectstore.ErrObjectNotFound) { - continue - } - - return 0, fmt.Errorf("objectstore: backend %d read size: %w", i, err) - } - - return 0, objectstore.ErrObjectNotFound -} diff --git a/object/store/cursor.go b/object/store/cursor.go deleted file mode 100644 index c6008ccd..00000000 --- a/object/store/cursor.go +++ /dev/null @@ -1,7 +0,0 @@ -package objectstore - -// type Cursor any -// -// Then make all read functions accept and provide a Cursor -// nil must always be accepted and would exhibit the same behavior as right now -// Non-nil behavior is implementation-defined: e.g., pack selection diff --git a/object/store/doc.go b/object/store/doc.go deleted file mode 100644 index 45acc47c..00000000 --- a/object/store/doc.go +++ /dev/null @@ -1,19 +0,0 @@ -// Package objectstore provides interfaces for object storage backends. -// -// Reading stores only respond to object-ID queries in terms of headers (type -// and size), raw bytes, and streaming payloads, but they do not parse commits, -// trees, blobs, or tags into typed values. Turning stored objects into typed -// objects is the job of [codeberg.org/lindenii/furgit/object/fetch]. -// -// This package does not define one unified writing interface. Backends have -// very different write models: writing one loose object is natural, while -// writing one object into a packfile backend is wasteful. Instead, we define -// distinct optional capabilities for object-wise writes, pack-wise writes, -// and compose them against quarantined writes. Where one logical quarantine -// supports multiple write shapes together, this package also defines a -// coordinated writer quarantine capability. -// -// Concrete implementations generally inherit the contract documented by the -// interfaces they satisfy. Implementation docs focus on additional guarantees -// and implementation-specific behavior. -package objectstore diff --git a/object/store/dual/doc.go b/object/store/dual/doc.go deleted file mode 100644 index 104120ec..00000000 --- a/object/store/dual/doc.go +++ /dev/null @@ -1,8 +0,0 @@ -// Package dual provides one logical object store backed by separate object-wise -// and pack-wise stores. -// -// Dual composes a store that handles individual object writes with a store that -// handles pack-wise writes, while exposing one mixed reader over both. -// Coordinated quarantine operations span both stores, but quarantine promotion -// is non-atomic. -package dual diff --git a/object/store/dual/dual.go b/object/store/dual/dual.go deleted file mode 100644 index 3072ae77..00000000 --- a/object/store/dual/dual.go +++ /dev/null @@ -1,33 +0,0 @@ -package dual - -import objectstore "codeberg.org/lindenii/furgit/object/store" - -type objectSide interface { - objectstore.Reader - objectstore.ObjectWriter - objectstore.ObjectQuarantiner -} - -type packSide interface { - objectstore.Reader - objectstore.PackWriter - objectstore.PackQuarantiner -} - -// Dual composes one object-wise store and one pack-wise store into one logical -// object store. -// -// Reads are served from the combined object reader of both stores. Individual -// object writes are routed to the object-wise store, and pack writes are routed -// to the pack-wise store. Coordinated quarantines go across both stores. -type Dual struct { - object objectSide - pack packSide - reader objectstore.Reader -} - -var ( - _ objectstore.Reader = (*Dual)(nil) - _ objectstore.Writer = (*Dual)(nil) - _ objectstore.Quarantiner = (*Dual)(nil) -) diff --git a/object/store/dual/dual_test.go b/object/store/dual/dual_test.go deleted file mode 100644 index 1d25a775..00000000 --- a/object/store/dual/dual_test.go +++ /dev/null @@ -1,266 +0,0 @@ -package dual_test - -import ( - "bytes" - "os" - "path/filepath" - "strings" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - objectid "codeberg.org/lindenii/furgit/object/id" - objectstore "codeberg.org/lindenii/furgit/object/store" - "codeberg.org/lindenii/furgit/object/store/dual" - "codeberg.org/lindenii/furgit/object/store/loose" - "codeberg.org/lindenii/furgit/object/store/packed" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -func fixturePath(t *testing.T, algo objectid.Algorithm, name string) string { - t.Helper() - - return filepath.Join("..", "packed", "internal", "ingest", "testdata", "fixtures", algo.String(), name) -} - -func fixtureBytes(t *testing.T, algo objectid.Algorithm, name string) []byte { - t.Helper() - - path := fixturePath(t, algo, name) - dir := filepath.Dir(path) - base := filepath.Base(path) - - root, err := os.OpenRoot(dir) - if err != nil { - t.Fatalf("open fixture root %q: %v", dir, err) - } - - defer func() { - err := root.Close() - if err != nil { - t.Fatalf("close fixture root %q: %v", dir, err) - } - }() - - data, err := root.ReadFile(base) - if err != nil { - t.Fatalf("read fixture %q: %v", base, err) - } - - return data -} - -func fixtureMetadata(t *testing.T, algo objectid.Algorithm) map[string]string { - t.Helper() - - data := fixtureBytes(t, algo, "METADATA.txt") - out := make(map[string]string) - - for line := range strings.SplitSeq(strings.TrimSpace(string(data)), "\n") { - line = strings.TrimSpace(line) - if line == "" { - continue - } - - key, value, ok := strings.Cut(line, "=") - if !ok { - t.Fatalf("invalid fixture metadata line %q", line) - } - - out[strings.TrimSpace(key)] = strings.TrimSpace(value) - } - - return out -} - -func fixtureOID(t *testing.T, algo objectid.Algorithm, key string) objectid.ObjectID { - t.Helper() - - meta := fixtureMetadata(t, algo) - - hex, ok := meta[key] - if !ok { - t.Fatalf("missing fixture metadata key %q", key) - } - - id, err := objectid.ParseHex(algo, hex) - if err != nil { - t.Fatalf("parse fixture metadata oid %q: %v", hex, err) - } - - return id -} - -func newDualStore(t *testing.T, repo *testgit.TestRepo, algo objectid.Algorithm) *dual.Dual { - t.Helper() - - objectsRoot := repo.OpenObjectsRoot(t) - - looseStore, err := loose.New(objectsRoot, algo) - if err != nil { - t.Fatalf("loose.New: %v", err) - } - - packRoot := repo.OpenPackRoot(t) - - packedStore, err := packed.New(packRoot, algo, packed.Options{WriteRev: true}) - if err != nil { - t.Fatalf("packed.New: %v", err) - } - - return dual.New(looseStore, packedStore) -} - -func TestDualReadsWritesAndQuarantine(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - head := fixtureOID(t, algo, "head") - packBytes := fixtureBytes(t, algo, "nonthin.pack") - - repo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - store := newDualStore(t, repo, algo) - - quarantiner, ok := any(store).(objectstore.Quarantiner) - if !ok { - t.Fatal("dual does not implement Quarantiner") - } - - quarantine, err := quarantiner.BeginQuarantine(objectstore.QuarantineOptions{}) - if err != nil { - t.Fatalf("BeginQuarantine: %v", err) - } - - err = quarantine.WritePack(bytes.NewReader(packBytes), objectstore.PackWriteOptions{RequireTrailingEOF: true}) - if err != nil { - t.Fatalf("quarantine.WritePack: %v", err) - } - - objectQ, ok := any(quarantine).(objectstore.ObjectQuarantine) - if !ok { - t.Fatal("pack quarantine does not also implement ObjectQuarantine") - } - - looseContent := []byte("dual quarantine loose object\n") - - looseID, err := objectQ.WriteBytesContent(objecttype.TypeBlob, looseContent) - if err != nil { - t.Fatalf("quarantine.WriteBytesContent: %v", err) - } - - ty, _, err := quarantine.ReadHeader(head) - if err != nil { - t.Fatalf("quarantine.ReadHeader(pack): %v", err) - } - - if ty != objecttype.TypeCommit { - t.Fatalf("quarantine.ReadHeader(pack) type = %v, want commit", ty) - } - - ty, got, err := quarantine.ReadBytesContent(looseID) - if err != nil { - t.Fatalf("quarantine.ReadBytesContent(loose): %v", err) - } - - if ty != objecttype.TypeBlob { - t.Fatalf("quarantine.ReadBytesContent(loose) type = %v, want blob", ty) - } - - if !bytes.Equal(got, looseContent) { - t.Fatal("quarantine.ReadBytesContent(loose) mismatch") - } - - _, _, err = store.ReadHeader(head) - if err == nil { - t.Fatal("store.ReadHeader unexpectedly saw quarantined pack object before promote") - } - - _, _, err = store.ReadBytesContent(looseID) - if err == nil { - t.Fatal("store.ReadBytesContent unexpectedly saw quarantined loose object before promote") - } - - err = quarantine.Promote() - if err != nil { - t.Fatalf("quarantine.Promote: %v", err) - } - - err = store.Refresh() - if err != nil { - t.Fatalf("store.Refresh: %v", err) - } - - ty, _, err = store.ReadHeader(head) - if err != nil { - t.Fatalf("store.ReadHeader(pack): %v", err) - } - - if ty != objecttype.TypeCommit { - t.Fatalf("store.ReadHeader(pack) type = %v, want commit", ty) - } - - ty, got, err = store.ReadBytesContent(looseID) - if err != nil { - t.Fatalf("store.ReadBytesContent(loose): %v", err) - } - - if ty != objecttype.TypeBlob { - t.Fatalf("store.ReadBytesContent(loose) type = %v, want blob", ty) - } - - if !bytes.Equal(got, looseContent) { - t.Fatal("store.ReadBytesContent(loose) mismatch") - } - }) -} - -func TestDualQuarantineDiscardDropsBothHalves(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - head := fixtureOID(t, algo, "head") - packBytes := fixtureBytes(t, algo, "nonthin.pack") - - repo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - store := newDualStore(t, repo, algo) - - quarantiner, ok := any(store).(objectstore.Quarantiner) - if !ok { - t.Fatal("expected objectstore.Quarantiner") - } - - quarantine, err := quarantiner.BeginQuarantine(objectstore.QuarantineOptions{}) - if err != nil { - t.Fatalf("BeginQuarantine: %v", err) - } - - err = quarantine.WritePack(bytes.NewReader(packBytes), objectstore.PackWriteOptions{RequireTrailingEOF: true}) - if err != nil { - t.Fatalf("quarantine.WritePack: %v", err) - } - - looseID, err := quarantine.WriteBytesContent(objecttype.TypeBlob, []byte("discarded dual object\n")) - if err != nil { - t.Fatalf("quarantine.WriteBytesContent: %v", err) - } - - err = quarantine.Discard() - if err != nil { - t.Fatalf("quarantine.Discard: %v", err) - } - - err = store.Refresh() - if err != nil { - t.Fatalf("store.Refresh: %v", err) - } - - _, _, err = store.ReadHeader(head) - if err == nil { - t.Fatal("store.ReadHeader unexpectedly saw discarded pack object") - } - - _, _, err = store.ReadBytesContent(looseID) - if err == nil { - t.Fatal("store.ReadBytesContent unexpectedly saw discarded loose object") - } - }) -} diff --git a/object/store/dual/new.go b/object/store/dual/new.go deleted file mode 100644 index ef38bc7a..00000000 --- a/object/store/dual/new.go +++ /dev/null @@ -1,29 +0,0 @@ -package dual - -import ( - objectstore "codeberg.org/lindenii/furgit/object/store" - objectmix "codeberg.org/lindenii/furgit/object/store/mix" -) - -// New creates one dual object store from borrowed object-wise and pack-wise -// stores. -// -// Labels: Deps-Borrowed, Life-Parent. -func New( - object interface { - objectstore.Reader - objectstore.ObjectWriter - objectstore.ObjectQuarantiner - }, - pack interface { - objectstore.Reader - objectstore.PackWriter - objectstore.PackQuarantiner - }, -) *Dual { - return &Dual{ - object: object, - pack: pack, - reader: objectmix.New(object, pack), - } -} diff --git a/object/store/dual/quarantine.go b/object/store/dual/quarantine.go deleted file mode 100644 index fb1048af..00000000 --- a/object/store/dual/quarantine.go +++ /dev/null @@ -1,114 +0,0 @@ -package dual - -import ( - "io" - - objectid "codeberg.org/lindenii/furgit/object/id" - objectstore "codeberg.org/lindenii/furgit/object/store" - objectmix "codeberg.org/lindenii/furgit/object/store/mix" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// quarantine is one coordinated dual quarantine over both stores. -type quarantine struct { - objectQ objectstore.ObjectQuarantine - packQ objectstore.PackQuarantine - reader objectstore.Reader -} - -var ( - _ objectstore.ObjectQuarantine = (*quarantine)(nil) - _ objectstore.PackQuarantine = (*quarantine)(nil) - _ objectstore.Quarantine = (*quarantine)(nil) -) - -func newQuarantine( - objectQ objectstore.ObjectQuarantine, - packQ objectstore.PackQuarantine, -) *quarantine { - return &quarantine{ - objectQ: objectQ, - packQ: packQ, - reader: objectmix.New(objectQ, packQ), - } -} - -// ReadBytesFull reads a full serialized object as "type size\0content" from -// either quarantined store. -func (quarantine *quarantine) ReadBytesFull(id objectid.ObjectID) ([]byte, error) { - return quarantine.reader.ReadBytesFull(id) -} - -// ReadBytesContent reads an object's type and content bytes from either -// quarantined store. -func (quarantine *quarantine) ReadBytesContent(id objectid.ObjectID) (objecttype.Type, []byte, error) { - return quarantine.reader.ReadBytesContent(id) -} - -// ReadReaderFull reads a full serialized object stream as -// "type size\0content" from either quarantined store. -func (quarantine *quarantine) ReadReaderFull(id objectid.ObjectID) (io.ReadCloser, error) { - return quarantine.reader.ReadReaderFull(id) -} - -// ReadReaderContent reads an object's type, declared content length, and -// content stream from either quarantined store. -func (quarantine *quarantine) ReadReaderContent(id objectid.ObjectID) (objecttype.Type, int64, io.ReadCloser, error) { - return quarantine.reader.ReadReaderContent(id) -} - -// ReadSize reads an object's declared content length from either quarantined -// store. -func (quarantine *quarantine) ReadSize(id objectid.ObjectID) (int64, error) { - return quarantine.reader.ReadSize(id) -} - -// ReadHeader reads an object's type and declared content length from either -// quarantined store. -func (quarantine *quarantine) ReadHeader(id objectid.ObjectID) (objecttype.Type, int64, error) { - return quarantine.reader.ReadHeader(id) -} - -// Refresh refreshes both quarantined stores and the combined quarantined reader. -func (quarantine *quarantine) Refresh() error { - err := quarantine.objectQ.Refresh() - if err != nil { - return err - } - - err = quarantine.packQ.Refresh() - if err != nil { - return err - } - - return quarantine.reader.Refresh() -} - -// WriteReaderContent writes one typed object content stream to the quarantined -// object-wise store. -func (quarantine *quarantine) WriteReaderContent(ty objecttype.Type, size int64, src io.Reader) (objectid.ObjectID, error) { - return quarantine.objectQ.WriteReaderContent(ty, size, src) -} - -// WriteReaderFull writes one full serialized object stream as -// "type size\0content" to the quarantined object-wise store. -func (quarantine *quarantine) WriteReaderFull(src io.Reader) (objectid.ObjectID, error) { - return quarantine.objectQ.WriteReaderFull(src) -} - -// WriteBytesContent writes one typed object content byte slice to the -// quarantined object-wise store. -func (quarantine *quarantine) WriteBytesContent(ty objecttype.Type, content []byte) (objectid.ObjectID, error) { - return quarantine.objectQ.WriteBytesContent(ty, content) -} - -// WriteBytesFull writes one full serialized object byte slice as -// "type size\0content" to the quarantined object-wise store. -func (quarantine *quarantine) WriteBytesFull(raw []byte) (objectid.ObjectID, error) { - return quarantine.objectQ.WriteBytesFull(raw) -} - -// WritePack ingests one pack stream into the quarantined pack-wise store. -func (quarantine *quarantine) WritePack(src io.Reader, opts objectstore.PackWriteOptions) error { - return quarantine.packQ.WritePack(src, opts) -} diff --git a/object/store/dual/quarantine_begin.go b/object/store/dual/quarantine_begin.go deleted file mode 100644 index 5c6bc934..00000000 --- a/object/store/dual/quarantine_begin.go +++ /dev/null @@ -1,22 +0,0 @@ -package dual - -import objectstore "codeberg.org/lindenii/furgit/object/store" - -// BeginQuarantine creates one coordinated dual quarantine spanning both stores. -// -// Labels: Deps-Borrowed, Life-Parent, Close-No. -func (dual *Dual) BeginQuarantine(opts objectstore.QuarantineOptions) (objectstore.Quarantine, error) { - objectQ, err := dual.object.BeginObjectQuarantine(opts.Object) - if err != nil { - return nil, err - } - - packQ, err := dual.pack.BeginPackQuarantine(opts.Pack) - if err != nil { - _ = objectQ.Discard() - - return nil, err - } - - return newQuarantine(objectQ, packQ), nil -} diff --git a/object/store/dual/quarantine_discard.go b/object/store/dual/quarantine_discard.go deleted file mode 100644 index 67f15d6c..00000000 --- a/object/store/dual/quarantine_discard.go +++ /dev/null @@ -1,11 +0,0 @@ -package dual - -// Discard abandons both quarantine halves and invalidates the receiver. -func (quarantine *quarantine) Discard() error { - err := quarantine.packQ.Discard() - if err != nil { - return err - } - - return quarantine.objectQ.Discard() -} diff --git a/object/store/dual/quarantine_promote.go b/object/store/dual/quarantine_promote.go deleted file mode 100644 index 4d0a45b8..00000000 --- a/object/store/dual/quarantine_promote.go +++ /dev/null @@ -1,13 +0,0 @@ -package dual - -// Promote publishes both quarantine halves and invalidates the receiver. -// -// Promotion is coordinated and ordered, but not atomic. -func (quarantine *quarantine) Promote() error { - err := quarantine.packQ.Promote() - if err != nil { - return err - } - - return quarantine.objectQ.Promote() -} diff --git a/object/store/dual/reader.go b/object/store/dual/reader.go deleted file mode 100644 index 7b499d5d..00000000 --- a/object/store/dual/reader.go +++ /dev/null @@ -1,57 +0,0 @@ -package dual - -import ( - "io" - - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// ReadBytesFull reads a full serialized object as "type size\0content" from -// either store. -func (dual *Dual) ReadBytesFull(id objectid.ObjectID) ([]byte, error) { - return dual.reader.ReadBytesFull(id) -} - -// ReadBytesContent reads an object's type and content bytes from either store. -func (dual *Dual) ReadBytesContent(id objectid.ObjectID) (objecttype.Type, []byte, error) { - return dual.reader.ReadBytesContent(id) -} - -// ReadReaderFull reads a full serialized object stream as "type size\0content" -// from either store. -func (dual *Dual) ReadReaderFull(id objectid.ObjectID) (io.ReadCloser, error) { - return dual.reader.ReadReaderFull(id) -} - -// ReadReaderContent reads an object's type, declared content length, and -// content stream from either store. -func (dual *Dual) ReadReaderContent(id objectid.ObjectID) (objecttype.Type, int64, io.ReadCloser, error) { - return dual.reader.ReadReaderContent(id) -} - -// ReadSize reads an object's declared content length from either store. -func (dual *Dual) ReadSize(id objectid.ObjectID) (int64, error) { - return dual.reader.ReadSize(id) -} - -// ReadHeader reads an object's type and declared content length from either -// store. -func (dual *Dual) ReadHeader(id objectid.ObjectID) (objecttype.Type, int64, error) { - return dual.reader.ReadHeader(id) -} - -// Refresh refreshes both underlying stores and the combined read view. -func (dual *Dual) Refresh() error { - err := dual.object.Refresh() - if err != nil { - return err - } - - err = dual.pack.Refresh() - if err != nil { - return err - } - - return dual.reader.Refresh() -} diff --git a/object/store/dual/writer_object.go b/object/store/dual/writer_object.go deleted file mode 100644 index 7aefe9ea..00000000 --- a/object/store/dual/writer_object.go +++ /dev/null @@ -1,32 +0,0 @@ -package dual - -import ( - "io" - - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// WriteReaderContent writes one typed object content stream to the object-wise -// store. -func (dual *Dual) WriteReaderContent(ty objecttype.Type, size int64, src io.Reader) (objectid.ObjectID, error) { - return dual.object.WriteReaderContent(ty, size, src) -} - -// WriteReaderFull writes one full serialized object stream as -// "type size\0content" to the object-wise store. -func (dual *Dual) WriteReaderFull(src io.Reader) (objectid.ObjectID, error) { - return dual.object.WriteReaderFull(src) -} - -// WriteBytesContent writes one typed object content byte slice to the -// object-wise store. -func (dual *Dual) WriteBytesContent(ty objecttype.Type, content []byte) (objectid.ObjectID, error) { - return dual.object.WriteBytesContent(ty, content) -} - -// WriteBytesFull writes one full serialized object byte slice as -// "type size\0content" to the object-wise store. -func (dual *Dual) WriteBytesFull(raw []byte) (objectid.ObjectID, error) { - return dual.object.WriteBytesFull(raw) -} diff --git a/object/store/dual/writer_pack.go b/object/store/dual/writer_pack.go deleted file mode 100644 index 5ac8648b..00000000 --- a/object/store/dual/writer_pack.go +++ /dev/null @@ -1,12 +0,0 @@ -package dual - -import ( - "io" - - objectstore "codeberg.org/lindenii/furgit/object/store" -) - -// WritePack ingests one pack stream into the pack-wise store. -func (dual *Dual) WritePack(src io.Reader, opts objectstore.PackWriteOptions) error { - return dual.pack.WritePack(src, opts) -} diff --git a/object/store/errors.go b/object/store/errors.go deleted file mode 100644 index 0e36b400..00000000 --- a/object/store/errors.go +++ /dev/null @@ -1,8 +0,0 @@ -package objectstore - -import "errors" - -// ErrObjectNotFound indicates that an object does not exist in a backend. -// This error must only be produced by object stores, when it has no -// specified object ID, but no other unexpected conditions were encountered. -var ErrObjectNotFound = errors.New("objectstore: object not found") diff --git a/object/store/loose/helpers_test.go b/object/store/loose/helpers_test.go deleted file mode 100644 index 97cec9d7..00000000 --- a/object/store/loose/helpers_test.go +++ /dev/null @@ -1,107 +0,0 @@ -package loose_test - -import ( - "io" - "os" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - objectheader "codeberg.org/lindenii/furgit/object/header" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/store/loose" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -func openLooseStore(t *testing.T, testRepo *testgit.TestRepo, algo objectid.Algorithm) *loose.Store { - t.Helper() - - root := testRepo.OpenObjectsRoot(t) - - store, err := loose.New(root, algo) - if err != nil { - t.Fatalf("loose.New: %v", err) - } - - return store -} - -func mustReadAllAndClose(t *testing.T, reader io.ReadCloser) []byte { - t.Helper() - - data, err := io.ReadAll(reader) - if err != nil { - _ = reader.Close() - - t.Fatalf("ReadAll: %v", err) - } - - err = reader.Close() - if err != nil { - t.Fatalf("Close: %v", err) - } - - return data -} - -func expectedRawObject(t *testing.T, testRepo *testgit.TestRepo, id objectid.ObjectID) (objecttype.Type, []byte, []byte) { - t.Helper() - - typeName := testRepo.Run(t, "cat-file", "-t", id.String()) - - ty, ok := objecttype.Parse(typeName) - if !ok { - t.Fatalf("ParseName(%q) failed", typeName) - } - - body := testRepo.CatFile(t, typeName, id) - - header, ok := objectheader.Encode(ty, int64(len(body))) - if !ok { - t.Fatalf("objectheader.Encode failed") - } - - raw := make([]byte, len(header)+len(body)) - copy(raw, header) - copy(raw[len(header):], body) - - return ty, body, raw -} - -func corruptLooseObjectTrailer(t *testing.T, testRepo *testgit.TestRepo, id objectid.ObjectID) { - t.Helper() - - root := testRepo.OpenObjectsRoot(t) - - hex := id.String() - relPath := hex[:2] + "/" + hex[2:] - - file, err := root.OpenFile(relPath, os.O_RDWR, 0) - if err != nil { - t.Fatalf("OpenFile(%q): %v", relPath, err) - } - - defer func() { _ = file.Close() }() - - info, err := file.Stat() - if err != nil { - t.Fatalf("Stat(%q): %v", relPath, err) - } - - if info.Size() == 0 { - t.Fatalf("corrupt trailer on empty file %q", relPath) - } - - last := make([]byte, 1) - - _, err = file.ReadAt(last, info.Size()-1) - if err != nil { - t.Fatalf("ReadAt(%q): %v", relPath, err) - } - - last[0] ^= 0xff - - _, err = file.WriteAt(last, info.Size()-1) - if err != nil { - t.Fatalf("WriteAt(%q): %v", relPath, err) - } -} diff --git a/object/store/loose/parse.go b/object/store/loose/parse.go deleted file mode 100644 index dfb420ba..00000000 --- a/object/store/loose/parse.go +++ /dev/null @@ -1,55 +0,0 @@ -package loose - -import ( - "bufio" - "errors" - "io" - "os" - - "codeberg.org/lindenii/furgit/internal/compress/zlib" - objectheader "codeberg.org/lindenii/furgit/object/header" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// decodeAll inflates the full loose object payload from file. -func decodeAll(file *os.File) ([]byte, error) { - zr, err := zlib.NewReader(file) - if err != nil { - return nil, err - } - - defer func() { _ = zr.Close() }() - - return io.ReadAll(zr) -} - -// parseRaw parses a loose object payload in "type size\0content" format. -func parseRaw(raw []byte) (objecttype.Type, []byte, error) { - ty, size, headerLen, ok := objectheader.Parse(raw) - if !ok { - return objecttype.TypeInvalid, nil, errors.New("objectstore/loose: malformed object header") - } - - content := raw[headerLen:] - if int64(len(content)) != size { - return objecttype.TypeInvalid, nil, errors.New("objectstore/loose: object header size/content mismatch") - } - - return ty, content, nil -} - -// readHeader reads and parses a loose object header from br, and returns -// the raw header bytes including the trailing NUL. -func readHeader(br *bufio.Reader) ([]byte, objecttype.Type, int64, error) { - header, err := br.ReadSlice(0) - if err != nil { - return nil, objecttype.TypeInvalid, 0, err - } - - ty, size, _, ok := objectheader.Parse(header) - if !ok { - return nil, objecttype.TypeInvalid, 0, errors.New("objectstore/loose: malformed object header") - } - - return header, ty, size, nil -} diff --git a/object/store/loose/paths.go b/object/store/loose/paths.go deleted file mode 100644 index 0593cc0d..00000000 --- a/object/store/loose/paths.go +++ /dev/null @@ -1,43 +0,0 @@ -package loose - -import ( - "errors" - "fmt" - "io/fs" - "os" - "path/filepath" - - objectid "codeberg.org/lindenii/furgit/object/id" - objectstore "codeberg.org/lindenii/furgit/object/store" -) - -// objectPath returns the loose object path for id relative to the objects root. -func (store *Store) objectPath(id objectid.ObjectID) (string, error) { - if id.Algorithm() != store.algo { - return "", fmt.Errorf("objectstore/loose: object id algorithm mismatch: got %s want %s", id.Algorithm(), store.algo) - } - - hex := id.String() - - return filepath.Join(hex[:2], hex[2:]), nil -} - -// openObject opens the loose object file for id. -// Missing files cause objectstore.ErrObjectNotFound. -func (store *Store) openObject(id objectid.ObjectID) (*os.File, error) { - relPath, err := store.objectPath(id) - if err != nil { - return nil, err - } - - file, err := store.root.Open(relPath) - if err != nil { - if errors.Is(err, fs.ErrNotExist) { - return nil, objectstore.ErrObjectNotFound - } - - return nil, err - } - - return file, nil -} diff --git a/object/store/loose/quarantine.go b/object/store/loose/quarantine.go deleted file mode 100644 index 52fb8120..00000000 --- a/object/store/loose/quarantine.go +++ /dev/null @@ -1,19 +0,0 @@ -package loose - -import ( - "os" - - objectstore "codeberg.org/lindenii/furgit/object/store" -) - -var _ objectstore.ObjectQuarantiner = (*Store)(nil) - -type objectQuarantine struct { - *Store - - parent *Store - tempName string - tempRoot *os.Root -} - -var _ objectstore.ObjectQuarantine = (*objectQuarantine)(nil) diff --git a/object/store/loose/quarantine_begin.go b/object/store/loose/quarantine_begin.go deleted file mode 100644 index dd27f968..00000000 --- a/object/store/loose/quarantine_begin.go +++ /dev/null @@ -1,63 +0,0 @@ -package loose - -import ( - "crypto/rand" - "errors" - "fmt" - "io/fs" - "os" - - objectstore "codeberg.org/lindenii/furgit/object/store" -) - -// BeginObjectQuarantine creates one quarantined loose store rooted privately -// beneath the destination loose root. -// -// Labels: Deps-Borrowed, Life-Parent, Close-No. -func (store *Store) BeginObjectQuarantine(_ objectstore.ObjectQuarantineOptions) (objectstore.ObjectQuarantine, error) { - tempName, tempRoot, err := createLooseQuarantineRoot(store.root) - if err != nil { - return nil, err - } - - quarantineStore, err := New(tempRoot, store.algo) - if err != nil { - _ = tempRoot.Close() - _ = store.root.RemoveAll(tempName) - - return nil, err - } - - return &objectQuarantine{ - Store: quarantineStore, - parent: store, - tempName: tempName, - tempRoot: tempRoot, - }, nil -} - -func createLooseQuarantineRoot(parent *os.Root) (string, *os.Root, error) { - for range 32 { - name := "tmp_looseq_" + rand.Text() - - err := parent.Mkdir(name, 0o700) - if err == nil { - root, err := parent.OpenRoot(name) - if err == nil { - return name, root, nil - } - - _ = parent.RemoveAll(name) - - return "", nil, err - } - - if errors.Is(err, fs.ErrExist) { - continue - } - - return "", nil, err - } - - return "", nil, fmt.Errorf("objectstore/loose: unable to create quarantine directory") -} diff --git a/object/store/loose/quarantine_discard.go b/object/store/loose/quarantine_discard.go deleted file mode 100644 index 3e783d0e..00000000 --- a/object/store/loose/quarantine_discard.go +++ /dev/null @@ -1,18 +0,0 @@ -package loose - -// Discard removes the quarantine and invalidates the receiver. -func (quarantine *objectQuarantine) Discard() error { - closeErr := quarantine.Close() - tempRootErr := quarantine.tempRoot.Close() - removeErr := quarantine.parent.root.RemoveAll(quarantine.tempName) - - if closeErr != nil { - return closeErr - } - - if tempRootErr != nil { - return tempRootErr - } - - return removeErr -} diff --git a/object/store/loose/quarantine_promote.go b/object/store/loose/quarantine_promote.go deleted file mode 100644 index 66bb41df..00000000 --- a/object/store/loose/quarantine_promote.go +++ /dev/null @@ -1,116 +0,0 @@ -package loose - -import ( - "errors" - "fmt" - "io/fs" - "os" - "path/filepath" -) - -// Promote publishes all quarantined loose objects into the parent loose store -// and invalidates the receiver. -func (quarantine *objectQuarantine) Promote() error { - closeErr := quarantine.Close() - promoteErr := promoteLooseQuarantine(quarantine.parent, quarantine.tempName, quarantine.tempRoot) - tempRootErr := quarantine.tempRoot.Close() - removeErr := quarantine.parent.root.RemoveAll(quarantine.tempName) - - if closeErr != nil { - return closeErr - } - - if promoteErr != nil { - return promoteErr - } - - if tempRootErr != nil { - return tempRootErr - } - - return removeErr -} - -func promoteLooseQuarantine(parent *Store, tempName string, tempRoot *os.Root) error { - entries, err := fs.ReadDir(tempRoot.FS(), ".") - if err != nil && !errors.Is(err, fs.ErrNotExist) { - return err - } - - for _, entry := range entries { - if !entry.IsDir() { - return fmt.Errorf("objectstore/loose: quarantine contains unexpected file %q", entry.Name()) - } - - if len(entry.Name()) != 2 || !isHexString(entry.Name()) { - return fmt.Errorf("objectstore/loose: quarantine contains invalid shard %q", entry.Name()) - } - - err := promoteLooseQuarantineShard(parent, tempName, tempRoot, entry.Name()) - if err != nil { - return err - } - } - - return nil -} - -func promoteLooseQuarantineShard(parent *Store, tempName string, tempRoot *os.Root, shard string) error { - entries, err := fs.ReadDir(tempRoot.FS(), shard) - if err != nil { - return err - } - - err = parent.root.MkdirAll(shard, 0o755) - if err != nil { - return err - } - - wantNameLen := parent.algo.HexLen() - 2 - - for _, entry := range entries { - if entry.IsDir() { - return fmt.Errorf("objectstore/loose: quarantine shard %q contains unexpected directory %q", shard, entry.Name()) - } - - if len(entry.Name()) != wantNameLen || !isHexString(entry.Name()) { - return fmt.Errorf("objectstore/loose: quarantine shard %q contains invalid object path %q", shard, entry.Name()) - } - - err := promoteLooseQuarantineObject(parent.root, filepath.Join(tempName, shard, entry.Name()), filepath.Join(shard, entry.Name())) - if err != nil { - return err - } - } - - return nil -} - -func promoteLooseQuarantineObject(root *os.Root, src, dst string) error { - err := root.Link(src, dst) - if err == nil { - _ = root.Remove(src) - - return nil - } - - if errors.Is(err, fs.ErrExist) { - _ = root.Remove(src) - - return nil - } - - return fmt.Errorf("objectstore/loose: promote quarantine %q -> %q: %w", src, dst, err) -} - -func isHexString(s string) bool { - for _, ch := range s { - if ('0' <= ch && ch <= '9') || ('a' <= ch && ch <= 'f') || ('A' <= ch && ch <= 'F') { - continue - } - - return false - } - - return true -} diff --git a/object/store/loose/quarantine_test.go b/object/store/loose/quarantine_test.go deleted file mode 100644 index 4fd1b8f9..00000000 --- a/object/store/loose/quarantine_test.go +++ /dev/null @@ -1,119 +0,0 @@ -package loose_test - -import ( - "bytes" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - objectid "codeberg.org/lindenii/furgit/object/id" - objectstore "codeberg.org/lindenii/furgit/object/store" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -func TestLooseQuarantinePromotePublishesWrittenObjects(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - store := openLooseStore(t, testRepo, algo) - - quarantiner, ok := any(store).(objectstore.ObjectQuarantiner) - if !ok { - t.Fatal("loose store does not implement ObjectQuarantiner") - } - - quarantine, err := quarantiner.BeginObjectQuarantine(objectstore.ObjectQuarantineOptions{}) - if err != nil { - t.Fatalf("BeginObjectQuarantine: %v", err) - } - - content := []byte("quarantined loose object\n") - - id, err := quarantine.WriteBytesContent(objecttype.TypeBlob, content) - if err != nil { - t.Fatalf("quarantine.WriteBytesContent: %v", err) - } - - ty, got, err := quarantine.ReadBytesContent(id) - if err != nil { - t.Fatalf("quarantine.ReadBytesContent: %v", err) - } - - if ty != objecttype.TypeBlob { - t.Fatalf("quarantine.ReadBytesContent type = %v, want %v", ty, objecttype.TypeBlob) - } - - if !bytes.Equal(got, content) { - t.Fatal("quarantine.ReadBytesContent mismatch") - } - - _, _, err = store.ReadBytesContent(id) - if err == nil { - t.Fatal("store.ReadBytesContent unexpectedly saw quarantined object before promote") - } - - err = quarantine.Promote() - if err != nil { - t.Fatalf("quarantine.Promote: %v", err) - } - - err = store.Refresh() - if err != nil { - t.Fatalf("store.Refresh: %v", err) - } - - ty, got, err = store.ReadBytesContent(id) - if err != nil { - t.Fatalf("store.ReadBytesContent after promote: %v", err) - } - - if ty != objecttype.TypeBlob { - t.Fatalf("store.ReadBytesContent type = %v, want %v", ty, objecttype.TypeBlob) - } - - if !bytes.Equal(got, content) { - t.Fatal("store.ReadBytesContent mismatch") - } - }) -} - -func TestLooseQuarantineDiscardDropsWrittenObjects(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - store := openLooseStore(t, testRepo, algo) - - quarantiner, ok := any(store).(objectstore.ObjectQuarantiner) - if !ok { - t.Fatal("expected objectstore.ObjectQuarantiner") - } - - quarantine, err := quarantiner.BeginObjectQuarantine(objectstore.ObjectQuarantineOptions{}) - if err != nil { - t.Fatalf("BeginObjectQuarantine: %v", err) - } - - content := []byte("discarded loose object\n") - - id, err := quarantine.WriteBytesContent(objecttype.TypeBlob, content) - if err != nil { - t.Fatalf("quarantine.WriteBytesContent: %v", err) - } - - err = quarantine.Discard() - if err != nil { - t.Fatalf("quarantine.Discard: %v", err) - } - - err = store.Refresh() - if err != nil { - t.Fatalf("store.Refresh: %v", err) - } - - _, _, err = store.ReadBytesContent(id) - if err == nil { - t.Fatal("store.ReadBytesContent unexpectedly saw discarded object") - } - }) -} diff --git a/object/store/loose/read_bytes.go b/object/store/loose/read_bytes.go deleted file mode 100644 index 5ed3b82b..00000000 --- a/object/store/loose/read_bytes.go +++ /dev/null @@ -1,55 +0,0 @@ -package loose - -import ( - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// readBytesParsed reads, inflates, and parses a loose object in one pass. -// It returns the full raw payload and its parsed type and content. -func (store *Store) readBytesParsed(id objectid.ObjectID) ([]byte, objecttype.Type, []byte, error) { - file, err := store.openObject(id) - if err != nil { - return nil, objecttype.TypeInvalid, nil, err - } - - defer func() { _ = file.Close() }() - - raw, err := decodeAll(file) - if err != nil { - return nil, objecttype.TypeInvalid, nil, err - } - - ty, content, err := parseRaw(raw) - if err != nil { - return nil, objecttype.TypeInvalid, nil, err - } - - return raw, ty, content, nil -} - -// ReadBytesFull reads a full serialized object as "type size\0content". -// -// It inflates and parses the full loose object, including verifying the zlib -// Adler-32 trailer. -func (store *Store) ReadBytesFull(id objectid.ObjectID) ([]byte, error) { - raw, _, _, err := store.readBytesParsed(id) - if err != nil { - return nil, err - } - - return raw, nil -} - -// ReadBytesContent reads an object's type and content bytes. -// -// Like ReadBytesFull, it inflates and parses the full loose object, including -// verifying the zlib Adler-32 trailer. -func (store *Store) ReadBytesContent(id objectid.ObjectID) (objecttype.Type, []byte, error) { - _, ty, content, err := store.readBytesParsed(id) - if err != nil { - return objecttype.TypeInvalid, nil, err - } - - return ty, content, nil -} diff --git a/object/store/loose/read_header.go b/object/store/loose/read_header.go deleted file mode 100644 index 37bf40de..00000000 --- a/object/store/loose/read_header.go +++ /dev/null @@ -1,37 +0,0 @@ -package loose - -import ( - "bufio" - - "codeberg.org/lindenii/furgit/internal/compress/zlib" - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// ReadHeader reads an object's type and declared content length. -// -// It parses only enough of the zlib-decoded object to recover the object -// header. It does not verify that the remaining object content is readable and -// does not verify the zlib Adler-32 trailer. -func (store *Store) ReadHeader(id objectid.ObjectID) (objecttype.Type, int64, error) { - file, err := store.openObject(id) - if err != nil { - return objecttype.TypeInvalid, 0, err - } - - defer func() { _ = file.Close() }() - - zr, err := zlib.NewReader(file) - if err != nil { - return objecttype.TypeInvalid, 0, err - } - - defer func() { _ = zr.Close() }() - - _, ty, size, err := readHeader(bufio.NewReader(zr)) - if err != nil { - return objecttype.TypeInvalid, 0, err - } - - return ty, size, nil -} diff --git a/object/store/loose/read_reader.go b/object/store/loose/read_reader.go deleted file mode 100644 index c8c8d736..00000000 --- a/object/store/loose/read_reader.go +++ /dev/null @@ -1,114 +0,0 @@ -package loose - -import ( - "bufio" - "bytes" - "errors" - "io" - "os" - - "codeberg.org/lindenii/furgit/internal/compress/zlib" - "codeberg.org/lindenii/furgit/internal/iolimit" - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -type objectReader struct { - // reader is the stream exposed by Read. - reader io.Reader - // file is the underlying loose object file and is closed by Close. - file *os.File - // zr is the zlib decoder and is closed by Close. - zr io.ReadCloser -} - -func (reader *objectReader) Read(dst []byte) (int, error) { - return reader.reader.Read(dst) -} - -func (reader *objectReader) Close() error { - errZlib := reader.zr.Close() - errFile := reader.file.Close() - - return errors.Join(errZlib, errFile) -} - -// openInflated opens and zlib-decodes a loose object file. -// The caller owns both returned closers and must close them. -func (store *Store) openInflated(id objectid.ObjectID) (*os.File, io.ReadCloser, error) { - file, err := store.openObject(id) - if err != nil { - return nil, nil, err - } - - zr, err := zlib.NewReader(file) - if err != nil { - _ = file.Close() - - return nil, nil, err - } - - return file, zr, nil -} - -// ReadReaderFull reads a full serialized object stream as "type size\0content". -// -// Close releases resources only. It does not drain unread data for additional -// validation. In particular, malformed trailing compressed data, trailing bytes -// past the declared object size, and the zlib Adler-32 trailer may go -// unverified unless the caller reads to io.EOF. -func (store *Store) ReadReaderFull(id objectid.ObjectID) (io.ReadCloser, error) { - file, zr, err := store.openInflated(id) - if err != nil { - return nil, err - } - - br := bufio.NewReader(zr) - - header, _, size, err := readHeader(br) - if err != nil { - _ = zr.Close() - _ = file.Close() - - return nil, err - } - - return &objectReader{ - reader: io.MultiReader( - bytes.NewReader(header), - iolimit.ExpectLengthReader(br, size), - ), - file: file, - zr: zr, - }, nil -} - -// ReadReaderContent reads an object's type, declared content length, and -// content stream. -// -// Close releases resources only. It does not drain unread data for additional -// validation. In particular, malformed trailing compressed data, trailing bytes -// past the declared object size, and the zlib Adler-32 trailer may go -// unverified unless the caller reads to io.EOF. -func (store *Store) ReadReaderContent(id objectid.ObjectID) (objecttype.Type, int64, io.ReadCloser, error) { - file, zr, err := store.openInflated(id) - if err != nil { - return objecttype.TypeInvalid, 0, nil, err - } - - br := bufio.NewReader(zr) - - _, ty, size, err := readHeader(br) - if err != nil { - _ = zr.Close() - _ = file.Close() - - return objecttype.TypeInvalid, 0, nil, err - } - - return ty, size, &objectReader{ - reader: iolimit.ExpectLengthReader(br, size), - file: file, - zr: zr, - }, nil -} diff --git a/object/store/loose/read_size.go b/object/store/loose/read_size.go deleted file mode 100644 index 2ececc49..00000000 --- a/object/store/loose/read_size.go +++ /dev/null @@ -1,13 +0,0 @@ -package loose - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// ReadSize reads an object's declared content length. -// -// Like ReadHeader, it parses only enough of the zlib-decoded object to recover -// the header and does not verify the zlib Adler-32 trailer. -func (store *Store) ReadSize(id objectid.ObjectID) (int64, error) { - _, size, err := store.ReadHeader(id) - - return size, err -} diff --git a/object/store/loose/read_test.go b/object/store/loose/read_test.go deleted file mode 100644 index fcb4fe17..00000000 --- a/object/store/loose/read_test.go +++ /dev/null @@ -1,212 +0,0 @@ -package loose_test - -import ( - "bytes" - "errors" - "os" - "strings" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - objectid "codeberg.org/lindenii/furgit/object/id" - objectstore "codeberg.org/lindenii/furgit/object/store" - "codeberg.org/lindenii/furgit/object/store/loose" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -func TestLooseStoreReadAgainstGit(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - blobID := testRepo.HashObject(t, "blob", []byte("blob body\n")) - _, treeID, commitID := testRepo.MakeCommit(t, "subject\n\nbody") - tagID := testRepo.TagAnnotated(t, "v1", commitID, "tag message") - - store := openLooseStore(t, testRepo, algo) - - tests := []struct { - name string - id objectid.ObjectID - }{ - {name: "blob", id: blobID}, - {name: "tree", id: treeID}, - {name: "commit", id: commitID}, - {name: "tag", id: tagID}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - wantType, wantBody, wantRaw := expectedRawObject(t, testRepo, tt.id) - - gotRaw, err := store.ReadBytesFull(tt.id) - if err != nil { - t.Fatalf("ReadBytesFull: %v", err) - } - - if !bytes.Equal(gotRaw, wantRaw) { - t.Fatalf("ReadBytesFull mismatch") - } - - gotType, gotBody, err := store.ReadBytesContent(tt.id) - if err != nil { - t.Fatalf("ReadBytesContent: %v", err) - } - - if gotType != wantType { - t.Fatalf("ReadBytesContent type = %v, want %v", gotType, wantType) - } - - if !bytes.Equal(gotBody, wantBody) { - t.Fatalf("ReadBytesContent body mismatch") - } - - headType, headSize, err := store.ReadHeader(tt.id) - if err != nil { - t.Fatalf("ReadHeader: %v", err) - } - - if headType != wantType { - t.Fatalf("ReadHeader type = %v, want %v", headType, wantType) - } - - if headSize != int64(len(wantBody)) { - t.Fatalf("ReadHeader size = %d, want %d", headSize, len(wantBody)) - } - - fullReader, err := store.ReadReaderFull(tt.id) - if err != nil { - t.Fatalf("ReadReaderFull: %v", err) - } - - got := mustReadAllAndClose(t, fullReader) - if !bytes.Equal(got, wantRaw) { - t.Fatalf("ReadReaderFull stream mismatch") - } - - contentType, contentSize, contentReader, err := store.ReadReaderContent(tt.id) - if err != nil { - t.Fatalf("ReadReaderContent: %v", err) - } - - if contentType != wantType { - t.Fatalf("ReadReaderContent type = %v, want %v", contentType, wantType) - } - - if contentSize != int64(len(wantBody)) { - t.Fatalf("ReadReaderContent size = %d, want %d", contentSize, len(wantBody)) - } - - got = mustReadAllAndClose(t, contentReader) - if !bytes.Equal(got, wantBody) { - t.Fatalf("ReadReaderContent stream mismatch") - } - }) - } - }) -} - -func TestLooseStoreErrors(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - store := openLooseStore(t, testRepo, algo) - - notFoundID, err := objectid.ParseHex(algo, strings.Repeat("0", algo.HexLen())) - if err != nil { - t.Fatalf("ParseHex(notFoundID): %v", err) - } - - _, err = store.ReadBytesFull(notFoundID) - if !errors.Is(err, objectstore.ErrObjectNotFound) { - t.Fatalf("ReadBytesFull not-found error = %v", err) - } - - _, _, err = store.ReadBytesContent(notFoundID) - if !errors.Is(err, objectstore.ErrObjectNotFound) { - t.Fatalf("ReadBytesContent not-found error = %v", err) - } - - _, err = store.ReadReaderFull(notFoundID) - if !errors.Is(err, objectstore.ErrObjectNotFound) { - t.Fatalf("ReadReaderFull not-found error = %v", err) - } - - _, _, _, err = store.ReadReaderContent(notFoundID) - if !errors.Is(err, objectstore.ErrObjectNotFound) { - t.Fatalf("ReadReaderContent not-found error = %v", err) - } - - _, _, err = store.ReadHeader(notFoundID) - if !errors.Is(err, objectstore.ErrObjectNotFound) { - t.Fatalf("ReadHeader not-found error = %v", err) - } - - var otherAlgo objectid.Algorithm - if algo == objectid.AlgorithmSHA1 { - otherAlgo = objectid.AlgorithmSHA256 - } else { - otherAlgo = objectid.AlgorithmSHA1 - } - - otherID, err := objectid.ParseHex(otherAlgo, strings.Repeat("1", otherAlgo.HexLen())) - if err != nil { - t.Fatalf("ParseHex(otherID): %v", err) - } - - _, err = store.ReadBytesFull(otherID) - if err == nil || !strings.Contains(err.Error(), "algorithm mismatch") { - t.Fatalf("ReadBytesFull algorithm-mismatch error = %v", err) - } - }) -} - -func TestLooseStoreNewValidation(t *testing.T) { - t.Parallel() - - root, err := os.OpenRoot(t.TempDir()) - if err != nil { - t.Fatalf("OpenRoot: %v", err) - } - - defer func() { _ = root.Close() }() - - _, err = loose.New(root, objectid.AlgorithmUnknown) - if err == nil { - t.Fatalf("loose.New(root, unknown) expected error") - } -} - -func TestLooseStoreReadHeaderDoesNotVerifyAdler32(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - store := openLooseStore(t, testRepo, algo) - - content := []byte("header-only-check\n") - - id, err := store.WriteBytesContent(objecttype.TypeBlob, content) - if err != nil { - t.Fatalf("WriteBytesContent: %v", err) - } - - corruptLooseObjectTrailer(t, testRepo, id) - - ty, size, err := store.ReadHeader(id) - if err != nil { - t.Fatalf("ReadHeader: %v", err) - } - - if ty != objecttype.TypeBlob { - t.Fatalf("ReadHeader type = %v, want %v", ty, objecttype.TypeBlob) - } - - if size != int64(len(content)) { - t.Fatalf("ReadHeader size = %d, want %d", size, len(content)) - } - - _, err = store.ReadBytesFull(id) - if err == nil { - t.Fatalf("ReadBytesFull on corrupted trailer succeeded") - } - }) -} diff --git a/object/store/loose/refresh.go b/object/store/loose/refresh.go deleted file mode 100644 index b720ebc6..00000000 --- a/object/store/loose/refresh.go +++ /dev/null @@ -1,6 +0,0 @@ -package loose - -// Refresh is a no-op for loose object stores. -func (store *Store) Refresh() error { - return nil -} diff --git a/object/store/loose/store.go b/object/store/loose/store.go deleted file mode 100644 index ea466284..00000000 --- a/object/store/loose/store.go +++ /dev/null @@ -1,43 +0,0 @@ -// Package loose provides a loose object backend (objects/XX/YYYYY..). -package loose - -import ( - "os" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// Store reads loose Git objects from an objects directory root. -// -// Loose objects are zlib streams whose trailer uses Adler-32. Which reads -// consume enough of the stream to reach and verify that trailer is documented -// on the individual methods. -// -// Labels: Close-Caller. -type Store struct { - // root is the objects directory capability used for all object file access. - // Object files are opened by relative paths like "/". - // Store borrows this root. - root *os.Root - // algo is the expected object ID algorithm for lookups. - algo objectid.Algorithm -} - -// New creates a loose-object store rooted at an objects directory for algo. -// -// Labels: Deps-Borrowed, Life-Parent. -func New(root *os.Root, algo objectid.Algorithm) (*Store, error) { - if algo.Size() == 0 { - return nil, objectid.ErrInvalidAlgorithm - } - - return &Store{ - root: root, - algo: algo, - }, nil -} - -// Close releases resources associated with the backend. -// -// Labels: MT-Unsafe. -func (store *Store) Close() error { return nil } diff --git a/object/store/loose/write_bytes.go b/object/store/loose/write_bytes.go deleted file mode 100644 index ffc65117..00000000 --- a/object/store/loose/write_bytes.go +++ /dev/null @@ -1,18 +0,0 @@ -package loose - -import ( - "bytes" - - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// WriteBytesFull writes a full serialized object as "type size\0content". -func (store *Store) WriteBytesFull(raw []byte) (objectid.ObjectID, error) { - return store.WriteReaderFull(bytes.NewReader(raw)) -} - -// WriteBytesContent writes typed content bytes as a loose object. -func (store *Store) WriteBytesContent(ty objecttype.Type, content []byte) (objectid.ObjectID, error) { - return store.WriteReaderContent(ty, int64(len(content)), bytes.NewReader(content)) -} diff --git a/object/store/loose/write_reader.go b/object/store/loose/write_reader.go deleted file mode 100644 index f686f279..00000000 --- a/object/store/loose/write_reader.go +++ /dev/null @@ -1,81 +0,0 @@ -package loose - -import ( - "fmt" - "io" - - objectheader "codeberg.org/lindenii/furgit/object/header" - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// WriteReaderContent writes one loose object from typed content bytes read from src. -// src must provide exactly size bytes. -// size is required because loose object headers are "type size\0content", so the -// header must be emitted before streaming content without buffering. -func (store *Store) WriteReaderContent(ty objecttype.Type, size int64, src io.Reader) (objectid.ObjectID, error) { - if size < 0 { - return objectid.ObjectID{}, fmt.Errorf("objectstore/loose: negative content size: %d", size) - } - - header, ok := objectheader.Encode(ty, size) - if !ok { - return objectid.ObjectID{}, fmt.Errorf("objectstore/loose: failed to encode object header for type %v", ty) - } - - writer, err := store.newStreamWriter(false) - if err != nil { - return objectid.ObjectID{}, err - } - - writer.headerDone = true - writer.expectedContentLeft = size - - err = writer.writeRawChunk(header) - if err != nil { - _ = writer.Close() - _ = store.root.Remove(writer.tmpRelPath) - - return objectid.ObjectID{}, err - } - - return writeReaderIntoStreamWriter(writer, src) -} - -// WriteReaderFull writes one loose object from raw bytes "type size\0content" -// read from src. -func (store *Store) WriteReaderFull(src io.Reader) (objectid.ObjectID, error) { - writer, err := store.newStreamWriter(true) - if err != nil { - return objectid.ObjectID{}, err - } - - return writeReaderIntoStreamWriter(writer, src) -} - -// writeReaderIntoStreamWriter copies src into writer and publishes the object. -func writeReaderIntoStreamWriter(writer *streamWriter, src io.Reader) (objectid.ObjectID, error) { - _, err := io.Copy(writer, src) - if err != nil { - _ = writer.Close() - _ = writer.store.root.Remove(writer.tmpRelPath) - - return objectid.ObjectID{}, err - } - - err = writer.Close() - if err != nil { - _ = writer.store.root.Remove(writer.tmpRelPath) - - return objectid.ObjectID{}, err - } - - id, err := writer.finalize() - if err != nil { - _ = writer.store.root.Remove(writer.tmpRelPath) - - return objectid.ObjectID{}, err - } - - return id, nil -} diff --git a/object/store/loose/write_temp_object_file.go b/object/store/loose/write_temp_object_file.go deleted file mode 100644 index 1a78db48..00000000 --- a/object/store/loose/write_temp_object_file.go +++ /dev/null @@ -1,30 +0,0 @@ -package loose - -import ( - "crypto/rand" - "errors" - "io/fs" - "os" - "path/filepath" -) - -// createTempObjectFile creates a unique temporary object file within dir. -// The returned path is relative to the objects root. -func (store *Store) createTempObjectFile(dir string) (string, *os.File, error) { - for range 16 { - relPath := filepath.Join(dir, tempObjectFilePrefix+rand.Text()) - - file, err := store.root.OpenFile(relPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o644) - if err == nil { - return relPath, file, nil - } - - if errors.Is(err, fs.ErrExist) { - continue - } - - return "", nil, err - } - - return "", nil, errors.New("objectstore/loose: failed to create temporary object file") -} diff --git a/object/store/loose/write_test.go b/object/store/loose/write_test.go deleted file mode 100644 index 30d8dbdb..00000000 --- a/object/store/loose/write_test.go +++ /dev/null @@ -1,137 +0,0 @@ -package loose_test - -import ( - "bytes" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - objectheader "codeberg.org/lindenii/furgit/object/header" - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -func TestLooseStoreWriteReaderContentAgainstGit(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - store := openLooseStore(t, testRepo, algo) - - content := []byte("written-by-content-reader\n") - expectedHex := testRepo.RunInput(t, content, "hash-object", "-t", "blob", "--stdin") - - expectedID, err := objectid.ParseHex(algo, expectedHex) - if err != nil { - t.Fatalf("ParseHex(expected): %v", err) - } - - writtenID, err := store.WriteReaderContent(objecttype.TypeBlob, int64(len(content)), bytes.NewReader(content)) - if err != nil { - t.Fatalf("WriteReaderContent: %v", err) - } - - if writtenID != expectedID { - t.Fatalf("WriteReaderContent id = %s, want %s", writtenID, expectedID) - } - - gotBody := testRepo.CatFile(t, "blob", writtenID) - if !bytes.Equal(gotBody, content) { - t.Fatalf("git cat-file body mismatch") - } - - // Writing the same object again should succeed and return the same ID. - writtenID2, err := store.WriteReaderContent(objecttype.TypeBlob, int64(len(content)), bytes.NewReader(content)) - if err != nil { - t.Fatalf("WriteReaderContent second: %v", err) - } - - if writtenID2 != expectedID { - t.Fatalf("WriteReaderContent second id = %s, want %s", writtenID2, expectedID) - } - }) -} - -func TestLooseStoreWriteReaderFullAgainstGit(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - store := openLooseStore(t, testRepo, algo) - - body := []byte("full-reader-body\n") - - header, ok := objectheader.Encode(objecttype.TypeBlob, int64(len(body))) - if !ok { - t.Fatalf("objectheader.Encode failed") - } - - raw := make([]byte, len(header)+len(body)) - copy(raw, header) - copy(raw[len(header):], body) - - wantID := algo.Sum(raw) - - gotID, err := store.WriteReaderFull(bytes.NewReader(raw)) - if err != nil { - t.Fatalf("WriteReaderFull: %v", err) - } - - if gotID != wantID { - t.Fatalf("WriteReaderFull id = %s, want %s", gotID, wantID) - } - - gotBody := testRepo.CatFile(t, "blob", gotID) - if !bytes.Equal(gotBody, body) { - t.Fatalf("git cat-file body mismatch") - } - }) -} - -func TestLooseStoreReaderValidationErrors(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - t.Run("content overflow", func(t *testing.T) { - t.Parallel() - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - store := openLooseStore(t, testRepo, algo) - - _, err := store.WriteReaderContent(objecttype.TypeBlob, 1, bytes.NewReader([]byte("hello"))) - if err == nil { - t.Fatalf("expected error after overflow") - } - }) - - t.Run("content short", func(t *testing.T) { - t.Parallel() - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - store := openLooseStore(t, testRepo, algo) - - _, err := store.WriteReaderContent(objecttype.TypeBlob, 5, bytes.NewReader([]byte("x"))) - if err == nil { - t.Fatalf("expected error for short content") - } - }) - - t.Run("full malformed header", func(t *testing.T) { - t.Parallel() - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - store := openLooseStore(t, testRepo, algo) - - _, err := store.WriteReaderFull(bytes.NewReader([]byte("not-a-header"))) - if err == nil { - t.Fatalf("expected error for malformed header") - } - }) - - t.Run("full size mismatch", func(t *testing.T) { - t.Parallel() - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - store := openLooseStore(t, testRepo, algo) - - raw := []byte("blob 1\x00hello") - - _, err := store.WriteReaderFull(bytes.NewReader(raw)) - if err == nil { - t.Fatalf("expected error after mismatch") - } - }) - }) -} diff --git a/object/store/loose/write_writer.go b/object/store/loose/write_writer.go deleted file mode 100644 index 0d6b5b80..00000000 --- a/object/store/loose/write_writer.go +++ /dev/null @@ -1,94 +0,0 @@ -package loose - -import ( - "errors" - "hash" - "os" - - "codeberg.org/lindenii/furgit/internal/compress/zlib" -) - -const tempObjectFilePrefix = "tmp_obj_" - -// streamWriter incrementally hashes and deflates an object into a temp file. -// Finalize validates size accounting and atomically renames the temp file. -type streamWriter struct { - // store owns path and root operations used by this write session. - store *Store - // file is the temporary destination file under objects/. - file *os.File - // zw compresses raw object bytes into file. - zw *zlib.Writer - // hash receives the same raw bytes used to compute the resulting object ID. - hash hash.Hash - - // tmpRelPath is the relative path of file under the objects root. - tmpRelPath string - - // fullMode selects full-object input ("type size\0content") as opposed to content-only input. - fullMode bool - - // headerBuf accumulates header bytes while fullMode parses up to the first NUL. - headerBuf []byte - // headerDone reports whether the full-object header has been parsed. - headerDone bool - // expectedContentLeft tracks remaining declared content bytes. - expectedContentLeft int64 - - closed bool - finalized bool -} - -// newStreamWriter creates a stream writer with a temp file rooted in objects/. -func (store *Store) newStreamWriter(fullMode bool) (*streamWriter, error) { - hashFn, err := store.algo.New() - if err != nil { - return nil, err - } - - tmpRelPath, file, err := store.createTempObjectFile(".") - if err != nil { - return nil, err - } - - return &streamWriter{ - store: store, - file: file, - zw: zlib.NewWriter(file), - hash: hashFn, - tmpRelPath: tmpRelPath, - fullMode: fullMode, - headerBuf: make([]byte, 0, 64), - }, nil -} - -// Write validates and writes raw bytes into the stream. -// In full mode, it parses and enforces the streamed header-declared content size. -func (writer *streamWriter) Write(src []byte) (int, error) { - if writer.finalized { - return 0, errors.New("objectstore/loose: write after finalize") - } - - if writer.closed { - return 0, errors.New("objectstore/loose: write after close") - } - - if writer.fullMode { - err := writer.acceptFull(src) - if err != nil { - return 0, err - } - } else { - err := writer.acceptContent(int64(len(src))) - if err != nil { - return 0, err - } - } - - err := writer.writeRawChunk(src) - if err != nil { - return 0, err - } - - return len(src), nil -} diff --git a/object/store/loose/write_writer_accept.go b/object/store/loose/write_writer_accept.go deleted file mode 100644 index bf55966a..00000000 --- a/object/store/loose/write_writer_accept.go +++ /dev/null @@ -1,61 +0,0 @@ -package loose - -import ( - "bytes" - "errors" - - objectheader "codeberg.org/lindenii/furgit/object/header" -) - -// acceptFull validates and accounts raw full-object input. -func (writer *streamWriter) acceptFull(src []byte) error { - if !writer.headerDone { - nul := bytes.IndexByte(src, 0) - if nul >= 0 { - headerChunkLen := nul + 1 - writer.headerBuf = append(writer.headerBuf, src[:headerChunkLen]...) - - _, size, _, ok := objectheader.Parse(writer.headerBuf) - if !ok { - return errors.New("objectstore/loose: malformed object header") - } - - writer.headerDone = true - writer.expectedContentLeft = size - - return writer.acceptContent(int64(len(src) - headerChunkLen)) - } - - writer.headerBuf = append(writer.headerBuf, src...) - - return nil - } - - return writer.acceptContent(int64(len(src))) -} - -// acceptContent validates and accounts content byte counts. -func (writer *streamWriter) acceptContent(n int64) error { - if n > writer.expectedContentLeft { - return errors.New("objectstore/loose: object content exceeds declared size") - } - - writer.expectedContentLeft -= n - - return nil -} - -// writeRawChunk forwards raw bytes to the hash and deflate pipeline. -func (writer *streamWriter) writeRawChunk(src []byte) error { - _, err := writer.hash.Write(src) - if err != nil { - return err - } - - _, err = writer.zw.Write(src) - if err != nil { - return err - } - - return nil -} diff --git a/object/store/loose/write_writer_finalize.go b/object/store/loose/write_writer_finalize.go deleted file mode 100644 index 71e275db..00000000 --- a/object/store/loose/write_writer_finalize.go +++ /dev/null @@ -1,89 +0,0 @@ -package loose - -import ( - "errors" - "io/fs" - "path/filepath" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// Close flushes and closes the underlying zlib stream and temp file. -func (writer *streamWriter) Close() error { - errZlib := writer.zw.Close() - errSync := writer.file.Sync() - errFile := writer.file.Close() - - writer.closed = true - writer.file = nil - - return errors.Join(errZlib, errSync, errFile) -} - -// finalize validates write completeness and atomically publishes the object. -// Publication is no-clobber: it links tmpRelPath to the object path and treats -// existing destination objects as success. -func (writer *streamWriter) finalize() (objectid.ObjectID, error) { - writer.finalized = true - - var zero objectid.ObjectID - - if !writer.closed { - err := writer.Close() - if err != nil { - return zero, err - } - } - - if writer.fullMode && !writer.headerDone { - return zero, errors.New("objectstore/loose: missing full object header") - } - - if writer.expectedContentLeft != 0 { - return zero, errors.New("objectstore/loose: object content shorter than declared size") - } - - idBytes := writer.hash.Sum(nil) - - id, err := objectid.FromBytes(writer.store.algo, idBytes) - if err != nil { - return zero, err - } - - relPath, err := writer.store.objectPath(id) - if err != nil { - return zero, err - } - - dir := filepath.Dir(relPath) - - err = writer.store.root.MkdirAll(dir, 0o755) - if err != nil { - return zero, err - } - - cleanup := true - - defer func() { - if cleanup { - _ = writer.store.root.Remove(writer.tmpRelPath) - } - }() - - err = writer.store.root.Link(writer.tmpRelPath, relPath) - if err != nil { - if errors.Is(err, fs.ErrExist) { - cleanup = false - _ = writer.store.root.Remove(writer.tmpRelPath) - - return id, nil - } - - return zero, err - } - - cleanup = false - _ = writer.store.root.Remove(writer.tmpRelPath) - - return id, nil -} diff --git a/object/store/memory/algorithm.go b/object/store/memory/algorithm.go deleted file mode 100644 index bf7f3a82..00000000 --- a/object/store/memory/algorithm.go +++ /dev/null @@ -1,8 +0,0 @@ -package memory - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// Algorithm returns the object ID algorithm used by the store. -func (store *Store) Algorithm() objectid.Algorithm { - return store.algo -} diff --git a/object/store/memory/doc.go b/object/store/memory/doc.go deleted file mode 100644 index cb40d466..00000000 --- a/object/store/memory/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package memory provides one in-memory object store. -package memory diff --git a/object/store/memory/object.go b/object/store/memory/object.go deleted file mode 100644 index a85175c7..00000000 --- a/object/store/memory/object.go +++ /dev/null @@ -1,9 +0,0 @@ -package memory - -import objecttype "codeberg.org/lindenii/furgit/object/type" - -// storedObject is one in-memory object entry. -type storedObject struct { - ty objecttype.Type - content []byte -} diff --git a/object/store/memory/read_bytes.go b/object/store/memory/read_bytes.go deleted file mode 100644 index 48d3694a..00000000 --- a/object/store/memory/read_bytes.go +++ /dev/null @@ -1,37 +0,0 @@ -package memory - -import ( - objectheader "codeberg.org/lindenii/furgit/object/header" - objectid "codeberg.org/lindenii/furgit/object/id" - objectstore "codeberg.org/lindenii/furgit/object/store" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// ReadBytesFull reads one full object, including the object header. -func (store *Store) ReadBytesFull(id objectid.ObjectID) ([]byte, error) { - obj, ok := store.objects[id] - if !ok { - return nil, objectstore.ErrObjectNotFound - } - - header, ok := objectheader.Encode(obj.ty, int64(len(obj.content))) - if !ok { - panic("failed to encode object header") - } - - raw := make([]byte, len(header)+len(obj.content)) - copy(raw, header) - copy(raw[len(header):], obj.content) - - return raw, nil -} - -// ReadBytesContent reads one object body. -func (store *Store) ReadBytesContent(id objectid.ObjectID) (objecttype.Type, []byte, error) { - obj, ok := store.objects[id] - if !ok { - return objecttype.TypeInvalid, nil, objectstore.ErrObjectNotFound - } - - return obj.ty, append([]byte(nil), obj.content...), nil -} diff --git a/object/store/memory/read_header.go b/object/store/memory/read_header.go deleted file mode 100644 index da3acd1c..00000000 --- a/object/store/memory/read_header.go +++ /dev/null @@ -1,17 +0,0 @@ -package memory - -import ( - objectid "codeberg.org/lindenii/furgit/object/id" - objectstore "codeberg.org/lindenii/furgit/object/store" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// ReadHeader reads one object header. -func (store *Store) ReadHeader(id objectid.ObjectID) (objecttype.Type, int64, error) { - obj, ok := store.objects[id] - if !ok { - return objecttype.TypeInvalid, 0, objectstore.ErrObjectNotFound - } - - return obj.ty, int64(len(obj.content)), nil -} diff --git a/object/store/memory/read_reader.go b/object/store/memory/read_reader.go deleted file mode 100644 index 425c3034..00000000 --- a/object/store/memory/read_reader.go +++ /dev/null @@ -1,29 +0,0 @@ -package memory - -import ( - "bytes" - "io" - - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// ReadReaderFull reads one full object through a reader. -func (store *Store) ReadReaderFull(id objectid.ObjectID) (io.ReadCloser, error) { - raw, err := store.ReadBytesFull(id) - if err != nil { - return nil, err - } - - return io.NopCloser(bytes.NewReader(raw)), nil -} - -// ReadReaderContent reads one object body through a reader. -func (store *Store) ReadReaderContent(id objectid.ObjectID) (objecttype.Type, int64, io.ReadCloser, error) { - ty, content, err := store.ReadBytesContent(id) - if err != nil { - return objecttype.TypeInvalid, 0, nil, err - } - - return ty, int64(len(content)), io.NopCloser(bytes.NewReader(content)), nil -} diff --git a/object/store/memory/read_size.go b/object/store/memory/read_size.go deleted file mode 100644 index 7045bd61..00000000 --- a/object/store/memory/read_size.go +++ /dev/null @@ -1,13 +0,0 @@ -package memory - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// ReadSize reads one object size. -func (store *Store) ReadSize(id objectid.ObjectID) (int64, error) { - _, size, err := store.ReadHeader(id) - if err != nil { - return 0, err - } - - return size, nil -} diff --git a/object/store/memory/refresh.go b/object/store/memory/refresh.go deleted file mode 100644 index 1e18eef3..00000000 --- a/object/store/memory/refresh.go +++ /dev/null @@ -1,6 +0,0 @@ -package memory - -// Refresh is a no-op for in-memory object stores. -func (store *Store) Refresh() error { - return nil -} diff --git a/object/store/memory/store.go b/object/store/memory/store.go deleted file mode 100644 index ff66da50..00000000 --- a/object/store/memory/store.go +++ /dev/null @@ -1,28 +0,0 @@ -package memory - -import ( - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// Store is one in-memory object store. -// -// Labels: Close-Caller. -type Store struct { - algo objectid.Algorithm - objects map[objectid.ObjectID]storedObject -} - -// New builds one empty in-memory store for one object format. -func New(algo objectid.Algorithm) *Store { - return &Store{ - algo: algo, - objects: make(map[objectid.ObjectID]storedObject), - } -} - -// Close closes the in-memory store. -// -// Labels: MT-Unsafe. -func (store *Store) Close() error { - return nil -} diff --git a/object/store/memory/write_bytes.go b/object/store/memory/write_bytes.go deleted file mode 100644 index 241169d9..00000000 --- a/object/store/memory/write_bytes.go +++ /dev/null @@ -1,35 +0,0 @@ -package memory - -import ( - "bytes" - - objectheader "codeberg.org/lindenii/furgit/object/header" - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// WriteBytesContent writes one typed object content byte slice. -func (store *Store) WriteBytesContent(ty objecttype.Type, content []byte) (objectid.ObjectID, error) { - id := store.algo.Sum(buildRawObject(ty, content)) - store.objects[id] = storedObject{ty: ty, content: append([]byte(nil), content...)} - - return id, nil -} - -// WriteBytesFull writes one full serialized object byte slice as "type size\0content". -func (store *Store) WriteBytesFull(raw []byte) (objectid.ObjectID, error) { - return store.WriteReaderFull(bytes.NewReader(raw)) -} - -func buildRawObject(ty objecttype.Type, body []byte) []byte { - header, ok := objectheader.Encode(ty, int64(len(body))) - if !ok { - panic("failed to encode object header") - } - - raw := make([]byte, len(header)+len(body)) - copy(raw, header) - copy(raw[len(header):], body) - - return raw -} diff --git a/object/store/memory/write_reader.go b/object/store/memory/write_reader.go deleted file mode 100644 index 0fa6a13f..00000000 --- a/object/store/memory/write_reader.go +++ /dev/null @@ -1,55 +0,0 @@ -package memory - -import ( - "errors" - "fmt" - "io" - - objectheader "codeberg.org/lindenii/furgit/object/header" - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// WriteReaderContent writes one typed object content stream. -func (store *Store) WriteReaderContent(ty objecttype.Type, size int64, src io.Reader) (objectid.ObjectID, error) { - if size < 0 { - return objectid.ObjectID{}, fmt.Errorf("objectstore/memory: negative content size: %d", size) - } - - content, err := io.ReadAll(io.LimitReader(src, size+1)) - if err != nil { - return objectid.ObjectID{}, err - } - - switch { - case int64(len(content)) > size: - return objectid.ObjectID{}, errors.New("objectstore/memory: object content longer than declared size") - case int64(len(content)) < size: - return objectid.ObjectID{}, errors.New("objectstore/memory: object content shorter than declared size") - } - - return store.WriteBytesContent(ty, content) -} - -// WriteReaderFull writes one full serialized object stream as "type size\0content". -func (store *Store) WriteReaderFull(src io.Reader) (objectid.ObjectID, error) { - raw, err := io.ReadAll(src) - if err != nil { - return objectid.ObjectID{}, err - } - - ty, size, headerLen, ok := objectheader.Parse(raw) - if !ok { - return objectid.ObjectID{}, errors.New("objectstore/memory: malformed object header") - } - - content := raw[headerLen:] - if int64(len(content)) != size { - return objectid.ObjectID{}, errors.New("objectstore/memory: object header size/content mismatch") - } - - id := store.algo.Sum(raw) - store.objects[id] = storedObject{ty: ty, content: append([]byte(nil), content...)} - - return id, nil -} diff --git a/object/store/memory/write_test.go b/object/store/memory/write_test.go deleted file mode 100644 index 9f38a14b..00000000 --- a/object/store/memory/write_test.go +++ /dev/null @@ -1,192 +0,0 @@ -package memory_test - -import ( - "bytes" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - objectheader "codeberg.org/lindenii/furgit/object/header" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/store/memory" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -func TestStoreWriteReaderContent(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - store := memory.New(algo) - content := []byte("memory-content\n") - - gotID, err := store.WriteReaderContent(objecttype.TypeBlob, int64(len(content)), bytes.NewReader(content)) - if err != nil { - t.Fatalf("WriteReaderContent: %v", err) - } - - wantID := algo.Sum(buildRawObject(t, objecttype.TypeBlob, content)) - if gotID != wantID { - t.Fatalf("WriteReaderContent id = %s, want %s", gotID, wantID) - } - - gotType, gotContent, err := store.ReadBytesContent(gotID) - if err != nil { - t.Fatalf("ReadBytesContent: %v", err) - } - - if gotType != objecttype.TypeBlob { - t.Fatalf("ReadBytesContent type = %v, want %v", gotType, objecttype.TypeBlob) - } - - if !bytes.Equal(gotContent, content) { - t.Fatalf("ReadBytesContent content mismatch") - } - }) -} - -func TestStoreWriteReaderFull(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - store := memory.New(algo) - content := []byte("memory-full\n") - raw := buildRawObject(t, objecttype.TypeBlob, content) - - gotID, err := store.WriteReaderFull(bytes.NewReader(raw)) - if err != nil { - t.Fatalf("WriteReaderFull: %v", err) - } - - wantID := algo.Sum(raw) - if gotID != wantID { - t.Fatalf("WriteReaderFull id = %s, want %s", gotID, wantID) - } - - gotRaw, err := store.ReadBytesFull(gotID) - if err != nil { - t.Fatalf("ReadBytesFull: %v", err) - } - - if !bytes.Equal(gotRaw, raw) { - t.Fatalf("ReadBytesFull mismatch") - } - }) -} - -func TestStoreWriteBytes(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - store := memory.New(algo) - content := []byte("memory-bytes\n") - - gotID, err := store.WriteBytesContent(objecttype.TypeBlob, content) - if err != nil { - t.Fatalf("WriteBytesContent: %v", err) - } - - wantID := algo.Sum(buildRawObject(t, objecttype.TypeBlob, content)) - if gotID != wantID { - t.Fatalf("WriteBytesContent id = %s, want %s", gotID, wantID) - } - - raw := buildRawObject(t, objecttype.TypeBlob, content) - - gotID2, err := store.WriteBytesFull(raw) - if err != nil { - t.Fatalf("WriteBytesFull: %v", err) - } - - if gotID2 != wantID { - t.Fatalf("WriteBytesFull id = %s, want %s", gotID2, wantID) - } - }) -} - -func TestStoreWriteReaderValidationErrors(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - t.Run("content overflow", func(t *testing.T) { - t.Parallel() - - store := memory.New(algo) - - _, err := store.WriteReaderContent(objecttype.TypeBlob, 1, bytes.NewReader([]byte("hello"))) - if err == nil { - t.Fatalf("expected error after overflow") - } - }) - - t.Run("content short", func(t *testing.T) { - t.Parallel() - - store := memory.New(algo) - - _, err := store.WriteReaderContent(objecttype.TypeBlob, 5, bytes.NewReader([]byte("x"))) - if err == nil { - t.Fatalf("expected error for short content") - } - }) - - t.Run("full malformed header", func(t *testing.T) { - t.Parallel() - - store := memory.New(algo) - - _, err := store.WriteReaderFull(bytes.NewReader([]byte("not-a-header"))) - if err == nil { - t.Fatalf("expected error for malformed header") - } - }) - - t.Run("full size mismatch", func(t *testing.T) { - t.Parallel() - - store := memory.New(algo) - - _, err := store.WriteReaderFull(bytes.NewReader([]byte("blob 1\x00hello"))) - if err == nil { - t.Fatalf("expected error after mismatch") - } - }) - - t.Run("bytes malformed header", func(t *testing.T) { - t.Parallel() - - store := memory.New(algo) - - _, err := store.WriteBytesFull([]byte("not-a-header")) - if err == nil { - t.Fatalf("expected error for malformed byte header") - } - }) - }) -} - -func TestBuildRawObjectMatchesObjectHeaderEncode(t *testing.T) { - t.Parallel() - - content := []byte("body") - raw := buildRawObject(t, objecttype.TypeBlob, content) - - header, ok := objectheader.Encode(objecttype.TypeBlob, int64(len(content))) - if !ok { - t.Fatalf("objectheader.Encode failed") - } - - want := append(append([]byte(nil), header...), content...) - if !bytes.Equal(raw, want) { - t.Fatalf("buildRawObject mismatch") - } -} - -func buildRawObject(tb testing.TB, ty objecttype.Type, body []byte) []byte { //nolint:unparam - tb.Helper() - - header, ok := objectheader.Encode(ty, int64(len(body))) - if !ok { - tb.Fatalf("objectheader.Encode(%v, %d) failed", ty, len(body)) - } - - raw := make([]byte, len(header)+len(body)) - copy(raw, header) - copy(raw[len(header):], body) - - return raw -} diff --git a/object/store/mix/bytes.go b/object/store/mix/bytes.go deleted file mode 100644 index 5b62ff06..00000000 --- a/object/store/mix/bytes.go +++ /dev/null @@ -1,51 +0,0 @@ -package mix - -import ( - "errors" - "fmt" - - objectid "codeberg.org/lindenii/furgit/object/id" - objectstore "codeberg.org/lindenii/furgit/object/store" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// ReadBytesFull reads a full serialized object from one backend that has it. -func (mix *Mix) ReadBytesFull(id objectid.ObjectID) ([]byte, error) { - for i, backend := 0, mix.firstBackend(); backend != nil; i, backend = i+1, mix.nextBackend(backend) { - full, err := backend.ReadBytesFull(id) - if err == nil { - mix.touchBackend(backend) - - return full, nil - } - - if errors.Is(err, objectstore.ErrObjectNotFound) { - continue - } - - return nil, fmt.Errorf("objectstore: backend %d read bytes full: %w", i, err) - } - - return nil, objectstore.ErrObjectNotFound -} - -// ReadBytesContent reads an object's type and content bytes from one backend -// that has it. -func (mix *Mix) ReadBytesContent(id objectid.ObjectID) (objecttype.Type, []byte, error) { - for i, backend := 0, mix.firstBackend(); backend != nil; i, backend = i+1, mix.nextBackend(backend) { - ty, content, err := backend.ReadBytesContent(id) - if err == nil { - mix.touchBackend(backend) - - return ty, content, nil - } - - if errors.Is(err, objectstore.ErrObjectNotFound) { - continue - } - - return objecttype.TypeInvalid, nil, fmt.Errorf("objectstore: backend %d read bytes content: %w", i, err) - } - - return objecttype.TypeInvalid, nil, objectstore.ErrObjectNotFound -} diff --git a/object/store/mix/header.go b/object/store/mix/header.go deleted file mode 100644 index d57375ec..00000000 --- a/object/store/mix/header.go +++ /dev/null @@ -1,30 +0,0 @@ -package mix - -import ( - "errors" - "fmt" - - objectid "codeberg.org/lindenii/furgit/object/id" - objectstore "codeberg.org/lindenii/furgit/object/store" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// ReadHeader reads object header data from one backend that has it. -func (mix *Mix) ReadHeader(id objectid.ObjectID) (objecttype.Type, int64, error) { - for i, backend := 0, mix.firstBackend(); backend != nil; i, backend = i+1, mix.nextBackend(backend) { - ty, size, err := backend.ReadHeader(id) - if err == nil { - mix.touchBackend(backend) - - return ty, size, nil - } - - if errors.Is(err, objectstore.ErrObjectNotFound) { - continue - } - - return objecttype.TypeInvalid, 0, fmt.Errorf("objectstore: backend %d read header: %w", i, err) - } - - return objecttype.TypeInvalid, 0, objectstore.ErrObjectNotFound -} diff --git a/object/store/mix/mix.go b/object/store/mix/mix.go deleted file mode 100644 index 65ed97c8..00000000 --- a/object/store/mix/mix.go +++ /dev/null @@ -1,20 +0,0 @@ -// Package mix provides an adaptive wrapper over multiple object storage -// backends. -package mix - -import ( - "sync" - - objectstore "codeberg.org/lindenii/furgit/object/store" -) - -// Mix queries multiple object databases with an MRU backend preference. -// -// Labels: Close-Caller. -type Mix struct { - mu sync.RWMutex - - backendHead *backendNode - backendTail *backendNode - backendNodeByStore map[objectstore.Reader]*backendNode -} diff --git a/object/store/mix/mru.go b/object/store/mix/mru.go deleted file mode 100644 index b48f1448..00000000 --- a/object/store/mix/mru.go +++ /dev/null @@ -1,74 +0,0 @@ -package mix - -import objectstore "codeberg.org/lindenii/furgit/object/store" - -type backendNode struct { - backend objectstore.Reader - prev *backendNode - next *backendNode -} - -//nolint:ireturn -func (mix *Mix) firstBackend() objectstore.Reader { - mix.mu.RLock() - defer mix.mu.RUnlock() - - if mix.backendHead == nil { - return nil - } - - return mix.backendHead.backend -} - -//nolint:ireturn -func (mix *Mix) nextBackend(current objectstore.Reader) objectstore.Reader { - mix.mu.RLock() - defer mix.mu.RUnlock() - - node := mix.backendNodeByStore[current] - if node == nil || node.next == nil { - return nil - } - - return node.next.backend -} - -func (mix *Mix) touchBackend(backend objectstore.Reader) { - if backend == nil { - return - } - - if !mix.mu.TryLock() { - return - } - defer mix.mu.Unlock() - - node := mix.backendNodeByStore[backend] - if node == nil || node == mix.backendHead { - return - } - - if node.prev != nil { - node.prev.next = node.next - } - - if node.next != nil { - node.next.prev = node.prev - } - - if mix.backendTail == node { - mix.backendTail = node.prev - } - - node.prev = nil - - node.next = mix.backendHead - if mix.backendHead != nil { - mix.backendHead.prev = node - } - - mix.backendHead = node - if mix.backendTail == nil { - mix.backendTail = node - } -} diff --git a/object/store/mix/new.go b/object/store/mix/new.go deleted file mode 100644 index abc6c8ee..00000000 --- a/object/store/mix/new.go +++ /dev/null @@ -1,40 +0,0 @@ -package mix - -import objectstore "codeberg.org/lindenii/furgit/object/store" - -// New creates a Mix from backends. -// -// The provided backends must be non-nil and distinct. -// -// Labels: Deps-Borrowed, Life-Parent. -func New(backends ...objectstore.Reader) *Mix { - nodeByStore := make(map[objectstore.Reader]*backendNode, len(backends)) - - var ( - head *backendNode - tail *backendNode - ) - - for _, backend := range backends { - node := &backendNode{ - backend: backend, - prev: tail, - } - if tail != nil { - tail.next = node - } - - if head == nil { - head = node - } - - tail = node - nodeByStore[backend] = node - } - - return &Mix{ - backendHead: head, - backendTail: tail, - backendNodeByStore: nodeByStore, - } -} diff --git a/object/store/mix/reader.go b/object/store/mix/reader.go deleted file mode 100644 index 8d515c50..00000000 --- a/object/store/mix/reader.go +++ /dev/null @@ -1,53 +0,0 @@ -package mix - -import ( - "errors" - "fmt" - "io" - - objectid "codeberg.org/lindenii/furgit/object/id" - objectstore "codeberg.org/lindenii/furgit/object/store" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// ReadReaderFull reads a full serialized object stream from one backend that -// has it. -func (mix *Mix) ReadReaderFull(id objectid.ObjectID) (io.ReadCloser, error) { - for i, backend := 0, mix.firstBackend(); backend != nil; i, backend = i+1, mix.nextBackend(backend) { - reader, err := backend.ReadReaderFull(id) - if err == nil { - mix.touchBackend(backend) - - return reader, nil - } - - if errors.Is(err, objectstore.ErrObjectNotFound) { - continue - } - - return nil, fmt.Errorf("objectstore: backend %d read reader full: %w", i, err) - } - - return nil, objectstore.ErrObjectNotFound -} - -// ReadReaderContent reads an object's type, declared content length, and -// content stream from one backend that has it. -func (mix *Mix) ReadReaderContent(id objectid.ObjectID) (objecttype.Type, int64, io.ReadCloser, error) { - for i, backend := 0, mix.firstBackend(); backend != nil; i, backend = i+1, mix.nextBackend(backend) { - ty, size, reader, err := backend.ReadReaderContent(id) - if err == nil { - mix.touchBackend(backend) - - return ty, size, reader, nil - } - - if errors.Is(err, objectstore.ErrObjectNotFound) { - continue - } - - return objecttype.TypeInvalid, 0, nil, fmt.Errorf("objectstore: backend %d read reader content: %w", i, err) - } - - return objecttype.TypeInvalid, 0, nil, objectstore.ErrObjectNotFound -} diff --git a/object/store/mix/refresh.go b/object/store/mix/refresh.go deleted file mode 100644 index bbae6efe..00000000 --- a/object/store/mix/refresh.go +++ /dev/null @@ -1,30 +0,0 @@ -package mix - -import ( - "errors" - - objectstore "codeberg.org/lindenii/furgit/object/store" -) - -// Refresh forwards refresh calls to refresh-capable backends. -func (mix *Mix) Refresh() error { - mix.mu.RLock() - - backends := make([]objectstore.Reader, 0, len(mix.backendNodeByStore)) - for node := mix.backendHead; node != nil; node = node.next { - backends = append(backends, node.backend) - } - - mix.mu.RUnlock() - - var errs []error - - for _, backend := range backends { - err := backend.Refresh() - if err != nil { - errs = append(errs, err) - } - } - - return errors.Join(errs...) -} diff --git a/object/store/mix/size.go b/object/store/mix/size.go deleted file mode 100644 index 4feb142e..00000000 --- a/object/store/mix/size.go +++ /dev/null @@ -1,29 +0,0 @@ -package mix - -import ( - "errors" - "fmt" - - objectid "codeberg.org/lindenii/furgit/object/id" - objectstore "codeberg.org/lindenii/furgit/object/store" -) - -// ReadSize reads object content length from one backend that has it. -func (mix *Mix) ReadSize(id objectid.ObjectID) (int64, error) { - for i, backend := 0, mix.firstBackend(); backend != nil; i, backend = i+1, mix.nextBackend(backend) { - size, err := backend.ReadSize(id) - if err == nil { - mix.touchBackend(backend) - - return size, nil - } - - if errors.Is(err, objectstore.ErrObjectNotFound) { - continue - } - - return 0, fmt.Errorf("objectstore: backend %d read size: %w", i, err) - } - - return 0, objectstore.ErrObjectNotFound -} diff --git a/object/store/packed/doc.go b/object/store/packed/doc.go deleted file mode 100644 index 55189aa1..00000000 --- a/object/store/packed/doc.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package packed provides Git object reading from, and pack writing to, -// an objects/pack directory. -package packed diff --git a/object/store/packed/internal/doc.go b/object/store/packed/internal/doc.go deleted file mode 100644 index 05a9c2be..00000000 --- a/object/store/packed/internal/doc.go +++ /dev/null @@ -1,6 +0,0 @@ -// Package internal encapsulates packed store implementation details. -// -// We have separate internal subpackages for ingest vs read and such, -// because these operations are so different that they almost share -// no code. This makes things clearer. -package internal diff --git a/object/store/packed/internal/ingest/TODO b/object/store/packed/internal/ingest/TODO deleted file mode 100644 index bfb722c1..00000000 --- a/object/store/packed/internal/ingest/TODO +++ /dev/null @@ -1 +0,0 @@ -multi-threaded delta resolution and index computation? diff --git a/object/store/packed/internal/ingest/byteslice_reader.go b/object/store/packed/internal/ingest/byteslice_reader.go deleted file mode 100644 index a1570ef3..00000000 --- a/object/store/packed/internal/ingest/byteslice_reader.go +++ /dev/null @@ -1,21 +0,0 @@ -package ingest - -import "io" - -// byteSliceReader implements io.ByteReader on []byte. -type byteSliceReader struct { - data []byte - pos int -} - -// ReadByte reads one byte from receiver. -func (reader *byteSliceReader) ReadByte() (byte, error) { - if reader.pos >= len(reader.data) { - return 0, io.EOF - } - - b := reader.data[reader.pos] - reader.pos++ - - return b, nil -} diff --git a/object/store/packed/internal/ingest/cache.go b/object/store/packed/internal/ingest/cache.go deleted file mode 100644 index 9a15f55f..00000000 --- a/object/store/packed/internal/ingest/cache.go +++ /dev/null @@ -1,53 +0,0 @@ -package ingest - -import ( - "codeberg.org/lindenii/furgit/internal/lru" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// deltaBaseCacheKey identifies one resolved base by record index. -type deltaBaseCacheKey struct { - recordIdx int -} - -// deltaBaseCacheValue stores one resolved base object payload. -type deltaBaseCacheValue struct { - realType objecttype.Type - content []byte -} - -// deltaBaseCache is a bounded LRU for resolved base payloads. -type deltaBaseCache struct { - lru *lru.Cache[deltaBaseCacheKey, deltaBaseCacheValue] -} - -// newDeltaBaseCache creates one bounded base cache. -func newDeltaBaseCache(maxBytes int64) *deltaBaseCache { - return &deltaBaseCache{ - lru: lru.New( - maxBytes, - func(_ deltaBaseCacheKey, value deltaBaseCacheValue) int64 { - return int64(len(value.content)) - }, - nil, - ), - } -} - -// get returns one cache entry for recordIdx. -func (cache *deltaBaseCache) get(recordIdx int) (objecttype.Type, []byte, bool) { - value, ok := cache.lru.Get(deltaBaseCacheKey{recordIdx: recordIdx}) - if !ok { - return objecttype.TypeInvalid, nil, false - } - - return value.realType, value.content, true -} - -// add stores one cache entry for recordIdx. -func (cache *deltaBaseCache) add(recordIdx int, realType objecttype.Type, content []byte) { - cache.lru.Add(deltaBaseCacheKey{recordIdx: recordIdx}, deltaBaseCacheValue{ - realType: realType, - content: content, - }) -} diff --git a/object/store/packed/internal/ingest/counting_writer.go b/object/store/packed/internal/ingest/counting_writer.go deleted file mode 100644 index 051ad9d1..00000000 --- a/object/store/packed/internal/ingest/counting_writer.go +++ /dev/null @@ -1,17 +0,0 @@ -package ingest - -import "io" - -// countingWriter counts bytes written to dst. -type countingWriter struct { - dst io.Writer - n int -} - -// Write writes src to dst and tracks output byte count. -func (writer *countingWriter) Write(src []byte) (int, error) { - n, err := writer.dst.Write(src) - writer.n += n - - return n, err -} diff --git a/object/store/packed/internal/ingest/crc.go b/object/store/packed/internal/ingest/crc.go deleted file mode 100644 index f55af4ff..00000000 --- a/object/store/packed/internal/ingest/crc.go +++ /dev/null @@ -1,22 +0,0 @@ -package ingest - -import "fmt" - -// beginEntryCRC starts inline CRC accumulation for one packed entry. -func (scanner *streamScanner) beginEntryCRC() { - scanner.entryCRC = 0 - scanner.inEntryCRC = true -} - -// endEntryCRC finishes inline CRC accumulation for one packed entry. -func (scanner *streamScanner) endEntryCRC() (uint32, error) { - if !scanner.inEntryCRC { - return 0, fmt.Errorf("packfile/ingest: entry CRC not started") - } - - crc := scanner.entryCRC - scanner.entryCRC = 0 - scanner.inEntryCRC = false - - return crc, nil -} diff --git a/object/store/packed/internal/ingest/delta_header.go b/object/store/packed/internal/ingest/delta_header.go deleted file mode 100644 index 110cf83b..00000000 --- a/object/store/packed/internal/ingest/delta_header.go +++ /dev/null @@ -1,11 +0,0 @@ -package ingest - -import deltaapply "codeberg.org/lindenii/furgit/format/packfile/delta/apply" - -// finalizeStreamPackHash consumes trailer bytes and verifies stream integrity. -// readDeltaHeaderSizes reads source and destination sizes from one delta payload. -func readDeltaHeaderSizes(payload []byte) (int, int, error) { - reader := &byteSliceReader{data: payload} - - return deltaapply.ReadHeaderSizes(reader) -} diff --git a/object/store/packed/internal/ingest/distance.go b/object/store/packed/internal/ingest/distance.go deleted file mode 100644 index 9bc4d886..00000000 --- a/object/store/packed/internal/ingest/distance.go +++ /dev/null @@ -1,30 +0,0 @@ -package ingest - -import ( - "fmt" - "io" -) - -// readOfsDistanceFromStream reads one ofs-delta encoded distance. -func readOfsDistanceFromStream(reader io.ByteReader) (uint64, int, error) { - first, err := reader.ReadByte() - if err != nil { - return 0, 0, fmt.Errorf("read ofs distance first byte: %w", err) - } - - dist := uint64(first & 0x7f) - consumed := 1 - - b := first - for b&0x80 != 0 { - b, err = reader.ReadByte() - if err != nil { - return 0, 0, fmt.Errorf("read ofs distance continuation: %w", err) - } - - consumed++ - dist = ((dist + 1) << 7) + uint64(b&0x7f) - } - - return dist, consumed, nil -} diff --git a/object/store/packed/internal/ingest/doc.go b/object/store/packed/internal/ingest/doc.go deleted file mode 100644 index 074012de..00000000 --- a/object/store/packed/internal/ingest/doc.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package ingest implements streaming ingestion of one Git pack stream into a -// packed destination root, producing .pack/.idx and optionally .rev. -package ingest diff --git a/object/store/packed/internal/ingest/drain.go b/object/store/packed/internal/ingest/drain.go deleted file mode 100644 index 7179a823..00000000 --- a/object/store/packed/internal/ingest/drain.go +++ /dev/null @@ -1,67 +0,0 @@ -package ingest - -import ( - "fmt" - "io" - - "codeberg.org/lindenii/furgit/internal/compress/zlib" - objectheader "codeberg.org/lindenii/furgit/object/header" - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// drainEntryPayload inflates one entry payload from stream and returns -// (inflatedLength, oidForBaseEntry). -func drainEntryPayload(state *ingestState, record objectRecord) (int64, objectid.ObjectID, error) { - var zero objectid.ObjectID - - reader, err := zlib.NewReader(state.stream) - if err != nil { - return 0, zero, &MalformedPackEntryError{Offset: record.offset, Reason: fmt.Sprintf("open zlib stream: %v", err)} - } - - defer func() { _ = reader.Close() }() - - var total int64 - - if record.packedType.IsBaseObject() { - header, ok := objectheader.Encode(record.packedType, record.declaredSize) - if !ok { - return 0, zero, &MalformedPackEntryError{Offset: record.offset, Reason: "encode object header"} - } - - hashImpl, err := state.algo.New() - if err != nil { - return 0, zero, err - } - - _, _ = hashImpl.Write(header) - - n, err := io.Copy(hashImpl, reader) - if err != nil { - return 0, zero, &MalformedPackEntryError{Offset: record.offset, Reason: fmt.Sprintf("inflate base object: %v", err)} - } - - total = n - - oid, err := objectid.FromBytes(state.algo, hashImpl.Sum(nil)) - if err != nil { - return 0, zero, err - } - - return total, oid, nil - } - - if record.packedType == objecttype.TypeOfsDelta || record.packedType == objecttype.TypeRefDelta { - n, err := io.Copy(io.Discard, reader) - if err != nil { - return 0, zero, &MalformedPackEntryError{Offset: record.offset, Reason: fmt.Sprintf("inflate delta payload: %v", err)} - } - - total = n - - return total, zero, nil - } - - return 0, zero, &MalformedPackEntryError{Offset: record.offset, Reason: "unsupported payload type"} -} diff --git a/object/store/packed/internal/ingest/entry.go b/object/store/packed/internal/ingest/entry.go deleted file mode 100644 index 363e213c..00000000 --- a/object/store/packed/internal/ingest/entry.go +++ /dev/null @@ -1,91 +0,0 @@ -package ingest - -import ( - "fmt" - - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// scanOneEntry scans one pack entry from stream and appends one record. -func scanOneEntry(state *ingestState, startOffset uint64) (uint64, error) { - state.stream.beginEntryCRC() - - record, err := parseEntryPrefix(state, startOffset) - if err != nil { - return 0, err - } - - payloadStartConsumed := state.stream.consumed - - contentLen, oid, err := drainEntryPayload(state, record) - if err != nil { - return 0, err - } - - consumedInput := state.stream.consumed - payloadStartConsumed - - if contentLen != record.declaredSize { - return 0, &MalformedPackEntryError{ - Offset: startOffset, - Reason: fmt.Sprintf("inflated size mismatch got %d want %d", contentLen, record.declaredSize), - } - } - - endOffset := startOffset + uint64(record.headerLen) + consumedInput - if endOffset > state.stream.consumed { - return 0, &MalformedPackEntryError{ - Offset: startOffset, - Reason: fmt.Sprintf("entry end offset overflow got %d > stream %d", endOffset, state.stream.consumed), - } - } - - record.packedLen = endOffset - startOffset - - record.dataOffset = startOffset + uint64(record.headerLen) - if record.packedLen < uint64(record.headerLen) { - return 0, &MalformedPackEntryError{Offset: startOffset, Reason: "negative payload span"} - } - - crc, err := state.stream.endEntryCRC() - if err != nil { - return 0, err - } - - record.crc32 = crc - - if record.packedType.IsBaseObject() { - record.objectID = oid - record.realType = record.packedType - record.resolved = true - } - - recordIdx := len(state.records) - state.records = append(state.records, record) - - state.offsetToRecord[record.offset] = recordIdx - if record.resolved { - state.objectToRecord[record.objectID] = recordIdx - } - - switch record.packedType { - case objecttype.TypeOfsDelta: - state.ofsDeltas = append(state.ofsDeltas, ofsDeltaRef{ - baseOffset: record.baseOffset, - recordIdx: recordIdx, - }) - case objecttype.TypeRefDelta: - state.refDeltas = append(state.refDeltas, refDeltaRef{ - baseObject: record.baseObject, - recordIdx: recordIdx, - }) - case objecttype.TypeInvalid, - objecttype.TypeCommit, - objecttype.TypeTree, - objecttype.TypeBlob, - objecttype.TypeTag, - objecttype.TypeFuture: - default: - } - - return endOffset, nil -} diff --git a/object/store/packed/internal/ingest/entry_header.go b/object/store/packed/internal/ingest/entry_header.go deleted file mode 100644 index c74fdc16..00000000 --- a/object/store/packed/internal/ingest/entry_header.go +++ /dev/null @@ -1,33 +0,0 @@ -package ingest - -import ( - "codeberg.org/lindenii/furgit/internal/intconv" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// encodePackEntryHeader encodes one non-delta packed entry header. -func encodePackEntryHeader(ty objecttype.Type, size int64) []byte { - var out [16]byte - - n := 0 - - s, err := intconv.Int64ToUint64(size) - if err != nil { - panic(err) - } - - c := (uint8(ty) << 4) | byte(s&0x0f) - - s >>= 4 - for s != 0 { - out[n] = c | 0x80 - n++ - c = byte(s & 0x7f) - s >>= 7 - } - - out[n] = c - n++ - - return append([]byte(nil), out[:n]...) -} diff --git a/object/store/packed/internal/ingest/entry_prefix.go b/object/store/packed/internal/ingest/entry_prefix.go deleted file mode 100644 index a107b4e8..00000000 --- a/object/store/packed/internal/ingest/entry_prefix.go +++ /dev/null @@ -1,95 +0,0 @@ -package ingest - -import ( - "fmt" - - "codeberg.org/lindenii/furgit/internal/intconv" - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// parseEntryPrefix parses one entry prefix from stream. -func parseEntryPrefix(state *ingestState, startOffset uint64) (objectRecord, error) { - var record objectRecord - - record.offset = startOffset - - first, err := state.stream.ReadByte() - if err != nil { - return record, &MalformedPackEntryError{Offset: startOffset, Reason: fmt.Sprintf("read first header byte: %v", err)} - } - - record.packedType = objecttype.Type((first >> 4) & 0x07) - size := int64(first & 0x0f) - headerLen := uint32(1) - shift := uint(4) - b := first - - for b&0x80 != 0 { - b, err = state.stream.ReadByte() - if err != nil { - return record, &MalformedPackEntryError{Offset: startOffset, Reason: fmt.Sprintf("read size continuation: %v", err)} - } - - headerLen++ - size |= int64(b&0x7f) << shift - shift += 7 - } - - if size < 0 { - return record, &MalformedPackEntryError{Offset: startOffset, Reason: "negative declared size"} - } - - record.declaredSize = size - - switch record.packedType { - case objecttype.TypeCommit, objecttype.TypeTree, objecttype.TypeBlob, objecttype.TypeTag: - case objecttype.TypeRefDelta: - baseRaw := make([]byte, state.algo.Size()) - - err := state.stream.readFull(baseRaw) - if err != nil { - return record, &MalformedPackEntryError{Offset: startOffset, Reason: fmt.Sprintf("read ref base: %v", err)} - } - - baseID, err := objectid.FromBytes(state.algo, baseRaw) - if err != nil { - return record, &MalformedPackEntryError{Offset: startOffset, Reason: fmt.Sprintf("parse ref base: %v", err)} - } - - record.baseObject = baseID - - baseRawLen, err := intconv.IntToUint32(len(baseRaw)) - if err != nil { - return record, err - } - - headerLen += baseRawLen - case objecttype.TypeOfsDelta: - dist, consumed, err := readOfsDistanceFromStream(state.stream) - if err != nil { - return record, &MalformedPackEntryError{Offset: startOffset, Reason: err.Error()} - } - - if startOffset <= dist { - return record, &MalformedPackEntryError{Offset: startOffset, Reason: "ofs base offset out of bounds"} - } - - record.baseOffset = startOffset - dist - - consumedUint32, err := intconv.IntToUint32(consumed) - if err != nil { - return record, err - } - - headerLen += consumedUint32 - case objecttype.TypeInvalid, objecttype.TypeFuture: - return record, &MalformedPackEntryError{Offset: startOffset, Reason: fmt.Sprintf("unsupported object type %d", record.packedType)} - default: - return record, &MalformedPackEntryError{Offset: startOffset, Reason: fmt.Sprintf("unsupported object type %d", record.packedType)} - } - - record.headerLen = headerLen - - return record, nil -} diff --git a/object/store/packed/internal/ingest/errors.go b/object/store/packed/internal/ingest/errors.go deleted file mode 100644 index cbad1e77..00000000 --- a/object/store/packed/internal/ingest/errors.go +++ /dev/null @@ -1,68 +0,0 @@ -package ingest - -import ( - "errors" - "fmt" -) - -// InvalidPackHeaderError reports an invalid or unsupported pack header. -type InvalidPackHeaderError struct { - Reason string -} - -// Error implements error. -func (err *InvalidPackHeaderError) Error() string { - return "packfile/ingest: invalid pack header: " + err.Reason -} - -// PackTrailerMismatchError reports a mismatch between computed and trailer pack hash. -type PackTrailerMismatchError struct{} - -// Error implements error. -func (err *PackTrailerMismatchError) Error() string { - return "packfile/ingest: pack trailer hash mismatch" -} - -// ThinPackUnresolvedError reports unresolved REF deltas when fixThin is disabled -// or when required bases cannot be found in base. -type ThinPackUnresolvedError struct { - Count int -} - -// Error implements error. -func (err *ThinPackUnresolvedError) Error() string { - return fmt.Sprintf("packfile/ingest: unresolved thin deltas: %d", err.Count) -} - -// MalformedPackEntryError reports malformed entry encoding at one pack offset. -type MalformedPackEntryError struct { - Offset uint64 - Reason string -} - -// Error implements error. -func (err *MalformedPackEntryError) Error() string { - return fmt.Sprintf("packfile/ingest: malformed pack entry at offset %d: %s", err.Offset, err.Reason) -} - -// DeltaCycleError reports a detected cycle in delta dependency resolution. -type DeltaCycleError struct { - Offset uint64 -} - -// Error implements error. -func (err *DeltaCycleError) Error() string { - return fmt.Sprintf("packfile/ingest: delta cycle detected at offset %d", err.Offset) -} - -// DestinationWriteError reports destination I/O failures. -type DestinationWriteError struct { - Op string -} - -// Error implements error. -func (err *DestinationWriteError) Error() string { - return "packfile/ingest: destination write failure: " + err.Op -} - -var errExternalThinBase = errors.New("packfile/ingest: external thin base required") diff --git a/object/store/packed/internal/ingest/file_section_writer.go b/object/store/packed/internal/ingest/file_section_writer.go deleted file mode 100644 index fa28c1a9..00000000 --- a/object/store/packed/internal/ingest/file_section_writer.go +++ /dev/null @@ -1,22 +0,0 @@ -package ingest - -import "os" - -// fileSectionWriter writes sequentially to file via WriteAt at one base offset. -type fileSectionWriter struct { - file *os.File - off int64 - pos int64 -} - -// Write writes src at current section position. -func (writer *fileSectionWriter) Write(src []byte) (int, error) { - if len(src) == 0 { - return 0, nil - } - - n, err := writer.file.WriteAt(src, writer.off+writer.pos) - writer.pos += int64(n) - - return n, err -} diff --git a/object/store/packed/internal/ingest/fill.go b/object/store/packed/internal/ingest/fill.go deleted file mode 100644 index eca4e4d6..00000000 --- a/object/store/packed/internal/ingest/fill.go +++ /dev/null @@ -1,44 +0,0 @@ -package ingest - -import ( - "errors" - "fmt" - "io" -) - -// fill ensures at least min unread bytes are available in receiver's buffer. -func (scanner *streamScanner) fill(minLen int) error { - if minLen <= 0 { - return nil - } - - if minLen > len(scanner.buf) { - return fmt.Errorf("packfile/ingest: fill(%d) exceeds scanner buffer", minLen) - } - - for scanner.n-scanner.off < minLen { - err := scanner.flushConsumedPrefix() - if err != nil { - return err - } - - readN, err := scanner.src.Read(scanner.buf[scanner.n:]) - if readN > 0 { - scanner.n += readN - } - - if err != nil { - if errors.Is(err, io.EOF) && scanner.n-scanner.off >= minLen { - return nil - } - - return err - } - - if readN == 0 { - return io.ErrNoProgress - } - } - - return nil -} diff --git a/object/store/packed/internal/ingest/finalize.go b/object/store/packed/internal/ingest/finalize.go deleted file mode 100644 index 6fe4edb2..00000000 --- a/object/store/packed/internal/ingest/finalize.go +++ /dev/null @@ -1,94 +0,0 @@ -package ingest - -import ( - "errors" - "fmt" - "io/fs" - "strings" - - "codeberg.org/lindenii/furgit/internal/intconv" -) - -// finalizeArtifacts links temporary files to final names and returns Result. -func finalizeArtifacts(state *ingestState) (Result, error) { - base := "pack-" + state.packHash.String() - packFinal := base + ".pack" - idxFinal := base + ".idx" - - revFinal := "" - if state.opts.WriteRev { - revFinal = base + ".rev" - } - - err := linkTempToFinal(state, state.packTmpName, packFinal) - if err != nil { - return Result{}, err - } - - err = linkTempToFinal(state, state.idxTmpName, idxFinal) - if err != nil { - return Result{}, err - } - - if state.opts.WriteRev { - err := linkTempToFinal(state, state.revTmpName, revFinal) - if err != nil { - return Result{}, err - } - } - - objectCount, err := intconv.IntToUint32(len(state.records)) - if err != nil { - return Result{}, err - } - - return Result{ - PackName: packFinal, - IdxName: idxFinal, - RevName: revFinal, - PackHash: state.packHash, - ObjectCount: objectCount, - ThinFixed: state.thinFixed, - }, nil -} - -// rollbackTemporaryArtifacts removes temporary files after failure. -func rollbackTemporaryArtifacts(state *ingestState) { - if state.packTmpName != "" { - _ = state.destination.Remove(state.packTmpName) - } - - if state.idxTmpName != "" { - _ = state.destination.Remove(state.idxTmpName) - } - - if state.revTmpName != "" { - _ = state.destination.Remove(state.revTmpName) - } -} - -// linkTempToFinal hard-links tmp to final, tolerating existing final paths. -func linkTempToFinal(state *ingestState, tmp, final string) error { - if tmp == "" || final == "" { - return fmt.Errorf("packfile/ingest: invalid finalize names tmp=%q final=%q", tmp, final) - } - - if strings.Contains(final, "/") { - return fmt.Errorf("packfile/ingest: final name must be leaf: %q", final) - } - - err := state.destination.Link(tmp, final) - if err == nil { - _ = state.destination.Remove(tmp) - - return nil - } - - if errors.Is(err, fs.ErrExist) { - _ = state.destination.Remove(tmp) - - return nil - } - - return err -} diff --git a/object/store/packed/internal/ingest/flush.go b/object/store/packed/internal/ingest/flush.go deleted file mode 100644 index 96753170..00000000 --- a/object/store/packed/internal/ingest/flush.go +++ /dev/null @@ -1,37 +0,0 @@ -package ingest - -import "fmt" - -// flush writes all consumed-but-unflushed bytes to destination pack file. -func (scanner *streamScanner) flush() error { - return scanner.flushConsumedPrefix() -} - -// flushConsumedPrefix writes scanner.buf[:scanner.off] and compacts unread -// bytes to the start of buffer. -func (scanner *streamScanner) flushConsumedPrefix() error { - if scanner.off == 0 { - return nil - } - - written := 0 - for written < scanner.off { - n, err := scanner.dstFile.Write(scanner.buf[written:scanner.off]) - if err != nil { - return &DestinationWriteError{Op: fmt.Sprintf("write pack: %v", err)} - } - - if n == 0 { - return &DestinationWriteError{Op: "write pack: short write"} - } - - written += n - } - - unread := scanner.n - scanner.off - copy(scanner.buf[:unread], scanner.buf[scanner.off:scanner.n]) - scanner.off = 0 - scanner.n = unread - - return nil -} diff --git a/object/store/packed/internal/ingest/hash.go b/object/store/packed/internal/ingest/hash.go deleted file mode 100644 index 4b739c20..00000000 --- a/object/store/packed/internal/ingest/hash.go +++ /dev/null @@ -1,27 +0,0 @@ -package ingest - -import ( - "fmt" - - objectheader "codeberg.org/lindenii/furgit/object/header" - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// hashCanonicalObject hashes canonical object bytes (header+content). -func hashCanonicalObject(algo objectid.Algorithm, ty objecttype.Type, content []byte) (objectid.ObjectID, error) { - header, ok := objectheader.Encode(ty, int64(len(content))) - if !ok { - return objectid.ObjectID{}, fmt.Errorf("packfile/ingest: encode object header for type %d", ty) - } - - hashImpl, err := algo.New() - if err != nil { - return objectid.ObjectID{}, err - } - - _, _ = hashImpl.Write(header) - _, _ = hashImpl.Write(content) - - return objectid.FromBytes(algo, hashImpl.Sum(nil)) -} diff --git a/object/store/packed/internal/ingest/header.go b/object/store/packed/internal/ingest/header.go deleted file mode 100644 index 6b90becc..00000000 --- a/object/store/packed/internal/ingest/header.go +++ /dev/null @@ -1,54 +0,0 @@ -package ingest - -import ( - "encoding/binary" - "fmt" - "io" - - "codeberg.org/lindenii/furgit/format/packfile" -) - -const packHeaderSize = 12 - -type packHeader struct { - Version uint32 - ObjectCount uint32 -} - -// readAndValidatePackHeader reads one PACK header from src and validates it. -func readAndValidatePackHeader(src io.Reader) (packHeader, [packHeaderSize]byte, error) { - var hdr [packHeaderSize]byte - - _, err := io.ReadFull(src, hdr[:]) - if err != nil { - return packHeader{}, [packHeaderSize]byte{}, &InvalidPackHeaderError{ - Reason: fmt.Sprintf("read header: %v", err), - } - } - - header, err := parseAndValidatePackHeader(hdr) - if err != nil { - return packHeader{}, [packHeaderSize]byte{}, err - } - - return header, hdr, nil -} - -// parseAndValidatePackHeader validates one already-read PACK header. -func parseAndValidatePackHeader(hdr [packHeaderSize]byte) (packHeader, error) { - if binary.BigEndian.Uint32(hdr[:4]) != packfile.Signature { - return packHeader{}, &InvalidPackHeaderError{Reason: "signature mismatch"} - } - - version := binary.BigEndian.Uint32(hdr[4:8]) - if !packfile.SupportedVersion(version) { - return packHeader{}, &InvalidPackHeaderError{ - Reason: fmt.Sprintf("unsupported version %d", version), - } - } - - return packHeader{ - Version: version, - ObjectCount: binary.BigEndian.Uint32(hdr[8:12]), - }, nil -} diff --git a/object/store/packed/internal/ingest/idx_write.go b/object/store/packed/internal/ingest/idx_write.go deleted file mode 100644 index fa139264..00000000 --- a/object/store/packed/internal/ingest/idx_write.go +++ /dev/null @@ -1,262 +0,0 @@ -package ingest - -import ( - "bytes" - "encoding/binary" - "fmt" - "hash" - "io" - "slices" - - "codeberg.org/lindenii/furgit/internal/intconv" - "codeberg.org/lindenii/furgit/internal/progress" -) - -const ( - idxMagicV2 = 0xff744f63 - idxVersionV2 = 2 -) - -// writeIdx writes idx v2 for resolved records. -func writeIdx(state *ingestState) error { - order := buildIdxOrder(state) - - hashImpl, err := state.algo.New() - if err != nil { - return err - } - - write := func(src []byte) error { - _, writeErr := state.idxFile.Write(src) - if writeErr != nil { - return writeErr - } - - _, writeErr = hashImpl.Write(src) - if writeErr != nil { - return writeErr - } - - return nil - } - - var ( - scratch [8]byte - fanout [256]uint32 - ) - - writeProgressf(state, "writing index fanout...\r") - - for _, recordIdx := range order { - idRaw := state.records[recordIdx].objectID.Bytes() - fanout[idRaw[0]]++ - } - - binary.BigEndian.PutUint32(scratch[:4], idxMagicV2) - binary.BigEndian.PutUint32(scratch[4:8], idxVersionV2) - - err = write(scratch[:8]) - if err != nil { - return err - } - - var cumulative uint32 - for i := range fanout { - cumulative += fanout[i] - binary.BigEndian.PutUint32(scratch[:4], cumulative) - - err := write(scratch[:4]) - if err != nil { - return err - } - } - - writeProgressf(state, "writing index fanout: done.\n") - - largeOffsetCount := 0 - - for idx := range state.records { - if state.records[idx].offset >= 0x80000000 { - largeOffsetCount++ - } - } - - oidMeter := progress.New(progress.Options{ - Writer: state.opts.Progress, - Title: "writing index object ids", - Total: uint64(len(order)), - }) - - var oidDone uint64 - - for _, recordIdx := range order { - idRaw := state.records[recordIdx].objectID.Bytes() - - err := write(idRaw) - if err != nil { - return err - } - - oidDone++ - oidMeter.Set(oidDone, 0) - } - - if oidDone > 0 { - oidMeter.Stop("done") - } - - crcMeter := progress.New(progress.Options{ - Writer: state.opts.Progress, - Title: "writing index crc32", - Total: uint64(len(order)), - }) - - var crcDone uint64 - - for _, recordIdx := range order { - binary.BigEndian.PutUint32(scratch[:4], state.records[recordIdx].crc32) - - err := write(scratch[:4]) - if err != nil { - return err - } - - crcDone++ - crcMeter.Set(crcDone, 0) - } - - if crcDone > 0 { - crcMeter.Stop("done") - } - - largeOffsets := make([]uint64, 0) - offsetMeter := progress.New(progress.Options{ - Writer: state.opts.Progress, - Title: "writing index offsets", - Total: uint64(len(order)), - }) - - var offsetDone uint64 - - for _, recordIdx := range order { - offset := state.records[recordIdx].offset - if offset >= 0x80000000 { - largeOffsetIdx, err := intconv.IntToUint32(len(largeOffsets)) - if err != nil { - return err - } - - word := 0x80000000 | largeOffsetIdx - - largeOffsets = append(largeOffsets, offset) - - binary.BigEndian.PutUint32(scratch[:4], word) - } else { - binary.BigEndian.PutUint32(scratch[:4], uint32(offset)) - } - - err := write(scratch[:4]) - if err != nil { - return err - } - - offsetDone++ - offsetMeter.Set(offsetDone, 0) - } - - if offsetDone > 0 { - offsetMeter.Stop("done") - } - - total, err := intconv.IntToUint64(largeOffsetCount) - if err != nil { - return err - } - - largeOffsetMeter := progress.New(progress.Options{ - Writer: state.opts.Progress, - Title: "writing index large offsets", - Total: total, - }) - - var largeOffsetDone uint64 - - for _, off := range largeOffsets { - binary.BigEndian.PutUint64(scratch[:8], off) - - err := write(scratch[:8]) - if err != nil { - return err - } - - largeOffsetDone++ - largeOffsetMeter.Set(largeOffsetDone, 0) - } - - if largeOffsetDone > 0 { - largeOffsetMeter.Stop("done") - } - - writeProgressf(state, "writing index trailer...\r") - - err = write(state.packHash.Bytes()) - if err != nil { - return err - } - - idxHash := hashImpl.Sum(nil) - - _, err = state.idxFile.Write(idxHash) - if err != nil { - return err - } - - err = state.idxFile.Sync() - if err != nil { - return err - } - - writeProgressf(state, "writing index trailer: done.\n") - - return nil -} - -// buildIdxOrder returns record indexes sorted by ObjectID. -func buildIdxOrder(state *ingestState) []int { - out := make([]int, 0, len(state.records)) - for idx := range state.records { - out = append(out, idx) - } - - slices.SortFunc(out, func(a, b int) int { - return bytes.Compare(state.records[a].objectID.Bytes(), state.records[b].objectID.Bytes()) - }) - - return out -} - -// verifyResolvedRecords checks that all records are fully resolved before index writing. -func verifyResolvedRecords(state *ingestState) error { - for idx, record := range state.records { - if !record.resolved { - return fmt.Errorf("packfile/ingest: unresolved record %d at offset %d", idx, record.offset) - } - } - - return nil -} - -// writeAndHash writes src to dst and updates hash. -func writeAndHash(dst io.Writer, hashImpl hash.Hash, src []byte) error { - _, err := dst.Write(src) - if err != nil { - return err - } - - _, err = hashImpl.Write(src) - if err != nil { - return err - } - - return nil -} diff --git a/object/store/packed/internal/ingest/ingest.go b/object/store/packed/internal/ingest/ingest.go deleted file mode 100644 index be65ff5f..00000000 --- a/object/store/packed/internal/ingest/ingest.go +++ /dev/null @@ -1,68 +0,0 @@ -package ingest - -import ( - "fmt" -) - -// ingest initializes transaction state and executes the ingest pipeline. -func ingest(state *ingestState) (out Result, err error) { - err = openTemporaryArtifacts(state) - if err != nil { - return Result{}, err - } - - defer func() { - _ = closeTemporaryArtifacts(state) - if err != nil { - rollbackTemporaryArtifacts(state) - } - }() - - err = streamPackAndScan(state) - if err != nil { - return Result{}, err - } - - err = resolveAll(state) - if err != nil { - return Result{}, err - } - - err = maybeFixThin(state) - if err != nil { - return Result{}, err - } - - if state.thinFixed { - err = resolveAll(state) - if err != nil { - return Result{}, err - } - } - - if len(state.unresolvedRefDeltas) > 0 { - return Result{}, &ThinPackUnresolvedError{Count: len(state.unresolvedRefDeltas)} - } - - err = verifyResolvedRecords(state) - if err != nil { - return Result{}, err - } - - err = state.packFile.Sync() - if err != nil { - return Result{}, &DestinationWriteError{Op: fmt.Sprintf("sync pack: %v", err)} - } - - err = writeIdx(state) - if err != nil { - return Result{}, err - } - - err = writeRev(state) - if err != nil { - return Result{}, err - } - - return finalizeArtifacts(state) -} diff --git a/object/store/packed/internal/ingest/ingest_test.go b/object/store/packed/internal/ingest/ingest_test.go deleted file mode 100644 index c99afe65..00000000 --- a/object/store/packed/internal/ingest/ingest_test.go +++ /dev/null @@ -1,411 +0,0 @@ -package ingest_test - -import ( - "bytes" - "encoding/binary" - "errors" - "io/fs" - "os" - "path/filepath" - "strings" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/store/packed/internal/ingest" -) - -type noExtraReadReader struct { - reader *bytes.Reader -} - -func (r *noExtraReadReader) Read(p []byte) (int, error) { - if r.reader.Len() == 0 { - return 0, errors.New("unexpected extra read after pack trailer") - } - - return r.reader.Read(p) -} - -// fixturePath returns one fixture file path for the selected algorithm. -func fixturePath(t *testing.T, algo objectid.Algorithm, name string) string { - t.Helper() - - dir := algo.String() - if dir == "" { - t.Fatalf("unsupported fixture algorithm: %v", algo) - } - - return filepath.Join("testdata", "fixtures", dir, name) -} - -// fixtureBytes reads one fixture file fully. -func fixtureBytes(t *testing.T, algo objectid.Algorithm, name string) []byte { - t.Helper() - - path := fixturePath(t, algo, name) - dir := filepath.Dir(path) - base := filepath.Base(path) - - root, err := os.OpenRoot(dir) - if err != nil { - t.Fatalf("open fixture root %q: %v", dir, err) - } - - defer func() { - err := root.Close() - if err != nil { - t.Fatalf("close fixture root %q: %v", dir, err) - } - }() - - data, err := root.ReadFile(base) - if err != nil { - t.Fatalf("read fixture %q: %v", base, err) - } - - return data -} - -// fixtureMetadata parses key=value metadata for one algorithm fixture set. -func fixtureMetadata(t *testing.T, algo objectid.Algorithm) map[string]string { - t.Helper() - - data := fixtureBytes(t, algo, "METADATA.txt") - - out := make(map[string]string) - for line := range strings.SplitSeq(strings.TrimSpace(string(data)), "\n") { - line = strings.TrimSpace(line) - if line == "" { - continue - } - - key, value, ok := strings.Cut(line, "=") - if !ok { - t.Fatalf("invalid fixture metadata line %q", line) - } - - out[strings.TrimSpace(key)] = strings.TrimSpace(value) - } - - return out -} - -// fixtureOID returns one fixture metadata object ID value. -func fixtureOID(t *testing.T, algo objectid.Algorithm, key string) objectid.ObjectID { - t.Helper() - - meta := fixtureMetadata(t, algo) - - hex, ok := meta[key] - if !ok { - t.Fatalf("missing fixture metadata key %q", key) - } - - id, err := objectid.ParseHex(algo, hex) - if err != nil { - t.Fatalf("parse fixture metadata oid %q: %v", hex, err) - } - - return id -} - -// verifyReindexOracle regenerates idx/rev with upstream git index-pack and -// compares bytes with files produced by ingest. -func verifyReindexOracle(t *testing.T, repo *testgit.TestRepo, packName, idxName, revName string) { - t.Helper() - - oracleDir := t.TempDir() - oracleIdxPath := filepath.Join(oracleDir, "oracle.idx") - _ = repo.Run(t, "index-pack", "--rev-index", "-o", oracleIdxPath, filepath.Join("objects", "pack", packName)) - oracleRevPath := strings.TrimSuffix(oracleIdxPath, ".idx") + ".rev" - - packRoot := repo.OpenPackRoot(t) - - gotIdx, err := packRoot.ReadFile(idxName) - if err != nil { - t.Fatalf("read idx: %v", err) - } - - oracleRoot, err := os.OpenRoot(oracleDir) - if err != nil { - t.Fatalf("open oracle root: %v", err) - } - - defer func() { - err := oracleRoot.Close() - if err != nil { - t.Fatalf("close oracle root: %v", err) - } - }() - - wantIdx, err := oracleRoot.ReadFile(filepath.Base(oracleIdxPath)) - if err != nil { - t.Fatalf("read oracle idx: %v", err) - } - - if !bytes.Equal(gotIdx, wantIdx) { - t.Fatal("idx bytes differ from git index-pack output") - } - - gotRev, err := packRoot.ReadFile(revName) - if err != nil { - t.Fatalf("read rev: %v", err) - } - - wantRev, err := oracleRoot.ReadFile(filepath.Base(oracleRevPath)) - if err != nil { - t.Fatalf("read oracle rev: %v", err) - } - - if !bytes.Equal(gotRev, wantRev) { - t.Fatal("rev bytes differ from git index-pack output") - } -} - -func TestIngestNonThinPackWritesPackIdxRev(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - head := fixtureOID(t, algo, "head") - packBytes := fixtureBytes(t, algo, "nonthin.pack") - - receiver := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - - packRoot := receiver.OpenPackRoot(t) - - result, err := ingest.WritePack(packRoot, algo, bytes.NewReader(packBytes), ingest.Options{ - WriteRev: true, - RequireTrailingEOF: true, - }) - if err != nil { - t.Fatalf("Ingest: %v", err) - } - - if result.ThinFixed { - t.Fatalf("ThinFixed = true, want false") - } - - if result.RevName == "" { - t.Fatal("RevName is empty") - } - - _, err = packRoot.Stat(result.PackName) - if err != nil { - t.Fatalf("stat pack: %v", err) - } - - _, err = packRoot.Stat(result.IdxName) - if err != nil { - t.Fatalf("stat idx: %v", err) - } - - _, err = packRoot.Stat(result.RevName) - if err != nil { - t.Fatalf("stat rev: %v", err) - } - - _ = receiver.Run(t, "verify-pack", "-v", filepath.Join("objects", "pack", result.IdxName)) - verifyReindexOracle(t, receiver, result.PackName, result.IdxName, result.RevName) - - receiver.UpdateRef(t, "refs/heads/main", head) - _ = receiver.Run(t, "fsck", "--full", "--strict", "--no-progress", "--no-dangling") - }) -} - -func TestIngestThinPackWithoutFixReturnsUnresolved(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - thinPack := fixtureBytes(t, algo, "thin.pack") - - receiver := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - packRoot := receiver.OpenPackRoot(t) - - _, err := ingest.WritePack(packRoot, algo, bytes.NewReader(thinPack), ingest.Options{ - WriteRev: true, - RequireTrailingEOF: true, - }) - if err == nil { - t.Fatal("Ingest error = nil, want error") - } - - if _, ok := errors.AsType[*ingest.ThinPackUnresolvedError](err); !ok { - t.Fatalf("Ingest error type = %T (%v), want *ThinPackUnresolvedError", err, err) - } - - entries, err := fs.ReadDir(packRoot.FS(), ".") - if err != nil { - t.Fatalf("ReadDir(pack): %v", err) - } - - for _, entry := range entries { - if strings.HasSuffix(entry.Name(), ".pack") { - t.Fatalf("found finalized pack file after failure: %v", entry.Name()) - } - } - }) -} - -func TestIngestThinPackWithFixThin(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - head := fixtureOID(t, algo, "head") - basePack := fixtureBytes(t, algo, "base.pack") - thinPack := fixtureBytes(t, algo, "thin.pack") - receiver := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - - packRoot := receiver.OpenPackRoot(t) - - _, err := ingest.WritePack(packRoot, algo, bytes.NewReader(basePack), ingest.Options{ - RequireTrailingEOF: true, - }) - if err != nil { - t.Fatalf("ingest base pack: %v", err) - } - - receiverRepo := receiver.OpenRepository(t) - - result, err := ingest.WritePack(packRoot, algo, bytes.NewReader(thinPack), ingest.Options{ - FixThin: true, - WriteRev: true, - ThinBase: receiverRepo.Objects(), - RequireTrailingEOF: true, - }) - if err != nil { - t.Fatalf("Ingest(thin): %v", err) - } - - if !result.ThinFixed { - t.Fatal("ThinFixed = false, want true") - } - - _ = receiver.Run(t, "verify-pack", "-v", filepath.Join("objects", "pack", result.IdxName)) - verifyReindexOracle(t, receiver, result.PackName, result.IdxName, result.RevName) - receiver.UpdateRef(t, "refs/heads/main", head) - _ = receiver.Run(t, "fsck", "--full", "--strict", "--no-progress", "--no-dangling") - }) -} - -func TestIngestPackTrailerMismatch(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - packBytes := fixtureBytes(t, algo, "nonthin.pack") - if len(packBytes) == 0 { - t.Fatal("empty pack stream") - } - - packBytes[len(packBytes)-1] ^= 0xff - - receiver := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - packRoot := receiver.OpenPackRoot(t) - - _, err := ingest.WritePack(packRoot, algo, bytes.NewReader(packBytes), ingest.Options{ - WriteRev: true, - RequireTrailingEOF: true, - }) - if err == nil { - t.Fatal("Ingest error = nil, want error") - } - - if _, ok := errors.AsType[*ingest.PackTrailerMismatchError](err); !ok { - t.Fatalf("Ingest error type = %T (%v), want *PackTrailerMismatchError", err, err) - } - - entries, err := fs.ReadDir(packRoot.FS(), ".") - if err != nil { - t.Fatalf("ReadDir(pack): %v", err) - } - - for _, entry := range entries { - if strings.HasSuffix(entry.Name(), ".pack") { - t.Fatalf("found finalized pack file after failure: %v", entry.Name()) - } - } - }) -} - -func zeroObjectPackBytes(t *testing.T, algo objectid.Algorithm) []byte { - t.Helper() - - hashImpl, err := algo.New() - if err != nil { - t.Fatalf("algo.New: %v", err) - } - - var header [12]byte - copy(header[:4], []byte{'P', 'A', 'C', 'K'}) - binary.BigEndian.PutUint32(header[4:8], 2) - binary.BigEndian.PutUint32(header[8:12], 0) - - _, _ = hashImpl.Write(header[:]) - - return append(header[:], hashImpl.Sum(nil)...) -} - -func TestIngestZeroObjectPackIsDiscardedInternally(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - packBytes := zeroObjectPackBytes(t, algo) - receiver := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - packRoot := receiver.OpenPackRoot(t) - - result, err := ingest.WritePack(packRoot, algo, bytes.NewReader(packBytes), ingest.Options{ - RequireTrailingEOF: true, - }) - if err != nil { - t.Fatalf("WritePack: %v", err) - } - - if result.ObjectCount != 0 { - t.Fatalf("ObjectCount = %d, want 0", result.ObjectCount) - } - - if result.PackName != "" { - t.Fatalf("PackName = %q, want empty", result.PackName) - } - - if result.IdxName != "" { - t.Fatalf("IdxName = %q, want empty", result.IdxName) - } - - if result.RevName != "" { - t.Fatalf("RevName = %q, want empty", result.RevName) - } - - entries, err := fs.ReadDir(packRoot.FS(), ".") - if err != nil { - t.Fatalf("ReadDir(pack): %v", err) - } - - if len(entries) != 0 { - t.Fatalf("unexpected files after zero-object pack: %d", len(entries)) - } - }) -} - -func TestIngestCanFinishWithoutTrailingEOF(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - head := fixtureOID(t, algo, "head") - packBytes := fixtureBytes(t, algo, "nonthin.pack") - - receiver := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - packRoot := receiver.OpenPackRoot(t) - - result, err := ingest.WritePack(packRoot, algo, &noExtraReadReader{reader: bytes.NewReader(packBytes)}, ingest.Options{ - WriteRev: true, - }) - if err != nil { - t.Fatalf("Ingest without trailing EOF: %v", err) - } - - receiver.UpdateRef(t, "refs/heads/main", head) - _ = receiver.Run(t, "verify-pack", "-v", filepath.Join("objects", "pack", result.IdxName)) - _ = receiver.Run(t, "fsck", "--full", "--strict", "--no-progress", "--no-dangling") - }) -} diff --git a/object/store/packed/internal/ingest/options.go b/object/store/packed/internal/ingest/options.go deleted file mode 100644 index 06c334c0..00000000 --- a/object/store/packed/internal/ingest/options.go +++ /dev/null @@ -1,26 +0,0 @@ -package ingest - -import ( - "codeberg.org/lindenii/furgit/common/iowrap" - objectstore "codeberg.org/lindenii/furgit/object/store" -) - -// Options controls one pack ingest operation. -type Options struct { - // FixThin appends missing local bases for thin packs. - FixThin bool - // WriteRev writes a .rev alongside the .pack and .idx. - WriteRev bool - // ThinBase supplies existing objects for thin-pack fixup. - ThinBase objectstore.Reader - // Progress receives human-readable progress messages. - // - // When nil, no progress output is emitted. - Progress iowrap.WriteFlusher - // RequireTrailingEOF requires the source to hit EOF after the pack trailer. - // - // This is suitable for exact pack-file readers, but should be disabled for - // full-duplex transport streams like receive-pack where the peer keeps the - // connection open to read the server response. - RequireTrailingEOF bool -} diff --git a/object/store/packed/internal/ingest/progress_write.go b/object/store/packed/internal/ingest/progress_write.go deleted file mode 100644 index afb39305..00000000 --- a/object/store/packed/internal/ingest/progress_write.go +++ /dev/null @@ -1,11 +0,0 @@ -package ingest - -import "codeberg.org/lindenii/furgit/internal/utils" - -func writeProgressf(state *ingestState, format string, args ...any) { - utils.BestEffortFprintf(state.opts.Progress, format, args...) - - if state.opts.Progress != nil { - _ = state.opts.Progress.Flush() - } -} diff --git a/object/store/packed/internal/ingest/record_content.go b/object/store/packed/internal/ingest/record_content.go deleted file mode 100644 index c66a1234..00000000 --- a/object/store/packed/internal/ingest/record_content.go +++ /dev/null @@ -1,29 +0,0 @@ -package ingest - -import ( - "fmt" - - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// readBaseRecordContent reads canonical base content for one non-delta record. -func readBaseRecordContent(state *ingestState, idx int) (objecttype.Type, []byte, error) { - record := state.records[idx] - if !record.packedType.IsBaseObject() { - return objecttype.TypeInvalid, nil, fmt.Errorf("packfile/ingest: record %d is not a base object", idx) - } - - content, err := inflateRecordPayload(state, idx) - if err != nil { - return objecttype.TypeInvalid, nil, err - } - - if int64(len(content)) != record.declaredSize { - return objecttype.TypeInvalid, nil, &MalformedPackEntryError{ - Offset: record.offset, - Reason: fmt.Sprintf("base content size mismatch got %d want %d", len(content), record.declaredSize), - } - } - - return record.packedType, content, nil -} diff --git a/object/store/packed/internal/ingest/record_delta.go b/object/store/packed/internal/ingest/record_delta.go deleted file mode 100644 index bc40367f..00000000 --- a/object/store/packed/internal/ingest/record_delta.go +++ /dev/null @@ -1,60 +0,0 @@ -package ingest - -import ( - "fmt" - - deltaapply "codeberg.org/lindenii/furgit/format/packfile/delta/apply" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// applyDeltaRecord applies one delta record onto base content. -func applyDeltaRecord(state *ingestState, idx int, baseType objecttype.Type, baseContent []byte) (objecttype.Type, []byte, error) { - record := state.records[idx] - if record.packedType != objecttype.TypeOfsDelta && record.packedType != objecttype.TypeRefDelta { - return objecttype.TypeInvalid, nil, fmt.Errorf("packfile/ingest: record %d is not a delta record", idx) - } - - deltaPayload, err := inflateRecordPayload(state, idx) - if err != nil { - return objecttype.TypeInvalid, nil, err - } - - if int64(len(deltaPayload)) != record.declaredSize { - return objecttype.TypeInvalid, nil, &MalformedPackEntryError{ - Offset: record.offset, - Reason: fmt.Sprintf("delta payload size mismatch got %d want %d", len(deltaPayload), record.declaredSize), - } - } - - srcSize, dstSize, err := readDeltaHeaderSizes(deltaPayload) - if err != nil { - return objecttype.TypeInvalid, nil, &MalformedPackEntryError{ - Offset: record.offset, - Reason: fmt.Sprintf("read delta header: %v", err), - } - } - - if srcSize != len(baseContent) { - return objecttype.TypeInvalid, nil, &MalformedPackEntryError{ - Offset: record.offset, - Reason: fmt.Sprintf("delta source size mismatch got %d want %d", srcSize, len(baseContent)), - } - } - - content, err := deltaapply.Apply(baseContent, deltaPayload) - if err != nil { - return objecttype.TypeInvalid, nil, &MalformedPackEntryError{ - Offset: record.offset, - Reason: fmt.Sprintf("apply delta: %v", err), - } - } - - if len(content) != dstSize { - return objecttype.TypeInvalid, nil, &MalformedPackEntryError{ - Offset: record.offset, - Reason: fmt.Sprintf("delta result size mismatch got %d want %d", len(content), dstSize), - } - } - - return baseType, content, nil -} diff --git a/object/store/packed/internal/ingest/record_inflate.go b/object/store/packed/internal/ingest/record_inflate.go deleted file mode 100644 index b8eca25b..00000000 --- a/object/store/packed/internal/ingest/record_inflate.go +++ /dev/null @@ -1,46 +0,0 @@ -package ingest - -import ( - "compress/zlib" - "fmt" - "io" - - "codeberg.org/lindenii/furgit/internal/intconv" -) - -// inflateRecordPayload inflates one record's zlib payload from pack file. -func inflateRecordPayload(state *ingestState, idx int) ([]byte, error) { - record := state.records[idx] - if record.packedLen < uint64(record.headerLen) { - return nil, &MalformedPackEntryError{Offset: record.offset, Reason: "entry packed span underflow"} - } - - compressedOffset := record.offset + uint64(record.headerLen) - compressedLen := record.packedLen - uint64(record.headerLen) - - compressedOffsetInt64, err := intconv.Uint64ToInt64(compressedOffset) - if err != nil { - return nil, err - } - - compressedLenInt64, err := intconv.Uint64ToInt64(compressedLen) - if err != nil { - return nil, err - } - - section := io.NewSectionReader(state.packFile, compressedOffsetInt64, compressedLenInt64) - - reader, err := zlib.NewReader(section) - if err != nil { - return nil, &MalformedPackEntryError{Offset: record.offset, Reason: fmt.Sprintf("open payload zlib: %v", err)} - } - - defer func() { _ = reader.Close() }() - - out, err := io.ReadAll(reader) - if err != nil { - return nil, &MalformedPackEntryError{Offset: record.offset, Reason: fmt.Sprintf("inflate payload: %v", err)} - } - - return out, nil -} diff --git a/object/store/packed/internal/ingest/record_resolve.go b/object/store/packed/internal/ingest/record_resolve.go deleted file mode 100644 index 7a9471dc..00000000 --- a/object/store/packed/internal/ingest/record_resolve.go +++ /dev/null @@ -1,116 +0,0 @@ -package ingest - -import ( - "fmt" - - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// resolveRecord resolves one record and returns canonical type/content. -func resolveRecord(state *ingestState, idx int, visiting map[int]struct{}) (objecttype.Type, []byte, error) { - if idx < 0 || idx >= len(state.records) { - return objecttype.TypeInvalid, nil, fmt.Errorf("packfile/ingest: record index out of bounds") - } - - if _, ok := visiting[idx]; ok { - return objecttype.TypeInvalid, nil, &DeltaCycleError{Offset: state.records[idx].offset} - } - - visiting[idx] = struct{}{} - defer delete(visiting, idx) - - record := &state.records[idx] - if ty, content, ok := state.baseCache.get(idx); ok { - return ty, content, nil - } - - if record.packedType.IsBaseObject() { - ty, content, err := readBaseRecordContent(state, idx) - if err != nil { - return objecttype.TypeInvalid, nil, err - } - - if record.resolved { - state.baseCache.add(idx, record.realType, content) - - return record.realType, content, nil - } - - id, err := hashCanonicalObject(state.algo, ty, content) - if err != nil { - return objecttype.TypeInvalid, nil, err - } - - record.objectID = id - record.realType = ty - record.resolved = true - state.objectToRecord[id] = idx - state.baseCache.add(idx, ty, content) - - return ty, content, nil - } - - var ( - baseType objecttype.Type - baseContent []byte - err error - ) - switch record.packedType { - case objecttype.TypeOfsDelta: - baseIdx, ok := state.offsetToRecord[record.baseOffset] - if !ok { - return objecttype.TypeInvalid, nil, &MalformedPackEntryError{ - Offset: record.offset, - Reason: "missing ofs-delta base entry", - } - } - - baseType, baseContent, err = resolveRecord(state, baseIdx, visiting) - if err != nil { - return objecttype.TypeInvalid, nil, err - } - case objecttype.TypeRefDelta: - baseIdx, ok := state.objectToRecord[record.baseObject] - if ok { - baseType, baseContent, err = resolveRecord(state, baseIdx, visiting) - if err != nil { - return objecttype.TypeInvalid, nil, err - } - } else { - return objecttype.TypeInvalid, nil, errExternalThinBase - } - case objecttype.TypeInvalid, - objecttype.TypeCommit, - objecttype.TypeTree, - objecttype.TypeBlob, - objecttype.TypeTag, - objecttype.TypeFuture: - return objecttype.TypeInvalid, nil, &MalformedPackEntryError{ - Offset: record.offset, - Reason: "unsupported delta type", - } - default: - return objecttype.TypeInvalid, nil, &MalformedPackEntryError{ - Offset: record.offset, - Reason: "unsupported delta type", - } - } - - ty, content, err := applyDeltaRecord(state, idx, baseType, baseContent) - if err != nil { - return objecttype.TypeInvalid, nil, err - } - - id, err := hashCanonicalObject(state.algo, ty, content) - if err != nil { - return objecttype.TypeInvalid, nil, err - } - - record.objectID = id - record.realType = ty - record.resolved = true - state.objectToRecord[id] = idx - state.baseCache.add(idx, ty, content) - - return ty, content, nil -} diff --git a/object/store/packed/internal/ingest/records.go b/object/store/packed/internal/ingest/records.go deleted file mode 100644 index 75f157fa..00000000 --- a/object/store/packed/internal/ingest/records.go +++ /dev/null @@ -1,46 +0,0 @@ -package ingest - -import ( - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// objectRecord stores metadata for one packed object entry. -type objectRecord struct { - // offset is the entry start offset in the pack file. - offset uint64 - // headerLen is packed entry header length in bytes. - headerLen uint32 - // packedLen is total packed entry length in bytes. - packedLen uint64 - // crc32 is the CRC over the full packed entry. - crc32 uint32 - // packedType is the entry type tag from the pack stream. - packedType objecttype.Type - // realType is canonical object type after delta resolution. - realType objecttype.Type - // declaredSize is the declared output object size for this entry. - declaredSize int64 - // dataOffset is compressed payload start offset for this entry. - dataOffset uint64 - // baseOffset is OFS base offset when packedType is OFS delta. - baseOffset uint64 - // baseObject is REF base object ID when packedType is REF delta. - baseObject objectid.ObjectID - // objectID is final resolved object ID. - objectID objectid.ObjectID - // resolved reports whether objectID/realType are finalized. - resolved bool -} - -// ofsDeltaRef maps one OFS delta record to its base offset. -type ofsDeltaRef struct { - baseOffset uint64 - recordIdx int -} - -// refDeltaRef maps one REF delta record to its base object ID. -type refDeltaRef struct { - baseObject objectid.ObjectID - recordIdx int -} diff --git a/object/store/packed/internal/ingest/resolve_all.go b/object/store/packed/internal/ingest/resolve_all.go deleted file mode 100644 index 90464015..00000000 --- a/object/store/packed/internal/ingest/resolve_all.go +++ /dev/null @@ -1,70 +0,0 @@ -package ingest - -import ( - "errors" - - "codeberg.org/lindenii/furgit/internal/progress" -) - -// resolveAll resolves all delta records and finalizes ObjectID/RealType for every record. -func resolveAll(state *ingestState) error { - state.unresolvedRefDeltas = state.unresolvedRefDeltas[:0] - - var pending uint32 - - for idx := range state.records { - if !state.records[idx].resolved { - pending++ - } - } - - if pending == 0 { - return nil - } - - var done uint32 - - meter := progress.New(progress.Options{ - Writer: state.opts.Progress, - Title: "resolving deltas", - Total: uint64(pending), - }) - - for idx := range state.records { - if state.records[idx].resolved { - continue - } - - done++ - meter.Set(uint64(done), 0) - - visiting := make(map[int]struct{}) - - ty, content, err := resolveRecord(state, idx, visiting) - if err != nil { - if errors.Is(err, errExternalThinBase) { - state.unresolvedRefDeltas = append(state.unresolvedRefDeltas, idx) - - continue - } - - return err - } - - id, err := hashCanonicalObject(state.algo, ty, content) - if err != nil { - return err - } - - record := &state.records[idx] - record.realType = ty - record.objectID = id - record.resolved = true - state.objectToRecord[id] = idx - state.baseCache.add(idx, ty, content) - } - - meter.Stop("done") - - return nil -} diff --git a/object/store/packed/internal/ingest/result.go b/object/store/packed/internal/ingest/result.go deleted file mode 100644 index 9a285f09..00000000 --- a/object/store/packed/internal/ingest/result.go +++ /dev/null @@ -1,23 +0,0 @@ -package ingest - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// Result describes one successful ingest transaction. -type Result struct { - // PackName is the destination-relative filename of the written .pack. - PackName string - // IdxName is the destination-relative filename of the written .idx. - IdxName string - // RevName is the destination-relative filename of the written .rev. - // - // RevName is empty when writeRev is false. - RevName string - // PackHash is the final pack hash (same hash embedded in .idx/.rev trailers). - PackHash objectid.ObjectID - // ObjectCount is the final object count in the resulting pack. - // - // If thin fixup appends objects, this includes appended base objects. - ObjectCount uint32 - // ThinFixed reports whether thin fixup appended local bases. - ThinFixed bool -} diff --git a/object/store/packed/internal/ingest/rev_write.go b/object/store/packed/internal/ingest/rev_write.go deleted file mode 100644 index 16d27085..00000000 --- a/object/store/packed/internal/ingest/rev_write.go +++ /dev/null @@ -1,137 +0,0 @@ -package ingest - -import ( - "encoding/binary" - "slices" - - "codeberg.org/lindenii/furgit/internal/intconv" - "codeberg.org/lindenii/furgit/internal/progress" -) - -const ( - revMagic = 0x52494458 - revVersion = 1 -) - -// writeRev writes rev index for resolved records. -func writeRev(state *ingestState) error { - if !state.opts.WriteRev { - return nil - } - - idxOrder := buildIdxOrder(state) - - recordToIdxPos := make([]int, len(state.records)) - for pos, recordIdx := range idxOrder { - recordToIdxPos[recordIdx] = pos - } - - packOrder := buildPackOrder(state) - - hashImpl, err := state.algo.New() - if err != nil { - return err - } - - var scratch [8]byte - - writeProgressf(state, "writing reverse index header...\r") - binary.BigEndian.PutUint32(scratch[:4], revMagic) - - err = writeAndHash(state.revFile, hashImpl, scratch[:4]) - if err != nil { - return err - } - - binary.BigEndian.PutUint32(scratch[:4], revVersion) - - err = writeAndHash(state.revFile, hashImpl, scratch[:4]) - if err != nil { - return err - } - - binary.BigEndian.PutUint32(scratch[:4], state.algo.PackHashID()) - - err = writeAndHash(state.revFile, hashImpl, scratch[:4]) - if err != nil { - return err - } - - writeProgressf(state, "writing reverse index header: done.\n") - - entriesMeter := progress.New(progress.Options{ - Writer: state.opts.Progress, - Title: "writing reverse index entries", - Total: uint64(len(packOrder)), - }) - - var entriesDone uint64 - - for _, recordIdx := range packOrder { - recordPos, err := intconv.IntToUint32(recordToIdxPos[recordIdx]) - if err != nil { - return err - } - - binary.BigEndian.PutUint32(scratch[:4], recordPos) - - err = writeAndHash(state.revFile, hashImpl, scratch[:4]) - if err != nil { - return err - } - - entriesDone++ - entriesMeter.Set(entriesDone, 0) - } - - if entriesDone > 0 { - entriesMeter.Stop("done") - } - - writeProgressf(state, "writing reverse index trailer...\r") - - err = writeAndHash(state.revFile, hashImpl, state.packHash.Bytes()) - if err != nil { - return err - } - - revHash := hashImpl.Sum(nil) - - _, err = state.revFile.Write(revHash) - if err != nil { - return err - } - - err = state.revFile.Sync() - if err != nil { - return err - } - - writeProgressf(state, "writing reverse index trailer: done.\n") - - return nil -} - -// buildPackOrder returns record indexes sorted by pack offset. -func buildPackOrder(state *ingestState) []int { - out := make([]int, 0, len(state.records)) - for idx := range state.records { - out = append(out, idx) - } - - slices.SortFunc(out, func(a, b int) int { - offA := state.records[a].offset - - offB := state.records[b].offset - switch { - case offA < offB: - return -1 - case offA > offB: - return 1 - default: - return 0 - } - }) - - return out -} diff --git a/object/store/packed/internal/ingest/rewrite_header_trailer.go b/object/store/packed/internal/ingest/rewrite_header_trailer.go deleted file mode 100644 index f1f18a39..00000000 --- a/object/store/packed/internal/ingest/rewrite_header_trailer.go +++ /dev/null @@ -1,89 +0,0 @@ -package ingest - -import ( - "encoding/binary" - "io" - - "codeberg.org/lindenii/furgit/internal/intconv" - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// rewritePackHeaderAndTrailer rewrites object count and trailer hash using ReadAt/WriteAt. -func rewritePackHeaderAndTrailer(state *ingestState) error { - var countRaw [4]byte - - recordCountUint32, err := intconv.IntToUint32(len(state.records)) - if err != nil { - return err - } - - binary.BigEndian.PutUint32(countRaw[:], recordCountUint32) - - _, err = state.packFile.WriteAt(countRaw[:], 8) - if err != nil { - return err - } - - info, err := state.packFile.Stat() - if err != nil { - return err - } - - endWithoutTrailer := info.Size() - - hashImpl, err := state.algo.New() - if err != nil { - return err - } - - var ( - buf [128 << 10]byte - pos int64 - ) - for pos < endWithoutTrailer { - want := int64(len(buf)) - - remaining := endWithoutTrailer - pos - if remaining < want { - want = remaining - } - - n, err := state.packFile.ReadAt(buf[:want], pos) - if err != nil && err != io.EOF { - return err - } - - if n == 0 { - return io.ErrUnexpectedEOF - } - - _, _ = hashImpl.Write(buf[:n]) - pos += int64(n) - } - - sum := hashImpl.Sum(nil) - - _, err = state.packFile.WriteAt(sum, endWithoutTrailer) - if err != nil { - return err - } - - packHash, err := objectid.FromBytes(state.algo, sum) - if err != nil { - return err - } - - state.packHash = packHash - state.objectCountHeader = recordCountUint32 - - sumLenInt64 := int64(len(sum)) - - newConsumed, err := intconv.Int64ToUint64(endWithoutTrailer + sumLenInt64) - if err != nil { - return err - } - - state.stream.consumed = newConsumed - - return nil -} diff --git a/object/store/packed/internal/ingest/scan.go b/object/store/packed/internal/ingest/scan.go deleted file mode 100644 index ddd1eaf3..00000000 --- a/object/store/packed/internal/ingest/scan.go +++ /dev/null @@ -1,105 +0,0 @@ -package ingest - -import ( - "fmt" - - "codeberg.org/lindenii/furgit/internal/progress" - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// streamPackAndScan copies src into temp .pack while scanning packed entries. -func streamPackAndScan(state *ingestState) error { - hashImpl, err := state.algo.New() - if err != nil { - return err - } - - state.stream = newStreamScanner( - state.src, - state.packFile, - hashImpl, - state.algo.Size(), - ) - - writeProgressf(state, "validating pack header...\r") - - err = seedStreamWithPackHeader(state) - if err != nil { - return err - } - - writeProgressf(state, "validating pack header: done.\n") - - state.records = make([]objectRecord, 0, state.objectCountHeader) - state.ofsDeltas = make([]ofsDeltaRef, 0, state.objectCountHeader) - state.refDeltas = make([]refDeltaRef, 0, state.objectCountHeader) - - total := state.objectCountHeader - meter := progress.New(progress.Options{ - Writer: state.opts.Progress, - Title: "receiving objects", - Total: uint64(total), - Throughput: true, - }) - - for i := range total { - nextOffset, err := scanOneEntry(state, state.stream.consumed) - if err != nil { - return err - } - - if nextOffset != state.stream.consumed { - return fmt.Errorf("packfile/ingest: internal stream offset mismatch") - } - - done := i + 1 - meter.Set(uint64(done), state.stream.consumed) - } - - meter.Stop("done") - - err = state.stream.finishAndFlushTrailer(state.opts.RequireTrailingEOF) - if err != nil { - return err - } - - if len(state.stream.packTrailer) != state.algo.Size() { - return fmt.Errorf("packfile/ingest: invalid trailer size") - } - - packHash, err := objectid.FromBytes(state.algo, state.stream.packTrailer) - if err != nil { - return err - } - - state.packHash = packHash - - return state.stream.flush() -} - -// seedStreamWithPackHeader writes the already-validated PACK header to output, -// seeds the running pack hash, and advances stream offset accounting. -func seedStreamWithPackHeader(state *ingestState) error { - written := 0 - for written < len(state.packHeaderRaw) { - n, err := state.packFile.Write(state.packHeaderRaw[written:]) - if err != nil { - return &DestinationWriteError{Op: fmt.Sprintf("write pack header: %v", err)} - } - - if n == 0 { - return &DestinationWriteError{Op: "write pack header: short write"} - } - - written += n - } - - _, err := state.stream.hash.Write(state.packHeaderRaw[:]) - if err != nil { - return err - } - - state.stream.consumed = packHeaderSize - - return nil -} diff --git a/object/store/packed/internal/ingest/state.go b/object/store/packed/internal/ingest/state.go deleted file mode 100644 index 0412eb32..00000000 --- a/object/store/packed/internal/ingest/state.go +++ /dev/null @@ -1,70 +0,0 @@ -package ingest - -import ( - "io" - "os" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -const ( - defaultDeltaBaseCacheMaxBytes = 32 << 20 -) - -// ingestState holds mutable state for one Ingest call. -type ingestState struct { - src io.Reader - destination *os.Root - algo objectid.Algorithm - opts Options - - packHeaderRaw [packHeaderSize]byte - - packFile *os.File - packTmpName string - idxFile *os.File - idxTmpName string - revFile *os.File - revTmpName string - - stream *streamScanner - - records []objectRecord - ofsDeltas []ofsDeltaRef - refDeltas []refDeltaRef - unresolvedRefDeltas []int - offsetToRecord map[uint64]int - objectToRecord map[objectid.ObjectID]int - - baseCache *deltaBaseCache - packHash objectid.ObjectID - - objectCountHeader uint32 - thinFixed bool -} - -// newIngestState constructs one call-local ingest state. -func newIngestState( - src io.Reader, - destination *os.Root, - algo objectid.Algorithm, - opts Options, - header packHeader, - headerRaw [packHeaderSize]byte, -) (*ingestState, error) { - if algo.Size() == 0 { - return nil, objectid.ErrInvalidAlgorithm - } - - return &ingestState{ - src: src, - destination: destination, - algo: algo, - opts: opts, - packHeaderRaw: headerRaw, - objectCountHeader: header.ObjectCount, - offsetToRecord: make(map[uint64]int), - objectToRecord: make(map[objectid.ObjectID]int), - baseCache: newDeltaBaseCache(defaultDeltaBaseCacheMaxBytes), - }, nil -} diff --git a/object/store/packed/internal/ingest/stream.go b/object/store/packed/internal/ingest/stream.go deleted file mode 100644 index a403087a..00000000 --- a/object/store/packed/internal/ingest/stream.go +++ /dev/null @@ -1,111 +0,0 @@ -package ingest - -import ( - "errors" - "hash" - "io" - "os" -) - -const streamScannerBufferSize = 64 << 10 - -// streamScanner incrementally reads/consumes one pack stream while mirroring -// consumed bytes into one destination pack file. -type streamScanner struct { - src io.Reader - dstFile *os.File - - // Input buffer window: buf[off:n] is unread. - buf []byte - off int - n int - - // Absolute consumed stream bytes. - consumed uint64 - - // Running pack hash over consumed bytes while hashEnabled is true. - hash hash.Hash - hashSize int - hashEnabled bool - - // Entry CRC state while one entry is being consumed. - entryCRC uint32 - inEntryCRC bool - - packTrailer []byte -} - -// newStreamScanner constructs one scanner with fixed input buffering. -func newStreamScanner(src io.Reader, dstFile *os.File, hash hash.Hash, hashSize int) *streamScanner { - return &streamScanner{ - src: src, - dstFile: dstFile, - buf: make([]byte, streamScannerBufferSize), - hash: hash, - hashSize: hashSize, - hashEnabled: true, - } -} - -// Read implements io.Reader. -func (scanner *streamScanner) Read(dst []byte) (int, error) { - if len(dst) == 0 { - return 0, nil - } - - if scanner.n-scanner.off == 0 { - err := scanner.fill(1) - if err != nil { - if errors.Is(err, io.EOF) { - return 0, io.EOF - } - - return 0, err - } - } - - unread := scanner.n - scanner.off - if unread == 0 { - return 0, io.EOF - } - - n := min(len(dst), unread) - - copy(dst, scanner.buf[scanner.off:scanner.off+n]) - - err := scanner.use(n) - if err != nil { - return 0, err - } - - return n, nil -} - -// ReadByte implements io.ByteReader without allocation. -func (scanner *streamScanner) ReadByte() (byte, error) { - if scanner.n-scanner.off == 0 { - err := scanner.fill(1) - if err != nil { - return 0, err - } - } - - b := scanner.buf[scanner.off] - - err := scanner.use(1) - if err != nil { - return 0, err - } - - return b, nil -} - -// readFull reads exactly len(dst) bytes through receiver. -func (scanner *streamScanner) readFull(dst []byte) error { - _, err := io.ReadFull(scanner, dst) - if err != nil { - return err - } - - return nil -} diff --git a/object/store/packed/internal/ingest/temp.go b/object/store/packed/internal/ingest/temp.go deleted file mode 100644 index d0b7862c..00000000 --- a/object/store/packed/internal/ingest/temp.go +++ /dev/null @@ -1,103 +0,0 @@ -package ingest - -import ( - "crypto/rand" - "errors" - "fmt" - "io/fs" - "os" -) - -// openTemporaryArtifacts creates/open temp pack/idx/(rev) files under destination. -func openTemporaryArtifacts(state *ingestState) error { - packName, packFile, err := createTempFile(state.destination, "tmp_pack_") - if err != nil { - return err - } - - idxName, idxFile, err := createTempFile(state.destination, "tmp_idx_") - if err != nil { - _ = packFile.Close() - _ = state.destination.Remove(packName) - - return err - } - - revName := "" - - var revFile *os.File - if state.opts.WriteRev { - revName, revFile, err = createTempFile(state.destination, "tmp_rev_") - if err != nil { - _ = idxFile.Close() - _ = state.destination.Remove(idxName) - _ = packFile.Close() - _ = state.destination.Remove(packName) - - return err - } - } - - state.packTmpName = packName - state.packFile = packFile - state.idxTmpName = idxName - state.idxFile = idxFile - state.revTmpName = revName - state.revFile = revFile - - return nil -} - -// closeTemporaryArtifacts closes all temporary artifact file descriptors. -func closeTemporaryArtifacts(state *ingestState) error { - var out error - - if state.packFile != nil { - err := state.packFile.Close() - if err != nil && out == nil { - out = err - } - - state.packFile = nil - } - - if state.idxFile != nil { - err := state.idxFile.Close() - if err != nil && out == nil { - out = err - } - - state.idxFile = nil - } - - if state.revFile != nil { - err := state.revFile.Close() - if err != nil && out == nil { - out = err - } - - state.revFile = nil - } - - return out -} - -// createTempFile creates one temporary file under root using prefix. -func createTempFile(root *os.Root, prefix string) (string, *os.File, error) { - for range 32 { - name := prefix + rand.Text() - - file, err := root.OpenFile(name, os.O_CREATE|os.O_EXCL|os.O_RDWR, 0o644) - if err == nil { - return name, file, nil - } - - if errors.Is(err, fs.ErrExist) { - continue - } - - return "", nil, fmt.Errorf("packfile/ingest: create temp file %q: %w", name, err) - } - - return "", nil, fmt.Errorf("packfile/ingest: unable to create temporary file for prefix %q", prefix) -} diff --git a/object/store/packed/internal/ingest/testdata/fixtures/sha1/METADATA.txt b/object/store/packed/internal/ingest/testdata/fixtures/sha1/METADATA.txt deleted file mode 100644 index 5fcbfe26..00000000 --- a/object/store/packed/internal/ingest/testdata/fixtures/sha1/METADATA.txt +++ /dev/null @@ -1,3 +0,0 @@ -format=sha1 -head=200c960359dad025b4170284c518919eb4a24305 -base=4bc507fc631ea78474d83c47548743c9f1dda0dc diff --git a/object/store/packed/internal/ingest/testdata/fixtures/sha1/base.pack b/object/store/packed/internal/ingest/testdata/fixtures/sha1/base.pack deleted file mode 100644 index 3d7a4903..00000000 Binary files a/object/store/packed/internal/ingest/testdata/fixtures/sha1/base.pack and /dev/null differ diff --git a/object/store/packed/internal/ingest/testdata/fixtures/sha1/nonthin.pack b/object/store/packed/internal/ingest/testdata/fixtures/sha1/nonthin.pack deleted file mode 100644 index ea07c9a0..00000000 Binary files a/object/store/packed/internal/ingest/testdata/fixtures/sha1/nonthin.pack and /dev/null differ diff --git a/object/store/packed/internal/ingest/testdata/fixtures/sha1/thin.pack b/object/store/packed/internal/ingest/testdata/fixtures/sha1/thin.pack deleted file mode 100644 index 95084feb..00000000 Binary files a/object/store/packed/internal/ingest/testdata/fixtures/sha1/thin.pack and /dev/null differ diff --git a/object/store/packed/internal/ingest/testdata/fixtures/sha256/METADATA.txt b/object/store/packed/internal/ingest/testdata/fixtures/sha256/METADATA.txt deleted file mode 100644 index 8a5ea0a2..00000000 --- a/object/store/packed/internal/ingest/testdata/fixtures/sha256/METADATA.txt +++ /dev/null @@ -1,3 +0,0 @@ -format=sha256 -head=35cc0f4cd1c73524187540494058d233a2ecbd071c85d496a2250d8e0c805ef8 -base=b4abe46895f0bb5aa22fd42d28d428413f265359734c288752e3c2d270eec276 diff --git a/object/store/packed/internal/ingest/testdata/fixtures/sha256/base.pack b/object/store/packed/internal/ingest/testdata/fixtures/sha256/base.pack deleted file mode 100644 index 52ceef74..00000000 Binary files a/object/store/packed/internal/ingest/testdata/fixtures/sha256/base.pack and /dev/null differ diff --git a/object/store/packed/internal/ingest/testdata/fixtures/sha256/nonthin.pack b/object/store/packed/internal/ingest/testdata/fixtures/sha256/nonthin.pack deleted file mode 100644 index 50db05d0..00000000 Binary files a/object/store/packed/internal/ingest/testdata/fixtures/sha256/nonthin.pack and /dev/null differ diff --git a/object/store/packed/internal/ingest/testdata/fixtures/sha256/thin.pack b/object/store/packed/internal/ingest/testdata/fixtures/sha256/thin.pack deleted file mode 100644 index b331b915..00000000 Binary files a/object/store/packed/internal/ingest/testdata/fixtures/sha256/thin.pack and /dev/null differ diff --git a/object/store/packed/internal/ingest/thin_append.go b/object/store/packed/internal/ingest/thin_append.go deleted file mode 100644 index 779d477f..00000000 --- a/object/store/packed/internal/ingest/thin_append.go +++ /dev/null @@ -1,91 +0,0 @@ -package ingest - -import ( - "compress/zlib" - "hash/crc32" - "io" - - "codeberg.org/lindenii/furgit/internal/intconv" - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// appendBaseObject appends one base object as a new packed non-delta entry. -func appendBaseObject(state *ingestState, id objectid.ObjectID, realType objecttype.Type, content []byte) (int, error) { - start := state.stream.consumed - - header := encodePackEntryHeader(realType, int64(len(content))) - - startInt64, err := intconv.Uint64ToInt64(start) - if err != nil { - return 0, err - } - - _, err = state.packFile.WriteAt(header, startInt64) - if err != nil { - return 0, err - } - - headerLenInt64 := int64(len(header)) - section := &fileSectionWriter{file: state.packFile, off: startInt64 + headerLenInt64} - crc := crc32.NewIEEE() - - _, err = crc.Write(header) - if err != nil { - return 0, err - } - - counting := &countingWriter{dst: section} - - zw := zlib.NewWriter(io.MultiWriter(counting, crc)) - - _, err = zw.Write(content) - if err != nil { - return 0, err - } - - err = zw.Close() - if err != nil { - return 0, err - } - - headerLenUint64, err := intconv.IntToUint64(len(header)) - if err != nil { - return 0, err - } - - countingNUint64, err := intconv.IntToUint64(counting.n) - if err != nil { - return 0, err - } - - packedLen := headerLenUint64 + countingNUint64 - end := start + packedLen - state.stream.consumed = end - - headerLenUint32, err := intconv.IntToUint32(len(header)) - if err != nil { - return 0, err - } - - record := objectRecord{ - offset: start, - headerLen: headerLenUint32, - packedLen: packedLen, - crc32: crc.Sum32(), - packedType: realType, - realType: realType, - declaredSize: int64(len(content)), - dataOffset: start + headerLenUint64, - objectID: id, - resolved: true, - } - - recordIdx := len(state.records) - state.records = append(state.records, record) - state.offsetToRecord[start] = recordIdx - state.objectToRecord[id] = recordIdx - state.baseCache.add(recordIdx, realType, content) - - return recordIdx, nil -} diff --git a/object/store/packed/internal/ingest/thin_fix.go b/object/store/packed/internal/ingest/thin_fix.go deleted file mode 100644 index 5d701c52..00000000 --- a/object/store/packed/internal/ingest/thin_fix.go +++ /dev/null @@ -1,99 +0,0 @@ -package ingest - -import ( - "errors" - "fmt" - - "codeberg.org/lindenii/furgit/internal/intconv" - "codeberg.org/lindenii/furgit/internal/progress" - objectstore "codeberg.org/lindenii/furgit/object/store" -) - -// maybeFixThin appends missing bases and rewrites pack header/trailer when needed. -func maybeFixThin(state *ingestState) error { - if len(state.unresolvedRefDeltas) == 0 { - return nil - } - - writeProgressf( - state, - "fixing thin pack: %d unresolved bases\r", - len(state.unresolvedRefDeltas), - ) - - if !state.opts.FixThin { - return &ThinPackUnresolvedError{Count: len(state.unresolvedRefDeltas)} - } - - if state.opts.ThinBase == nil { - return &ThinPackUnresolvedError{Count: len(state.unresolvedRefDeltas)} - } - - hashSize := int64(state.algo.Size()) - - info, err := state.packFile.Stat() - if err != nil { - return err - } - - size := info.Size() - if size < hashSize { - return fmt.Errorf("packfile/ingest: pack too short to trim trailer") - } - - newEnd := size - hashSize - - err = state.packFile.Truncate(newEnd) - if err != nil { - return err - } - - consumed, err := intconv.Int64ToUint64(newEnd) - if err != nil { - return err - } - - state.stream.consumed = consumed - - baseIDs := unresolvedThinBaseIDs(state) - - total := len(baseIDs) - meter := progress.New(progress.Options{ - Writer: state.opts.Progress, - Title: "fixing thin pack", - Total: uint64(total), - }) - meter.Set(0, 0) - - var appended uint64 - - for _, id := range baseIDs { - ty, content, err := state.opts.ThinBase.ReadBytesContent(id) - if err != nil { - if errors.Is(err, objectstore.ErrObjectNotFound) { - continue - } - - return fmt.Errorf("packfile/ingest: read thin base %s: %w", id, err) - } - - _, err = appendBaseObject(state, id, ty, content) - if err != nil { - return err - } - - state.thinFixed = true - - appended++ - meter.Set(appended, 0) - } - - err = rewritePackHeaderAndTrailer(state) - if err != nil { - return err - } - - meter.Stop(fmt.Sprintf("appended %d/%d, done", appended, total)) - - return nil -} diff --git a/object/store/packed/internal/ingest/thin_unresolved.go b/object/store/packed/internal/ingest/thin_unresolved.go deleted file mode 100644 index 757cc0e2..00000000 --- a/object/store/packed/internal/ingest/thin_unresolved.go +++ /dev/null @@ -1,34 +0,0 @@ -package ingest - -import ( - "bytes" - "slices" - - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// unresolvedThinBaseIDs returns sorted unique unresolved ref base IDs. -func unresolvedThinBaseIDs(state *ingestState) []objectid.ObjectID { - seen := make(map[objectid.ObjectID]struct{}) - - for _, idx := range state.unresolvedRefDeltas { - record := state.records[idx] - if record.packedType != objecttype.TypeRefDelta { - continue - } - - seen[record.baseObject] = struct{}{} - } - - out := make([]objectid.ObjectID, 0, len(seen)) - for id := range seen { - out = append(out, id) - } - - slices.SortFunc(out, func(a, b objectid.ObjectID) int { - return bytes.Compare(a.RawBytes(), b.RawBytes()) - }) - - return out -} diff --git a/object/store/packed/internal/ingest/trailer.go b/object/store/packed/internal/ingest/trailer.go deleted file mode 100644 index 7a26a8f2..00000000 --- a/object/store/packed/internal/ingest/trailer.go +++ /dev/null @@ -1,58 +0,0 @@ -package ingest - -import ( - "bytes" - "errors" - "fmt" - "io" -) - -// finishAndFlushTrailer reads trailer hash bytes, verifies trailer checksum, -// and optionally requires the source stream to hit EOF afterward. -func (scanner *streamScanner) finishAndFlushTrailer(requireTrailingEOF bool) error { - if scanner.hashSize <= 0 { - return fmt.Errorf("packfile/ingest: invalid hash size") - } - - trailer := make([]byte, scanner.hashSize) - - scanner.hashEnabled = false - - err := scanner.readFull(trailer) - if err != nil { - return &PackTrailerMismatchError{} - } - - scanner.packTrailer = append(scanner.packTrailer[:0], trailer...) - - if scanner.n-scanner.off > 0 { - return fmt.Errorf("packfile/ingest: pack has trailing garbage") - } - - if !requireTrailingEOF { - computed := scanner.hash.Sum(nil) - if !bytes.Equal(computed, trailer) { - return &PackTrailerMismatchError{} - } - - return nil - } - - var probe [1]byte - - n, err := scanner.Read(probe[:]) - if n > 0 || err == nil { - return fmt.Errorf("packfile/ingest: pack has trailing garbage") - } - - if !errors.Is(err, io.EOF) { - return err - } - - computed := scanner.hash.Sum(nil) - if !bytes.Equal(computed, trailer) { - return &PackTrailerMismatchError{} - } - - return nil -} diff --git a/object/store/packed/internal/ingest/use.go b/object/store/packed/internal/ingest/use.go deleted file mode 100644 index 97f8757a..00000000 --- a/object/store/packed/internal/ingest/use.go +++ /dev/null @@ -1,34 +0,0 @@ -package ingest - -import ( - "fmt" - "hash/crc32" -) - -// use consumes n unread bytes and updates accounting/checksum state. -func (scanner *streamScanner) use(n int) error { - if n < 0 || n > scanner.n-scanner.off { - return fmt.Errorf("packfile/ingest: invalid consume length %d", n) - } - - if n == 0 { - return nil - } - - chunk := scanner.buf[scanner.off : scanner.off+n] - if scanner.hashEnabled { - _, err := scanner.hash.Write(chunk) - if err != nil { - return err - } - } - - if scanner.inEntryCRC { - scanner.entryCRC = crc32.Update(scanner.entryCRC, crc32.IEEETable, chunk) - } - - scanner.off += n - scanner.consumed += uint64(n) - - return nil -} diff --git a/object/store/packed/internal/ingest/write.go b/object/store/packed/internal/ingest/write.go deleted file mode 100644 index efd27323..00000000 --- a/object/store/packed/internal/ingest/write.go +++ /dev/null @@ -1,50 +0,0 @@ -package ingest - -import ( - "bufio" - "io" - "os" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// WritePack ingests one pack stream into destination and writes pack artifacts. -// -// Artifacts are published under content-addressed final names derived from the -// resulting pack hash. If those final names already exist, WritePack treats -// that as success and removes its temporary files. -func WritePack( - destination *os.Root, - algo objectid.Algorithm, - src io.Reader, - opts Options, -) (Result, error) { - if algo.Size() == 0 { - return Result{}, objectid.ErrInvalidAlgorithm - } - - reader := bufio.NewReader(src) - - header, headerRaw, err := readAndValidatePackHeader(reader) - if err != nil { - return Result{}, err - } - - if header.ObjectCount == 0 { - return discardZeroObjectPack(reader, algo, opts, headerRaw) - } - - state, err := newIngestState( - reader, - destination, - algo, - opts, - header, - headerRaw, - ) - if err != nil { - return Result{}, err - } - - return ingest(state) -} diff --git a/object/store/packed/internal/ingest/write_empty.go b/object/store/packed/internal/ingest/write_empty.go deleted file mode 100644 index 0d3401f0..00000000 --- a/object/store/packed/internal/ingest/write_empty.go +++ /dev/null @@ -1,58 +0,0 @@ -package ingest - -import ( - "bytes" - "errors" - "io" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -func discardZeroObjectPack( - src io.Reader, - algo objectid.Algorithm, - opts Options, - headerRaw [packHeaderSize]byte, -) (Result, error) { - hashImpl, err := algo.New() - if err != nil { - return Result{}, err - } - - _, _ = hashImpl.Write(headerRaw[:]) - - trailer := make([]byte, algo.Size()) - - _, err = io.ReadFull(src, trailer) - if err != nil { - return Result{}, &PackTrailerMismatchError{} - } - - computed := hashImpl.Sum(nil) - if !bytes.Equal(computed, trailer) { - return Result{}, &PackTrailerMismatchError{} - } - - if opts.RequireTrailingEOF { - var probe [1]byte - - n, err := src.Read(probe[:]) - if n > 0 || err == nil { - return Result{}, errors.New("packfile/ingest: pack has trailing garbage") - } - - if err != io.EOF { - return Result{}, err - } - } - - packHash, err := objectid.FromBytes(algo, trailer) - if err != nil { - return Result{}, err - } - - return Result{ - PackHash: packHash, - ObjectCount: 0, - }, nil -} diff --git a/object/store/packed/internal/reading/TODO b/object/store/packed/internal/reading/TODO deleted file mode 100644 index f4a5f48e..00000000 --- a/object/store/packed/internal/reading/TODO +++ /dev/null @@ -1,3 +0,0 @@ -* Per delta-plan memo map -* Internal handle/request context (might expose it externally later and add to global interface) -* Audit on mutex diff --git a/object/store/packed/internal/reading/close.go b/object/store/packed/internal/reading/close.go deleted file mode 100644 index 62c62025..00000000 --- a/object/store/packed/internal/reading/close.go +++ /dev/null @@ -1,35 +0,0 @@ -package reading - -// Close releases mapped pack/index resources associated with the store. -// -// Labels: MT-Unsafe. -func (store *Store) Close() error { - store.stateMu.Lock() - packs := store.packs - store.stateMu.Unlock() - store.idxMu.RLock() - indexes := store.idxByPack - store.idxMu.RUnlock() - - var closeErr error - - for _, pack := range packs { - err := pack.close() - if err != nil && closeErr == nil { - closeErr = err - } - } - - for _, index := range indexes { - err := index.close() - if err != nil && closeErr == nil { - closeErr = err - } - } - - store.cacheMu.Lock() - store.deltaCache.clear() - store.cacheMu.Unlock() - - return closeErr -} diff --git a/object/store/packed/internal/reading/delta_build_chain.go b/object/store/packed/internal/reading/delta_build_chain.go deleted file mode 100644 index a0e3151d..00000000 --- a/object/store/packed/internal/reading/delta_build_chain.go +++ /dev/null @@ -1,65 +0,0 @@ -package reading - -import ( - "fmt" - - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// deltaBuildChain walks one object's chain and builds a reconstruction chain. -func (store *Store) deltaBuildChain(start location) (deltaChain, error) { - visited := make(map[location]struct{}) - current := start - - var chain deltaChain - - for { - if _, ok := visited[current]; ok { - return deltaChain{}, fmt.Errorf("objectstore/packed: delta cycle while resolving object") - } - - visited[current] = struct{}{} - - _, meta, err := store.entryMetaAt(current) - if err != nil { - return deltaChain{}, err - } - - if meta.ty.IsBaseObject() { - chain.baseLoc = current - chain.baseType = meta.ty - - return chain, nil - } - - switch meta.ty { - case objecttype.TypeRefDelta: - chain.deltas = append(chain.deltas, deltaNode{ - loc: current, - dataOffset: meta.dataOffset, - }) - - next, err := store.lookup(meta.baseRefID) - if err != nil { - return deltaChain{}, err - } - - current = next - case objecttype.TypeOfsDelta: - chain.deltas = append(chain.deltas, deltaNode{ - loc: current, - dataOffset: meta.dataOffset, - }) - current = location{ - packName: current.packName, - offset: meta.baseOfs, - } - case objecttype.TypeCommit, objecttype.TypeTree, objecttype.TypeBlob, objecttype.TypeTag: - return deltaChain{}, fmt.Errorf("objectstore/packed: internal invariant violation for base type %d", meta.ty) - case objecttype.TypeInvalid, objecttype.TypeFuture: - return deltaChain{}, fmt.Errorf("objectstore/packed: unsupported pack type %d", meta.ty) - default: - return deltaChain{}, fmt.Errorf("objectstore/packed: unsupported pack type %d", meta.ty) - } - } -} diff --git a/object/store/packed/internal/reading/delta_cache.go b/object/store/packed/internal/reading/delta_cache.go deleted file mode 100644 index 4259eb81..00000000 --- a/object/store/packed/internal/reading/delta_cache.go +++ /dev/null @@ -1,61 +0,0 @@ -package reading - -import ( - "codeberg.org/lindenii/furgit/internal/lru" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -const defaultDeltaCacheMaxBytes = 32 << 20 - -// deltaBaseKey identifies one base object by pack location. -type deltaBaseKey struct { - packName string - offset uint64 -} - -// deltaBaseValue stores one cached base object body. -type deltaBaseValue struct { - ty objecttype.Type - content []byte -} - -// deltaCache wraps a weighted LRU for resolved delta bases. -type deltaCache struct { - lru *lru.Cache[deltaBaseKey, deltaBaseValue] -} - -// newDeltaCache creates a delta base cache with a byte budget. -func newDeltaCache(maxBytes int64) *deltaCache { - return &deltaCache{ - lru: lru.New( - maxBytes, - func(_ deltaBaseKey, value deltaBaseValue) int64 { - return int64(len(value.content)) - }, - nil, - ), - } -} - -// get returns a cloned cached base object value. -func (cache *deltaCache) get(key deltaBaseKey) (objecttype.Type, []byte, bool) { - value, ok := cache.lru.Get(key) - if !ok { - return objecttype.TypeInvalid, nil, false - } - - return value.ty, append([]byte(nil), value.content...), true -} - -// add stores a cloned base object value. -func (cache *deltaCache) add(key deltaBaseKey, ty objecttype.Type, content []byte) { - cache.lru.Add(key, deltaBaseValue{ - ty: ty, - content: append([]byte(nil), content...), - }) -} - -// clear removes all cached entries. -func (cache *deltaCache) clear() { - cache.lru.Clear() -} diff --git a/object/store/packed/internal/reading/delta_chain.go b/object/store/packed/internal/reading/delta_chain.go deleted file mode 100644 index 6e82873e..00000000 --- a/object/store/packed/internal/reading/delta_chain.go +++ /dev/null @@ -1,13 +0,0 @@ -package reading - -import objecttype "codeberg.org/lindenii/furgit/object/type" - -// deltaChain describes how to reconstruct one requested object. -type deltaChain struct { - // baseLoc points to the innermost base object. - baseLoc location - // baseType is the canonical object type resolved from baseLoc. - baseType objecttype.Type - // deltas contains delta objects from target down toward base. - deltas []deltaNode -} diff --git a/object/store/packed/internal/reading/delta_node.go b/object/store/packed/internal/reading/delta_node.go deleted file mode 100644 index 56f7b078..00000000 --- a/object/store/packed/internal/reading/delta_node.go +++ /dev/null @@ -1,9 +0,0 @@ -package reading - -// deltaNode describes one delta object in a reconstruction chain. -type deltaNode struct { - // loc identifies the delta object's pack location. - loc location - // dataOffset points to the start of the delta zlib payload in pack. - dataOffset int -} diff --git a/object/store/packed/internal/reading/delta_resolve_chain.go b/object/store/packed/internal/reading/delta_resolve_chain.go deleted file mode 100644 index ec9c39e2..00000000 --- a/object/store/packed/internal/reading/delta_resolve_chain.go +++ /dev/null @@ -1,61 +0,0 @@ -package reading - -import ( - "fmt" - - deltaapply "codeberg.org/lindenii/furgit/format/packfile/delta/apply" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// deltaResolveChain resolves one object chain into content bytes. -func (store *Store) deltaResolveChain(chain deltaChain, declaredSize int64) (objecttype.Type, []byte, error) { - ty, out, nextDelta, err := store.deltaResolveChainStart(chain) - if err != nil { - return objecttype.TypeInvalid, nil, err - } - - for i := nextDelta; i >= 0; i-- { - node := chain.deltas[i] - - pack, err := store.openPack(node.loc.packName) - if err != nil { - return objecttype.TypeInvalid, nil, err - } - - delta, err := inflateAt(pack, node.dataOffset, -1) - if err != nil { - return objecttype.TypeInvalid, nil, err - } - - out, err = deltaapply.Apply(out, delta) - if err != nil { - return objecttype.TypeInvalid, nil, err - } - - store.cacheMu.Lock() - store.deltaCache.add( - deltaBaseKey{packName: node.loc.packName, offset: node.loc.offset}, - ty, - out, - ) - store.cacheMu.Unlock() - } - - if int64(len(out)) != declaredSize { - return objecttype.TypeInvalid, nil, fmt.Errorf( - "objectstore/packed: resolved content size mismatch: got %d want %d", - len(out), - declaredSize, - ) - } - - if ty != chain.baseType { - return objecttype.TypeInvalid, nil, fmt.Errorf( - "objectstore/packed: resolved content type mismatch: got %d want %d", - ty, - chain.baseType, - ) - } - - return ty, out, nil -} diff --git a/object/store/packed/internal/reading/delta_resolve_chain_start.go b/object/store/packed/internal/reading/delta_resolve_chain_start.go deleted file mode 100644 index 17274027..00000000 --- a/object/store/packed/internal/reading/delta_resolve_chain_start.go +++ /dev/null @@ -1,58 +0,0 @@ -package reading - -import ( - "fmt" - - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// deltaResolveChainStart finds the nearest cached chain node or inflates the -// innermost base object. It returns the starting bytes and the next delta index -// to apply in reverse order. -func (store *Store) deltaResolveChainStart(chain deltaChain) (objecttype.Type, []byte, int, error) { - for i, node := range chain.deltas { - store.cacheMu.RLock() - ty, out, ok := store.deltaCache.get( - deltaBaseKey{packName: node.loc.packName, offset: node.loc.offset}, - ) - store.cacheMu.RUnlock() - - if ok { - return ty, out, i - 1, nil - } - } - - store.cacheMu.RLock() - ty, out, ok := store.deltaCache.get( - deltaBaseKey{packName: chain.baseLoc.packName, offset: chain.baseLoc.offset}, - ) - store.cacheMu.RUnlock() - - if ok { - return ty, out, len(chain.deltas) - 1, nil - } - - pack, meta, err := store.entryMetaAt(chain.baseLoc) - if err != nil { - return objecttype.TypeInvalid, nil, 0, err - } - - if !meta.ty.IsBaseObject() { - return objecttype.TypeInvalid, nil, 0, fmt.Errorf("objectstore/packed: delta chain base is not a base object") - } - - base, err := inflateAt(pack, meta.dataOffset, meta.size) - if err != nil { - return objecttype.TypeInvalid, nil, 0, err - } - - store.cacheMu.Lock() - store.deltaCache.add( - deltaBaseKey{packName: chain.baseLoc.packName, offset: chain.baseLoc.offset}, - meta.ty, - base, - ) - store.cacheMu.Unlock() - - return meta.ty, base, len(chain.deltas) - 1, nil -} diff --git a/object/store/packed/internal/reading/delta_resolve_content.go b/object/store/packed/internal/reading/delta_resolve_content.go deleted file mode 100644 index 71eb69cf..00000000 --- a/object/store/packed/internal/reading/delta_resolve_content.go +++ /dev/null @@ -1,26 +0,0 @@ -package reading - -import objecttype "codeberg.org/lindenii/furgit/object/type" - -// deltaResolveContent resolves one object's content bytes from its pack location. -func (store *Store) deltaResolveContent(start location) (objecttype.Type, []byte, error) { - chain, err := store.deltaBuildChain(start) - if err != nil { - return objecttype.TypeInvalid, nil, err - } - - pack, meta, err := store.entryMetaAt(start) - if err != nil { - return objecttype.TypeInvalid, nil, err - } - - declaredSize := meta.size - if !meta.ty.IsBaseObject() { - declaredSize, err = deltaDeclaredSizeAt(pack, meta.dataOffset) - if err != nil { - return objecttype.TypeInvalid, nil, err - } - } - - return store.deltaResolveChain(chain, declaredSize) -} diff --git a/object/store/packed/internal/reading/delta_size.go b/object/store/packed/internal/reading/delta_size.go deleted file mode 100644 index 8a85fad9..00000000 --- a/object/store/packed/internal/reading/delta_size.go +++ /dev/null @@ -1,27 +0,0 @@ -package reading - -import ( - "bufio" - - deltaapply "codeberg.org/lindenii/furgit/format/packfile/delta/apply" -) - -// deltaDeclaredSizeAt returns the resolved object size declared by one delta -// stream header at dataOffset. -func deltaDeclaredSizeAt(pack *packFile, dataOffset int) (int64, error) { - reader, err := zlibReaderAt(pack, dataOffset) - if err != nil { - return 0, err - } - - defer func() { _ = reader.Close() }() - - br := bufio.NewReaderSize(reader, 32) - - _, size, err := deltaapply.ReadHeaderSizes(br) - if err != nil { - return 0, err - } - - return int64(size), nil -} diff --git a/object/store/packed/internal/reading/doc.go b/object/store/packed/internal/reading/doc.go deleted file mode 100644 index a513d3bd..00000000 --- a/object/store/packed/internal/reading/doc.go +++ /dev/null @@ -1,6 +0,0 @@ -// Package reading implements the packed-store read path: pack and index -// discovery, lookup, caching, and object reads from existing packfiles. -// -// Obviously, this internal package is not meant to be used by anyone -// other than object/store/packed. -package reading diff --git a/object/store/packed/internal/reading/entry_inflate.go b/object/store/packed/internal/reading/entry_inflate.go deleted file mode 100644 index 82b2a7a8..00000000 --- a/object/store/packed/internal/reading/entry_inflate.go +++ /dev/null @@ -1,64 +0,0 @@ -package reading - -import ( - "bytes" - "fmt" - "io" - "math" - - "codeberg.org/lindenii/furgit/internal/compress/zlib" - "codeberg.org/lindenii/furgit/internal/iolimit" -) - -// zlibReaderAt opens a zlib reader starting at data offset within pack. -func zlibReaderAt(pack *packFile, offset int) (io.ReadCloser, error) { - if offset < 0 || offset > len(pack.data) { - return nil, fmt.Errorf("objectstore/packed: pack %q zlib offset out of bounds", pack.name) - } - - return zlib.NewReader(bytes.NewReader(pack.data[offset:])) -} - -// inflateAt inflates one entry payload from data offset. -func inflateAt(pack *packFile, offset int, expectedSize int64) ([]byte, error) { - reader, err := zlibReaderAt(pack, offset) - if err != nil { - return nil, err - } - - defer func() { _ = reader.Close() }() - - if expectedSize >= 0 { - if expectedSize > int64(math.MaxInt) { - return nil, fmt.Errorf( - "objectstore/packed: pack %q expected inflated size overflows int: %d", - pack.name, - expectedSize, - ) - } - - reader := iolimit.ExpectLengthReader(reader, expectedSize) - body := make([]byte, int(expectedSize)) - - _, err := io.ReadFull(reader, body) - if err != nil { - return nil, err - } - - var probe [1]byte - - _, err = reader.Read(probe[:]) - if err != nil && err != io.EOF { - return nil, err - } - - return body, nil - } - - body, err := io.ReadAll(reader) - if err != nil { - return nil, err - } - - return body, nil -} diff --git a/object/store/packed/internal/reading/entry_meta.go b/object/store/packed/internal/reading/entry_meta.go deleted file mode 100644 index 336dc3b9..00000000 --- a/object/store/packed/internal/reading/entry_meta.go +++ /dev/null @@ -1,16 +0,0 @@ -package reading - -// entryMetaAt parses one pack entry header at location. -func (store *Store) entryMetaAt(loc location) (*packFile, entryMeta, error) { - pack, err := store.openPack(loc.packName) - if err != nil { - return nil, entryMeta{}, err - } - - meta, err := parseEntryMeta(pack, store.algo, loc.offset) - if err != nil { - return nil, entryMeta{}, err - } - - return pack, meta, nil -} diff --git a/object/store/packed/internal/reading/entry_parse.go b/object/store/packed/internal/reading/entry_parse.go deleted file mode 100644 index ecbfb6cb..00000000 --- a/object/store/packed/internal/reading/entry_parse.go +++ /dev/null @@ -1,71 +0,0 @@ -package reading - -import ( - "fmt" - - packfmt "codeberg.org/lindenii/furgit/format/packfile" - "codeberg.org/lindenii/furgit/internal/intconv" - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// entryMeta describes one parsed pack entry header. -type entryMeta struct { - // ty is the pack entry type tag. - ty objecttype.Type - // size is the declared resulting content size. - size int64 - // dataOffset points to the zlib payload start. - dataOffset int - // baseRefID is set for ref-delta entries. - baseRefID objectid.ObjectID - // baseOfs is set for ofs-delta entries. - baseOfs uint64 -} - -// parseEntryMeta parses one pack entry header at offset. -func parseEntryMeta(pack *packFile, algo objectid.Algorithm, offset uint64) (entryMeta, error) { - var zero entryMeta - if offset >= uint64(len(pack.data)) { - return zero, fmt.Errorf("objectstore/packed: pack %q offset %d out of bounds", pack.name, offset) - } - - pos, err := intconv.Uint64ToInt(offset) - if err != nil { - return zero, fmt.Errorf("objectstore/packed: pack %q offset conversion: %w", pack.name, err) - } - - entry, err := packfmt.ParseEntry(pack.data[pos:], algo.Size()) - if err != nil { - return zero, fmt.Errorf("objectstore/packed: pack %q: %w", pack.name, err) - } - - meta := entryMeta{ - ty: entry.Type, - size: entry.Size, - dataOffset: pos + entry.DataOffset, - } - switch meta.ty { - case objecttype.TypeRefDelta: - baseID, err := objectid.FromBytes(algo, entry.RefBaseID) - if err != nil { - return zero, fmt.Errorf("objectstore/packed: pack %q invalid ref-delta base id: %w", pack.name, err) - } - - meta.baseRefID = baseID - case objecttype.TypeOfsDelta: - if offset <= entry.OfsBaseDistance { - return zero, fmt.Errorf("objectstore/packed: pack %q has invalid ofs-delta base", pack.name) - } - - meta.baseOfs = offset - entry.OfsBaseDistance - case objecttype.TypeCommit, objecttype.TypeTree, objecttype.TypeBlob, objecttype.TypeTag: - // Base object types do not have delta base metadata. - case objecttype.TypeInvalid, objecttype.TypeFuture: - return zero, fmt.Errorf("objectstore/packed: pack %q has unsupported entry type %d", pack.name, meta.ty) - default: - return zero, fmt.Errorf("objectstore/packed: pack %q has unsupported entry type %d", pack.name, meta.ty) - } - - return meta, nil -} diff --git a/object/store/packed/internal/reading/helpers_test.go b/object/store/packed/internal/reading/helpers_test.go deleted file mode 100644 index 5a37d2f1..00000000 --- a/object/store/packed/internal/reading/helpers_test.go +++ /dev/null @@ -1,102 +0,0 @@ -package reading_test - -import ( - "fmt" - "io" - "strconv" - "strings" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - objectheader "codeberg.org/lindenii/furgit/object/header" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/store/packed" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -func openPackedStore(t *testing.T, testRepo *testgit.TestRepo, algo objectid.Algorithm) *packed.Store { - t.Helper() - - root := testRepo.OpenPackRoot(t) - - store, err := packed.New(root, algo, packed.Options{}) - if err != nil { - t.Fatalf("packed.New: %v", err) - } - - return store -} - -func mustReadAllAndClose(t *testing.T, reader io.ReadCloser) []byte { - t.Helper() - - data, err := io.ReadAll(reader) - if err != nil { - _ = reader.Close() - - t.Fatalf("ReadAll: %v", err) - } - - err = reader.Close() - if err != nil { - t.Fatalf("Close: %v", err) - } - - return data -} - -func expectedRawObject(t *testing.T, testRepo *testgit.TestRepo, id objectid.ObjectID) (objecttype.Type, []byte, []byte) { - t.Helper() - - typeName := testRepo.Run(t, "cat-file", "-t", id.String()) - - ty, ok := objecttype.Parse(typeName) - if !ok { - t.Fatalf("ParseName(%q) failed", typeName) - } - - body := testRepo.CatFile(t, typeName, id) - - header, ok := objectheader.Encode(ty, int64(len(body))) - if !ok { - t.Fatalf("objectheader.Encode failed") - } - - raw := make([]byte, len(header)+len(body)) - copy(raw, header) - copy(raw[len(header):], body) - - return ty, body, raw -} - -func createPackedFixtureRepo(t *testing.T, algo objectid.Algorithm) (*testgit.TestRepo, []objectid.ObjectID) { - t.Helper() - - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - blobID, treeID, commitID := testRepo.MakeCommit(t, "packed store base commit") - testRepo.Run(t, "update-ref", "refs/heads/main", commitID.String()) - tagID := testRepo.TagAnnotated(t, "v1.0.0", commitID, "packed-store-tag") - - parent := commitID - - for i := range 24 { - content := "common-prefix\n" + strings.Repeat("line-"+strconv.Itoa(i%3)+"\n", 256) + fmt.Sprintf("tail-%d\n", i) - nextBlob, nextTree := testRepo.MakeSingleFileTree(t, fmt.Sprintf("file-%02d.txt", i), []byte(content)) - nextCommit := testRepo.CommitTree(t, nextTree, fmt.Sprintf("commit-%02d", i), parent) - testRepo.Run(t, "update-ref", "refs/heads/main", nextCommit.String()) - parent = nextCommit - - _ = nextBlob - _ = nextTree - } - - testRepo.Repack(t, "-a", "-d", "-f", "--window=64", "--depth=64") - - return testRepo, []objectid.ObjectID{ - blobID, - treeID, - commitID, - tagID, - parent, - } -} diff --git a/object/store/packed/internal/reading/idx.go b/object/store/packed/internal/reading/idx.go deleted file mode 100644 index 3c91e1a2..00000000 --- a/object/store/packed/internal/reading/idx.go +++ /dev/null @@ -1,36 +0,0 @@ -package reading - -import ( - "os" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// idxFile stores one mapped and validated idx v2 file. -type idxFile struct { - // idxName is the basename of this .idx file. - idxName string - // packName is the matching .pack basename. - packName string - // algo is the hash algorithm encoded by the index. - algo objectid.Algorithm - - // file is the opened index file descriptor. - file *os.File - // data is the mapped index bytes. - data []byte - - // fanout stores fanout table values. - fanout [256]uint32 - // numObjects equals fanout[255]. - numObjects int - - // namesOffset starts the sorted object-id table. - namesOffset int - // offset32Offset starts the 32-bit offset table. - offset32Offset int - // offset64Offset starts the 64-bit offset table. - offset64Offset int - // offset64Count is the number of 64-bit offset entries. - offset64Count int -} diff --git a/object/store/packed/internal/reading/idx_candidates_mru.go b/object/store/packed/internal/reading/idx_candidates_mru.go deleted file mode 100644 index 08ab6f85..00000000 --- a/object/store/packed/internal/reading/idx_candidates_mru.go +++ /dev/null @@ -1,136 +0,0 @@ -package reading - -// packCandidateNode is one node in the candidate MRU order list. -type packCandidateNode struct { - packName string - prev *packCandidateNode - next *packCandidateNode -} - -func (store *Store) reconcileMRU(candidates []packCandidate) { - store.mruMu.Lock() - defer store.mruMu.Unlock() - - if store.mruNodeByPack == nil { - store.mruNodeByPack = make(map[string]*packCandidateNode, len(candidates)) - } - - present := make(map[string]struct{}, len(candidates)) - for _, candidate := range candidates { - present[candidate.packName] = struct{}{} - } - - ordered := make([]string, 0, len(candidates)) - - for node := store.mruHead; node != nil; node = node.next { - if _, ok := present[node.packName]; !ok { - continue - } - - ordered = append(ordered, node.packName) - delete(present, node.packName) - } - - for _, candidate := range candidates { - if _, ok := present[candidate.packName]; !ok { - continue - } - - ordered = append(ordered, candidate.packName) - delete(present, candidate.packName) - } - - store.mruHead = nil - store.mruTail = nil - store.mruNodeByPack = make(map[string]*packCandidateNode, len(ordered)) - - for _, packName := range ordered { - node := &packCandidateNode{ - packName: packName, - prev: store.mruTail, - } - if store.mruTail != nil { - store.mruTail.next = node - } - - if store.mruHead == nil { - store.mruHead = node - } - - store.mruTail = node - store.mruNodeByPack[packName] = node - } -} - -// touchCandidate moves one candidate to the front of the lookup order. -// This is done on a best-effort basis. -func (store *Store) touchCandidate(packName string) { - if !store.mruMu.TryLock() { - return - } - defer store.mruMu.Unlock() - - node := store.mruNodeByPack[packName] - if node == nil || node == store.mruHead { - return - } - - if node.prev != nil { - node.prev.next = node.next - } - - if node.next != nil { - node.next.prev = node.prev - } - - if store.mruTail == node { - store.mruTail = node.prev - } - - node.prev = nil - - node.next = store.mruHead - if store.mruHead != nil { - store.mruHead.prev = node - } - - store.mruHead = node - if store.mruTail == nil { - store.mruTail = node - } -} - -// firstCandidatePackName returns the current head pack name, or "" when none -// are available. -func (store *Store) firstCandidatePackName(snapshot *candidateSnapshot) string { - store.mruMu.RLock() - defer store.mruMu.RUnlock() - - for node := store.mruHead; node != nil; node = node.next { - if _, ok := snapshot.candidateByPack[node.packName]; ok { - return node.packName - } - } - - return "" -} - -// nextCandidatePackName returns the pack name after currentPack in current MRU -// order, or "" at end / when currentPack is not present. -func (store *Store) nextCandidatePackName(currentPack string, snapshot *candidateSnapshot) string { - store.mruMu.RLock() - defer store.mruMu.RUnlock() - - node := store.mruNodeByPack[currentPack] - if node == nil { - return "" - } - - for node = node.next; node != nil; node = node.next { - if _, ok := snapshot.candidateByPack[node.packName]; ok { - return node.packName - } - } - - return "" -} diff --git a/object/store/packed/internal/reading/idx_close.go b/object/store/packed/internal/reading/idx_close.go deleted file mode 100644 index 1590854c..00000000 --- a/object/store/packed/internal/reading/idx_close.go +++ /dev/null @@ -1,28 +0,0 @@ -package reading - -import "syscall" - -// close unmaps and closes one idx handle. -func (index *idxFile) close() error { - var closeErr error - - if index.data != nil { - err := syscall.Munmap(index.data) - if err != nil && closeErr == nil { - closeErr = err - } - - index.data = nil - } - - if index.file != nil { - err := index.file.Close() - if err != nil && closeErr == nil { - closeErr = err - } - - index.file = nil - } - - return closeErr -} diff --git a/object/store/packed/internal/reading/idx_lookup.go b/object/store/packed/internal/reading/idx_lookup.go deleted file mode 100644 index bb02fb20..00000000 --- a/object/store/packed/internal/reading/idx_lookup.go +++ /dev/null @@ -1,91 +0,0 @@ -package reading - -import ( - "bytes" - "encoding/binary" - "fmt" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// lookup resolves one object ID to its pack offset within this index. -func (index *idxFile) lookup(id objectid.ObjectID) (uint64, bool, error) { - if id.Algorithm() != index.algo { - return 0, false, fmt.Errorf("objectstore/packed: object id algorithm mismatch") - } - - idBytes := (&id).RawBytes() - - hashSize := len(idBytes) - if hashSize != index.algo.Size() { - return 0, false, fmt.Errorf("objectstore/packed: unexpected object id length") - } - - first := int(idBytes[0]) - - lo := 0 - if first > 0 { - lo = int(index.fanout[first-1]) - } - - hi := int(index.fanout[first]) - if lo < 0 || hi < 0 || lo > hi || hi > index.numObjects { - return 0, false, fmt.Errorf("objectstore/packed: idx %q has invalid fanout bounds", index.idxName) - } - - for lo < hi { - mid := lo + (hi-lo)/2 - - nameOffset := index.namesOffset + mid*hashSize - if nameOffset < 0 || nameOffset+hashSize > len(index.data) { - return 0, false, fmt.Errorf("objectstore/packed: idx %q truncated name table", index.idxName) - } - - cmp := bytes.Compare(index.data[nameOffset:nameOffset+hashSize], idBytes) - if cmp == 0 { - offset, err := index.offsetAt(mid) - if err != nil { - return 0, false, err - } - - return offset, true, nil - } - - if cmp < 0 { - lo = mid + 1 - } else { - hi = mid - } - } - - return 0, false, nil -} - -// offsetAt resolves the pack offset for one object index entry. -func (index *idxFile) offsetAt(objectIndex int) (uint64, error) { - if objectIndex < 0 || objectIndex >= index.numObjects { - return 0, fmt.Errorf("objectstore/packed: idx %q offset index out of bounds", index.idxName) - } - - wordOffset := index.offset32Offset + objectIndex*4 - if wordOffset < 0 || wordOffset+4 > len(index.data) { - return 0, fmt.Errorf("objectstore/packed: idx %q truncated 32-bit offset table", index.idxName) - } - - word := binary.BigEndian.Uint32(index.data[wordOffset : wordOffset+4]) - if word&0x80000000 == 0 { - return uint64(word), nil - } - - pos := int(word & 0x7fffffff) - if pos < 0 || pos >= index.offset64Count { - return 0, fmt.Errorf("objectstore/packed: idx %q invalid 64-bit offset position", index.idxName) - } - - offOffset := index.offset64Offset + pos*8 - if offOffset < 0 || offOffset+8 > len(index.data)-2*index.algo.Size() { - return 0, fmt.Errorf("objectstore/packed: idx %q truncated 64-bit offset table", index.idxName) - } - - return binary.BigEndian.Uint64(index.data[offOffset : offOffset+8]), nil -} diff --git a/object/store/packed/internal/reading/idx_lookup_candidates.go b/object/store/packed/internal/reading/idx_lookup_candidates.go deleted file mode 100644 index c89ada7a..00000000 --- a/object/store/packed/internal/reading/idx_lookup_candidates.go +++ /dev/null @@ -1,126 +0,0 @@ -package reading - -import ( - "fmt" - "os" - "slices" - "strings" -) - -// packCandidate describes one discovered pack/index pair. -type packCandidate struct { - // packName is the .pack basename. - packName string - // idxName is the .idx basename. - idxName string - // mtime is the pack file modification time for initial ordering. - mtime int64 -} - -type candidateSnapshot struct { - candidates []packCandidate - candidateByPack map[string]packCandidate -} - -// Refresh rescans objects/pack and atomically installs a fresh candidate list -// for future lookups. -// -// Refresh does not invalidate existing readers. Cached pack/index mappings, -// including ones for previously visible candidates, may be retained until -// Close. -func (store *Store) Refresh() error { - store.refreshMu.Lock() - defer store.refreshMu.Unlock() - - candidates, err := store.discoverCandidates() - if err != nil { - return err - } - - candidateByPack := make(map[string]packCandidate, len(candidates)) - for _, candidate := range candidates { - candidateByPack[candidate.packName] = candidate - } - - store.reconcileMRU(candidates) - - store.candidates.Store(&candidateSnapshot{ - candidates: candidates, - candidateByPack: candidateByPack, - }) - - return nil -} - -func (store *Store) ensureCandidates() (*candidateSnapshot, error) { - snapshot := store.candidates.Load() - if snapshot != nil { - return snapshot, nil - } - - err := store.Refresh() - if err != nil { - return nil, err - } - - return store.candidates.Load(), nil -} - -// discoverCandidates scans the objects/pack root and returns sorted pack/index -// pairs. -func (store *Store) discoverCandidates() ([]packCandidate, error) { - dir, err := store.root.Open(".") - if err != nil { - if os.IsNotExist(err) { - return nil, nil - } - - return nil, err - } - - defer func() { _ = dir.Close() }() - - entries, err := dir.ReadDir(-1) - if err != nil { - return nil, err - } - - candidates := make([]packCandidate, 0, len(entries)) - for _, entry := range entries { - if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".idx") { - continue - } - - idxName := entry.Name() - packName := strings.TrimSuffix(idxName, ".idx") + ".pack" - - packInfo, err := store.root.Stat(packName) - if err != nil { - if os.IsNotExist(err) { - return nil, fmt.Errorf("objectstore/packed: missing pack file for index %q", idxName) - } - - return nil, err - } - - candidates = append(candidates, packCandidate{ - packName: packName, - idxName: idxName, - mtime: packInfo.ModTime().UnixNano(), - }) - } - - slices.SortFunc(candidates, func(a, b packCandidate) int { - if a.mtime != b.mtime { - if a.mtime > b.mtime { - return -1 - } - - return 1 - } - - return strings.Compare(a.packName, b.packName) - }) - - return candidates, nil -} diff --git a/object/store/packed/internal/reading/idx_open.go b/object/store/packed/internal/reading/idx_open.go deleted file mode 100644 index 8f73c867..00000000 --- a/object/store/packed/internal/reading/idx_open.go +++ /dev/null @@ -1,98 +0,0 @@ -package reading - -import ( - "fmt" - "os" - "syscall" - - "codeberg.org/lindenii/furgit/internal/intconv" - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// openIndex returns one opened and parsed index, caching it by pack basename. -func (store *Store) openIndex(candidate packCandidate) (*idxFile, error) { - store.idxMu.RLock() - - index, ok := store.idxByPack[candidate.packName] - if ok { - store.idxMu.RUnlock() - - return index, nil - } - - store.idxMu.RUnlock() - - index, err := openIdxFile(store.root, candidate.idxName, candidate.packName, store.algo) - if err != nil { - return nil, err - } - - store.idxMu.Lock() - - existing, ok := store.idxByPack[candidate.packName] - if ok { - store.idxMu.Unlock() - - _ = index.close() - - return existing, nil - } - - store.idxByPack[candidate.packName] = index - store.idxMu.Unlock() - - return index, nil -} - -// openIdxFile maps and validates one idx v2 file. -func openIdxFile(root *os.Root, idxName, packName string, algo objectid.Algorithm) (*idxFile, error) { - file, err := root.Open(idxName) - if err != nil { - return nil, err - } - - info, err := file.Stat() - if err != nil { - _ = file.Close() - - return nil, err - } - - size := info.Size() - if size < 0 || size > int64(int(^uint(0)>>1)) { - _ = file.Close() - - return nil, fmt.Errorf("objectstore/packed: idx %q has unsupported size", idxName) - } - - fd, err := intconv.UintptrToInt(file.Fd()) - if err != nil { - _ = file.Close() - - return nil, err - } - - data, err := syscall.Mmap(fd, 0, int(size), syscall.PROT_READ, syscall.MAP_PRIVATE) - if err != nil { - _ = file.Close() - - return nil, err - } - - index := &idxFile{ - idxName: idxName, - packName: packName, - algo: algo, - file: file, - data: data, - } - - err = index.parse() - if err != nil { - _ = index.close() - - return nil, err - } - - return index, nil -} diff --git a/object/store/packed/internal/reading/idx_parse.go b/object/store/packed/internal/reading/idx_parse.go deleted file mode 100644 index d38aaf4d..00000000 --- a/object/store/packed/internal/reading/idx_parse.go +++ /dev/null @@ -1,78 +0,0 @@ -package reading - -import ( - "encoding/binary" - "fmt" -) - -const ( - idxMagicV2 = 0xff744f63 - idxVersionV2 = 2 -) - -// parse validates mapped idx v2 structure and stores table boundaries. -func (index *idxFile) parse() error { - hashSize := index.algo.Size() - if hashSize <= 0 { - return fmt.Errorf("objectstore/packed: idx %q has invalid hash algorithm", index.idxName) - } - - minLen := 8 + 256*4 + 2*hashSize - if len(index.data) < minLen { - return fmt.Errorf("objectstore/packed: idx %q too short", index.idxName) - } - - if binary.BigEndian.Uint32(index.data[:4]) != idxMagicV2 { - return fmt.Errorf("objectstore/packed: idx %q invalid magic", index.idxName) - } - - if binary.BigEndian.Uint32(index.data[4:8]) != idxVersionV2 { - return fmt.Errorf("objectstore/packed: idx %q unsupported version", index.idxName) - } - - prev := uint32(0) - - for i := range 256 { - base := 8 + i*4 - - cur := binary.BigEndian.Uint32(index.data[base : base+4]) - if cur < prev { - return fmt.Errorf("objectstore/packed: idx %q has non-monotonic fanout table", index.idxName) - } - - index.fanout[i] = cur - prev = cur - } - - index.numObjects = int(index.fanout[255]) - if index.numObjects < 0 { - return fmt.Errorf("objectstore/packed: idx %q has invalid object count", index.idxName) - } - - namesBytes := index.numObjects * hashSize - crcBytes := index.numObjects * 4 - offset32Bytes := index.numObjects * 4 - - minSize := 8 + 256*4 + namesBytes + crcBytes + offset32Bytes + 2*hashSize - if minSize < 0 || len(index.data) < minSize { - return fmt.Errorf("objectstore/packed: idx %q has truncated tables", index.idxName) - } - - index.namesOffset = 8 + 256*4 - index.offset32Offset = index.namesOffset + namesBytes + crcBytes - index.offset64Offset = index.offset32Offset + offset32Bytes - - offset64Bytes := len(index.data) - index.offset64Offset - 2*hashSize - if offset64Bytes < 0 || offset64Bytes%8 != 0 { - return fmt.Errorf("objectstore/packed: idx %q has malformed 64-bit offset table", index.idxName) - } - - index.offset64Count = offset64Bytes / 8 - - maxOffset64Count := max(index.numObjects-1, 0) - if index.offset64Count > maxOffset64Count { - return fmt.Errorf("objectstore/packed: idx %q has oversized 64-bit offset table", index.idxName) - } - - return nil -} diff --git a/object/store/packed/internal/reading/location.go b/object/store/packed/internal/reading/location.go deleted file mode 100644 index f315dd1d..00000000 --- a/object/store/packed/internal/reading/location.go +++ /dev/null @@ -1,7 +0,0 @@ -package reading - -// location identifies one object entry in a specific pack file. -type location struct { - packName string - offset uint64 -} diff --git a/object/store/packed/internal/reading/new.go b/object/store/packed/internal/reading/new.go deleted file mode 100644 index d8a12db3..00000000 --- a/object/store/packed/internal/reading/new.go +++ /dev/null @@ -1,33 +0,0 @@ -package reading - -import ( - "fmt" - "os" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// New creates a packed-object store rooted at an objects/pack directory. -// -// Labels: Deps-Borrowed, Life-Parent. -func New(root *os.Root, algo objectid.Algorithm, opts Options) (*Store, error) { - if algo.Size() == 0 { - return nil, objectid.ErrInvalidAlgorithm - } - - switch opts.RefreshPolicy { - case RefreshPolicyOnMissing, RefreshPolicyNever: - default: - return nil, fmt.Errorf("objectstore/packed: invalid refresh policy %d", opts.RefreshPolicy) - } - - return &Store{ - root: root, - algo: algo, - refreshPolicy: opts.RefreshPolicy, - mruNodeByPack: make(map[string]*packCandidateNode), - idxByPack: make(map[string]*idxFile), - packs: make(map[string]*packFile), - deltaCache: newDeltaCache(defaultDeltaCacheMaxBytes), - }, nil -} diff --git a/object/store/packed/internal/reading/options.go b/object/store/packed/internal/reading/options.go deleted file mode 100644 index 0c5b76af..00000000 --- a/object/store/packed/internal/reading/options.go +++ /dev/null @@ -1,16 +0,0 @@ -package reading - -// RefreshPolicy configures when candidate pack/index discovery refreshes. -type RefreshPolicy uint8 - -const ( - // RefreshPolicyOnMissing refreshes candidates once after a lookup miss. - RefreshPolicyOnMissing RefreshPolicy = iota - // RefreshPolicyNever disables automatic refresh after lookup misses. - RefreshPolicyNever -) - -// Options configures a packed object store. -type Options struct { - RefreshPolicy RefreshPolicy -} diff --git a/object/store/packed/internal/reading/pack.go b/object/store/packed/internal/reading/pack.go deleted file mode 100644 index 431ed5f9..00000000 --- a/object/store/packed/internal/reading/pack.go +++ /dev/null @@ -1,82 +0,0 @@ -package reading - -import ( - "encoding/binary" - "fmt" - "os" - "syscall" - - packfmt "codeberg.org/lindenii/furgit/format/packfile" - "codeberg.org/lindenii/furgit/internal/intconv" -) - -// packFile stores one mapped and validated .pack file. -type packFile struct { - // name is the .pack basename. - name string - // file is the opened pack file descriptor. - file *os.File - // data is the mapped pack bytes. - data []byte -} - -// openPackFile maps and validates one pack file. -func openPackFile(name string, file *os.File, size int64) (*packFile, error) { - if size < 12 { - return nil, fmt.Errorf("objectstore/packed: pack %q too short", name) - } - - if size > int64(int(^uint(0)>>1)) { - return nil, fmt.Errorf("objectstore/packed: pack %q has unsupported size", name) - } - - fd, err := intconv.UintptrToInt(file.Fd()) - if err != nil { - return nil, err - } - - data, err := syscall.Mmap(fd, 0, int(size), syscall.PROT_READ, syscall.MAP_PRIVATE) - if err != nil { - return nil, err - } - - if binary.BigEndian.Uint32(data[:4]) != packfmt.Signature { - _ = syscall.Munmap(data) - - return nil, fmt.Errorf("objectstore/packed: pack %q invalid signature", name) - } - - version := binary.BigEndian.Uint32(data[4:8]) - if !packfmt.SupportedVersion(version) { - _ = syscall.Munmap(data) - - return nil, fmt.Errorf("objectstore/packed: pack %q unsupported version %d", name, version) - } - - return &packFile{name: name, file: file, data: data}, nil -} - -// close unmaps and closes one pack handle. -func (pack *packFile) close() error { - var closeErr error - - if pack.data != nil { - err := syscall.Munmap(pack.data) - if err != nil && closeErr == nil { - closeErr = err - } - - pack.data = nil - } - - if pack.file != nil { - err := pack.file.Close() - if err != nil && closeErr == nil { - closeErr = err - } - - pack.file = nil - } - - return closeErr -} diff --git a/object/store/packed/internal/reading/pack_idx_checksum.go b/object/store/packed/internal/reading/pack_idx_checksum.go deleted file mode 100644 index b2ad09f1..00000000 --- a/object/store/packed/internal/reading/pack_idx_checksum.go +++ /dev/null @@ -1,34 +0,0 @@ -package reading - -import ( - "bytes" - "fmt" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// verifyMappedPackMatchesMappedIdx compares one mapped pack trailer hash with -// the pack hash recorded in one mapped idx trailer. -func verifyMappedPackMatchesMappedIdx(packData, idxData []byte, algo objectid.Algorithm) error { - hashSize := algo.Size() - if hashSize <= 0 { - return objectid.ErrInvalidAlgorithm - } - - if len(packData) < hashSize { - return fmt.Errorf("objectstore/packed: pack too short for trailer hash") - } - - if len(idxData) < hashSize*2 { - return fmt.Errorf("objectstore/packed: idx too short for trailer hashes") - } - - packTrailerHash := packData[len(packData)-hashSize:] - - idxPackHash := idxData[len(idxData)-hashSize*2 : len(idxData)-hashSize] - if !bytes.Equal(packTrailerHash, idxPackHash) { - return fmt.Errorf("objectstore/packed: pack hash does not match idx") - } - - return nil -} diff --git a/object/store/packed/internal/reading/read_bytes.go b/object/store/packed/internal/reading/read_bytes.go deleted file mode 100644 index f0821687..00000000 --- a/object/store/packed/internal/reading/read_bytes.go +++ /dev/null @@ -1,46 +0,0 @@ -package reading - -import ( - "fmt" - - objectheader "codeberg.org/lindenii/furgit/object/header" - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// ReadBytesContent reads an object's type and content bytes. -// -// It fully resolves the requested object bytes. For base pack entries, this -// includes verifying that the zlib stream inflates to exactly the declared -// object size and verifying the Adler-32 trailer. -func (store *Store) ReadBytesContent(id objectid.ObjectID) (objecttype.Type, []byte, error) { - loc, err := store.lookup(id) - if err != nil { - return objecttype.TypeInvalid, nil, err - } - - return store.deltaResolveContent(loc) -} - -// ReadBytesFull reads a full serialized object as "type size\0content". -// -// Like ReadBytesContent, it fully resolves the requested object bytes. For -// base pack entries, this includes verifying that the zlib stream inflates to -// exactly the declared object size and verifying the Adler-32 trailer. -func (store *Store) ReadBytesFull(id objectid.ObjectID) ([]byte, error) { - ty, content, err := store.ReadBytesContent(id) - if err != nil { - return nil, err - } - - header, ok := objectheader.Encode(ty, int64(len(content))) - if !ok { - return nil, fmt.Errorf("objectstore/packed: failed to encode object header for type %d", ty) - } - - out := make([]byte, len(header)+len(content)) - copy(out, header) - copy(out[len(header):], content) - - return out, nil -} diff --git a/object/store/packed/internal/reading/read_closer.go b/object/store/packed/internal/reading/read_closer.go deleted file mode 100644 index 4ef4c039..00000000 --- a/object/store/packed/internal/reading/read_closer.go +++ /dev/null @@ -1,19 +0,0 @@ -package reading - -import "io" - -// readCloser proxies reads and closes one underlying closer. -type readCloser struct { - reader io.Reader - closer io.Closer -} - -// Read proxies reads to the underlying reader. -func (reader *readCloser) Read(dst []byte) (int, error) { - return reader.reader.Read(dst) -} - -// Close closes the underlying closer. -func (reader *readCloser) Close() error { - return reader.closer.Close() -} diff --git a/object/store/packed/internal/reading/read_header.go b/object/store/packed/internal/reading/read_header.go deleted file mode 100644 index d627a6b3..00000000 --- a/object/store/packed/internal/reading/read_header.go +++ /dev/null @@ -1,20 +0,0 @@ -package reading - -import ( - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// ReadHeader reads an object's type and declared content size. -// -// It resolves header metadata only. It does not verify that the full pack entry -// payload is readable and does not verify any zlib Adler-32 trailer for -// compressed entry data. -func (store *Store) ReadHeader(id objectid.ObjectID) (objecttype.Type, int64, error) { - loc, err := store.lookup(id) - if err != nil { - return objecttype.TypeInvalid, 0, err - } - - return store.resolveHeaderAt(loc) -} diff --git a/object/store/packed/internal/reading/read_header_resolve.go b/object/store/packed/internal/reading/read_header_resolve.go deleted file mode 100644 index a2916b73..00000000 --- a/object/store/packed/internal/reading/read_header_resolve.go +++ /dev/null @@ -1,65 +0,0 @@ -package reading - -import ( - "fmt" - - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// resolveHeaderAt resolves one object's canonical type and declared content size. -func (store *Store) resolveHeaderAt(start location) (objecttype.Type, int64, error) { - visited := make(map[location]struct{}) - current := start - declaredSize := int64(-1) - - for { - if _, ok := visited[current]; ok { - return objecttype.TypeInvalid, 0, fmt.Errorf("objectstore/packed: delta cycle while resolving object header") - } - - visited[current] = struct{}{} - - pack, meta, err := store.entryMetaAt(current) - if err != nil { - return objecttype.TypeInvalid, 0, err - } - - if declaredSize < 0 { - if meta.ty.IsBaseObject() { - declaredSize = meta.size - } else { - size, err := deltaDeclaredSizeAt(pack, meta.dataOffset) - if err != nil { - return objecttype.TypeInvalid, 0, err - } - - declaredSize = size - } - } - - if meta.ty.IsBaseObject() { - return meta.ty, declaredSize, nil - } - - switch meta.ty { - case objecttype.TypeRefDelta: - next, err := store.lookup(meta.baseRefID) - if err != nil { - return objecttype.TypeInvalid, 0, err - } - - current = next - case objecttype.TypeOfsDelta: - current = location{ - packName: current.packName, - offset: meta.baseOfs, - } - case objecttype.TypeCommit, objecttype.TypeTree, objecttype.TypeBlob, objecttype.TypeTag: - return objecttype.TypeInvalid, 0, fmt.Errorf("objectstore/packed: internal invariant violation for base type %d", meta.ty) - case objecttype.TypeInvalid, objecttype.TypeFuture: - return objecttype.TypeInvalid, 0, fmt.Errorf("objectstore/packed: unsupported pack type %d", meta.ty) - default: - return objecttype.TypeInvalid, 0, fmt.Errorf("objectstore/packed: unsupported pack type %d", meta.ty) - } - } -} diff --git a/object/store/packed/internal/reading/read_reader.go b/object/store/packed/internal/reading/read_reader.go deleted file mode 100644 index 3fa0f592..00000000 --- a/object/store/packed/internal/reading/read_reader.go +++ /dev/null @@ -1,92 +0,0 @@ -package reading - -import ( - "bytes" - "fmt" - "io" - - "codeberg.org/lindenii/furgit/internal/iolimit" - objectheader "codeberg.org/lindenii/furgit/object/header" - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// ReadReaderContent reads an object's type, declared content size, and content -// stream. -// -// Close releases reader-local resources only. It does not drain unread data for -// additional validation. In particular, malformed trailing compressed data, -// trailing bytes past the declared object size, and the zlib Adler-32 trailer -// may go unverified unless the caller reads to io.EOF. -func (store *Store) ReadReaderContent(id objectid.ObjectID) (objecttype.Type, int64, io.ReadCloser, error) { - loc, err := store.lookup(id) - if err != nil { - return objecttype.TypeInvalid, 0, nil, err - } - - pack, meta, err := store.entryMetaAt(loc) - if err != nil { - return objecttype.TypeInvalid, 0, nil, err - } - - if meta.ty.IsBaseObject() { - zr, err := zlibReaderAt(pack, meta.dataOffset) - if err != nil { - return objecttype.TypeInvalid, 0, nil, err - } - - return meta.ty, meta.size, &readCloser{ - reader: iolimit.ExpectLengthReader(zr, meta.size), - closer: zr, - }, nil - } - - ty, content, err := store.deltaResolveContent(loc) - if err != nil { - return objecttype.TypeInvalid, 0, nil, err - } - - return ty, int64(len(content)), io.NopCloser(bytes.NewReader(content)), nil -} - -// ReadReaderFull reads a full serialized object stream as "type size\0content". -// -// Close releases reader-local resources only. It does not drain unread data for -// additional validation. In particular, malformed trailing compressed data, -// trailing bytes past the declared object size, and the zlib Adler-32 trailer -// may go unverified unless the caller reads to io.EOF. -func (store *Store) ReadReaderFull(id objectid.ObjectID) (io.ReadCloser, error) { - loc, err := store.lookup(id) - if err != nil { - return nil, err - } - - pack, meta, err := store.entryMetaAt(loc) - if err != nil { - return nil, err - } - - if meta.ty.IsBaseObject() { - header, ok := objectheader.Encode(meta.ty, meta.size) - if !ok { - return nil, fmt.Errorf("objectstore/packed: failed to encode object header for type %d", meta.ty) - } - - zr, err := zlibReaderAt(pack, meta.dataOffset) - if err != nil { - return nil, err - } - - return &readCloser{ - reader: io.MultiReader(bytes.NewReader(header), iolimit.ExpectLengthReader(zr, meta.size)), - closer: zr, - }, nil - } - - raw, err := store.ReadBytesFull(id) - if err != nil { - return nil, err - } - - return io.NopCloser(bytes.NewReader(raw)), nil -} diff --git a/object/store/packed/internal/reading/read_size.go b/object/store/packed/internal/reading/read_size.go deleted file mode 100644 index 3c1e05b1..00000000 --- a/object/store/packed/internal/reading/read_size.go +++ /dev/null @@ -1,45 +0,0 @@ -package reading - -import ( - "fmt" - - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// ReadSize reads an object's declared content size. -// -// Like ReadHeader, it resolves header metadata only. It does not verify that -// the full pack entry payload is readable and does not verify any zlib -// Adler-32 trailer for compressed entry data. -func (store *Store) ReadSize(id objectid.ObjectID) (int64, error) { - loc, err := store.lookup(id) - if err != nil { - return 0, err - } - - return store.resolveSizeAt(loc) -} - -// resolveSizeAt resolves one object's declared content size from location. -func (store *Store) resolveSizeAt(start location) (int64, error) { - pack, meta, err := store.entryMetaAt(start) - if err != nil { - return 0, err - } - - if meta.ty.IsBaseObject() { - return meta.size, nil - } - - switch meta.ty { - case objecttype.TypeRefDelta, objecttype.TypeOfsDelta: - return deltaDeclaredSizeAt(pack, meta.dataOffset) - case objecttype.TypeInvalid, objecttype.TypeFuture: - return 0, fmt.Errorf("objectstore/packed: unsupported pack type %d", meta.ty) - case objecttype.TypeCommit, objecttype.TypeTree, objecttype.TypeBlob, objecttype.TypeTag: - return 0, fmt.Errorf("objectstore/packed: internal invariant violation for base type %d", meta.ty) - default: - return 0, fmt.Errorf("objectstore/packed: unsupported pack type %d", meta.ty) - } -} diff --git a/object/store/packed/internal/reading/read_test.go b/object/store/packed/internal/reading/read_test.go deleted file mode 100644 index 8a92b603..00000000 --- a/object/store/packed/internal/reading/read_test.go +++ /dev/null @@ -1,301 +0,0 @@ -package reading_test - -import ( - "bytes" - "errors" - "fmt" - "io/fs" - "strconv" - "strings" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - objectid "codeberg.org/lindenii/furgit/object/id" - objectstore "codeberg.org/lindenii/furgit/object/store" - "codeberg.org/lindenii/furgit/object/store/packed" -) - -func TestPackedStoreReadAgainstGit(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo, ids := createPackedFixtureRepo(t, algo) - store := openPackedStore(t, testRepo, algo) - - for _, id := range ids { - t.Run(id.String(), func(t *testing.T) { - wantType, wantBody, wantRaw := expectedRawObject(t, testRepo, id) - - gotHeaderType, gotHeaderSize, err := store.ReadHeader(id) - if err != nil { - t.Fatalf("ReadHeader: %v", err) - } - - if gotHeaderType != wantType { - t.Fatalf("ReadHeader type = %v, want %v", gotHeaderType, wantType) - } - - if gotHeaderSize != int64(len(wantBody)) { - t.Fatalf("ReadHeader size = %d, want %d", gotHeaderSize, len(wantBody)) - } - - gotSize, err := store.ReadSize(id) - if err != nil { - t.Fatalf("ReadSize: %v", err) - } - - if gotSize != int64(len(wantBody)) { - t.Fatalf("ReadSize = %d, want %d", gotSize, len(wantBody)) - } - - gotRaw, err := store.ReadBytesFull(id) - if err != nil { - t.Fatalf("ReadBytesFull: %v", err) - } - - if !bytes.Equal(gotRaw, wantRaw) { - t.Fatalf("ReadBytesFull mismatch") - } - - gotType, gotBody, err := store.ReadBytesContent(id) - if err != nil { - t.Fatalf("ReadBytesContent: %v", err) - } - - if gotType != wantType { - t.Fatalf("ReadBytesContent type = %v, want %v", gotType, wantType) - } - - if !bytes.Equal(gotBody, wantBody) { - t.Fatalf("ReadBytesContent mismatch") - } - - fullReader, err := store.ReadReaderFull(id) - if err != nil { - t.Fatalf("ReadReaderFull: %v", err) - } - - got := mustReadAllAndClose(t, fullReader) - if !bytes.Equal(got, wantRaw) { - t.Fatalf("ReadReaderFull mismatch") - } - - contentType, contentSize, contentReader, err := store.ReadReaderContent(id) - if err != nil { - t.Fatalf("ReadReaderContent: %v", err) - } - - if contentType != wantType { - t.Fatalf("ReadReaderContent type = %v, want %v", contentType, wantType) - } - - if contentSize != int64(len(wantBody)) { - t.Fatalf("ReadReaderContent size = %d, want %d", contentSize, len(wantBody)) - } - - got = mustReadAllAndClose(t, contentReader) - if !bytes.Equal(got, wantBody) { - t.Fatalf("ReadReaderContent mismatch") - } - }) - } - }) -} - -func TestPackedStoreErrors(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo, _ := createPackedFixtureRepo(t, algo) - store := openPackedStore(t, testRepo, algo) - - notFoundID, err := objectid.ParseHex(algo, strings.Repeat("0", algo.HexLen())) - if err != nil { - t.Fatalf("ParseHex(notFound): %v", err) - } - - _, err = store.ReadBytesFull(notFoundID) - if !errors.Is(err, objectstore.ErrObjectNotFound) { - t.Fatalf("ReadBytesFull not-found error = %v", err) - } - - _, _, err = store.ReadBytesContent(notFoundID) - if !errors.Is(err, objectstore.ErrObjectNotFound) { - t.Fatalf("ReadBytesContent not-found error = %v", err) - } - - _, err = store.ReadReaderFull(notFoundID) - if !errors.Is(err, objectstore.ErrObjectNotFound) { - t.Fatalf("ReadReaderFull not-found error = %v", err) - } - - _, _, _, err = store.ReadReaderContent(notFoundID) - if !errors.Is(err, objectstore.ErrObjectNotFound) { - t.Fatalf("ReadReaderContent not-found error = %v", err) - } - - _, _, err = store.ReadHeader(notFoundID) - if !errors.Is(err, objectstore.ErrObjectNotFound) { - t.Fatalf("ReadHeader not-found error = %v", err) - } - - _, err = store.ReadSize(notFoundID) - if !errors.Is(err, objectstore.ErrObjectNotFound) { - t.Fatalf("ReadSize not-found error = %v", err) - } - - var otherAlgo objectid.Algorithm - - for _, candidate := range objectid.SupportedAlgorithms() { - if candidate != algo { - otherAlgo = candidate - - break - } - } - - if otherAlgo != objectid.AlgorithmUnknown { - mismatchID, err := objectid.ParseHex(otherAlgo, strings.Repeat("0", otherAlgo.HexLen())) - if err != nil { - t.Fatalf("ParseHex(mismatch): %v", err) - } - - _, err = store.ReadBytesFull(mismatchID) - if err == nil || !strings.Contains(err.Error(), "algorithm mismatch") { - t.Fatalf("ReadBytesFull algorithm-mismatch error = %v", err) - } - } - }) -} - -func TestPackedStoreNewValidation(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo, _ := createPackedFixtureRepo(t, algo) - - store := openPackedStore(t, testRepo, algo) - - err := store.Close() - if err != nil { - t.Fatalf("Close: %v", err) - } - }) -} - -func TestPackedStoreInvalidAlgorithm(t *testing.T) { - t.Parallel() - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: objectid.AlgorithmSHA1, Bare: true}) - - root := testRepo.OpenPackRoot(t) - - _, err := packed.New(root, objectid.AlgorithmUnknown, packed.Options{}) - if !errors.Is(err, objectid.ErrInvalidAlgorithm) { - t.Fatalf("packed.New invalid algorithm error = %v", err) - } -} - -func TestPackedStoreReadHeaderUsesResolvedObjectSizeForDelta(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - - var parent objectid.ObjectID - - for i := range 96 { - content := strings.Repeat("common-line-"+strconv.Itoa(i%7)+"\n", 384) + fmt.Sprintf("tail-%03d\n", i) - - _, treeID := testRepo.MakeSingleFileTree(t, "file.txt", []byte(content)) - if i == 0 { - parent = testRepo.CommitTree(t, treeID, "delta-header-size-0") - - continue - } - - parent = testRepo.CommitTree(t, treeID, fmt.Sprintf("delta-header-size-%03d", i), parent) - } - - testRepo.UpdateRef(t, "refs/heads/main", parent) - testRepo.Repack(t, "-a", "-d", "-f", "--window=128", "--depth=128") - - deltaID, wantResolvedSize := findDeltaObjectWithResolvedSizeMismatch(t, testRepo, algo) - store := openPackedStore(t, testRepo, algo) - - _, gotSize, err := store.ReadHeader(deltaID) - if err != nil { - t.Fatalf("ReadHeader(%s): %v", deltaID, err) - } - - if gotSize != wantResolvedSize { - t.Fatalf("ReadHeader(%s) size = %d, want resolved size %d", deltaID, gotSize, wantResolvedSize) - } - - gotReadSize, err := store.ReadSize(deltaID) - if err != nil { - t.Fatalf("ReadSize(%s): %v", deltaID, err) - } - - if gotReadSize != wantResolvedSize { - t.Fatalf("ReadSize(%s) = %d, want resolved size %d", deltaID, gotReadSize, wantResolvedSize) - } - }) -} - -func findDeltaObjectWithResolvedSizeMismatch(t *testing.T, testRepo *testgit.TestRepo, algo objectid.Algorithm) (objectid.ObjectID, int64) { - t.Helper() - - packRoot := testRepo.OpenPackRoot(t) - - entries, err := fs.ReadDir(packRoot.FS(), ".") - if err != nil { - t.Fatalf("ReadDir(pack): %v", err) - } - - var idxName string - - for _, entry := range entries { - if strings.HasSuffix(entry.Name(), ".idx") { - idxName = entry.Name() - - break - } - } - - if idxName == "" { - t.Fatalf("no idx files found") - } - - verifyOut := testRepo.Run(t, "verify-pack", "-v", "objects/pack/"+idxName) - for line := range strings.SplitSeq(strings.TrimSpace(verifyOut), "\n") { - fields := strings.Fields(line) - if len(fields) < 7 { - continue - } - - idHex := fields[0] - - deltaStreamSize, err := strconv.ParseInt(fields[2], 10, 64) - if err != nil { - continue - } - - resolvedSizeStr := testRepo.Run(t, "cat-file", "-s", idHex) - - resolvedSize, err := strconv.ParseInt(strings.TrimSpace(resolvedSizeStr), 10, 64) - if err != nil { - t.Fatalf("parse cat-file size for %s: %v", idHex, err) - } - - if deltaStreamSize == resolvedSize { - continue - } - - id, err := objectid.ParseHex(algo, idHex) - if err != nil { - t.Fatalf("ParseHex(%s): %v", idHex, err) - } - - return id, resolvedSize - } - - t.Fatalf("did not find a delta object with mismatched stream/resolved size") - - return objectid.ObjectID{}, 0 -} diff --git a/object/store/packed/internal/reading/store.go b/object/store/packed/internal/reading/store.go deleted file mode 100644 index cb4829ab..00000000 --- a/object/store/packed/internal/reading/store.go +++ /dev/null @@ -1,52 +0,0 @@ -package reading - -import ( - "os" - "sync" - "sync/atomic" - - objectid "codeberg.org/lindenii/furgit/object/id" - objectstore "codeberg.org/lindenii/furgit/object/store" -) - -// Store reads Git objects from pack/index files under an objects/pack root. -// -// Cached pack/index mappings are retained until Close. -// -// Labels: Close-Caller. -type Store struct { - // root is the borrowed objects/pack capability used for all file access. - root *os.Root - // algo is the expected object ID algorithm for lookups. - algo objectid.Algorithm - // refreshPolicy controls automatic candidate refresh on lookup misses. - refreshPolicy RefreshPolicy - - // candidates stores the latest immutable candidate snapshot. - candidates atomic.Pointer[candidateSnapshot] - // refreshMu serializes candidate refresh. - refreshMu sync.Mutex - // mruMu guards candidate MRU linked-list state. - mruMu sync.RWMutex - // mruHead is the first pack in MRU order. - mruHead *packCandidateNode - // mruTail is the last pack in MRU order. - mruTail *packCandidateNode - // mruNodeByPack maps pack basename to MRU node. - mruNodeByPack map[string]*packCandidateNode - // idxByPack caches opened and parsed indexes by pack basename. - idxByPack map[string]*idxFile - - // stateMu guards pack cache and close state. - stateMu sync.RWMutex - // idxMu guards parsed index cache. - idxMu sync.RWMutex - // cacheMu guards delta cache operations. - cacheMu sync.RWMutex - // packs caches opened .pack handles by basename. - packs map[string]*packFile - // deltaCache caches resolved base objects by pack location. - deltaCache *deltaCache -} - -var _ objectstore.Reader = (*Store)(nil) diff --git a/object/store/packed/internal/reading/store_lookup.go b/object/store/packed/internal/reading/store_lookup.go deleted file mode 100644 index 9d863113..00000000 --- a/object/store/packed/internal/reading/store_lookup.go +++ /dev/null @@ -1,106 +0,0 @@ -package reading - -import ( - "errors" - - objectid "codeberg.org/lindenii/furgit/object/id" - objectstore "codeberg.org/lindenii/furgit/object/store" -) - -// lookup resolves one object ID to its pack location. -func (store *Store) lookup(id objectid.ObjectID) (location, error) { - var zero location - if id.Algorithm() != store.algo { - return zero, errors.New("objectstore/packed: object id algorithm mismatch") - } - - snapshot, err := store.ensureCandidates() - if err != nil { - return zero, err - } - - loc, ok, err := store.lookupInCandidates(id, snapshot) - if err != nil { - return zero, err - } - - if ok { - return loc, nil - } - - if store.refreshPolicy == RefreshPolicyOnMissing { //nolint:nestif - err = store.Refresh() - if err != nil { - return zero, err - } - - refreshed := store.candidates.Load() - if refreshed != nil && refreshed != snapshot { - loc, ok, err = store.lookupInCandidates(id, refreshed) - if err != nil { - return zero, err - } - - if ok { - return loc, nil - } - } - } - - return zero, objectstore.ErrObjectNotFound -} - -func (store *Store) lookupInCandidates( - id objectid.ObjectID, - snapshot *candidateSnapshot, -) (location, bool, error) { - var zero location - - nextPackName := store.firstCandidatePackName(snapshot) - for nextPackName != "" { - candidate, ok := snapshot.candidateByPack[nextPackName] - if !ok { - nextPackName = store.firstCandidatePackName(snapshot) - - continue - } - - nextPackName = store.nextCandidatePackName(candidate.packName, snapshot) - - index, err := store.openIndex(candidate) - if err != nil { - return zero, false, err - } - - offset, ok, err := index.lookup(id) - if err != nil { - return zero, false, err - } - - if ok { - store.touchCandidate(candidate.packName) - - return location{packName: index.packName, offset: offset}, true, nil - } - } - - for _, candidate := range snapshot.candidates { - index, err := store.openIndex(candidate) - if err != nil { - return zero, false, err - } - - offset, ok, err := index.lookup(id) - if err != nil { - return zero, false, err - } - - if ok { - store.touchCandidate(candidate.packName) - - return location{packName: index.packName, offset: offset}, true, nil - } - } - - return zero, false, nil -} diff --git a/object/store/packed/internal/reading/store_open_pack.go b/object/store/packed/internal/reading/store_open_pack.go deleted file mode 100644 index 35cb960a..00000000 --- a/object/store/packed/internal/reading/store_open_pack.go +++ /dev/null @@ -1,57 +0,0 @@ -package reading - -// openPack returns one opened and validated pack handle. -func (store *Store) openPack(name string) (*packFile, error) { - store.stateMu.RLock() - - pack, ok := store.packs[name] - if ok { - store.stateMu.RUnlock() - - return pack, nil - } - - store.stateMu.RUnlock() - - file, err := store.root.Open(name) - if err != nil { - return nil, err - } - - info, err := file.Stat() - if err != nil { - _ = file.Close() - - return nil, err - } - - pack, err = openPackFile(name, file, info.Size()) - if err != nil { - _ = file.Close() - - return nil, err - } - - err = store.verifyPackMatchesIndexes(pack) - if err != nil { - _ = pack.close() - - return nil, err - } - - store.stateMu.Lock() - - existing, ok := store.packs[name] - if ok { - store.stateMu.Unlock() - - _ = pack.close() - - return existing, nil - } - - store.packs[name] = pack - store.stateMu.Unlock() - - return pack, nil -} diff --git a/object/store/packed/internal/reading/trailer_match.go b/object/store/packed/internal/reading/trailer_match.go deleted file mode 100644 index 8c7500b9..00000000 --- a/object/store/packed/internal/reading/trailer_match.go +++ /dev/null @@ -1,29 +0,0 @@ -package reading - -import "fmt" - -// verifyPackMatchesIndexes checks that one opened pack's trailer hash matches -// every loaded index that references the same pack name. -func (store *Store) verifyPackMatchesIndexes(pack *packFile) error { - snapshot, err := store.ensureCandidates() - if err != nil { - return err - } - - candidate, ok := snapshot.candidateByPack[pack.name] - if !ok { - return fmt.Errorf("objectstore/packed: missing index for pack %q", pack.name) - } - - index, err := store.openIndex(candidate) - if err != nil { - return err - } - - err = verifyMappedPackMatchesMappedIdx(pack.data, index.data, store.algo) - if err != nil { - return fmt.Errorf("objectstore/packed: pack %q does not match idx %q: %w", pack.name, index.idxName, err) - } - - return nil -} diff --git a/object/store/packed/new.go b/object/store/packed/new.go deleted file mode 100644 index cdc1b50f..00000000 --- a/object/store/packed/new.go +++ /dev/null @@ -1,25 +0,0 @@ -package packed - -import ( - "os" - - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/store/packed/internal/reading" -) - -// New creates a packed-object store rooted at an objects/pack directory. -// -// Labels: Deps-Borrowed, Life-Parent. -func New(root *os.Root, algo objectid.Algorithm, opts Options) (*Store, error) { - reader, err := reading.New(root, algo, opts.toReadingOptions()) - if err != nil { - return nil, err - } - - return &Store{ - root: root, - algo: algo, - opts: opts, - reader: reader, - }, nil -} diff --git a/object/store/packed/options.go b/object/store/packed/options.go deleted file mode 100644 index 718efc29..00000000 --- a/object/store/packed/options.go +++ /dev/null @@ -1,7 +0,0 @@ -package packed - -// Options configures a packed object store. -type Options struct { - RefreshPolicy RefreshPolicy - WriteRev bool -} diff --git a/object/store/packed/options_refresh.go b/object/store/packed/options_refresh.go deleted file mode 100644 index ee3d5f2e..00000000 --- a/object/store/packed/options_refresh.go +++ /dev/null @@ -1,11 +0,0 @@ -package packed - -// RefreshPolicy configures when candidate pack/index discovery refreshes. -type RefreshPolicy uint8 - -const ( - // RefreshPolicyOnMissing refreshes candidates once after a lookup miss. - RefreshPolicyOnMissing RefreshPolicy = iota - // RefreshPolicyNever disables automatic refresh after lookup misses. - RefreshPolicyNever -) diff --git a/object/store/packed/quarantine.go b/object/store/packed/quarantine.go deleted file mode 100644 index a8f6d08c..00000000 --- a/object/store/packed/quarantine.go +++ /dev/null @@ -1,19 +0,0 @@ -package packed - -import ( - "os" - - objectstore "codeberg.org/lindenii/furgit/object/store" -) - -var _ objectstore.PackQuarantiner = (*Store)(nil) - -type packQuarantine struct { - *Store - - parent *Store - tempName string - tempRoot *os.Root -} - -var _ objectstore.PackQuarantine = (*packQuarantine)(nil) diff --git a/object/store/packed/quarantine_begin.go b/object/store/packed/quarantine_begin.go deleted file mode 100644 index 06b9a8a6..00000000 --- a/object/store/packed/quarantine_begin.go +++ /dev/null @@ -1,63 +0,0 @@ -package packed - -import ( - "crypto/rand" - "errors" - "fmt" - "io/fs" - "os" - - objectstore "codeberg.org/lindenii/furgit/object/store" -) - -// BeginPackQuarantine creates one quarantined packed store rooted privately -// beneath the destination pack root. -// -// Labels: Deps-Borrowed, Life-Parent, Close-No. -func (store *Store) BeginPackQuarantine(_ objectstore.PackQuarantineOptions) (objectstore.PackQuarantine, error) { - tempName, tempRoot, err := createPackQuarantineRoot(store.root) - if err != nil { - return nil, err - } - - quarantineStore, err := New(tempRoot, store.algo, store.opts) - if err != nil { - _ = tempRoot.Close() - _ = store.root.RemoveAll(tempName) - - return nil, err - } - - return &packQuarantine{ - Store: quarantineStore, - parent: store, - tempName: tempName, - tempRoot: tempRoot, - }, nil -} - -func createPackQuarantineRoot(parent *os.Root) (string, *os.Root, error) { - for range 32 { - name := "tmp_packq_" + rand.Text() - - err := parent.Mkdir(name, 0o700) - if err == nil { - root, err := parent.OpenRoot(name) - if err == nil { - return name, root, nil - } - - _ = parent.RemoveAll(name) - - return "", nil, err - } - - if errors.Is(err, fs.ErrExist) { - continue - } - - return "", nil, err - } - - return "", nil, fmt.Errorf("packed: unable to create quarantine directory") -} diff --git a/object/store/packed/quarantine_discard.go b/object/store/packed/quarantine_discard.go deleted file mode 100644 index a1dc7310..00000000 --- a/object/store/packed/quarantine_discard.go +++ /dev/null @@ -1,18 +0,0 @@ -package packed - -// Discard removes the quarantine and invalidates the receiver. -func (quarantine *packQuarantine) Discard() error { - closeErr := quarantine.Close() - tempRootErr := quarantine.tempRoot.Close() - removeErr := quarantine.parent.root.RemoveAll(quarantine.tempName) - - if closeErr != nil { - return closeErr - } - - if tempRootErr != nil { - return tempRootErr - } - - return removeErr -} diff --git a/object/store/packed/quarantine_promote.go b/object/store/packed/quarantine_promote.go deleted file mode 100644 index a4eb426d..00000000 --- a/object/store/packed/quarantine_promote.go +++ /dev/null @@ -1,89 +0,0 @@ -package packed - -import ( - "errors" - "fmt" - "io/fs" - "os" - "slices" - "strings" -) - -// Promote publishes all finalized pack artifacts in the quarantine into the -// parent packed store and invalidates the receiver. -func (quarantine *packQuarantine) Promote() error { - closeErr := quarantine.Close() - promoteErr := promotePackQuarantine(quarantine.parent.root, quarantine.tempName, quarantine.tempRoot) - tempRootErr := quarantine.tempRoot.Close() - removeErr := quarantine.parent.root.RemoveAll(quarantine.tempName) - - if closeErr != nil { - return closeErr - } - - if tempRootErr != nil { - return tempRootErr - } - - if promoteErr != nil { - return promoteErr - } - - return removeErr -} - -func promotePackQuarantine(parent *os.Root, tempName string, tempRoot *os.Root) error { - entries, err := fs.ReadDir(tempRoot.FS(), ".") - if err != nil && !errors.Is(err, fs.ErrNotExist) { - return err - } - - slices.SortFunc(entries, func(left, right fs.DirEntry) int { - return packPromotionPriority(left.Name()) - packPromotionPriority(right.Name()) - }) - - for _, entry := range entries { - if entry.IsDir() { - return fmt.Errorf("packed: quarantine contains unexpected directory %q", entry.Name()) - } - - err := promotePackQuarantineFile(parent, tempName, entry.Name()) - if err != nil { - return err - } - } - - return nil -} - -func promotePackQuarantineFile(parent *os.Root, tempName, name string) error { - src := tempName + "/" + name - - err := parent.Link(src, name) - if err == nil { - _ = parent.Remove(src) - - return nil - } - - if errors.Is(err, fs.ErrExist) { - _ = parent.Remove(src) - - return nil - } - - return fmt.Errorf("packed: promote quarantine %q -> %q: %w", src, name, err) -} - -func packPromotionPriority(name string) int { - switch { - case strings.HasPrefix(name, "pack-") && strings.HasSuffix(name, ".pack"): - return 1 - case strings.HasPrefix(name, "pack-") && strings.HasSuffix(name, ".rev"): - return 2 - case strings.HasPrefix(name, "pack-") && strings.HasSuffix(name, ".idx"): - return 3 - default: - return 0 - } -} diff --git a/object/store/packed/quarantine_test.go b/object/store/packed/quarantine_test.go deleted file mode 100644 index 036da535..00000000 --- a/object/store/packed/quarantine_test.go +++ /dev/null @@ -1,215 +0,0 @@ -package packed_test - -import ( - "bytes" - "os" - "path/filepath" - "strings" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - objectid "codeberg.org/lindenii/furgit/object/id" - objectstore "codeberg.org/lindenii/furgit/object/store" - "codeberg.org/lindenii/furgit/object/store/packed" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -func fixturePath(t *testing.T, algo objectid.Algorithm, name string) string { - t.Helper() - - return filepath.Join("internal", "ingest", "testdata", "fixtures", algo.String(), name) -} - -func fixtureBytes(t *testing.T, algo objectid.Algorithm, name string) []byte { - t.Helper() - - path := fixturePath(t, algo, name) - dir := filepath.Dir(path) - base := filepath.Base(path) - - root, err := os.OpenRoot(dir) - if err != nil { - t.Fatalf("open fixture root %q: %v", dir, err) - } - - defer func() { - err := root.Close() - if err != nil { - t.Fatalf("close fixture root %q: %v", dir, err) - } - }() - - data, err := root.ReadFile(base) - if err != nil { - t.Fatalf("read fixture %q: %v", base, err) - } - - return data -} - -func fixtureMetadata(t *testing.T, algo objectid.Algorithm) map[string]string { - t.Helper() - - data := fixtureBytes(t, algo, "METADATA.txt") - out := make(map[string]string) - - for line := range strings.SplitSeq(strings.TrimSpace(string(data)), "\n") { - line = strings.TrimSpace(line) - if line == "" { - continue - } - - key, value, ok := strings.Cut(line, "=") - if !ok { - t.Fatalf("invalid fixture metadata line %q", line) - } - - out[strings.TrimSpace(key)] = strings.TrimSpace(value) - } - - return out -} - -func fixtureOID(t *testing.T, algo objectid.Algorithm, key string) objectid.ObjectID { - t.Helper() - - meta := fixtureMetadata(t, algo) - - hex, ok := meta[key] - if !ok { - t.Fatalf("missing fixture metadata key %q", key) - } - - id, err := objectid.ParseHex(algo, hex) - if err != nil { - t.Fatalf("parse fixture metadata oid %q: %v", hex, err) - } - - return id -} - -func TestPackQuarantinePromotePublishesWrittenObjects(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - head := fixtureOID(t, algo, "head") - packBytes := fixtureBytes(t, algo, "nonthin.pack") - - repo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - packRoot := repo.OpenPackRoot(t) - - store, err := packed.New(packRoot, algo, packed.Options{WriteRev: true}) - if err != nil { - t.Fatalf("packed.New: %v", err) - } - - defer func() { - err := store.Close() - if err != nil { - t.Fatalf("store.Close: %v", err) - } - }() - - quarantiner, ok := any(store).(objectstore.PackQuarantiner) - if !ok { - t.Fatal("packed store does not implement PackQuarantiner") - } - - quarantine, err := quarantiner.BeginPackQuarantine(objectstore.PackQuarantineOptions{}) - if err != nil { - t.Fatalf("BeginPackQuarantine: %v", err) - } - - err = quarantine.WritePack(bytes.NewReader(packBytes), objectstore.PackWriteOptions{RequireTrailingEOF: true}) - if err != nil { - t.Fatalf("quarantine.WritePack: %v", err) - } - - ty, _, err := quarantine.ReadHeader(head) - if err != nil { - t.Fatalf("quarantine.ReadHeader: %v", err) - } - - if ty != objecttype.TypeCommit { - t.Fatalf("quarantine.ReadHeader type = %v, want commit", ty) - } - - _, _, err = store.ReadHeader(head) - if err == nil { - t.Fatal("store.ReadHeader unexpectedly saw quarantined object before promote") - } - - err = quarantine.Promote() - if err != nil { - t.Fatalf("quarantine.Promote: %v", err) - } - - err = store.Refresh() - if err != nil { - t.Fatalf("store.Refresh: %v", err) - } - - ty, _, err = store.ReadHeader(head) - if err != nil { - t.Fatalf("store.ReadHeader after promote: %v", err) - } - - if ty != objecttype.TypeCommit { - t.Fatalf("store.ReadHeader type = %v, want commit", ty) - } - }) -} - -func TestPackQuarantineDiscardDropsWrittenObjects(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - head := fixtureOID(t, algo, "head") - packBytes := fixtureBytes(t, algo, "nonthin.pack") - - repo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - packRoot := repo.OpenPackRoot(t) - - store, err := packed.New(packRoot, algo, packed.Options{WriteRev: true}) - if err != nil { - t.Fatalf("packed.New: %v", err) - } - - defer func() { - err := store.Close() - if err != nil { - t.Fatalf("store.Close: %v", err) - } - }() - - quarantiner, ok := any(store).(objectstore.PackQuarantiner) - if !ok { - t.Fatalf("expected objectstore.PackQuarantiner") - } - - quarantine, err := quarantiner.BeginPackQuarantine(objectstore.PackQuarantineOptions{}) - if err != nil { - t.Fatalf("BeginPackQuarantine: %v", err) - } - - err = quarantine.WritePack(bytes.NewReader(packBytes), objectstore.PackWriteOptions{RequireTrailingEOF: true}) - if err != nil { - t.Fatalf("quarantine.WritePack: %v", err) - } - - err = quarantine.Discard() - if err != nil { - t.Fatalf("quarantine.Discard: %v", err) - } - - err = store.Refresh() - if err != nil { - t.Fatalf("store.Refresh: %v", err) - } - - _, _, err = store.ReadHeader(head) - if err == nil { - t.Fatal("store.ReadHeader unexpectedly saw discarded object") - } - }) -} diff --git a/object/store/packed/reader.go b/object/store/packed/reader.go deleted file mode 100644 index 45b9e8d9..00000000 --- a/object/store/packed/reader.go +++ /dev/null @@ -1,65 +0,0 @@ -package packed - -import ( - "io" - - objectid "codeberg.org/lindenii/furgit/object/id" - objectstore "codeberg.org/lindenii/furgit/object/store" - "codeberg.org/lindenii/furgit/object/store/packed/internal/reading" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -var _ objectstore.Reader = (*Store)(nil) - -// ReadBytesFull reads a full serialized object as "type size\0content". -func (store *Store) ReadBytesFull(id objectid.ObjectID) ([]byte, error) { - return store.reader.ReadBytesFull(id) -} - -// ReadBytesContent reads an object's type and content bytes. -func (store *Store) ReadBytesContent(id objectid.ObjectID) (objecttype.Type, []byte, error) { - return store.reader.ReadBytesContent(id) -} - -// ReadReaderFull reads a full serialized object stream as "type size\0content". -func (store *Store) ReadReaderFull(id objectid.ObjectID) (io.ReadCloser, error) { - return store.reader.ReadReaderFull(id) -} - -// ReadReaderContent reads an object's type, declared content length, and -// content stream. -func (store *Store) ReadReaderContent(id objectid.ObjectID) (objecttype.Type, int64, io.ReadCloser, error) { - return store.reader.ReadReaderContent(id) -} - -// ReadSize reads an object's declared content length. -func (store *Store) ReadSize(id objectid.ObjectID) (int64, error) { - return store.reader.ReadSize(id) -} - -// ReadHeader reads an object's type and declared content length. -func (store *Store) ReadHeader(id objectid.ObjectID) (objecttype.Type, int64, error) { - return store.reader.ReadHeader(id) -} - -// Refresh updates the packed-store view of on-disk pack/index candidates. -func (store *Store) Refresh() error { - return store.reader.Refresh() -} - -func (opts Options) toReadingOptions() reading.Options { - var refreshPolicy reading.RefreshPolicy - - switch opts.RefreshPolicy { - case RefreshPolicyOnMissing: - refreshPolicy = reading.RefreshPolicyOnMissing - case RefreshPolicyNever: - refreshPolicy = reading.RefreshPolicyNever - default: - refreshPolicy = reading.RefreshPolicy(opts.RefreshPolicy) - } - - return reading.Options{ - RefreshPolicy: refreshPolicy, - } -} diff --git a/object/store/packed/store.go b/object/store/packed/store.go deleted file mode 100644 index 2fe84c81..00000000 --- a/object/store/packed/store.go +++ /dev/null @@ -1,23 +0,0 @@ -package packed - -import ( - "os" - - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/store/packed/internal/reading" -) - -// Store reads Git objects from pack/index files under an objects/pack root. -// -// Labels: Close-Caller. -type Store struct { - root *os.Root - algo objectid.Algorithm - opts Options - reader *reading.Store -} - -// Close releases mapped pack/index resources associated with the store. -func (store *Store) Close() error { - return store.reader.Close() -} diff --git a/object/store/packed/writer.go b/object/store/packed/writer.go deleted file mode 100644 index a96ea750..00000000 --- a/object/store/packed/writer.go +++ /dev/null @@ -1,22 +0,0 @@ -package packed - -import ( - "io" - - objectstore "codeberg.org/lindenii/furgit/object/store" - "codeberg.org/lindenii/furgit/object/store/packed/internal/ingest" -) - -var _ objectstore.PackWriter = (*Store)(nil) - -// WritePack ingests one pack stream into the packed store. -func (store *Store) WritePack(src io.Reader, opts objectstore.PackWriteOptions) error { - _, err := ingest.WritePack(store.root, store.algo, src, ingest.Options{ - WriteRev: store.opts.WriteRev, - ThinBase: opts.ThinBase, - Progress: opts.Progress, - RequireTrailingEOF: opts.RequireTrailingEOF, - }) - - return err -} diff --git a/object/store/quarantine.go b/object/store/quarantine.go deleted file mode 100644 index 5fa97ee7..00000000 --- a/object/store/quarantine.go +++ /dev/null @@ -1,20 +0,0 @@ -package objectstore - -// Quarantine represents one quarantined write that accepts both object- -// wise and pack-wise writes. -type Quarantine interface { - BaseQuarantine - Writer -} - -// QuarantineOptions controls the options for one coordinated quarantine creation. -type QuarantineOptions struct { - Object ObjectQuarantineOptions - Pack PackQuarantineOptions -} - -// Quarantiner creates coordinated quarantines that support both object- -// wise and pack-wise writes. -type Quarantiner interface { - BeginQuarantine(opts QuarantineOptions) (Quarantine, error) -} diff --git a/object/store/reader.go b/object/store/reader.go deleted file mode 100644 index 52a556bd..00000000 --- a/object/store/reader.go +++ /dev/null @@ -1,55 +0,0 @@ -package objectstore - -import ( - "io" - - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// Reader reads Git objects by object ID. -// -// Methods may perform implementation-defined integrity verification beyond -// successfully producing their documented result. -// -// Labels: MT-Safe. -type Reader interface { - // ReadBytesFull reads a full serialized object as "type size\0content". - // - // In a valid repository, hashing this payload with the same algorithm yields - // the requested object ID. Readers should treat this as a repository - // invariant and should not re-verify it on every read. - // - // Labels: Life-Parent. - ReadBytesFull(id objectid.ObjectID) ([]byte, error) - - // ReadBytesContent reads an object's type and content bytes. - // - // Labels: Life-Parent. - ReadBytesContent(id objectid.ObjectID) (objecttype.Type, []byte, error) - - // ReadReaderFull reads a full serialized object stream as "type size\0content". - // - // Labels: Life-Parent, Close-Caller. - ReadReaderFull(id objectid.ObjectID) (io.ReadCloser, error) - - // ReadReaderContent reads an object's type, declared content length, - // and content stream. - // - // Labels: Life-Parent, Close-Caller. - ReadReaderContent(id objectid.ObjectID) (objecttype.Type, int64, io.ReadCloser, error) - - // ReadSize reads an object's declared content length. - // - // This is equivalent to ReadHeader(...).size and may be cheaper than - // ReadHeader when callers do not need object type. - ReadSize(id objectid.ObjectID) (int64, error) - - // ReadHeader reads an object's type and declared content length. - ReadHeader(id objectid.ObjectID) (objecttype.Type, int64, error) - - // Refresh updates any backend-local discovery/cache view of on-disk objects. - // - // Backends without dynamic discovery should do nothing and return nil. - Refresh() error -} diff --git a/object/store/writer.go b/object/store/writer.go deleted file mode 100644 index 9fa05aba..00000000 --- a/object/store/writer.go +++ /dev/null @@ -1,8 +0,0 @@ -package objectstore - -// Writer represents a store that could perform both pack ingestions -// and individual object writes. -type Writer interface { - PackWriter - ObjectWriter -} diff --git a/object/store/writer_object.go b/object/store/writer_object.go deleted file mode 100644 index a18a5d84..00000000 --- a/object/store/writer_object.go +++ /dev/null @@ -1,37 +0,0 @@ -package objectstore - -import ( - "io" - - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// ObjectWriter writes individual Git objects. -type ObjectWriter interface { - // WriteReaderContent writes one typed object content stream. - WriteReaderContent(ty objecttype.Type, size int64, src io.Reader) (objectid.ObjectID, error) - - // WriteReaderFull writes one full serialized object stream as "type size\0content". - WriteReaderFull(src io.Reader) (objectid.ObjectID, error) - - // WriteBytesContent writes one typed object content byte slice. - WriteBytesContent(ty objecttype.Type, content []byte) (objectid.ObjectID, error) - - // WriteBytesFull writes one full serialized object byte slice as "type size\0content". - WriteBytesFull(raw []byte) (objectid.ObjectID, error) -} - -// ObjectQuarantine represents one quarantined object-wise write. -type ObjectQuarantine interface { - BaseQuarantine - ObjectWriter -} - -// ObjectQuarantineOptions controls the options for one object quarantine creation. -type ObjectQuarantineOptions struct{} - -// ObjectQuarantiner creates quarantines for object-wise writes. -type ObjectQuarantiner interface { - BeginObjectQuarantine(opts ObjectQuarantineOptions) (ObjectQuarantine, error) -} diff --git a/object/store/writer_pack.go b/object/store/writer_pack.go deleted file mode 100644 index 0f78c429..00000000 --- a/object/store/writer_pack.go +++ /dev/null @@ -1,58 +0,0 @@ -package objectstore - -import ( - "io" - - "codeberg.org/lindenii/furgit/common/iowrap" -) - -// PackWriteOptions controls one pack write operation. -type PackWriteOptions struct { - // ThinBase supplies the wider object reader used to complete thin packs - // during ingestion. - // - // This is an option for the write operation rather than on a particular - // pack-backed store because any pack-accepting store is not generally - // expected to know the entire repository object universe around it. - // In a normal repository, thin bases usually come from a broader view - // such as mix(loose, packed), and should not be treated as a property of - // the destination pack-accepting store. Thus, in almost all pack-ingesting - // operations, a thin base reader would be required, and hence it is - // included here. - // - // When nil, external thin-base repair is disabled and unresolved thin deltas - // fail ingestion. - ThinBase Reader - - // Progress receives human-readable progress messages. - // - // When nil, no progress output is emitted. - Progress iowrap.WriteFlusher - - // RequireTrailingEOF requires the source to hit EOF after the pack trailer. - // - // This is suitable for exact pack-file readers, but should be disabled for - // full-duplex transport streams like receive-pack where the peer keeps the - // connection open to read the server response. - RequireTrailingEOF bool -} - -// PackWriter writes Git pack streams. -type PackWriter interface { - // WritePack ingests one pack stream. - WritePack(src io.Reader, opts PackWriteOptions) error -} - -// PackQuarantine represents one quarantined pack-wise write. -type PackQuarantine interface { - BaseQuarantine - PackWriter -} - -// PackQuarantineOptions controls the options for one pack quarantine creation. -type PackQuarantineOptions struct{} - -// PackQuarantiner creates quarantines for pack-wise writes. -type PackQuarantiner interface { - BeginPackQuarantine(opts PackQuarantineOptions) (PackQuarantine, error) -} diff --git a/object/stored/doc.go b/object/stored/doc.go deleted file mode 100644 index d57cbd55..00000000 --- a/object/stored/doc.go +++ /dev/null @@ -1,7 +0,0 @@ -// Package stored wraps parsed objects with the object IDs they were loaded -// under. -// -// Parsed Git object values do not carry storage identity on their own. This -// package provides a small generic wrapper for the common case where callers -// need both the parsed object value and the object ID it was read from. -package stored diff --git a/object/stored/id.go b/object/stored/id.go deleted file mode 100644 index 956d069e..00000000 --- a/object/stored/id.go +++ /dev/null @@ -1,8 +0,0 @@ -package stored - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// ID returns the object ID. -func (stored *Stored[T]) ID() objectid.ObjectID { - return stored.id -} diff --git a/object/stored/new.go b/object/stored/new.go deleted file mode 100644 index 8b0ef881..00000000 --- a/object/stored/new.go +++ /dev/null @@ -1,11 +0,0 @@ -package stored - -import ( - "codeberg.org/lindenii/furgit/object" - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// New creates one stored object wrapper. -func New[T object.Object](id objectid.ObjectID, obj T) *Stored[T] { - return &Stored[T]{id: id, obj: obj} -} diff --git a/object/stored/object.go b/object/stored/object.go deleted file mode 100644 index ab22b9c8..00000000 --- a/object/stored/object.go +++ /dev/null @@ -1,6 +0,0 @@ -package stored - -// Object returns the wrapped object as itself. -func (stored *Stored[T]) Object() T { - return stored.obj -} diff --git a/object/stored/stored.go b/object/stored/stored.go deleted file mode 100644 index eb776f31..00000000 --- a/object/stored/stored.go +++ /dev/null @@ -1,13 +0,0 @@ -package stored - -import ( - "codeberg.org/lindenii/furgit/object" - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// Stored represents a stored object, -// i.e., an object along with its object ID. -type Stored[T object.Object] struct { - id objectid.ObjectID - obj T -} diff --git a/object/tag/parse.go b/object/tag/parse.go deleted file mode 100644 index 92fa0d8b..00000000 --- a/object/tag/parse.go +++ /dev/null @@ -1,89 +0,0 @@ -package tag - -import ( - "bytes" - "errors" - "fmt" - - objectid "codeberg.org/lindenii/furgit/object/id" - objectsignature "codeberg.org/lindenii/furgit/object/signature" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// Parse decodes a tag object body. -func Parse(body []byte, algo objectid.Algorithm) (*Tag, error) { - t := new(Tag) - i := 0 - - var haveTarget, haveType bool - - for i < len(body) { - rel := bytes.IndexByte(body[i:], '\n') - if rel < 0 { - return nil, errors.New("object: tag: missing newline") - } - - line := body[i : i+rel] - i += rel + 1 - - if len(line) == 0 { - break - } - - key, value, found := bytes.Cut(line, []byte{' '}) - if !found { - return nil, errors.New("object: tag: malformed header") - } - - switch string(key) { - case "object": - id, err := objectid.ParseHex(algo, string(value)) - if err != nil { - return nil, fmt.Errorf("object: tag: object: %w", err) - } - - t.Target = id - haveTarget = true - case "type": - ty, ok := objecttype.Parse(string(value)) - if !ok { - return nil, errors.New("object: tag: unknown target type") - } - - t.TargetType = ty - haveType = true - case "tag": - t.Name = append([]byte(nil), value...) - case "tagger": - idt, err := objectsignature.Parse(value) - if err != nil { - return nil, fmt.Errorf("object: tag: tagger: %w", err) - } - - t.Tagger = idt - case "gpgsig", "gpgsig-sha256": - for i < len(body) { - nextRel := bytes.IndexByte(body[i:], '\n') - if nextRel < 0 { - return nil, errors.New("object: tag: unterminated gpgsig") - } - - if body[i] != ' ' { - break - } - - i += nextRel + 1 - } - default: - // Ignore unknown headers for now. - } - } - - if !haveTarget || !haveType { - return nil, errors.New("object: tag: missing required headers") - } - - t.Message = append([]byte(nil), body[i:]...) - - return t, nil -} diff --git a/object/tag/parse_test.go b/object/tag/parse_test.go deleted file mode 100644 index 293350ed..00000000 --- a/object/tag/parse_test.go +++ /dev/null @@ -1,47 +0,0 @@ -package tag_test - -import ( - "bytes" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/tag" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -func TestTagParseFromGit(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - _, _, commitID := testRepo.MakeCommit(t, "subject\n\nbody") - tagID := testRepo.TagAnnotated(t, "v1", commitID, "tag message") - - rawBody := testRepo.CatFile(t, "tag", tagID) - - parsed, err := tag.Parse(rawBody, algo) - if err != nil { - t.Fatalf("ParseTag: %v", err) - } - - if parsed.Target != commitID { - t.Fatalf("tag target mismatch: got %s want %s", parsed.Target, commitID) - } - - if parsed.TargetType != objecttype.TypeCommit { - t.Fatalf("tag target type = %v, want %v", parsed.TargetType, objecttype.TypeCommit) - } - - if !bytes.Equal(parsed.Name, []byte("v1")) { - t.Fatalf("tag name = %q, want %q", parsed.Name, "v1") - } - - if parsed.Tagger == nil { - t.Fatalf("expected tagger") - } - - if !bytes.Contains(parsed.Message, []byte("tag message")) { - t.Fatalf("tag message mismatch: %q", parsed.Message) - } - }) -} diff --git a/object/tag/serialize.go b/object/tag/serialize.go deleted file mode 100644 index 4f9d6664..00000000 --- a/object/tag/serialize.go +++ /dev/null @@ -1,68 +0,0 @@ -package tag - -import ( - "bytes" - "errors" - "fmt" - - objectheader "codeberg.org/lindenii/furgit/object/header" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// SerializeWithoutHeader renders the raw tag body bytes. -func (tag *Tag) SerializeWithoutHeader() ([]byte, error) { - if tag.Target.Algorithm().Size() == 0 { - return nil, errors.New("object: tag: missing target id") - } - - var buf bytes.Buffer - fmt.Fprintf(&buf, "object %s\n", tag.Target.String()) - - tyName, ok := tag.TargetType.Name() - if !ok { - return nil, fmt.Errorf("object: tag: invalid target type %d", tag.TargetType) - } - - buf.WriteString("type ") - buf.WriteString(tyName) - buf.WriteByte('\n') - - buf.WriteString("tag ") - buf.Write(tag.Name) - buf.WriteByte('\n') - - if tag.Tagger != nil { - taggerBytes, err := tag.Tagger.Serialize() - if err != nil { - return nil, err - } - - buf.WriteString("tagger ") - buf.Write(taggerBytes) - buf.WriteByte('\n') - } - - buf.WriteByte('\n') - buf.Write(tag.Message) - - return buf.Bytes(), nil -} - -// SerializeWithHeader renders the raw object (header + body). -func (tag *Tag) SerializeWithHeader() ([]byte, error) { - body, err := tag.SerializeWithoutHeader() - if err != nil { - return nil, err - } - - header, ok := objectheader.Encode(objecttype.TypeTag, int64(len(body))) - if !ok { - return nil, errors.New("object: tag: failed to encode object header") - } - - raw := make([]byte, len(header)+len(body)) - copy(raw, header) - copy(raw[len(header):], body) - - return raw, nil -} diff --git a/object/tag/serialize_test.go b/object/tag/serialize_test.go deleted file mode 100644 index a1311c39..00000000 --- a/object/tag/serialize_test.go +++ /dev/null @@ -1,35 +0,0 @@ -package tag_test - -import ( - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/tag" -) - -func TestTagSerialize(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - _, _, commitID := testRepo.MakeCommit(t, "subject\n\nbody") - tagID := testRepo.TagAnnotated(t, "v1", commitID, "tag message") - - rawBody := testRepo.CatFile(t, "tag", tagID) - - parsed, err := tag.Parse(rawBody, algo) - if err != nil { - t.Fatalf("ParseTag: %v", err) - } - - rawObj, err := parsed.SerializeWithHeader() - if err != nil { - t.Fatalf("SerializeWithHeader: %v", err) - } - - gotID := algo.Sum(rawObj) - if gotID != tagID { - t.Fatalf("tag id mismatch: got %s want %s", gotID, tagID) - } - }) -} diff --git a/object/tag/tag.go b/object/tag/tag.go deleted file mode 100644 index e01f8ac9..00000000 --- a/object/tag/tag.go +++ /dev/null @@ -1,24 +0,0 @@ -// Package tag provides parsed annotated tag objects and tag serialization. -// -// It parses annotated tags into ordinary Go values for reading and -// construction. It does not preserve the exact original byte layout needed for -// signature verification; callers that need signature-verification payload -// fidelity should use [codeberg.org/lindenii/furgit/object/signed/tag]. -package tag - -import ( - objectid "codeberg.org/lindenii/furgit/object/id" - objectsignature "codeberg.org/lindenii/furgit/object/signature" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// Tag represents a fully materialized Git annotated tag object. -// -// Labels: MT-Unsafe. -type Tag struct { - Target objectid.ObjectID - TargetType objecttype.Type - Name []byte - Tagger *objectsignature.Signature - Message []byte -} diff --git a/object/tag/type.go b/object/tag/type.go deleted file mode 100644 index 215103ab..00000000 --- a/object/tag/type.go +++ /dev/null @@ -1,10 +0,0 @@ -package tag - -import objecttype "codeberg.org/lindenii/furgit/object/type" - -// ObjectType returns TypeTag. -func (tag *Tag) ObjectType() objecttype.Type { - _ = tag - - return objecttype.TypeTag -} diff --git a/object/tree/entry.go b/object/tree/entry.go deleted file mode 100644 index b3089b74..00000000 --- a/object/tree/entry.go +++ /dev/null @@ -1,57 +0,0 @@ -package tree - -import ( - "bytes" - "slices" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// TreeEntry represents a single entry in a tree. -type TreeEntry struct { - Mode FileMode - // Name is part of the tree ordering. Mutating it after insertion may break - // Tree ordering and lookup behavior. - Name []byte - ID objectid.ObjectID -} - -func (tree *Tree) entry(name []byte, searchIsTree bool) *TreeEntry { - index, ok := slices.BinarySearchFunc(tree.Entries, name, func(entry TreeEntry, name []byte) int { - return TreeEntryNameCompare(entry.Name, entry.Mode, name, searchIsTree) - }) - if !ok { - return nil - } - - entry := &tree.Entries[index] - if !bytes.Equal(entry.Name, name) { - return nil - } - - return entry -} - -func (tree *Tree) entryIndex(name []byte) (int, bool) { - index, ok := tree.entryIndexWithMode(name, true) - if ok { - return index, true - } - - return tree.entryIndexWithMode(name, false) -} - -func (tree *Tree) entryIndexWithMode(name []byte, searchIsTree bool) (int, bool) { - index, ok := slices.BinarySearchFunc(tree.Entries, name, func(entry TreeEntry, name []byte) int { - return TreeEntryNameCompare(entry.Name, entry.Mode, name, searchIsTree) - }) - if !ok { - return 0, false - } - - if !bytes.Equal(tree.Entries[index].Name, name) { - return 0, false - } - - return index, true -} diff --git a/object/tree/helpers_test.go b/object/tree/helpers_test.go deleted file mode 100644 index 3da92ce4..00000000 --- a/object/tree/helpers_test.go +++ /dev/null @@ -1,114 +0,0 @@ -package tree_test - -import ( - "bytes" - "fmt" - "strings" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - "codeberg.org/lindenii/furgit/object/tree" -) - -func buildGitMktreeInput(entries []tree.TreeEntry) string { - var b strings.Builder - for _, e := range entries { - fmt.Fprintf(&b, "%o %s %s\t%s\n", e.Mode, mktreeTypeFromMode(e.Mode), e.ID.String(), e.Name) - } - - return b.String() -} - -func mktreeTypeFromMode(mode tree.FileMode) string { - switch mode { - case tree.FileModeDir: - return "tree" - case tree.FileModeRegular, tree.FileModeExecutable, tree.FileModeSymlink: - return "blob" - case tree.FileModeGitlink: - return "commit" - default: - return "" - } -} - -func gitLsTreeNames(out []byte) [][]byte { - if len(out) == 0 { - return nil - } - - parts := bytes.Split(out, []byte{0}) - if len(parts) > 0 && len(parts[len(parts)-1]) == 0 { - parts = parts[:len(parts)-1] - } - - names := make([][]byte, 0, len(parts)) - for _, name := range parts { - names = append(names, append([]byte(nil), name...)) - } - - return names -} - -func adversarialRootEntries(t *testing.T, testRepo *testgit.TestRepo) []tree.TreeEntry { - t.Helper() - - blobA := testRepo.HashObject(t, "blob", []byte("blob-A\n")) - blobB := testRepo.HashObject(t, "blob", []byte("blob-B\n")) - blobC := testRepo.HashObject(t, "blob", []byte("blob-C\n")) - - subDirA := testRepo.Mktree(t, - fmt.Sprintf("100644 blob %s\tnested-a.txt\n100755 blob %s\trun-a.sh\n", blobA.String(), blobB.String())) - subDirB := testRepo.Mktree(t, - fmt.Sprintf("100644 blob %s\tnested-b.txt\n100644 blob %s\tz-last\n", blobB.String(), blobC.String())) - subDirC := testRepo.Mktree(t, - fmt.Sprintf("120000 blob %s\tlink-c\n100644 blob %s\tchild\n", blobC.String(), blobA.String())) - subDirD := testRepo.Mktree(t, - fmt.Sprintf("100644 blob %s\tleaf\n", blobA.String())) - - return []tree.TreeEntry{ - {Mode: tree.FileModeRegular, Name: []byte("z"), ID: blobA}, - {Mode: tree.FileModeRegular, Name: []byte("A"), ID: blobB}, - {Mode: tree.FileModeRegular, Name: []byte("aa"), ID: blobC}, - {Mode: tree.FileModeRegular, Name: []byte("a0"), ID: blobA}, - {Mode: tree.FileModeRegular, Name: []byte("a-"), ID: blobB}, - {Mode: tree.FileModeRegular, Name: []byte("a."), ID: blobC}, - {Mode: tree.FileModeRegular, Name: []byte("a_"), ID: blobA}, - {Mode: tree.FileModeRegular, Name: []byte("a~"), ID: blobB}, - {Mode: tree.FileModeRegular, Name: []byte("Z"), ID: blobC}, - {Mode: tree.FileModeRegular, Name: []byte("0"), ID: blobA}, - {Mode: tree.FileModeRegular, Name: []byte("9"), ID: blobB}, - {Mode: tree.FileModeRegular, Name: []byte("00"), ID: blobC}, - {Mode: tree.FileModeRegular, Name: []byte("这是一些非 ASCII 的字符"), ID: blobC}, - {Mode: tree.FileModeRegular, Name: []byte("𲰼是新进入 Unicode 的字符"), ID: blobC}, - {Mode: tree.FileModeRegular, Name: []byte("Emoji 👀"), ID: blobC}, - {Mode: tree.FileModeRegular, Name: []byte("_"), ID: blobA}, - {Mode: tree.FileModeRegular, Name: []byte("-dash"), ID: blobB}, - {Mode: tree.FileModeRegular, Name: []byte("dot.file"), ID: blobC}, - {Mode: tree.FileModeRegular, Name: []byte(".hidden"), ID: blobA}, - {Mode: tree.FileModeRegular, Name: []byte("CAPS"), ID: blobB}, - {Mode: tree.FileModeRegular, Name: []byte("caps"), ID: blobC}, - {Mode: tree.FileModeRegular, Name: []byte("mixCase"), ID: blobA}, - {Mode: tree.FileModeRegular, Name: []byte("name with space"), ID: blobB}, - {Mode: tree.FileModeRegular, Name: []byte("name-with-dash"), ID: blobC}, - {Mode: tree.FileModeRegular, Name: []byte("name.with.dot"), ID: blobA}, - {Mode: tree.FileModeRegular, Name: []byte("name_with_underscore"), ID: blobB}, - {Mode: tree.FileModeRegular, Name: []byte("tilde~name"), ID: blobC}, - {Mode: tree.FileModeRegular, Name: []byte("brace{name}"), ID: blobA}, - {Mode: tree.FileModeRegular, Name: []byte("plus+name"), ID: blobB}, - {Mode: tree.FileModeRegular, Name: []byte("equal=name"), ID: blobC}, - {Mode: tree.FileModeRegular, Name: []byte("at@name"), ID: blobA}, - {Mode: tree.FileModeRegular, Name: []byte("percent%name"), ID: blobB}, - {Mode: tree.FileModeRegular, Name: []byte("caret^name"), ID: blobC}, - {Mode: tree.FileModeRegular, Name: []byte("comma,name"), ID: blobA}, - {Mode: tree.FileModeRegular, Name: []byte("semi;name"), ID: blobB}, - {Mode: tree.FileModeRegular, Name: []byte("paren(name)"), ID: blobC}, - {Mode: tree.FileModeRegular, Name: []byte("bracket[name]"), ID: blobA}, - {Mode: tree.FileModeExecutable, Name: []byte("exec.sh"), ID: blobB}, - {Mode: tree.FileModeSymlink, Name: []byte("sym.link"), ID: blobC}, - {Mode: tree.FileModeDir, Name: []byte("dir"), ID: subDirA}, - {Mode: tree.FileModeDir, Name: []byte("dir0"), ID: subDirB}, - {Mode: tree.FileModeDir, Name: []byte("dir.space"), ID: subDirC}, - {Mode: tree.FileModeDir, Name: []byte("x"), ID: subDirD}, - } -} diff --git a/object/tree/insert.go b/object/tree/insert.go deleted file mode 100644 index 22bda74f..00000000 --- a/object/tree/insert.go +++ /dev/null @@ -1,24 +0,0 @@ -package tree - -import ( - "fmt" - "slices" -) - -// InsertEntry inserts a tree entry while preserving Git ordering. -// -// InsertEntry copies newEntry.Name. -func (tree *Tree) InsertEntry(newEntry TreeEntry) error { - if tree.entry(newEntry.Name, true) != nil || tree.entry(newEntry.Name, false) != nil { - return fmt.Errorf("object: tree: entry %q already exists", newEntry.Name) - } - - newEntry.Name = append([]byte(nil), newEntry.Name...) - - insertAt, _ := slices.BinarySearchFunc(tree.Entries, newEntry.Name, func(entry TreeEntry, name []byte) int { - return TreeEntryNameCompare(entry.Name, entry.Mode, name, newEntry.Mode == FileModeDir) - }) - tree.Entries = slices.Insert(tree.Entries, insertAt, newEntry) - - return nil -} diff --git a/object/tree/lookup.go b/object/tree/lookup.go deleted file mode 100644 index 249efd0f..00000000 --- a/object/tree/lookup.go +++ /dev/null @@ -1,18 +0,0 @@ -package tree - -// Entry looks up a tree entry by name. -// -// The returned pointer refers to storage within tree.Entries and must not be -// retained across InsertEntry or RemoveEntry calls. -func (tree *Tree) Entry(name []byte) *TreeEntry { - if len(tree.Entries) == 0 { - return nil - } - - index, ok := tree.entryIndex(name) - if !ok { - return nil - } - - return &tree.Entries[index] -} diff --git a/object/tree/mode.go b/object/tree/mode.go deleted file mode 100644 index b1cbc6bc..00000000 --- a/object/tree/mode.go +++ /dev/null @@ -1,12 +0,0 @@ -package tree - -// FileMode represents the mode of a file in a Git tree. -type FileMode uint32 - -const ( - FileModeDir FileMode = 0o40000 - FileModeRegular FileMode = 0o100644 - FileModeExecutable FileMode = 0o100755 - FileModeSymlink FileMode = 0o120000 - FileModeGitlink FileMode = 0o160000 -) diff --git a/object/tree/mode_details.go b/object/tree/mode_details.go deleted file mode 100644 index 9c34fd7c..00000000 --- a/object/tree/mode_details.go +++ /dev/null @@ -1,10 +0,0 @@ -package tree - -type fileModeDetails struct { - isBlobLike bool - isRegularFile bool -} - -func (mode FileMode) details() fileModeDetails { - return fileModeTable[mode] -} diff --git a/object/tree/mode_has_same_type.go b/object/tree/mode_has_same_type.go deleted file mode 100644 index a058cb9c..00000000 --- a/object/tree/mode_has_same_type.go +++ /dev/null @@ -1,12 +0,0 @@ -package tree - -// HasSameType reports whether mode and other describe the same tree entry kind. -// -// Regular files and executable files have the same type for diff-status purposes. -func (mode FileMode) HasSameType(other FileMode) bool { - if mode == other { - return true - } - - return mode.details().isRegularFile && other.details().isRegularFile -} diff --git a/object/tree/mode_is_blob_like.go b/object/tree/mode_is_blob_like.go deleted file mode 100644 index 3ec3a308..00000000 --- a/object/tree/mode_is_blob_like.go +++ /dev/null @@ -1,8 +0,0 @@ -package tree - -// IsBlobLike reports whether mode names one blob-like tree entry kind. -// -// Blob-like entries store blob object IDs as their targets. -func (mode FileMode) IsBlobLike() bool { - return mode.details().isBlobLike -} diff --git a/object/tree/mode_is_regular_file.go b/object/tree/mode_is_regular_file.go deleted file mode 100644 index 115395c0..00000000 --- a/object/tree/mode_is_regular_file.go +++ /dev/null @@ -1,6 +0,0 @@ -package tree - -// IsRegularFile reports whether mode names one regular-file variant. -func (mode FileMode) IsRegularFile() bool { - return mode.details().isRegularFile -} diff --git a/object/tree/mode_table.go b/object/tree/mode_table.go deleted file mode 100644 index 1695f270..00000000 --- a/object/tree/mode_table.go +++ /dev/null @@ -1,24 +0,0 @@ -package tree - -var fileModeTable = map[FileMode]fileModeDetails{ //nolint:gochecknoglobals - FileModeDir: { - isBlobLike: false, - isRegularFile: false, - }, - FileModeRegular: { - isBlobLike: true, - isRegularFile: true, - }, - FileModeExecutable: { - isBlobLike: true, - isRegularFile: true, - }, - FileModeSymlink: { - isBlobLike: true, - isRegularFile: false, - }, - FileModeGitlink: { - isBlobLike: false, - isRegularFile: false, - }, -} diff --git a/object/tree/name.go b/object/tree/name.go deleted file mode 100644 index 02af3292..00000000 --- a/object/tree/name.go +++ /dev/null @@ -1,51 +0,0 @@ -package tree - -// TreeEntryNameCompare compares names using Git tree ordering rules. -func TreeEntryNameCompare(entryName []byte, entryMode FileMode, searchName []byte, searchIsTree bool) int { - isEntryTree := entryMode == FileModeDir - - entryLen := len(entryName) - if isEntryTree { - entryLen++ - } - - searchLen := len(searchName) - if searchIsTree { - searchLen++ - } - - n := min(searchLen, entryLen) - - for i := range n { - var ec, sc byte - if i < len(entryName) { - ec = entryName[i] - } else { - ec = '/' - } - - if i < len(searchName) { - sc = searchName[i] - } else { - sc = '/' - } - - if ec < sc { - return -1 - } - - if ec > sc { - return 1 - } - } - - if entryLen < searchLen { - return -1 - } - - if entryLen > searchLen { - return 1 - } - - return 0 -} diff --git a/object/tree/parse.go b/object/tree/parse.go deleted file mode 100644 index bb874828..00000000 --- a/object/tree/parse.go +++ /dev/null @@ -1,58 +0,0 @@ -package tree - -import ( - "bytes" - "fmt" - "strconv" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// Parse decodes a tree object body into a fully materialized Tree. -func Parse(body []byte, algo objectid.Algorithm) (*Tree, error) { - var entries []TreeEntry - - i := 0 - for i < len(body) { - space := bytes.IndexByte(body[i:], ' ') - if space < 0 { - return nil, fmt.Errorf("object: tree: missing mode terminator") - } - - modeBytes := body[i : i+space] - i += space + 1 - - nul := bytes.IndexByte(body[i:], 0) - if nul < 0 { - return nil, fmt.Errorf("object: tree: missing name terminator") - } - - nameBytes := body[i : i+nul] - i += nul + 1 - - idEnd := i + algo.Size() - if idEnd > len(body) { - return nil, fmt.Errorf("object: tree: truncated child object id") - } - - id, err := objectid.FromBytes(algo, body[i:idEnd]) - if err != nil { - return nil, err - } - - i = idEnd - - mode, err := strconv.ParseUint(string(modeBytes), 8, 32) - if err != nil { - return nil, fmt.Errorf("object: tree: parse mode: %w", err) - } - - entries = append(entries, TreeEntry{ - Mode: FileMode(mode), - Name: append([]byte(nil), nameBytes...), - ID: id, - }) - } - - return &Tree{Entries: entries}, nil -} diff --git a/object/tree/parse_test.go b/object/tree/parse_test.go deleted file mode 100644 index 2b98ede7..00000000 --- a/object/tree/parse_test.go +++ /dev/null @@ -1,109 +0,0 @@ -package tree_test - -import ( - "bytes" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/tree" -) - -func TestTreeParseFromGit(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - entries := adversarialRootEntries(t, testRepo) - - inserted := &tree.Tree{} - for _, entry := range entries { - err := inserted.InsertEntry(entry) - if err != nil { - t.Fatalf("InsertEntry(%q): %v", entry.Name, err) - } - } - - treeID := testRepo.Mktree(t, buildGitMktreeInput(inserted.Entries)) - - rawBody := testRepo.CatFile(t, "tree", treeID) - - parsed, err := tree.Parse(rawBody, algo) - if err != nil { - t.Fatalf("ParseTree: %v", err) - } - - if len(parsed.Entries) != len(inserted.Entries) { - t.Fatalf("entry count = %d, want %d", len(parsed.Entries), len(inserted.Entries)) - } - - for i := range inserted.Entries { - got := parsed.Entries[i] - - want := inserted.Entries[i] - if got.Mode != want.Mode || got.ID != want.ID || !bytes.Equal(got.Name, want.Name) { - t.Fatalf("entry[%d] mismatch: got (%o,%q,%s) want (%o,%q,%s)", - i, got.Mode, got.Name, got.ID, want.Mode, want.Name, want.ID) - } - } - - lsNames := gitLsTreeNames(testRepo.RunBytes(t, "ls-tree", "--name-only", "-z", treeID.String())) - if len(lsNames) != len(parsed.Entries) { - t.Fatalf("ls-tree names = %d, want %d", len(lsNames), len(parsed.Entries)) - } - - for i := range lsNames { - if !bytes.Equal(lsNames[i], parsed.Entries[i].Name) { - t.Fatalf("ordering mismatch at %d: git=%q parsed=%q", i, lsNames[i], parsed.Entries[i].Name) - } - } - - for _, want := range inserted.Entries { - got := parsed.Entry(want.Name) - - if got == nil { - t.Fatalf("Entry(%q) returned nil", want.Name) - } - - if got.Mode != want.Mode || got.ID != want.ID { - t.Fatalf("Entry(%q) mismatch", want.Name) - } - } - - if parsed.Entry([]byte("does-not-exist")) != nil { - t.Fatalf("Entry on missing name should be nil") - } - }) -} - -func TestTreeInsertEntryCopiesName(t *testing.T) { - t.Parallel() - - var tr tree.Tree - - name := []byte("alpha") - entry := tree.TreeEntry{ - Mode: tree.FileModeRegular, - Name: name, - ID: objectid.ObjectID{}, - } - - err := tr.InsertEntry(entry) - if err != nil { - t.Fatalf("InsertEntry: %v", err) - } - - name[0] = 'b' - - got := tr.Entry([]byte("alpha")) - if got == nil { - t.Fatalf("Entry(alpha) returned nil") - } - - if !bytes.Equal(got.Name, []byte("alpha")) { - t.Fatalf("stored name = %q, want %q", got.Name, []byte("alpha")) - } - - if tr.Entry([]byte("blpha")) != nil { - t.Fatalf("mutating caller name should not affect stored entry") - } -} diff --git a/object/tree/path_append.go b/object/tree/path_append.go deleted file mode 100644 index 609d5279..00000000 --- a/object/tree/path_append.go +++ /dev/null @@ -1,14 +0,0 @@ -package tree - -// AppendPath appends path to dst as one slash-separated byte path. -func AppendPath(dst []byte, path [][]byte) []byte { - for i := range path { - if i > 0 { - dst = append(dst, '/') - } - - dst = append(dst, path[i]...) - } - - return dst -} diff --git a/object/tree/path_clone.go b/object/tree/path_clone.go deleted file mode 100644 index a4668add..00000000 --- a/object/tree/path_clone.go +++ /dev/null @@ -1,16 +0,0 @@ -package tree - -import ( - "bytes" - "slices" -) - -// ClonePath returns one deep copy of path. -func ClonePath(path [][]byte) [][]byte { - cloned := slices.Clone(path) - for i := range cloned { - cloned[i] = bytes.Clone(cloned[i]) - } - - return cloned -} diff --git a/object/tree/path_prefix.go b/object/tree/path_prefix.go deleted file mode 100644 index ed658cee..00000000 --- a/object/tree/path_prefix.go +++ /dev/null @@ -1,19 +0,0 @@ -package tree - -import ( - "bytes" - "slices" -) - -// HasPathPrefix reports whether path begins with prefix as whole components. -func HasPathPrefix(path, prefix [][]byte) bool { - if len(prefix) == 0 { - return true - } - - if len(path) < len(prefix) { - return false - } - - return slices.EqualFunc(path[:len(prefix)], prefix, bytes.Equal) -} diff --git a/object/tree/path_split.go b/object/tree/path_split.go deleted file mode 100644 index c147dd25..00000000 --- a/object/tree/path_split.go +++ /dev/null @@ -1,19 +0,0 @@ -package tree - -import ( - "bytes" -) - -// SplitPath splits one slash-separated tree path into components. -func SplitPath(path []byte) [][]byte { - if len(path) == 0 { - return nil - } - - parts := bytes.Split(path, []byte{'/'}) - for i := range parts { - parts[i] = bytes.Clone(parts[i]) - } - - return parts -} diff --git a/object/tree/remove.go b/object/tree/remove.go deleted file mode 100644 index 94de88da..00000000 --- a/object/tree/remove.go +++ /dev/null @@ -1,22 +0,0 @@ -package tree - -import ( - "fmt" - "slices" -) - -// RemoveEntry removes a tree entry by name. -func (tree *Tree) RemoveEntry(name []byte) error { - if len(tree.Entries) == 0 { - return fmt.Errorf("object: tree: entry %q not found", name) - } - - index, ok := tree.entryIndex(name) - if !ok { - return fmt.Errorf("object: tree: entry %q not found", name) - } - - tree.Entries = slices.Delete(tree.Entries, index, index+1) - - return nil -} diff --git a/object/tree/serialize.go b/object/tree/serialize.go deleted file mode 100644 index 69deacda..00000000 --- a/object/tree/serialize.go +++ /dev/null @@ -1,55 +0,0 @@ -package tree - -import ( - "errors" - "strconv" - - objectheader "codeberg.org/lindenii/furgit/object/header" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// SerializeWithoutHeader renders the raw tree body bytes. -func (tree *Tree) SerializeWithoutHeader() ([]byte, error) { - var bodyLen int - - for _, entry := range tree.Entries { - mode := strconv.FormatUint(uint64(entry.Mode), 8) - bodyLen += len(mode) + 1 + len(entry.Name) + 1 + entry.ID.Algorithm().Size() - } - - body := make([]byte, bodyLen) - pos := 0 - - for _, entry := range tree.Entries { - mode := strconv.FormatUint(uint64(entry.Mode), 8) - pos += copy(body[pos:], mode) - body[pos] = ' ' - pos++ - pos += copy(body[pos:], entry.Name) - body[pos] = 0 - pos++ - id := entry.ID.Bytes() - pos += copy(body[pos:], id) - } - - return body, nil -} - -// SerializeWithHeader renders the raw object (header + body). -func (tree *Tree) SerializeWithHeader() ([]byte, error) { - body, err := tree.SerializeWithoutHeader() - if err != nil { - return nil, err - } - - header, ok := objectheader.Encode(objecttype.TypeTree, int64(len(body))) - if !ok { - return nil, errors.New("object: tree: failed to encode object header") - } - - raw := make([]byte, len(header)+len(body)) - copy(raw, header) - copy(raw[len(header):], body) - - return raw, nil -} diff --git a/object/tree/serialize_test.go b/object/tree/serialize_test.go deleted file mode 100644 index 9c9a2f1c..00000000 --- a/object/tree/serialize_test.go +++ /dev/null @@ -1,73 +0,0 @@ -package tree_test - -import ( - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/tree" -) - -func TestTreeSerialize(t *testing.T) { - t.Parallel() - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - entries := adversarialRootEntries(t, testRepo) - obj := &tree.Tree{} - - for i := len(entries) - 1; i >= 0; i-- { - err := obj.InsertEntry(entries[i]) - if err != nil { - t.Fatalf("InsertEntry(%q): %v", entries[i].Name, err) - } - } - - if len(obj.Entries) < 32 { - t.Fatalf("expected at least 32 entries, got %d", len(obj.Entries)) - } - - dup := obj.Entries[0] - - err := obj.InsertEntry(dup) - if err == nil { - t.Fatalf("duplicate InsertEntry should fail") - } - - removed := obj.Entries[len(obj.Entries)/2] - - err = obj.RemoveEntry(removed.Name) - if err != nil { - t.Fatalf("RemoveEntry(%q): %v", removed.Name, err) - } - - if obj.Entry(removed.Name) != nil { - t.Fatalf("Entry(%q) should be nil after remove", removed.Name) - } - - err = obj.RemoveEntry([]byte("no-such-entry")) - if err == nil { - t.Fatalf("RemoveEntry missing entry should fail") - } - - err = obj.InsertEntry(removed) - if err != nil { - t.Fatalf("re-InsertEntry(%q): %v", removed.Name, err) - } - - if obj.Entry(removed.Name) == nil { - t.Fatalf("Entry(%q) should exist after reinsert", removed.Name) - } - - wantTreeID := testRepo.Mktree(t, buildGitMktreeInput(obj.Entries)) - - rawObj, err := obj.SerializeWithHeader() - if err != nil { - t.Fatalf("SerializeWithHeader: %v", err) - } - - gotTreeID := algo.Sum(rawObj) - if gotTreeID != wantTreeID { - t.Fatalf("tree id mismatch: got %s want %s", gotTreeID, wantTreeID) - } - }) -} diff --git a/object/tree/tree.go b/object/tree/tree.go deleted file mode 100644 index d0c7f4f0..00000000 --- a/object/tree/tree.go +++ /dev/null @@ -1,12 +0,0 @@ -// Package tree provides representations, parsers, and serializers for tree objects. -package tree - -// Tree represents a fully materialized Git tree object. -// -// Labels: MT-Unsafe. -type Tree struct { - // Entries must be sorted by TreeEntryNameCompare. - // Use the Tree methods to preserve ordering and copy semantics rather than - // modifying the slice directly. - Entries []TreeEntry -} diff --git a/object/tree/type.go b/object/tree/type.go deleted file mode 100644 index 416544af..00000000 --- a/object/tree/type.go +++ /dev/null @@ -1,10 +0,0 @@ -package tree - -import objecttype "codeberg.org/lindenii/furgit/object/type" - -// ObjectType returns TypeTree. -func (tree *Tree) ObjectType() objecttype.Type { - _ = tree - - return objecttype.TypeTree -} diff --git a/object/type/details.go b/object/type/details.go deleted file mode 100644 index 17bdcfd4..00000000 --- a/object/type/details.go +++ /dev/null @@ -1,10 +0,0 @@ -package objecttype - -type typeDetails struct { - name string - isBaseObject bool -} - -func (ty Type) details() typeDetails { - return typeTable[ty] -} diff --git a/object/type/is_base.go b/object/type/is_base.go deleted file mode 100644 index cdc11f5b..00000000 --- a/object/type/is_base.go +++ /dev/null @@ -1,7 +0,0 @@ -package objecttype - -// IsBaseObject reports whether ty is one of the four canonical Git object -// types encoded directly in pack entries. -func (ty Type) IsBaseObject() bool { - return ty.details().isBaseObject -} diff --git a/object/type/name.go b/object/type/name.go deleted file mode 100644 index c95fe90b..00000000 --- a/object/type/name.go +++ /dev/null @@ -1,11 +0,0 @@ -package objecttype - -// Name returns the canonical Git object type name. -func (ty Type) Name() (string, bool) { - details := ty.details() - if details.name == "" { - return "", false - } - - return details.name, true -} diff --git a/object/type/parse.go b/object/type/parse.go deleted file mode 100644 index bc5ca736..00000000 --- a/object/type/parse.go +++ /dev/null @@ -1,8 +0,0 @@ -package objecttype - -// Parse parses a canonical Git object type name. -func Parse(name string) (Type, bool) { - ty, ok := typeByName[name] - - return ty, ok -} diff --git a/object/type/table.go b/object/type/table.go deleted file mode 100644 index 19cc760d..00000000 --- a/object/type/table.go +++ /dev/null @@ -1,21 +0,0 @@ -package objecttype - -//nolint:gochecknoglobals -var typeTable = [...]typeDetails{ - TypeInvalid: {}, - TypeCommit: {name: "commit", isBaseObject: true}, - TypeTree: {name: "tree", isBaseObject: true}, - TypeBlob: {name: "blob", isBaseObject: true}, - TypeTag: {name: "tag", isBaseObject: true}, - TypeFuture: {}, - TypeOfsDelta: {}, - TypeRefDelta: {}, -} - -//nolint:gochecknoglobals -var typeByName = map[string]Type{ - typeTable[TypeCommit].name: TypeCommit, - typeTable[TypeTree].name: TypeTree, - typeTable[TypeBlob].name: TypeBlob, - typeTable[TypeTag].name: TypeTag, -} diff --git a/object/type/type.go b/object/type/type.go deleted file mode 100644 index 18e0ac35..00000000 --- a/object/type/type.go +++ /dev/null @@ -1,16 +0,0 @@ -// Package objecttype provides Git object type tags and names. -package objecttype - -// Type mirrors Git object type tags in packfiles. -type Type uint8 - -const ( - TypeInvalid Type = 0 - TypeCommit Type = 1 - TypeTree Type = 2 - TypeBlob Type = 3 - TypeTag Type = 4 - TypeFuture Type = 5 - TypeOfsDelta Type = 6 - TypeRefDelta Type = 7 -) diff --git a/reachability/connected.go b/reachability/connected.go deleted file mode 100644 index 96211079..00000000 --- a/reachability/connected.go +++ /dev/null @@ -1,19 +0,0 @@ -package reachability - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// CheckConnected verifies that all objects reachable from wants (under the -// selected domain) can be fully traversed without missing-object/type/parse -// errors, excluding subgraphs rooted at haves. -// -// Even with commit-graph acceleration available, each visited commit is -// still validated against the object store. -func (r *Reachability) CheckConnected(domain Domain, haves, wants map[objectid.ObjectID]struct{}) error { - walk := r.Walk(domain, haves, wants) - - walk.strict = true - for range walk.Seq() { - } - - return walk.Err() -} diff --git a/reachability/doc.go b/reachability/doc.go deleted file mode 100644 index ccf5e29d..00000000 --- a/reachability/doc.go +++ /dev/null @@ -1,5 +0,0 @@ -// Package reachability traverses the reachable Git object graph. -// -// It supports both commit-domain and full object-domain traversal over -// one object store, and accepts an optional commit graph for performance. -package reachability diff --git a/reachability/domain.go b/reachability/domain.go deleted file mode 100644 index 1fe24fe8..00000000 --- a/reachability/domain.go +++ /dev/null @@ -1,22 +0,0 @@ -package reachability - -import "fmt" - -// Domain specifies which graph edges are traversed. -type Domain uint8 - -const ( - // DomainCommits traverses commit-parent edges and annotated-tag target edges. - DomainCommits Domain = iota - // DomainObjects traverses full commit/tree/blob objects. - DomainObjects -) - -func validateDomain(domain Domain) error { - switch domain { - case DomainCommits, DomainObjects: - return nil - default: - return fmt.Errorf("reachability: invalid domain %d", domain) - } -} diff --git a/reachability/integration_test.go b/reachability/integration_test.go deleted file mode 100644 index 20d9e3f7..00000000 --- a/reachability/integration_test.go +++ /dev/null @@ -1,373 +0,0 @@ -package reachability_test - -import ( - "errors" - "fmt" - "io/fs" - "maps" - "slices" - "strings" - "testing" - - giterrors "codeberg.org/lindenii/furgit/errors" - "codeberg.org/lindenii/furgit/internal/testgit" - objectfetch "codeberg.org/lindenii/furgit/object/fetch" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/reachability" -) - -func TestWalkCommitsMatchesGitRevList(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ - ObjectFormat: algo, - Bare: true, - RefFormat: "files", - }) - - _, tree1 := testRepo.MakeSingleFileTree(t, "base.txt", []byte("base\n")) - base := testRepo.CommitTree(t, tree1, "base") - - _, tree2 := testRepo.MakeSingleFileTree(t, "left.txt", []byte("left\n")) - left := testRepo.CommitTree(t, tree2, "left", base) - - _, tree3 := testRepo.MakeSingleFileTree(t, "right.txt", []byte("right\n")) - right := testRepo.CommitTree(t, tree3, "right", base) - - _, tree4 := testRepo.MakeSingleFileTree(t, "merge.txt", []byte("merge\n")) - merge := testRepo.CommitTree(t, tree4, "merge", left, right) - - tag1 := testRepo.TagAnnotated(t, "v1", merge, "v1") - tag2 := testRepo.TagAnnotated(t, "v2", tag1, "v2") - - r := openReachabilityFromTestRepo(t, testRepo) - walk := r.Walk( - reachability.DomainCommits, - nil, - map[objectid.ObjectID]struct{}{merge: {}}, - ) - - got := oidSetFromSeq(walk.Seq()) - - err := walk.Err() - if err != nil { - t.Fatalf("walk.Err(): %v", err) - } - - want := gitRevListSet(t, testRepo, false, []objectid.ObjectID{merge}, nil) - if !maps.Equal(got, want) { - t.Fatalf("commit walk mismatch:\n got=%v\nwant=%v", sortedOIDStrings(got), sortedOIDStrings(want)) - } - - peelWalk := r.Walk( - reachability.DomainCommits, - nil, - map[objectid.ObjectID]struct{}{tag2: {}}, - ) - - peelGot := oidSetFromSeq(peelWalk.Seq()) - - err = peelWalk.Err() - if err != nil { - t.Fatalf("peelWalk.Err(): %v", err) - } - - wantWithTags := maps.Clone(want) - wantWithTags[tag1] = struct{}{} - - wantWithTags[tag2] = struct{}{} - if !maps.Equal(peelGot, wantWithTags) { - t.Fatalf("tag-root commit walk mismatch:\n got=%v\nwant=%v", sortedOIDStrings(peelGot), sortedOIDStrings(wantWithTags)) - } - }) -} - -func TestWalkObjectsMatchesGitRevListObjects(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ - ObjectFormat: algo, - Bare: true, - RefFormat: "files", - }) - - aBlob := testRepo.HashObject(t, "blob", []byte("a\n")) - bBlob := testRepo.HashObject(t, "blob", []byte("b\n")) - nestedTree := testRepo.Mktree(t, fmt.Sprintf("100644 blob %s\tb.txt\n", bBlob)) - rootTree := testRepo.Mktree(t, - fmt.Sprintf("100644 blob %s\ta.txt\n040000 tree %s\tdir\n", aBlob, nestedTree), - ) - base := testRepo.CommitTree(t, rootTree, "base") - - cBlob := testRepo.HashObject(t, "blob", []byte("c\n")) - tree2 := testRepo.Mktree(t, fmt.Sprintf("100644 blob %s\tc.txt\n", cBlob)) - head := testRepo.CommitTree(t, tree2, "head", base) - tag := testRepo.TagAnnotated(t, "objtag", head, "objtag") - - r := openReachabilityFromTestRepo(t, testRepo) - walk := r.Walk( - reachability.DomainObjects, - nil, - map[objectid.ObjectID]struct{}{head: {}}, - ) - - got := oidSetFromSeq(walk.Seq()) - - err := walk.Err() - if err != nil { - t.Fatalf("walk.Err(): %v", err) - } - - want := gitRevListSet(t, testRepo, true, []objectid.ObjectID{head}, nil) - if !maps.Equal(got, want) { - t.Fatalf("object walk mismatch:\n got=%v\nwant=%v", sortedOIDStrings(got), sortedOIDStrings(want)) - } - - peelWalk := r.Walk( - reachability.DomainObjects, - nil, - map[objectid.ObjectID]struct{}{tag: {}}, - ) - - peelGot := oidSetFromSeq(peelWalk.Seq()) - - err = peelWalk.Err() - if err != nil { - t.Fatalf("peelWalk.Err(): %v", err) - } - - wantFromTag := gitRevListSet(t, testRepo, true, []objectid.ObjectID{tag}, nil) - if !maps.Equal(peelGot, wantFromTag) { - t.Fatalf("tag-root object walk mismatch:\n got=%v\nwant=%v", sortedOIDStrings(peelGot), sortedOIDStrings(wantFromTag)) - } - - walkWithHave := r.Walk( - reachability.DomainObjects, - map[objectid.ObjectID]struct{}{base: {}}, - map[objectid.ObjectID]struct{}{head: {}}, - ) - - withHave := oidSetFromSeq(walkWithHave.Seq()) - - err = walkWithHave.Err() - if err != nil { - t.Fatalf("walkWithHave.Err(): %v", err) - } - - _, ok := withHave[base] - if ok { - t.Fatalf("walk output unexpectedly contains have commit %s", base) - } - }) -} - -func TestCheckConnectedMissingObject(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ - ObjectFormat: algo, - Bare: true, - RefFormat: "files", - }) - - _, treeID, commitID := testRepo.MakeCommit(t, "missing") - - testRepo.RemoveLooseObject(t, treeID) - - r := openReachabilityFromTestRepo(t, testRepo) - - err := r.CheckConnected( - reachability.DomainObjects, - nil, - map[objectid.ObjectID]struct{}{commitID: {}}, - ) - if err == nil { - t.Fatal("expected error") - } - - missing, ok := errors.AsType[*giterrors.ObjectMissingError](err) - if !ok { - t.Fatalf("expected ObjectMissingError, got %T (%v)", err, err) - } - - if missing.OID != treeID { - t.Fatalf("missing oid = %s, want %s", missing.OID, treeID) - } - }) -} - -func TestWalkOnPackedOnlyRepo(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ - ObjectFormat: algo, - Bare: true, - RefFormat: "files", - }) - - _, tree1 := testRepo.MakeSingleFileTree(t, "one.txt", []byte("one\n")) - c1 := testRepo.CommitTree(t, tree1, "one") - _, tree2 := testRepo.MakeSingleFileTree(t, "two.txt", []byte("two\n")) - c2 := testRepo.CommitTree(t, tree2, "two", c1) - testRepo.UpdateRef(t, "refs/heads/main", c2) - testRepo.SymbolicRef(t, "HEAD", "refs/heads/main") - - testRepo.Repack(t, "-ad") - testRepo.Run(t, "prune-packed") - - assertPackedOnly(t, testRepo) - - r := openReachabilityFromTestRepo(t, testRepo) - walk := r.Walk( - reachability.DomainCommits, - nil, - map[objectid.ObjectID]struct{}{c2: {}}, - ) - - got := oidSetFromSeq(walk.Seq()) - - err := walk.Err() - if err != nil { - t.Fatalf("walk.Err(): %v", err) - } - - _, ok := got[c2] - if !ok { - t.Fatalf("walk output missing HEAD commit %s", c2) - } - - _, ok = got[c1] - if !ok { - t.Fatalf("walk output missing parent commit %s", c1) - } - }) -} - -func openReachabilityFromTestRepo(t *testing.T, testRepo *testgit.TestRepo) *reachability.Reachability { - t.Helper() - - return reachability.New(objectfetch.New(testRepo.OpenObjectStore(t)), nil) -} - -func oidSetFromSeq(seq func(func(objectid.ObjectID) bool)) map[objectid.ObjectID]struct{} { - out := make(map[objectid.ObjectID]struct{}) - - seq(func(id objectid.ObjectID) bool { - out[id] = struct{}{} - - return true - }) - - return out -} - -func gitRevListSet( - t *testing.T, - testRepo *testgit.TestRepo, - includeObjects bool, - wants []objectid.ObjectID, - haves []objectid.ObjectID, -) map[objectid.ObjectID]struct{} { - t.Helper() - - args := []string{"rev-list"} - if includeObjects { - args = append(args, "--objects") - } - - for _, want := range wants { - args = append(args, want.String()) - } - - if len(haves) > 0 { - args = append(args, "--not") - for _, have := range haves { - args = append(args, have.String()) - } - } - - out := testRepo.Run(t, args...) - set := make(map[objectid.ObjectID]struct{}) - - for line := range strings.SplitSeq(strings.TrimSpace(out), "\n") { - line = strings.TrimSpace(line) - if line == "" { - continue - } - - tok := line - - i := strings.IndexByte(tok, ' ') - if i >= 0 { - tok = tok[:i] - } - - id, err := objectid.ParseHex(testRepo.Algorithm(), tok) - if err != nil { - t.Fatalf("parse rev-list oid %q: %v", tok, err) - } - - set[id] = struct{}{} - } - - return set -} - -func sortedOIDStrings(set map[objectid.ObjectID]struct{}) []string { - out := make([]string, 0, len(set)) - for id := range set { - out = append(out, id.String()) - } - - slices.Sort(out) - - return out -} - -func assertPackedOnly(t *testing.T, testRepo *testgit.TestRepo) { - t.Helper() - - objectsRoot := testRepo.OpenObjectsRoot(t) - - entries, err := fs.ReadDir(objectsRoot.FS(), ".") - if err != nil { - t.Fatalf("ReadDir(objects): %v", err) - } - - for _, entry := range entries { - name := entry.Name() - if name == "pack" || name == "info" { - continue - } - - if len(name) == 2 && isHexDirName(name) { - subEntries, err := fs.ReadDir(objectsRoot.FS(), name) - if err != nil { - t.Fatalf("ReadDir(objects/%s): %v", name, err) - } - - if len(subEntries) != 0 { - t.Fatalf("found loose objects in objects/%s", name) - } - } - } -} - -func isHexDirName(name string) bool { - if len(name) != 2 { - return false - } - - for i := range 2 { - c := name[i] - if (c < '0' || c > '9') && (c < 'a' || c > 'f') { - return false - } - } - - return true -} diff --git a/reachability/reachability.go b/reachability/reachability.go deleted file mode 100644 index fd4d00e5..00000000 --- a/reachability/reachability.go +++ /dev/null @@ -1,22 +0,0 @@ -package reachability - -import ( - commitgraphread "codeberg.org/lindenii/furgit/format/commitgraph/read" - objectfetch "codeberg.org/lindenii/furgit/object/fetch" -) - -// Reachability provides graph traversal over objects in one object store. -// -// Labels: MT-Safe. -type Reachability struct { - fetcher *objectfetch.Fetcher - graph *commitgraphread.Reader -} - -// New builds a Reachability over one object fetcher with an optional -// commit-graph reader for faster commit-domain traversal. -// -// Labels: Deps-Borrowed, Life-Parent. -func New(fetcher *objectfetch.Fetcher, graph *commitgraphread.Reader) *Reachability { - return &Reachability{fetcher: fetcher, graph: graph} -} diff --git a/reachability/unit_test.go b/reachability/unit_test.go deleted file mode 100644 index 1f761108..00000000 --- a/reachability/unit_test.go +++ /dev/null @@ -1,424 +0,0 @@ -package reachability_test - -import ( - "errors" - "fmt" - "maps" - "slices" - "testing" - - giterrors "codeberg.org/lindenii/furgit/errors" - "codeberg.org/lindenii/furgit/internal/testgit" - objectfetch "codeberg.org/lindenii/furgit/object/fetch" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/store/memory" - "codeberg.org/lindenii/furgit/object/tree" - objecttype "codeberg.org/lindenii/furgit/object/type" - "codeberg.org/lindenii/furgit/reachability" -) - -type memStore struct { - *memory.Store - - readBytesByObjectID map[objectid.ObjectID]int -} - -// newCountingMemStore builds one in-memory store that records content-read -// counts by object ID. -func newCountingMemStore(algo objectid.Algorithm) *memStore { - return &memStore{ - Store: memory.New(algo), - readBytesByObjectID: make(map[objectid.ObjectID]int), - } -} - -func (store *memStore) ReadBytesContent(id objectid.ObjectID) (objecttype.Type, []byte, error) { - store.readBytesByObjectID[id]++ - - return store.Store.ReadBytesContent(id) -} - -func commitBody(tree objectid.ObjectID, parents ...objectid.ObjectID) []byte { - buf := fmt.Appendf(nil, "tree %s\n", tree.String()) - for _, parent := range parents { - buf = append(buf, fmt.Appendf(nil, "parent %s\n", parent.String())...) - } - - buf = append(buf, []byte("\nmsg\n")...) - - return buf -} - -func tagBody(target objectid.ObjectID, targetType objecttype.Type) []byte { - targetName, ok := targetType.Name() - if !ok { - panic("invalid tag target type") - } - - return fmt.Appendf(nil, "object %s\ntype %s\ntag t\n\nmsg\n", target.String(), targetName) -} - -func collectSeq(seq func(func(objectid.ObjectID) bool)) []objectid.ObjectID { - var out []objectid.ObjectID - - seq(func(id objectid.ObjectID) bool { - out = append(out, id) - - return true - }) - - return out -} - -func toSet(ids []objectid.ObjectID) map[objectid.ObjectID]struct{} { - set := make(map[objectid.ObjectID]struct{}, len(ids)) - for _, id := range ids { - set[id] = struct{}{} - } - - return set -} - -func TestWalkDomainCommitsIncludesTagNodes(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - store := newCountingMemStore(algo) - - blob, err := store.WriteBytesContent(objecttype.TypeBlob, []byte("blob\n")) - if err != nil { - t.Fatal(err) - } - - tree, err := store.WriteBytesContent(objecttype.TypeTree, mustSerializeTree(t, &tree.Tree{Entries: []tree.TreeEntry{{ - Mode: tree.FileModeRegular, - Name: []byte("f"), - ID: blob, - }}})) - if err != nil { - t.Fatal(err) - } - - commit1, err := store.WriteBytesContent(objecttype.TypeCommit, commitBody(tree)) - if err != nil { - t.Fatal(err) - } - - commit2, err := store.WriteBytesContent(objecttype.TypeCommit, commitBody(tree, commit1)) - if err != nil { - t.Fatal(err) - } - - tag1, err := store.WriteBytesContent(objecttype.TypeTag, tagBody(commit2, objecttype.TypeCommit)) - if err != nil { - t.Fatal(err) - } - - tag2, err := store.WriteBytesContent(objecttype.TypeTag, tagBody(tag1, objecttype.TypeTag)) - if err != nil { - t.Fatal(err) - } - - r := reachability.New(objectfetch.New(store), nil) - walk := r.Walk(reachability.DomainCommits, nil, map[objectid.ObjectID]struct{}{tag2: {}}) - - got := collectSeq(walk.Seq()) - - err = walk.Err() - if err != nil { - t.Fatalf("walk.Err(): %v", err) - } - - gotSet := toSet(got) - - wantSet := map[objectid.ObjectID]struct{}{tag2: {}, tag1: {}, commit2: {}, commit1: {}} - if !maps.Equal(gotSet, wantSet) { - t.Fatalf("walk output mismatch: got %v, want %v", slices.Collect(maps.Keys(gotSet)), slices.Collect(maps.Keys(wantSet))) - } - }) -} - -func TestWalkExcludesHavesCompletely(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - store := newCountingMemStore(algo) - - blob, err := store.WriteBytesContent(objecttype.TypeBlob, []byte("blob\n")) - if err != nil { - t.Fatal(err) - } - - tree, err := store.WriteBytesContent(objecttype.TypeTree, mustSerializeTree(t, &tree.Tree{Entries: []tree.TreeEntry{{ - Mode: tree.FileModeRegular, - Name: []byte("f"), - ID: blob, - }}})) - if err != nil { - t.Fatal(err) - } - - commit, err := store.WriteBytesContent(objecttype.TypeCommit, commitBody(tree)) - if err != nil { - t.Fatal(err) - } - - r := reachability.New(objectfetch.New(store), nil) - walk := r.Walk(reachability.DomainCommits, map[objectid.ObjectID]struct{}{commit: {}}, map[objectid.ObjectID]struct{}{commit: {}}) - - got := collectSeq(walk.Seq()) - - err = walk.Err() - if err != nil { - t.Fatalf("walk.Err(): %v", err) - } - - if len(got) != 0 { - t.Fatalf("expected empty output, got %v", got) - } - }) -} - -func TestWalkDomainCommitsRejectsNonCommitRootAfterPeel(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - store := newCountingMemStore(algo) - - blob, err := store.WriteBytesContent(objecttype.TypeBlob, []byte("blob\n")) - if err != nil { - t.Fatal(err) - } - - tree, err := store.WriteBytesContent(objecttype.TypeTree, mustSerializeTree(t, &tree.Tree{Entries: []tree.TreeEntry{{ - Mode: tree.FileModeRegular, - Name: []byte("f"), - ID: blob, - }}})) - if err != nil { - t.Fatal(err) - } - - tag, err := store.WriteBytesContent(objecttype.TypeTag, tagBody(tree, objecttype.TypeTree)) - if err != nil { - t.Fatal(err) - } - - r := reachability.New(objectfetch.New(store), nil) - walk := r.Walk(reachability.DomainCommits, nil, map[objectid.ObjectID]struct{}{tag: {}}) - _ = collectSeq(walk.Seq()) - - err = walk.Err() - if err == nil { - t.Fatal("expected error") - } - - typeErr, ok := errors.AsType[*giterrors.ObjectTypeError](err) - if !ok { - t.Fatalf("expected ObjectTypeError, got %T (%v)", err, err) - } - - if typeErr.Got != objecttype.TypeTree || typeErr.Want != objecttype.TypeCommit { - t.Fatalf("unexpected type error: %+v", typeErr) - } - }) -} - -func TestWalkDomainCommitsHaveTagStopsTraversal(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - store := newCountingMemStore(algo) - - blob, err := store.WriteBytesContent(objecttype.TypeBlob, []byte("blob\n")) - if err != nil { - t.Fatal(err) - } - - tree, err := store.WriteBytesContent(objecttype.TypeTree, mustSerializeTree(t, &tree.Tree{Entries: []tree.TreeEntry{{ - Mode: tree.FileModeRegular, - Name: []byte("f"), - ID: blob, - }}})) - if err != nil { - t.Fatal(err) - } - - commit1, err := store.WriteBytesContent(objecttype.TypeCommit, commitBody(tree)) - if err != nil { - t.Fatal(err) - } - - commit2, err := store.WriteBytesContent(objecttype.TypeCommit, commitBody(tree, commit1)) - if err != nil { - t.Fatal(err) - } - - tag1, err := store.WriteBytesContent(objecttype.TypeTag, tagBody(commit2, objecttype.TypeCommit)) - if err != nil { - t.Fatal(err) - } - - tag2, err := store.WriteBytesContent(objecttype.TypeTag, tagBody(tag1, objecttype.TypeTag)) - if err != nil { - t.Fatal(err) - } - - r := reachability.New(objectfetch.New(store), nil) - walk := r.Walk( - reachability.DomainCommits, - map[objectid.ObjectID]struct{}{tag1: {}}, - map[objectid.ObjectID]struct{}{tag2: {}}, - ) - - got := collectSeq(walk.Seq()) - - err = walk.Err() - if err != nil { - t.Fatalf("walk.Err(): %v", err) - } - - gotSet := toSet(got) - - wantSet := map[objectid.ObjectID]struct{}{tag2: {}} - if !maps.Equal(gotSet, wantSet) { - t.Fatalf("walk output mismatch: got %v, want %v", slices.Collect(maps.Keys(gotSet)), slices.Collect(maps.Keys(wantSet))) - } - }) -} - -func TestWalkDomainObjectsRecursesTreesAndSkipsBlobContentReads(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - store := newCountingMemStore(algo) - - blob1, err := store.WriteBytesContent(objecttype.TypeBlob, []byte("b1\n")) - if err != nil { - t.Fatal(err) - } - - blob2, err := store.WriteBytesContent(objecttype.TypeBlob, []byte("b2\n")) - if err != nil { - t.Fatal(err) - } - - gitlinkTarget := store.Algorithm().Sum([]byte("external-submodule")) - - subtree, err := store.WriteBytesContent(objecttype.TypeTree, mustSerializeTree(t, &tree.Tree{Entries: []tree.TreeEntry{{ - Mode: tree.FileModeRegular, - Name: []byte("nested"), - ID: blob2, - }}})) - if err != nil { - t.Fatal(err) - } - - rootTree, err := store.WriteBytesContent(objecttype.TypeTree, mustSerializeTree(t, &tree.Tree{Entries: []tree.TreeEntry{ - {Mode: tree.FileModeRegular, Name: []byte("a"), ID: blob1}, - {Mode: tree.FileModeDir, Name: []byte("dir"), ID: subtree}, - {Mode: tree.FileModeGitlink, Name: []byte("submodule"), ID: gitlinkTarget}, - }})) - if err != nil { - t.Fatal(err) - } - - commit, err := store.WriteBytesContent(objecttype.TypeCommit, commitBody(rootTree)) - if err != nil { - t.Fatal(err) - } - - r := reachability.New(objectfetch.New(store), nil) - walk := r.Walk(reachability.DomainObjects, nil, map[objectid.ObjectID]struct{}{commit: {}}) - - got := collectSeq(walk.Seq()) - - err = walk.Err() - if err != nil { - t.Fatalf("walk.Err(): %v", err) - } - - gotSet := toSet(got) - - wantSet := map[objectid.ObjectID]struct{}{commit: {}, rootTree: {}, subtree: {}, blob1: {}, blob2: {}} - if !maps.Equal(gotSet, wantSet) { - t.Fatalf("walk output mismatch: got %v, want %v", slices.Collect(maps.Keys(gotSet)), slices.Collect(maps.Keys(wantSet))) - } - - if store.readBytesByObjectID[blob1] != 0 || store.readBytesByObjectID[blob2] != 0 { - t.Fatalf("blob contents should not be read; counts: blob1=%d blob2=%d", store.readBytesByObjectID[blob1], store.readBytesByObjectID[blob2]) - } - }) -} - -func TestCheckConnectedReturnsConcreteMissingObject(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - store := newCountingMemStore(algo) - - blob, err := store.WriteBytesContent(objecttype.TypeBlob, []byte("blob\n")) - if err != nil { - t.Fatal(err) - } - - tree, err := store.WriteBytesContent(objecttype.TypeTree, mustSerializeTree(t, &tree.Tree{Entries: []tree.TreeEntry{{ - Mode: tree.FileModeRegular, - Name: []byte("f"), - ID: blob, - }}})) - if err != nil { - t.Fatal(err) - } - - missingParent := store.Algorithm().Sum([]byte("missing-parent")) - - commit, err := store.WriteBytesContent(objecttype.TypeCommit, commitBody(tree, missingParent)) - if err != nil { - t.Fatal(err) - } - - r := reachability.New(objectfetch.New(store), nil) - - err = r.CheckConnected(reachability.DomainCommits, nil, map[objectid.ObjectID]struct{}{commit: {}}) - if err == nil { - t.Fatal("expected error") - } - - missing, ok := errors.AsType[*giterrors.ObjectMissingError](err) - if !ok { - t.Fatalf("expected ObjectMissingError, got %T (%v)", err, err) - } - - if missing.OID != missingParent { - t.Fatalf("unexpected missing oid: got %s want %s", missing.OID, missingParent) - } - }) -} - -func TestWalkInvalidDomainReturnsPlainError(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - r := reachability.New(objectfetch.New(newCountingMemStore(algo)), nil) - walk := r.Walk(reachability.Domain(99), nil, nil) - - _ = collectSeq(walk.Seq()) - - err := walk.Err() - if err == nil { - t.Fatal("expected error") - } - }) -} - -func mustSerializeTree(tb testing.TB, tree *tree.Tree) []byte { - tb.Helper() - - body, err := tree.SerializeWithoutHeader() - if err != nil { - tb.Fatalf("SerializeWithoutHeader: %v", err) - } - - return body -} diff --git a/reachability/walk.go b/reachability/walk.go deleted file mode 100644 index 8e0a0902..00000000 --- a/reachability/walk.go +++ /dev/null @@ -1,43 +0,0 @@ -package reachability - -import ( - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// Walk is one single-use iterator traversal. -// -// Labels: MT-Unsafe. -type Walk struct { - reachability *Reachability - domain Domain - haves map[objectid.ObjectID]struct{} - wants map[objectid.ObjectID]struct{} - strict bool - - seqUsed bool - err error -} - -// Walk creates one single-use traversal over the selected domain. -// -// In DomainCommits, when a commit-graph reader is attached, parent expansion -// may use commit-graph metadata for speed. -// -// Walk retains haves and wants as provided. -// -// Labels: Life-Parent. -func (r *Reachability) Walk(domain Domain, haves, wants map[objectid.ObjectID]struct{}) *Walk { - walk := &Walk{ - reachability: r, - domain: domain, - haves: haves, - wants: wants, - } - - err := validateDomain(domain) - if err != nil { - walk.err = err - } - - return walk -} diff --git a/reachability/walk_expand.go b/reachability/walk_expand.go deleted file mode 100644 index e36534a2..00000000 --- a/reachability/walk_expand.go +++ /dev/null @@ -1,9 +0,0 @@ -package reachability - -func (walk *Walk) expand(item walkItem) ([]walkItem, error) { - if walk.domain == DomainCommits { - return walk.expandCommits(item) - } - - return walk.expandObjects(item) -} diff --git a/reachability/walk_expand_commits.go b/reachability/walk_expand_commits.go deleted file mode 100644 index eaeb4e72..00000000 --- a/reachability/walk_expand_commits.go +++ /dev/null @@ -1,60 +0,0 @@ -package reachability - -import ( - "fmt" - - "codeberg.org/lindenii/furgit/errors" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -func (walk *Walk) expandCommits(item walkItem) ([]walkItem, error) { - if walk.reachability.graph != nil { //nolint:nestif - next, graphUsed, err := walk.expandCommitsFromGraph(item.id) - if err != nil { - return nil, err - } - - if graphUsed && walk.strict { - err = walk.validateCommitObject(item.id) - if err != nil { - return nil, err - } - } - - if graphUsed { - return next, nil - } - } - - ty, _, err := walk.reachability.fetcher.Header(item.id) - if err != nil { - return nil, err - } - - switch ty { - case objecttype.TypeCommit: - commit, err := walk.reachability.fetcher.ExactCommit(item.id) - if err != nil { - return nil, err - } - - next := make([]walkItem, 0, len(commit.Object().Parents)) - for _, parent := range commit.Object().Parents { - next = append(next, walkItem{id: parent, want: objecttype.TypeInvalid}) - } - - return next, nil - case objecttype.TypeTag: - tag, err := walk.reachability.fetcher.ExactTag(item.id) - if err != nil { - return nil, err - } - - return []walkItem{{id: tag.Object().Target, want: objecttype.TypeInvalid}}, nil - case objecttype.TypeTree, objecttype.TypeBlob, objecttype.TypeInvalid, - objecttype.TypeFuture, objecttype.TypeOfsDelta, objecttype.TypeRefDelta: - return nil, &errors.ObjectTypeError{OID: item.id, Got: ty, Want: objecttype.TypeCommit} - } - - return nil, fmt.Errorf("reachability: unreachable object type %d", ty) -} diff --git a/reachability/walk_expand_commits_graph.go b/reachability/walk_expand_commits_graph.go deleted file mode 100644 index 863906f9..00000000 --- a/reachability/walk_expand_commits_graph.go +++ /dev/null @@ -1,56 +0,0 @@ -package reachability - -import ( - "errors" - - commitgraphread "codeberg.org/lindenii/furgit/format/commitgraph/read" - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -func (walk *Walk) expandCommitsFromGraph(id objectid.ObjectID) ([]walkItem, bool, error) { - pos, err := walk.reachability.graph.Lookup(id) - if err != nil { - if _, ok := errors.AsType[*commitgraphread.NotFoundError](err); ok { - return nil, false, nil - } - - return nil, true, err - } - - commit, err := walk.reachability.graph.CommitAt(pos) - if err != nil { - return nil, true, err - } - - next := make([]walkItem, 0, 2+len(commit.ExtraParents)) - - if commit.Parent1.Valid { - parentOID, err := walk.reachability.graph.OIDAt(commit.Parent1.Pos) - if err != nil { - return nil, true, err - } - - next = append(next, walkItem{id: parentOID, want: objecttype.TypeInvalid}) - } - - if commit.Parent2.Valid { - parentOID, err := walk.reachability.graph.OIDAt(commit.Parent2.Pos) - if err != nil { - return nil, true, err - } - - next = append(next, walkItem{id: parentOID, want: objecttype.TypeInvalid}) - } - - for _, parentPos := range commit.ExtraParents { - parentOID, err := walk.reachability.graph.OIDAt(parentPos) - if err != nil { - return nil, true, err - } - - next = append(next, walkItem{id: parentOID, want: objecttype.TypeInvalid}) - } - - return next, true, nil -} diff --git a/reachability/walk_expand_objects.go b/reachability/walk_expand_objects.go deleted file mode 100644 index 8b479021..00000000 --- a/reachability/walk_expand_objects.go +++ /dev/null @@ -1,69 +0,0 @@ -package reachability - -import ( - "fmt" - - "codeberg.org/lindenii/furgit/errors" - objecttree "codeberg.org/lindenii/furgit/object/tree" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -func (walk *Walk) expandObjects(item walkItem) ([]walkItem, error) { - ty, _, err := walk.reachability.fetcher.Header(item.id) - if err != nil { - return nil, err - } - - if item.want != objecttype.TypeInvalid && ty != item.want { - return nil, &errors.ObjectTypeError{OID: item.id, Got: ty, Want: item.want} - } - - switch ty { - case objecttype.TypeBlob: - return nil, nil - case objecttype.TypeCommit: - commit, err := walk.reachability.fetcher.ExactCommit(item.id) - if err != nil { - return nil, err - } - - next := make([]walkItem, 0, len(commit.Object().Parents)+1) - - next = append(next, walkItem{id: commit.Object().Tree, want: objecttype.TypeTree}) - for _, parent := range commit.Object().Parents { - next = append(next, walkItem{id: parent, want: objecttype.TypeCommit}) - } - - return next, nil - case objecttype.TypeTree: - tree, err := walk.reachability.fetcher.ExactTree(item.id) - if err != nil { - return nil, err - } - - next := make([]walkItem, 0, len(tree.Object().Entries)) - for _, entry := range tree.Object().Entries { - switch entry.Mode { - case objecttree.FileModeGitlink: - continue - case objecttree.FileModeDir: - next = append(next, walkItem{id: entry.ID, want: objecttype.TypeTree}) - case objecttree.FileModeRegular, objecttree.FileModeExecutable, objecttree.FileModeSymlink: - next = append(next, walkItem{id: entry.ID, want: objecttype.TypeBlob}) - } - } - - return next, nil - case objecttype.TypeTag: - tag, err := walk.reachability.fetcher.ExactTag(item.id) - if err != nil { - return nil, err - } - - return []walkItem{{id: tag.Object().Target, want: tag.Object().TargetType}}, nil - case objecttype.TypeInvalid, objecttype.TypeFuture, objecttype.TypeOfsDelta, objecttype.TypeRefDelta: - return nil, &errors.ObjectTypeError{OID: item.id, Got: ty, Want: item.want} - } - - return nil, fmt.Errorf("reachability: unreachable object type %d", ty) -} diff --git a/reachability/walk_item.go b/reachability/walk_item.go deleted file mode 100644 index da30e127..00000000 --- a/reachability/walk_item.go +++ /dev/null @@ -1,11 +0,0 @@ -package reachability - -import ( - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -type walkItem struct { - id objectid.ObjectID - want objecttype.Type -} diff --git a/reachability/walk_seq.go b/reachability/walk_seq.go deleted file mode 100644 index 669166f5..00000000 --- a/reachability/walk_seq.go +++ /dev/null @@ -1,71 +0,0 @@ -package reachability - -import ( - "errors" - "iter" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// Seq returns the traversal sequence. It is single-use. -// -// Labels: Life-Parent. -func (walk *Walk) Seq() iter.Seq[objectid.ObjectID] { - if walk.seqUsed { - return func(yield func(objectid.ObjectID) bool) { - _ = yield - - if walk.err == nil { - walk.err = errors.New("reachability: walk sequence already consumed") - } - } - } - - walk.seqUsed = true - - return func(yield func(objectid.ObjectID) bool) { - if walk.err != nil { - return - } - - stack := walk.initialStack() - - var err error - - visited := make(map[objectid.ObjectID]struct{}, len(stack)) - for len(stack) > 0 { - item := stack[len(stack)-1] - stack = stack[:len(stack)-1] - - if _, ok := walk.haves[item.id]; ok { - continue - } - - if _, ok := visited[item.id]; ok { - continue - } - - visited[item.id] = struct{}{} - - var next []walkItem - - next, err = walk.expand(item) - if err != nil { - walk.err = err - - return - } - - if !yield(item.id) { - return - } - - stack = append(stack, next...) - } - } -} - -// Err returns the terminal error, if any, once Seq has been consumed. -func (walk *Walk) Err() error { - return walk.err -} diff --git a/reachability/walk_stack.go b/reachability/walk_stack.go deleted file mode 100644 index 0a5084df..00000000 --- a/reachability/walk_stack.go +++ /dev/null @@ -1,16 +0,0 @@ -package reachability - -import objecttype "codeberg.org/lindenii/furgit/object/type" - -func (walk *Walk) initialStack() []walkItem { - if len(walk.wants) == 0 { - return nil - } - - stack := make([]walkItem, 0, len(walk.wants)) - for want := range walk.wants { - stack = append(stack, walkItem{id: want, want: objecttype.TypeInvalid}) - } - - return stack -} diff --git a/reachability/walk_verify.go b/reachability/walk_verify.go deleted file mode 100644 index c4ac6ecf..00000000 --- a/reachability/walk_verify.go +++ /dev/null @@ -1,22 +0,0 @@ -package reachability - -import ( - "codeberg.org/lindenii/furgit/errors" - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -func (walk *Walk) validateCommitObject(id objectid.ObjectID) error { - ty, _, err := walk.reachability.fetcher.Header(id) - if err != nil { - return err - } - - if ty != objecttype.TypeCommit { - return &errors.ObjectTypeError{OID: id, Got: ty, Want: objecttype.TypeCommit} - } - - _, err = walk.reachability.fetcher.ExactCommit(id) - - return err -} diff --git a/ref/detached.go b/ref/detached.go deleted file mode 100644 index e9b30906..00000000 --- a/ref/detached.go +++ /dev/null @@ -1,22 +0,0 @@ -package ref - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// Detached points directly to an object ID. -type Detached struct { - RefName string - ID objectid.ObjectID - - // Peeled is the peeled target when available (for annotated tags). - // - // This field is optional backend-provided metadata. Backends that do not - // have peel metadata available may leave it nil. - Peeled *objectid.ObjectID -} - -// Name returns the fully-qualified reference name. -func (ref Detached) Name() string { - return ref.RefName -} - -func (Detached) isRef() {} diff --git a/ref/doc.go b/ref/doc.go deleted file mode 100644 index 610c3013..00000000 --- a/ref/doc.go +++ /dev/null @@ -1,5 +0,0 @@ -// Package ref provides Git reference values. -// -// A reference is either [Detached], which points directly to an object ID, or -// [Symbolic], which points to another reference name. -package ref diff --git a/ref/name/branch.go b/ref/name/branch.go deleted file mode 100644 index 274a95e3..00000000 --- a/ref/name/branch.go +++ /dev/null @@ -1,25 +0,0 @@ -package refname - -import "strings" - -// Branch checks one branch shorthand and returns its fully-qualified -// refs/heads/... name. -// -// Unlike Git in-repository branch parsing, this helper does not expand @{-n}. -func Branch(name string) (string, error) { - full := "refs/heads/" + name - if strings.HasPrefix(name, "-") || full == "refs/heads/HEAD" { - return "", &NameError{Name: name, Reason: "invalid branch name"} - } - - err := validate(full, 0) - if err != nil { - return "", err - } - - if strings.HasPrefix(name, "refs/") { - return name, nil - } - - return full, nil -} diff --git a/ref/name/component.go b/ref/name/component.go deleted file mode 100644 index f5adba46..00000000 --- a/ref/name/component.go +++ /dev/null @@ -1,88 +0,0 @@ -package refname - -import "strings" - -func checkRefnameComponent(name string, flags *int, sanitized *strings.Builder, fullName string) (int, error) { - var last byte - - componentStart := sanitizedLen(sanitized) - - for i := range len(name) { - ch := name[i] - disp := refnameDisposition(ch) - - if sanitized != nil && disp != 1 { - sanitized.WriteByte(ch) - } - - switch disp { - case 1: - goto out - case 2: - if last == '.' { - if sanitized != nil { - truncateBuilder(sanitized, sanitized.Len()-1) - } else { - return 0, &NameError{Name: fullName, Reason: "name contains '..'"} - } - } - case 3: - if last == '@' { - if sanitized != nil { - overwriteLastByte(sanitized, '-') - } else { - return 0, &NameError{Name: fullName, Reason: "name contains '@{'"} - } - } - case 4: - if sanitized != nil { - overwriteLastByte(sanitized, '-') - } else { - return 0, &NameError{Name: fullName, Reason: "name contains one forbidden character"} - } - case 5: - if *flags&refnameRefspecPattern == 0 { - if sanitized != nil { - overwriteLastByte(sanitized, '-') - } else { - return 0, &NameError{Name: fullName, Reason: "name contains '*'"} - } - } - - *flags &^= refnameRefspecPattern - } - - last = ch - } - -out: - componentLen := strings.IndexByte(name, '/') - - if componentLen < 0 { - componentLen = len(name) - } - - if componentLen == 0 { - return 0, nil - } - - if name[0] == '.' { - if sanitized != nil { - overwriteBuilderAt(sanitized, componentStart, '-') - } else { - return 0, &NameError{Name: fullName, Reason: "component starts with '.'"} - } - } - - if componentLen >= len(lockSuffix) && name[componentLen-len(lockSuffix):componentLen] == lockSuffix { - if sanitized == nil { - return 0, &NameError{Name: fullName, Reason: "component ends with .lock"} - } - - for strings.HasSuffix(sanitized.String(), lockSuffix) { - truncateBuilder(sanitized, sanitized.Len()-len(lockSuffix)) - } - } - - return componentLen, nil -} diff --git a/ref/name/current.go b/ref/name/current.go deleted file mode 100644 index 3a5394cc..00000000 --- a/ref/name/current.go +++ /dev/null @@ -1,5 +0,0 @@ -package refname - -func isCurrentWorktreeRef(name string) bool { - return IsRootSyntax(name) || IsPerWorktree(name) -} diff --git a/ref/name/disposition.go b/ref/name/disposition.go deleted file mode 100644 index 5153e633..00000000 --- a/ref/name/disposition.go +++ /dev/null @@ -1,20 +0,0 @@ -package refname - -func refnameDisposition(ch byte) byte { - switch { - case ch == '/': - return 1 - case ch == '.': - return 2 - case ch == '{': - return 3 - case ch == '*': - return 5 - case ch < 0x20 || ch == 0x7f: - return 4 - case ch == ':' || ch == '?' || ch == '[' || ch == '\\' || ch == '^' || ch == '~' || ch == ' ' || ch == '\t': - return 4 - default: - return 0 - } -} diff --git a/ref/name/doc.go b/ref/name/doc.go deleted file mode 100644 index 48d06aac..00000000 --- a/ref/name/doc.go +++ /dev/null @@ -1,7 +0,0 @@ -// Package refname provides Git reference-name validation, normalization, and -// classification helpers. -// -// It includes branch and tag abbreviation expansion, worktree-qualified refs, -// update validation, and the distinction between ordinary refs, root refs, -// pseudo-refs, and filesystem-safe names. -package refname diff --git a/ref/name/errors.go b/ref/name/errors.go deleted file mode 100644 index e39bc73b..00000000 --- a/ref/name/errors.go +++ /dev/null @@ -1,14 +0,0 @@ -package refname - -import "fmt" - -// NameError reports one invalid reference name. -type NameError struct { - Name string - Reason string -} - -// Error implements error. -func (err *NameError) Error() string { - return fmt.Sprintf("ref: invalid name %q: %s", err.Name, err.Reason) -} diff --git a/ref/name/flags.go b/ref/name/flags.go deleted file mode 100644 index 72f0a58f..00000000 --- a/ref/name/flags.go +++ /dev/null @@ -1,6 +0,0 @@ -package refname - -const ( - refnameAllowOneLevel = 1 << iota - refnameRefspecPattern -) diff --git a/ref/name/length.go b/ref/name/length.go deleted file mode 100644 index 94c0322b..00000000 --- a/ref/name/length.go +++ /dev/null @@ -1,11 +0,0 @@ -package refname - -import "strings" - -func sanitizedLen(builder *strings.Builder) int { - if builder == nil { - return 0 - } - - return builder.Len() -} diff --git a/ref/name/lock.go b/ref/name/lock.go deleted file mode 100644 index 33db902f..00000000 --- a/ref/name/lock.go +++ /dev/null @@ -1,3 +0,0 @@ -package refname - -const lockSuffix = ".lock" diff --git a/ref/name/normalize.go b/ref/name/normalize.go deleted file mode 100644 index 9cbe7126..00000000 --- a/ref/name/normalize.go +++ /dev/null @@ -1,53 +0,0 @@ -package refname - -import "strings" - -// Normalize collapses slashes according to what Git wants -// then validates the normalized name. -func Normalize(name string, options Options) (string, error) { - normalized := collapseSlashes(name) - - err := validate(normalized, options.flags()) - if err != nil { - return "", err - } - - return normalized, nil -} - -func normalizeRefPath(path string) (string, bool) { - components := make([]string, 0, strings.Count(path, "/")+1) - i := 0 - - for i < len(path) { - for i < len(path) && path[i] == '/' { - i++ - } - - if i == len(path) { - break - } - - j := i - for j < len(path) && path[j] != '/' { - j++ - } - - component := path[i:j] - switch component { - case ".": - case "..": - if len(components) == 0 { - return "", false - } - - components = components[:len(components)-1] - default: - components = append(components, component) - } - - i = j - } - - return strings.Join(components, "/"), true -} diff --git a/ref/name/options.go b/ref/name/options.go deleted file mode 100644 index 5ae81541..00000000 --- a/ref/name/options.go +++ /dev/null @@ -1,30 +0,0 @@ -package refname - -import "fmt" - -// Options controls Git refname validation. -type Options struct { - // AllowOneLevel permits one-component refnames like HEAD. - AllowOneLevel bool - - // RefspecPattern permits one '*' anywhere in the refname. - RefspecPattern bool -} - -// String returns one stable text form of the options. -func (options Options) String() string { - return fmt.Sprintf("allow_onelevel=%t,refspec_pattern=%t", options.AllowOneLevel, options.RefspecPattern) -} - -func (options Options) flags() int { - var flags int - if options.AllowOneLevel { - flags |= refnameAllowOneLevel - } - - if options.RefspecPattern { - flags |= refnameRefspecPattern - } - - return flags -} diff --git a/ref/name/pseudo.go b/ref/name/pseudo.go deleted file mode 100644 index f0ad1ae8..00000000 --- a/ref/name/pseudo.go +++ /dev/null @@ -1,11 +0,0 @@ -package refname - -// IsPseudo reports whether name is one Git pseudo-ref. -func IsPseudo(name string) bool { - switch name { - case "FETCH_HEAD", "MERGE_HEAD": - return true - default: - return false - } -} diff --git a/ref/name/refname_test.go b/ref/name/refname_test.go deleted file mode 100644 index a37314f4..00000000 --- a/ref/name/refname_test.go +++ /dev/null @@ -1,622 +0,0 @@ -package refname_test - -import ( - "context" - "os/exec" - "strings" - "testing" - - "codeberg.org/lindenii/furgit/ref/name" -) - -func TestValidateNameAgainstGit(t *testing.T) { - t.Parallel() - - type testCase struct { - name string - opts refname.Options - } - - tests := []testCase{ - {name: ""}, - {name: "/"}, - {name: "/", opts: refname.Options{AllowOneLevel: true}}, - {name: "foo/bar/baz"}, - {name: "refs/heads/main"}, - {name: "refs/tags/v1.0.0"}, - {name: "refs///heads/foo"}, - {name: "heads/foo/"}, - {name: "/heads/foo"}, - {name: "///heads/foo"}, - {name: "./foo"}, - {name: "./foo/bar"}, - {name: "foo/./bar"}, - {name: "foo/bar/."}, - {name: ".refs/foo"}, - {name: "refs/heads/foo."}, - {name: "HEAD"}, - {name: "HEAD", opts: refname.Options{AllowOneLevel: true}}, - {name: "refs/heads/.main"}, - {name: "heads/foo..bar"}, - {name: "refs/heads/main.lock"}, - {name: "heads///foo.lock"}, - {name: "refs/heads/foo..bar"}, - {name: "refs/heads/foo bar"}, - {name: "refs/heads/foo@{bar"}, - {name: "heads/foo?bar"}, - {name: "foo./bar"}, - {name: "foo.lock/bar"}, - {name: "foo.lock///bar"}, - {name: "heads/foo@bar"}, - {name: "heads/foo\\bar"}, - {name: "heads/foo\tbar"}, - {name: "heads/foo\x7fbar"}, - {name: "heads/fu\xC3\x9F"}, - {name: "heads/*foo/bar", opts: refname.Options{RefspecPattern: true}}, - {name: "heads/foo*/bar", opts: refname.Options{RefspecPattern: true}}, - {name: "heads/f*o/bar", opts: refname.Options{RefspecPattern: true}}, - {name: "heads/f*o*/bar", opts: refname.Options{RefspecPattern: true}}, - {name: "heads/foo*/bar*", opts: refname.Options{RefspecPattern: true}}, - {name: "refs/heads/foo/bar."}, - {name: "refs//heads///main"}, - {name: "foo"}, - {name: "foo", opts: refname.Options{AllowOneLevel: true}}, - {name: "foo", opts: refname.Options{RefspecPattern: true}}, - {name: "foo", opts: refname.Options{AllowOneLevel: true, RefspecPattern: true}}, - {name: "foo/bar"}, - {name: "foo/bar", opts: refname.Options{AllowOneLevel: true}}, - {name: "foo/bar", opts: refname.Options{RefspecPattern: true}}, - {name: "foo/bar", opts: refname.Options{AllowOneLevel: true, RefspecPattern: true}}, - {name: "refs/heads/*"}, - {name: "refs/heads/*", opts: refname.Options{RefspecPattern: true}}, - {name: "refs/heads/feature*branch", opts: refname.Options{RefspecPattern: true}}, - {name: "refs/heads/foo*bar*baz", opts: refname.Options{RefspecPattern: true}}, - {name: "foo/*"}, - {name: "foo/*", opts: refname.Options{RefspecPattern: true}}, - {name: "foo/*", opts: refname.Options{AllowOneLevel: true}}, - {name: "foo/*", opts: refname.Options{AllowOneLevel: true, RefspecPattern: true}}, - {name: "*/foo"}, - {name: "*/foo", opts: refname.Options{RefspecPattern: true}}, - {name: "*/foo", opts: refname.Options{AllowOneLevel: true}}, - {name: "*/foo", opts: refname.Options{AllowOneLevel: true, RefspecPattern: true}}, - {name: "foo/*/bar"}, - {name: "foo/*/bar", opts: refname.Options{RefspecPattern: true}}, - {name: "foo/*/bar", opts: refname.Options{AllowOneLevel: true}}, - {name: "foo/*/bar", opts: refname.Options{AllowOneLevel: true, RefspecPattern: true}}, - {name: "*"}, - {name: "*", opts: refname.Options{AllowOneLevel: true}}, - {name: "*", opts: refname.Options{RefspecPattern: true}}, - {name: "*", opts: refname.Options{AllowOneLevel: true, RefspecPattern: true}}, - {name: "foo/*/*", opts: refname.Options{RefspecPattern: true}}, - {name: "foo/*/*", opts: refname.Options{AllowOneLevel: true, RefspecPattern: true}}, - {name: "*/foo/*", opts: refname.Options{RefspecPattern: true}}, - {name: "*/foo/*", opts: refname.Options{AllowOneLevel: true, RefspecPattern: true}}, - {name: "*/*/foo", opts: refname.Options{RefspecPattern: true}}, - {name: "*/*/foo", opts: refname.Options{AllowOneLevel: true, RefspecPattern: true}}, - {name: "/foo"}, - {name: "/foo", opts: refname.Options{AllowOneLevel: true}}, - {name: "/foo", opts: refname.Options{RefspecPattern: true}}, - {name: "/foo", opts: refname.Options{AllowOneLevel: true, RefspecPattern: true}}, - {name: "@"}, - } - - for _, tt := range tests { - t.Run(tt.name+"_"+tt.opts.String(), func(t *testing.T) { - t.Parallel() - - err := refname.Validate(tt.name, tt.opts) - gitErr := gitCheckRefFormat(t, tt.name, tt.opts) - - if (err == nil) != (gitErr == nil) { - t.Fatalf("ValidateName(%q, %+v) err=%v, git err=%v", tt.name, tt.opts, err, gitErr) - } - }) - } -} - -func TestNormalizeNameAgainstGit(t *testing.T) { - t.Parallel() - - type testCase struct { - name string - opts refname.Options - } - - tests := []testCase{ - {name: "/"}, - {name: "/", opts: refname.Options{AllowOneLevel: true}}, - {name: "///refs///heads//main"}, - {name: "refs////tags///v1"}, - {name: "refs///heads///"}, - {name: "HEAD", opts: refname.Options{AllowOneLevel: true}}, - {name: "refs/heads/*", opts: refname.Options{RefspecPattern: true}}, - {name: "refs///heads/foo"}, - {name: "/heads/foo", opts: refname.Options{AllowOneLevel: true}}, - {name: "///heads/foo"}, - {name: "heads/foo/../bar"}, - {name: "heads/./foo"}, - {name: "heads\\foo"}, - {name: "heads/foo.lock"}, - {name: "heads///foo.lock"}, - {name: "foo.lock/bar"}, - {name: "foo.lock///bar"}, - {name: "foo"}, - {name: "/foo", opts: refname.Options{AllowOneLevel: true}}, - {name: "/foo", opts: refname.Options{AllowOneLevel: true, RefspecPattern: true}}, - } - - for _, tt := range tests { - t.Run(tt.name+"_"+tt.opts.String(), func(t *testing.T) { - t.Parallel() - - got, err := refname.Normalize(tt.name, tt.opts) - want, gitErr := gitNormalizeRefFormat(t, tt.name, tt.opts) - - if (err == nil) != (gitErr == nil) { - t.Fatalf("NormalizeName(%q, %+v) err=%v, git err=%v", tt.name, tt.opts, err, gitErr) - } - - if err == nil && got != want { - t.Fatalf("NormalizeName(%q, %+v) = %q, want %q", tt.name, tt.opts, got, want) - } - }) - } -} - -func TestBranchNameAgainstGit(t *testing.T) { - t.Parallel() - - tests := []string{ - "main", - "feature/topic", - "-main", - "HEAD", - "@{-1}", - "feature.lock", - "topic@{1}", - "refs/heads/main", - "refs/heads/HEAD", - "refs/tags/x", - } - - for _, name := range tests { - t.Run(name, func(t *testing.T) { - t.Parallel() - - got, err := refname.Branch(name) - want, gitErr := gitCheckBranchName(t, name) - - if (err == nil) != (gitErr == nil) { - t.Fatalf("BranchName(%q) err=%v, git err=%v", name, err, gitErr) - } - - if err == nil && got != want { - t.Fatalf("BranchName(%q) = %q, want %q", name, got, want) - } - }) - } -} - -func TestTagName(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - }{ - {name: "v1.0.0"}, - {name: "main/topic"}, - {name: "-bad"}, - {name: "HEAD"}, - {name: "feature.lock"}, - {name: "refs/tags/v1.0.0"}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - got, err := refname.Tag(tt.name) - want, gitErr := gitCheckTagName(t, tt.name) - - if (err == nil) != (gitErr == nil) { - t.Fatalf("TagName(%q) err=%v, git err=%v", tt.name, err, gitErr) - } - - if err == nil && got != want { - t.Fatalf("TagName(%q) = %q, want %q", tt.name, got, want) - } - }) - } -} - -func TestIsSafeName(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - want bool - }{ - {name: "", want: false}, - {name: "HEAD", want: true}, - {name: "MERGE_HEAD", want: true}, - {name: "Head", want: false}, - {name: "refs/heads/main", want: true}, - {name: "refs/", want: false}, - {name: "refs//heads/main", want: false}, - {name: "refs/heads/main/", want: false}, - {name: "refs/foo/../bar", want: false}, - {name: "refs/foo/../../bar", want: false}, - {name: "refs/heads/main.lock", want: true}, - } - - for _, tt := range tests { - if got := refname.IsSafe(tt.name); got != tt.want { - t.Fatalf("IsSafeName(%q) = %v, want %v", tt.name, got, tt.want) - } - } -} - -func TestIsPerWorktree(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - want bool - }{ - {name: "refs/worktree/foo", want: true}, - {name: "refs/bisect/foo", want: true}, - {name: "refs/rewritten/foo", want: true}, - {name: "refs/heads/foo", want: false}, - {name: "worktrees/wt1/HEAD", want: false}, - } - - for _, tt := range tests { - if got := refname.IsPerWorktree(tt.name); got != tt.want { - t.Fatalf("IsPerWorktree(%q) = %v, want %v", tt.name, got, tt.want) - } - } -} - -func TestIsPseudo(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - want bool - }{ - {name: "FETCH_HEAD", want: true}, - {name: "MERGE_HEAD", want: true}, - {name: "HEAD", want: false}, - {name: "AUTO_MERGE", want: false}, - } - - for _, tt := range tests { - if got := refname.IsPseudo(tt.name); got != tt.want { - t.Fatalf("IsPseudo(%q) = %v, want %v", tt.name, got, tt.want) - } - } -} - -func TestIsRootSyntax(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - want bool - }{ - {name: "", want: true}, - {name: "HEAD", want: true}, - {name: "AUTO_MERGE", want: true}, - {name: "BISECT-EXPECTED_REV", want: true}, - {name: "refs/heads/main", want: false}, - {name: "Head", want: false}, - {name: "HEAD1", want: false}, - } - - for _, tt := range tests { - if got := refname.IsRootSyntax(tt.name); got != tt.want { - t.Fatalf("IsRootSyntax(%q) = %v, want %v", tt.name, got, tt.want) - } - } -} - -func TestIsRoot(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - want bool - }{ - {name: "HEAD", want: true}, - {name: "ORIG_HEAD", want: true}, - {name: "BOGUS_HEAD", want: true}, - {name: "CHERRY_PICK_HEAD", want: true}, - {name: "REVERT_HEAD", want: true}, - {name: "AUTO_MERGE", want: true}, - {name: "BISECT_EXPECTED_REV", want: true}, - {name: "NOTES_MERGE_PARTIAL", want: true}, - {name: "NOTES_MERGE_REF", want: true}, - {name: "MERGE_AUTOSTASH", want: true}, - {name: "FETCH_HEAD", want: false}, - {name: "MERGE_HEAD", want: false}, - {name: "Head", want: false}, - {name: "refs/heads/main", want: false}, - } - - for _, tt := range tests { - if got := refname.IsRoot(tt.name); got != tt.want { - t.Fatalf("IsRoot(%q) = %v, want %v", tt.name, got, tt.want) - } - } -} - -func TestParseWorktree(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - want refname.ParsedWorktreeRef - }{ - { - name: "refs/heads/main", - want: refname.ParsedWorktreeRef{ - Type: refname.WorktreeShared, - BareRefName: "refs/heads/main", - }, - }, - { - name: "HEAD", - want: refname.ParsedWorktreeRef{ - Type: refname.WorktreeCurrent, - BareRefName: "HEAD", - }, - }, - { - name: "refs/worktree/foo", - want: refname.ParsedWorktreeRef{ - Type: refname.WorktreeCurrent, - BareRefName: "refs/worktree/foo", - }, - }, - { - name: "main-worktree/HEAD", - want: refname.ParsedWorktreeRef{ - Type: refname.WorktreeMain, - BareRefName: "HEAD", - }, - }, - { - name: "main-worktree/FOO", - want: refname.ParsedWorktreeRef{ - Type: refname.WorktreeMain, - BareRefName: "FOO", - }, - }, - { - name: "main-worktree/refs/worktree/foo", - want: refname.ParsedWorktreeRef{ - Type: refname.WorktreeMain, - BareRefName: "refs/worktree/foo", - }, - }, - { - name: "main-worktree/", - want: refname.ParsedWorktreeRef{ - Type: refname.WorktreeMain, - BareRefName: "", - }, - }, - { - name: "main-worktree/refs/heads/main", - want: refname.ParsedWorktreeRef{ - Type: refname.WorktreeShared, - BareRefName: "main-worktree/refs/heads/main", - }, - }, - { - name: "worktrees/wt1/HEAD", - want: refname.ParsedWorktreeRef{ - Type: refname.WorktreeOther, - WorktreeName: "wt1", - BareRefName: "HEAD", - }, - }, - { - name: "worktrees/wt1/BAR", - want: refname.ParsedWorktreeRef{ - Type: refname.WorktreeOther, - WorktreeName: "wt1", - BareRefName: "BAR", - }, - }, - { - name: "worktrees/wt1/refs/bisect/foo", - want: refname.ParsedWorktreeRef{ - Type: refname.WorktreeOther, - WorktreeName: "wt1", - BareRefName: "refs/bisect/foo", - }, - }, - { - name: "worktrees/wt1/refs/heads/main", - want: refname.ParsedWorktreeRef{ - Type: refname.WorktreeShared, - BareRefName: "worktrees/wt1/refs/heads/main", - }, - }, - { - name: "worktrees/wt1", - want: refname.ParsedWorktreeRef{ - Type: refname.WorktreeOther, - WorktreeName: "wt1", - BareRefName: "", - }, - }, - } - - for _, tt := range tests { - if got := refname.ParseWorktree(tt.name); got != tt.want { - t.Fatalf("ParseWorktree(%q) = %#v, want %#v", tt.name, got, tt.want) - } - } -} - -func TestValidateUpdateName(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - hasNewValue bool - wantErr bool - }{ - {name: "refs/heads/main", hasNewValue: true, wantErr: false}, - {name: "HEAD", hasNewValue: true, wantErr: false}, - {name: "PSEUDOREF", hasNewValue: true, wantErr: false}, - {name: "FETCH_HEAD", hasNewValue: true, wantErr: true}, - {name: "MERGE_HEAD", hasNewValue: true, wantErr: true}, - {name: "refs/heads/.bad", hasNewValue: true, wantErr: true}, - {name: "foo/bar", hasNewValue: true, wantErr: false}, - {name: "foo/bar", hasNewValue: false, wantErr: true}, - {name: "PSEUDOREF", hasNewValue: false, wantErr: false}, - {name: "HEAD", hasNewValue: false, wantErr: false}, - } - - for _, tt := range tests { - err := refname.ValidateUpdateName(tt.name, tt.hasNewValue) - if (err != nil) != tt.wantErr { - t.Fatalf("ValidateUpdateName(%q, %v) err=%v, wantErr=%v", tt.name, tt.hasNewValue, err, tt.wantErr) - } - } -} - -func TestValidateSymbolicTarget(t *testing.T) { - t.Parallel() - - tests := []struct { - ref string - target string - wantErr bool - }{ - {ref: "HEAD", target: "refs/heads/main", wantErr: false}, - {ref: "HEAD", target: "foo", wantErr: true}, - {ref: "HEAD", target: "ORIG_HEAD", wantErr: true}, - {ref: "refs/heads/top", target: "ORIG_HEAD", wantErr: false}, - {ref: "refs/heads/top", target: "refs/heads/main", wantErr: false}, - {ref: "refs/heads/top", target: "worktrees/wt1/HEAD", wantErr: false}, - {ref: "refs/heads/top", target: "foo", wantErr: true}, - {ref: "refs/heads/top", target: "foo..bar", wantErr: true}, - {ref: "main-worktree/HEAD", target: "refs/heads/main", wantErr: false}, - {ref: "main-worktree/HEAD", target: "refs/tags/v1", wantErr: true}, - } - - for _, tt := range tests { - err := refname.ValidateSymbolicTarget(tt.ref, tt.target) - if (err != nil) != tt.wantErr { - t.Fatalf("ValidateSymbolicTarget(%q, %q) err=%v, wantErr=%v", tt.ref, tt.target, err, tt.wantErr) - } - } -} - -func TestSanitizeComponent(t *testing.T) { - t.Parallel() - - tests := []struct { - component string - want string - }{ - {component: ".", want: "-"}, - {component: "..", want: "-"}, - {component: "foo..bar", want: "foo.bar"}, - {component: "foo.lock", want: "foo"}, - {component: "foo.lock.lock", want: "foo"}, - {component: "foo bar", want: "foo-bar"}, - {component: "@", want: "-/@"}, - {component: "a@{b", want: "a@-b"}, - {component: "a*b", want: "a-b"}, - } - - for _, tt := range tests { - if got := refname.SanitizeComponent(tt.component); got != tt.want { - t.Fatalf("SanitizeComponent(%q) = %q, want %q", tt.component, got, tt.want) - } - } -} - -func gitCheckRefFormat(tb testing.TB, name string, opts refname.Options) error { - tb.Helper() - - args := []string{"check-ref-format"} - if opts.AllowOneLevel { - args = append(args, "--allow-onelevel") - } - - if opts.RefspecPattern { - args = append(args, "--refspec-pattern") - } - - args = append(args, name) - - return exec.CommandContext(context.Background(), "git", args...).Run() -} - -func gitNormalizeRefFormat(tb testing.TB, name string, opts refname.Options) (string, error) { - tb.Helper() - - args := []string{"check-ref-format", "--normalize"} - if opts.AllowOneLevel { - args = append(args, "--allow-onelevel") - } - - if opts.RefspecPattern { - args = append(args, "--refspec-pattern") - } - - args = append(args, name) - - out, err := exec.CommandContext(context.Background(), "git", args...).Output() - if err != nil { - return "", err - } - - return strings.TrimSuffix(string(out), "\n"), nil -} - -func gitCheckBranchName(tb testing.TB, name string) (string, error) { - tb.Helper() - - cmd := exec.CommandContext(context.Background(), "git", "check-ref-format", "--branch", name) - cmd.Dir = tb.TempDir() - - out, err := cmd.Output() - if err != nil { - return "", err - } - - branchName := strings.TrimSuffix(string(out), "\n") - if strings.HasPrefix(branchName, "refs/") { - return branchName, nil - } - - return "refs/heads/" + branchName, nil -} - -func gitCheckTagName(tb testing.TB, name string) (string, error) { - tb.Helper() - - if strings.HasPrefix(name, "-") || name == "HEAD" { - return "", exec.ErrNotFound - } - - //nolint:gosec - err := exec.CommandContext( - context.Background(), - "git", - "check-ref-format", - "refs/tags/"+name, - ).Run() - if err != nil { - return "", err - } - - return "refs/tags/" + name, nil -} diff --git a/ref/name/root.go b/ref/name/root.go deleted file mode 100644 index 43361846..00000000 --- a/ref/name/root.go +++ /dev/null @@ -1,21 +0,0 @@ -package refname - -import "strings" - -// IsRoot reports whether name is one root ref according to Git. -func IsRoot(name string) bool { - if !IsRootSyntax(name) || IsPseudo(name) { - return false - } - - if strings.HasSuffix(name, "_HEAD") { - return true - } - - switch name { - case "HEAD", "AUTO_MERGE", "BISECT_EXPECTED_REV", "NOTES_MERGE_PARTIAL", "NOTES_MERGE_REF", "MERGE_AUTOSTASH": - return true - default: - return false - } -} diff --git a/ref/name/root_syntax.go b/ref/name/root_syntax.go deleted file mode 100644 index 97a15cb9..00000000 --- a/ref/name/root_syntax.go +++ /dev/null @@ -1,13 +0,0 @@ -package refname - -// IsRootSyntax reports whether name matches Git's all-caps root-ref syntax. -func IsRootSyntax(name string) bool { - for i := range len(name) { - ch := name[i] - if (ch < 'A' || ch > 'Z') && ch != '-' && ch != '_' { - return false - } - } - - return true -} diff --git a/ref/name/safe.go b/ref/name/safe.go deleted file mode 100644 index b36d3b2f..00000000 --- a/ref/name/safe.go +++ /dev/null @@ -1,31 +0,0 @@ -package refname - -import "strings" - -// IsSafe reports whether name is one safe refname for direct filesystem -// operations; see refname_is_safe. -func IsSafe(name string) bool { - rest, ok := strings.CutPrefix(name, "refs/") - if ok { - if rest == "" || rest[0] == '/' || rest[len(rest)-1] == '/' { - return false - } - - normalized, normOK := normalizeRefPath(rest) - - return normOK && normalized == rest - } - - if name == "" { - return false - } - - for i := range len(name) { - ch := name[i] - if (ch < 'A' || ch > 'Z') && ch != '_' { - return false - } - } - - return true -} diff --git a/ref/name/sanitize.go b/ref/name/sanitize.go deleted file mode 100644 index f543de7c..00000000 --- a/ref/name/sanitize.go +++ /dev/null @@ -1,19 +0,0 @@ -package refname - -import ( - "fmt" - "strings" -) - -// SanitizeComponent mutates component until it satisfies -// sanitize_refname_component. -func SanitizeComponent(component string) string { - var builder strings.Builder - - err := checkOrSanitizeRefname(component, refnameAllowOneLevel, &builder) - if err != nil { - panic(fmt.Sprintf("ref: sanitize component %q: %v", component, err)) - } - - return builder.String() -} diff --git a/ref/name/slashes.go b/ref/name/slashes.go deleted file mode 100644 index 44d3e4ea..00000000 --- a/ref/name/slashes.go +++ /dev/null @@ -1,26 +0,0 @@ -package refname - -import "strings" - -func collapseSlashes(name string) string { - if name == "" { - return "" - } - - var builder strings.Builder - builder.Grow(len(name)) - - prev := byte('/') - - for i := range len(name) { - ch := name[i] - if prev == '/' && ch == '/' { - continue - } - - builder.WriteByte(ch) - prev = ch - } - - return builder.String() -} diff --git a/ref/name/tag.go b/ref/name/tag.go deleted file mode 100644 index 226c0fdd..00000000 --- a/ref/name/tag.go +++ /dev/null @@ -1,20 +0,0 @@ -package refname - -import "strings" - -// Tag checks one tag shorthand and returns its fully-qualified -// refs/tags/... name. -func Tag(name string) (string, error) { - if strings.HasPrefix(name, "-") || name == "HEAD" { - return "", &NameError{Name: name, Reason: "invalid tag name"} - } - - full := "refs/tags/" + name - - err := validate(full, 0) - if err != nil { - return "", err - } - - return full, nil -} diff --git a/ref/name/update.go b/ref/name/update.go deleted file mode 100644 index 92830f1a..00000000 --- a/ref/name/update.go +++ /dev/null @@ -1,56 +0,0 @@ -package refname - -import "strings" - -// ValidateUpdateName checks whether name is valid for one direct ref update. -// -// See transaction_refname_valid(); -// updates with a new OID use check_refname_format(..., ALLOW_ONELEVEL), -// while delete/verify style operations use refname_is_safe(). -func ValidateUpdateName(name string, hasNewValue bool) error { - if IsPseudo(name) { - return &NameError{Name: name, Reason: "pseudoref updates are not allowed"} - } - - if hasNewValue { - return Validate(name, Options{AllowOneLevel: true}) - } - - if !IsSafe(name) { - return &NameError{Name: name, Reason: "unsafe refname for update"} - } - - return nil -} - -// ValidateSymbolicTarget checks whether target is valid for one symref target. -// -// See refs_fsck_symref(); -// root refs are allowed directly, HEAD must point to refs/heads/..., -// and non-root targets must be valid full refnames rooted at refs/ or -// worktrees/. -func ValidateSymbolicTarget(refname string, target string) error { - parsed := ParseWorktree(refname) - if parsed.BareRefName == "HEAD" && !strings.HasPrefix(target, "refs/heads/") { - return &NameError{Name: target, Reason: refname + " must point to refs/heads/..."} - } - - if IsRoot(target) { - return nil - } - - err := Validate(target, Options{}) - if err != nil { - return err - } - - if strings.HasPrefix(target, "refs/") { - return nil - } - - if strings.HasPrefix(target, "worktrees/") { - return nil - } - - return &NameError{Name: target, Reason: "symref target is not a ref"} -} diff --git a/ref/name/utils.go b/ref/name/utils.go deleted file mode 100644 index 58944748..00000000 --- a/ref/name/utils.go +++ /dev/null @@ -1,22 +0,0 @@ -package refname - -import ( - "strings" -) - -func overwriteLastByte(builder *strings.Builder, ch byte) { - overwriteBuilderAt(builder, builder.Len()-1, ch) -} - -func overwriteBuilderAt(builder *strings.Builder, index int, ch byte) { - value := builder.String() - truncateBuilder(builder, index) - builder.WriteByte(ch) - builder.WriteString(value[index+1:]) -} - -func truncateBuilder(builder *strings.Builder, n int) { - value := builder.String() - builder.Reset() - builder.WriteString(value[:n]) -} diff --git a/ref/name/validate.go b/ref/name/validate.go deleted file mode 100644 index 1b8ad396..00000000 --- a/ref/name/validate.go +++ /dev/null @@ -1,65 +0,0 @@ -package refname - -import "strings" - -// Validate checks whether name is one valid Git refname. -func Validate(name string, options Options) error { - return validate(name, options.flags()) -} - -func validate(name string, flags int) error { - return checkOrSanitizeRefname(name, flags, nil) -} - -func checkOrSanitizeRefname(name string, flags int, sanitized *strings.Builder) error { - componentCount := 0 - remaining := name - - if name == "@" { - if sanitized == nil { - return &NameError{Name: name, Reason: "single @ is not allowed"} - } - - sanitized.WriteByte('-') - } - - for { - if sanitized != nil && sanitized.Len() > 0 { - sanitized.WriteByte('/') - } - - componentLen, err := checkRefnameComponent(remaining, &flags, sanitized, name) - switch { - case sanitized != nil && componentLen == 0: - case componentLen <= 0: - if err != nil { - return err - } - - return &NameError{Name: name, Reason: "component has zero length"} - case err != nil: - return err - } - - componentCount++ - - if componentLen == len(remaining) { - break - } - - remaining = remaining[componentLen+1:] - } - - componentLen := len(remaining) - if componentLen > 0 && remaining[componentLen-1] == '.' { - if sanitized == nil { - return &NameError{Name: name, Reason: "name ends with '.'"} - } - } - - if flags&refnameAllowOneLevel == 0 && componentCount < 2 { - return &NameError{Name: name, Reason: "one-level refname is not allowed"} - } - - return nil -} diff --git a/ref/name/worktree.go b/ref/name/worktree.go deleted file mode 100644 index 48ca215d..00000000 --- a/ref/name/worktree.go +++ /dev/null @@ -1,75 +0,0 @@ -package refname - -import "strings" - -// WorktreeType classifies one worktree-qualified refname prefix. -type WorktreeType uint8 - -const ( - // WorktreeShared is one ordinary shared refname. - WorktreeShared WorktreeType = iota - - // WorktreeCurrent is one current-worktree-only refname like HEAD or refs/worktree/... - WorktreeCurrent - - // WorktreeMain is one main-worktree-qualified refname like main-worktree/HEAD. - WorktreeMain - - // WorktreeOther is one other-worktree-qualified refname like worktrees/wt1/HEAD. - WorktreeOther -) - -// IsPerWorktree reports whether name is one per-worktree ref namespace. -func IsPerWorktree(name string) bool { - return strings.HasPrefix(name, "refs/worktree/") || - strings.HasPrefix(name, "refs/bisect/") || - strings.HasPrefix(name, "refs/rewritten/") -} - -// ParsedWorktreeRef is the result of parsing one worktree-qualified refname. -type ParsedWorktreeRef struct { - Type WorktreeType - WorktreeName string - BareRefName string -} - -// ParseWorktree parses Git's worktree ref prefixes. -func ParseWorktree(name string) ParsedWorktreeRef { - if bare, ok := strings.CutPrefix(name, "worktrees/"); ok { - worktreeName, rest, found := strings.Cut(bare, "/") - if !found { - return ParsedWorktreeRef{ - Type: WorktreeOther, - WorktreeName: worktreeName, - BareRefName: "", - } - } - - if isCurrentWorktreeRef(rest) { - return ParsedWorktreeRef{ - Type: WorktreeOther, - WorktreeName: worktreeName, - BareRefName: rest, - } - } - } - - if bare, ok := strings.CutPrefix(name, "main-worktree/"); ok && isCurrentWorktreeRef(bare) { - return ParsedWorktreeRef{ - Type: WorktreeMain, - BareRefName: bare, - } - } - - if isCurrentWorktreeRef(name) { - return ParsedWorktreeRef{ - Type: WorktreeCurrent, - BareRefName: name, - } - } - - return ParsedWorktreeRef{ - Type: WorktreeShared, - BareRefName: name, - } -} diff --git a/ref/ref.go b/ref/ref.go deleted file mode 100644 index 0c70cc26..00000000 --- a/ref/ref.go +++ /dev/null @@ -1,9 +0,0 @@ -package ref - -// Ref is a Git reference. -// -// Consider casting to [Detached] or [Symbolic]. -type Ref interface { - isRef() - Name() string -} diff --git a/ref/store/batch.go b/ref/store/batch.go deleted file mode 100644 index 11423cec..00000000 --- a/ref/store/batch.go +++ /dev/null @@ -1,69 +0,0 @@ -package refstore - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// Batch stages reference operations for one non-atomic apply. -// -// Unlike Transaction, Batch may reject some queued operations while still -// applying others successfully when Apply runs. -// -// A batch borrows its underlying store and is invalid after that store is -// closed. -// -// Labels: MT-Unsafe. -type Batch interface { - // Create creates one detached reference, requiring that the logical - // reference does not already exist. - Create(name string, newID objectid.ObjectID) error - // Update updates one detached reference, requiring that the current logical - // reference value matches oldID. - Update(name string, newID, oldID objectid.ObjectID) error - // Delete deletes one detached reference, requiring that the current logical - // reference value matches oldID. - Delete(name string, oldID objectid.ObjectID) error - // Verify verifies that the current logical reference value matches oldID. - Verify(name string, oldID objectid.ObjectID) error - - // CreateSymbolic creates one symbolic reference, requiring that the named - // reference does not already exist. - CreateSymbolic(name, newTarget string) error - // UpdateSymbolic updates one symbolic reference directly, requiring that its - // current target matches oldTarget. - UpdateSymbolic(name, newTarget, oldTarget string) error - // DeleteSymbolic deletes one symbolic reference directly, requiring that its - // current target matches oldTarget. - DeleteSymbolic(name, oldTarget string) error - // VerifySymbolic verifies that the named symbolic reference currently points - // at oldTarget. - VerifySymbolic(name, oldTarget string) error - - // Apply validates and applies queued operations, returning one result per - // queued operation in order. Fatal backend failures are returned separately. - // - // Malformed operations are rejected by the queueing methods above and do not - // enter the batch. - // - // Apply invalidates the receiver. - Apply() ([]BatchResult, error) - // Abort abandons the batch and releases any resources it holds. - // - // Abort invalidates the receiver. - Abort() error -} - -// BatchStatus reports the outcome for one queued batch operation. -type BatchStatus uint8 - -const ( - BatchStatusApplied BatchStatus = iota - BatchStatusRejected - BatchStatusFatal - BatchStatusNotAttempted -) - -// BatchResult reports the outcome for one queued batch operation. -type BatchResult struct { - Name string - Status BatchStatus - Error error -} diff --git a/ref/store/batch_store.go b/ref/store/batch_store.go deleted file mode 100644 index 725a05e5..00000000 --- a/ref/store/batch_store.go +++ /dev/null @@ -1,9 +0,0 @@ -package refstore - -// Batcher begins non-atomic reference batches. -type Batcher interface { - // BeginBatch creates one new queued batch. - // - // Labels: Life-Parent. - BeginBatch() (Batch, error) -} diff --git a/ref/store/chain/chain.go b/ref/store/chain/chain.go deleted file mode 100644 index a332f64c..00000000 --- a/ref/store/chain/chain.go +++ /dev/null @@ -1,12 +0,0 @@ -// Package chain provides a wrapper reference storage backend to query a chain -// of backends. -package chain - -import refstore "codeberg.org/lindenii/furgit/ref/store" - -// Chain queries multiple reference stores in order. -// -// Labels: Close-Caller. -type Chain struct { - backends []refstore.Reader -} diff --git a/ref/store/chain/close.go b/ref/store/chain/close.go deleted file mode 100644 index 75fa357e..00000000 --- a/ref/store/chain/close.go +++ /dev/null @@ -1,6 +0,0 @@ -package chain - -// Close releases wrapper-local resources. -// -// Labels: MT-Unsafe. -func (chain *Chain) Close() error { return nil } diff --git a/ref/store/chain/list.go b/ref/store/chain/list.go deleted file mode 100644 index c577ca85..00000000 --- a/ref/store/chain/list.go +++ /dev/null @@ -1,40 +0,0 @@ -package chain - -import ( - "fmt" - - "codeberg.org/lindenii/furgit/ref" -) - -// List lists references from every backend and deduplicates by ref name. -// -// First-seen wins, so earlier backends have precedence. -func (chain *Chain) List(pattern string) ([]ref.Ref, error) { - var refs []ref.Ref - - seen := map[string]struct{}{} - - for i, backend := range chain.backends { - listed, err := backend.List(pattern) - if err != nil { - return nil, fmt.Errorf("refstore: backend %d list: %w", i, err) - } - - for _, entry := range listed { - if entry == nil { - continue - } - - name := entry.Name() - if _, ok := seen[name]; ok { - continue - } - - seen[name] = struct{}{} - - refs = append(refs, entry) - } - } - - return refs, nil -} diff --git a/ref/store/chain/new.go b/ref/store/chain/new.go deleted file mode 100644 index 1c8a3a28..00000000 --- a/ref/store/chain/new.go +++ /dev/null @@ -1,14 +0,0 @@ -package chain - -import refstore "codeberg.org/lindenii/furgit/ref/store" - -// New creates an ordered reference store chain. -// -// The provided backends must be non-nil and distinct. -// -// Labels: Deps-Borrowed, Life-Parent. -func New(backends ...refstore.Reader) *Chain { - return &Chain{ - backends: append([]refstore.Reader(nil), backends...), - } -} diff --git a/ref/store/chain/resolve.go b/ref/store/chain/resolve.go deleted file mode 100644 index 06c3d8f5..00000000 --- a/ref/store/chain/resolve.go +++ /dev/null @@ -1,64 +0,0 @@ -package chain - -import ( - "errors" - "fmt" - - "codeberg.org/lindenii/furgit/ref" - refstore "codeberg.org/lindenii/furgit/ref/store" -) - -// Resolve resolves a reference from the first backend that has it. -// -//nolint:ireturn -func (chain *Chain) Resolve(name string) (ref.Ref, error) { - for i, backend := range chain.backends { - resolved, err := backend.Resolve(name) - if err == nil { - return resolved, nil - } - - if errors.Is(err, refstore.ErrReferenceNotFound) { - continue - } - - return nil, fmt.Errorf("refstore: backend %d resolve: %w", i, err) - } - - return nil, refstore.ErrReferenceNotFound -} - -// ResolveToDetached resolves symbolic references through Resolve until detached. -// -// It intentionally does not call backend ResolveToDetached. This allows symbolic -// references to cross backends in the chain. -func (chain *Chain) ResolveToDetached(name string) (ref.Detached, error) { - cur := name - - seen := map[string]struct{}{} - for { - if _, ok := seen[cur]; ok { - return ref.Detached{}, fmt.Errorf("refstore: symbolic reference cycle at %q", cur) - } - - seen[cur] = struct{}{} - - resolved, err := chain.Resolve(cur) - if err != nil { - return ref.Detached{}, err - } - - switch resolved := resolved.(type) { - case ref.Detached: - return resolved, nil - case ref.Symbolic: - if resolved.Target == "" { - return ref.Detached{}, fmt.Errorf("refstore: symbolic reference %q has empty target", resolved.Name()) - } - - cur = resolved.Target - default: - return ref.Detached{}, fmt.Errorf("refstore: unsupported reference type %T", resolved) - } - } -} diff --git a/ref/store/doc.go b/ref/store/doc.go deleted file mode 100644 index 8f7f39a6..00000000 --- a/ref/store/doc.go +++ /dev/null @@ -1,14 +0,0 @@ -// Package refstore provides interfaces for reference storage backends. -// -// Ref stores directly use reference values. Unlike object storage, they -// do not have a separate fetch layer to parse backend results into -// higher-level forms. -// -// The package separates read-only access from atomic transactions and -// non-atomic batches. Not every readable ref backend is writable, and not -// every writable backend necessarily offers the same update model. -// -// Concrete implementations generally inherit the contract documented by the -// interfaces they satisfy. Implementation docs focus on additional guarantees -// and implementation-specific behavior. -package refstore diff --git a/ref/store/errors.go b/ref/store/errors.go deleted file mode 100644 index 45583440..00000000 --- a/ref/store/errors.go +++ /dev/null @@ -1,7 +0,0 @@ -package refstore - -import "errors" - -// ErrReferenceNotFound indicates that a reference does not exist in a backend. -// TODO: Interface error? Just like object not found in objectstore. -var ErrReferenceNotFound = errors.New("refstore: reference not found") diff --git a/ref/store/files/batch.go b/ref/store/files/batch.go deleted file mode 100644 index d8037bbb..00000000 --- a/ref/store/files/batch.go +++ /dev/null @@ -1,11 +0,0 @@ -package files - -import refstore "codeberg.org/lindenii/furgit/ref/store" - -// Batch stages files-store updates for one non-atomic apply. -type Batch struct { - store *Store - ops []queuedUpdate -} - -var _ refstore.Batch = (*Batch)(nil) diff --git a/ref/store/files/batch_abort.go b/ref/store/files/batch_abort.go deleted file mode 100644 index c229ca06..00000000 --- a/ref/store/files/batch_abort.go +++ /dev/null @@ -1,6 +0,0 @@ -package files - -// Abort abandons the queued updates. -func (batch *Batch) Abort() error { - return nil -} diff --git a/ref/store/files/batch_apply.go b/ref/store/files/batch_apply.go deleted file mode 100644 index 1847b544..00000000 --- a/ref/store/files/batch_apply.go +++ /dev/null @@ -1,129 +0,0 @@ -package files - -import refstore "codeberg.org/lindenii/furgit/ref/store" - -// Apply validates and applies the queued updates. -func (batch *Batch) Apply() ([]refstore.BatchResult, error) { - results := make([]refstore.BatchResult, len(batch.ops)) - remainingIdx := make([]int, 0, len(batch.ops)) - remainingOps := make([]queuedUpdate, 0, len(batch.ops)) - seenTargets := make(map[string]struct{}, len(batch.ops)) - executor := &refUpdateExecutor{store: batch.store} - - for i, op := range batch.ops { - results[i].Name = op.name - - target, err := executor.resolveQueuedUpdateTarget(op) - if err != nil { - if isBatchRejected(err) { - results[i].Status = refstore.BatchStatusRejected - results[i].Error = batchResultError(err) - - continue - } - - results[i].Status = refstore.BatchStatusFatal - results[i].Error = batchResultError(err) - - for j := i + 1; j < len(results); j++ { - results[j].Name = batch.ops[j].name - results[j].Status = refstore.BatchStatusNotAttempted - results[j].Error = batchResultError(err) - } - - return results, err - } - - targetKey := updateTargetKey(target.loc) - if _, exists := seenTargets[targetKey]; exists { - results[i].Status = refstore.BatchStatusRejected - results[i].Error = &refstore.DuplicateUpdateError{} - - continue - } - - seenTargets[targetKey] = struct{}{} - - remainingIdx = append(remainingIdx, i) - remainingOps = append(remainingOps, op) - } - - for len(remainingOps) > 0 { - prepared, err := executor.prepareUpdates(remainingOps) - if err == nil { - err = executor.commitPreparedUpdates(prepared) - if err == nil { - for _, idx := range remainingIdx { - results[idx].Status = refstore.BatchStatusApplied - } - - return results, nil - } - - fatalName := batchResultName(err) - - fatalMarked := false - for i, idx := range remainingIdx { - if !fatalMarked && remainingOps[i].name == fatalName && fatalName != "" { - results[idx].Status = refstore.BatchStatusFatal - results[idx].Error = batchResultError(err) - fatalMarked = true - - continue - } - - results[idx].Status = refstore.BatchStatusNotAttempted - results[idx].Error = batchResultError(err) - } - - return results, err - } - - if !isBatchRejected(err) { - fatalName := batchResultName(err) - - fatalMarked := false - for i, idx := range remainingIdx { - if !fatalMarked && remainingOps[i].name == fatalName && fatalName != "" { - results[idx].Status = refstore.BatchStatusFatal - results[idx].Error = batchResultError(err) - fatalMarked = true - - continue - } - - results[idx].Status = refstore.BatchStatusNotAttempted - results[idx].Error = batchResultError(err) - } - - return results, err - } - - name := batchResultName(err) - rejectedAt := -1 - - for i, op := range remainingOps { - if op.name == name { - rejectedAt = i - - break - } - } - - if rejectedAt < 0 { - for _, idx := range remainingIdx { - results[idx].Status = refstore.BatchStatusNotAttempted - results[idx].Error = batchResultError(err) - } - - return results, err - } - - results[remainingIdx[rejectedAt]].Status = refstore.BatchStatusRejected - results[remainingIdx[rejectedAt]].Error = batchResultError(err) - remainingIdx = append(remainingIdx[:rejectedAt], remainingIdx[rejectedAt+1:]...) - remainingOps = append(remainingOps[:rejectedAt], remainingOps[rejectedAt+1:]...) - } - - return results, nil -} diff --git a/ref/store/files/batch_begin.go b/ref/store/files/batch_begin.go deleted file mode 100644 index c8a0dca7..00000000 --- a/ref/store/files/batch_begin.go +++ /dev/null @@ -1,13 +0,0 @@ -package files - -import refstore "codeberg.org/lindenii/furgit/ref/store" - -// BeginBatch creates one new files batch. -// -//nolint:ireturn -func (store *Store) BeginBatch() (refstore.Batch, error) { - return &Batch{ - store: store, - ops: make([]queuedUpdate, 0, 8), - }, nil -} diff --git a/ref/store/files/batch_queue.go b/ref/store/files/batch_queue.go deleted file mode 100644 index afec5a78..00000000 --- a/ref/store/files/batch_queue.go +++ /dev/null @@ -1,12 +0,0 @@ -package files - -func (batch *Batch) queue(op queuedUpdate) error { - err := (&refUpdateExecutor{store: batch.store}).validateQueuedUpdate(op) - if err != nil { - return err - } - - batch.ops = append(batch.ops, op) - - return nil -} diff --git a/ref/store/files/batch_queue_ops.go b/ref/store/files/batch_queue_ops.go deleted file mode 100644 index 441bcaba..00000000 --- a/ref/store/files/batch_queue_ops.go +++ /dev/null @@ -1,43 +0,0 @@ -package files - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// Create queues a detached reference creation. -func (batch *Batch) Create(name string, newID objectid.ObjectID) error { - return batch.queue(queuedUpdate{name: name, kind: updateCreate, newID: newID}) -} - -// Update queues a detached reference update. -func (batch *Batch) Update(name string, newID, oldID objectid.ObjectID) error { - return batch.queue(queuedUpdate{name: name, kind: updateReplace, newID: newID, oldID: oldID}) -} - -// Delete queues a detached reference deletion. -func (batch *Batch) Delete(name string, oldID objectid.ObjectID) error { - return batch.queue(queuedUpdate{name: name, kind: updateDelete, oldID: oldID}) -} - -// Verify queues a detached reference verification. -func (batch *Batch) Verify(name string, oldID objectid.ObjectID) error { - return batch.queue(queuedUpdate{name: name, kind: updateVerify, oldID: oldID}) -} - -// CreateSymbolic queues a symbolic reference creation. -func (batch *Batch) CreateSymbolic(name, newTarget string) error { - return batch.queue(queuedUpdate{name: name, kind: updateCreateSymbolic, newTarget: newTarget}) -} - -// UpdateSymbolic queues a symbolic reference update. -func (batch *Batch) UpdateSymbolic(name, newTarget, oldTarget string) error { - return batch.queue(queuedUpdate{name: name, kind: updateReplaceSymbolic, newTarget: newTarget, oldTarget: oldTarget}) -} - -// DeleteSymbolic queues a symbolic reference deletion. -func (batch *Batch) DeleteSymbolic(name, oldTarget string) error { - return batch.queue(queuedUpdate{name: name, kind: updateDeleteSymbolic, oldTarget: oldTarget}) -} - -// VerifySymbolic queues a symbolic reference verification. -func (batch *Batch) VerifySymbolic(name, oldTarget string) error { - return batch.queue(queuedUpdate{name: name, kind: updateVerifySymbolic, oldTarget: oldTarget}) -} diff --git a/ref/store/files/batch_rejection.go b/ref/store/files/batch_rejection.go deleted file mode 100644 index 7f31536c..00000000 --- a/ref/store/files/batch_rejection.go +++ /dev/null @@ -1,28 +0,0 @@ -package files - -import ( - "errors" - - refstore "codeberg.org/lindenii/furgit/ref/store" -) - -func isBatchRejected(err error) bool { - _, invalidName := errors.AsType[*refstore.InvalidNameError](err) - _, invalidValue := errors.AsType[*refstore.InvalidValueError](err) - _, duplicateUpdate := errors.AsType[*refstore.DuplicateUpdateError](err) - _, createExists := errors.AsType[*refstore.CreateExistsError](err) - _, incorrectOldValue := errors.AsType[*refstore.IncorrectOldValueError](err) - _, expectedDetached := errors.AsType[*refstore.ExpectedDetachedError](err) - _, expectedSymbolic := errors.AsType[*refstore.ExpectedSymbolicError](err) - _, nameConflict := errors.AsType[*refstore.NameConflictError](err) - - return errors.Is(err, refstore.ErrReferenceNotFound) || - invalidName || - invalidValue || - duplicateUpdate || - createExists || - incorrectOldValue || - expectedDetached || - expectedSymbolic || - nameConflict -} diff --git a/ref/store/files/batch_result_error.go b/ref/store/files/batch_result_error.go deleted file mode 100644 index 06d68273..00000000 --- a/ref/store/files/batch_result_error.go +++ /dev/null @@ -1,21 +0,0 @@ -package files - -import "errors" - -func batchResultError(err error) error { - updateErr, ok := errors.AsType[*updateContextError](err) - if ok { - return updateErr.err - } - - return err -} - -func batchResultName(err error) string { - updateErr, ok := errors.AsType[*updateContextError](err) - if !ok { - return "" - } - - return updateErr.name -} diff --git a/ref/store/files/batch_test.go b/ref/store/files/batch_test.go deleted file mode 100644 index 80575e5e..00000000 --- a/ref/store/files/batch_test.go +++ /dev/null @@ -1,129 +0,0 @@ -package files_test - -import ( - "errors" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - objectid "codeberg.org/lindenii/furgit/object/id" - refstore "codeberg.org/lindenii/furgit/ref/store" -) - -func TestBatchApplyRejectsStaleDeleteAndAppliesIndependentDelete(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - _, _, commitID := testRepo.MakeCommit(t, "base") - _, _, staleID := testRepo.MakeCommit(t, "stale") - testRepo.UpdateRef(t, "refs/heads/main", commitID) - testRepo.UpdateRef(t, "refs/heads/topic", commitID) - - store := openFilesStore(t, testRepo, algo) - - batch, err := store.BeginBatch() - if err != nil { - t.Fatalf("BeginBatch: %v", err) - } - - err = batch.Delete("refs/heads/main", staleID) - if err != nil { - t.Fatalf("Delete(main) queue: %v", err) - } - - err = batch.Delete("refs/heads/topic", commitID) - if err != nil { - t.Fatalf("Delete(topic) queue: %v", err) - } - - results, err := batch.Apply() - if err != nil { - t.Fatalf("Apply: %v", err) - } - - if len(results) != 2 { - t.Fatalf("len(results) = %d, want 2", len(results)) - } - - if results[0].Status != refstore.BatchStatusRejected { - t.Fatalf("results[0].Status = %v, want rejected", results[0].Status) - } - - if _, ok := errors.AsType[*refstore.IncorrectOldValueError](results[0].Error); !errors.Is(results[0].Error, refstore.ErrReferenceNotFound) && !ok { - t.Fatalf("results[0].Error = %v, want stale-value rejection", results[0].Error) - } - - if results[1].Status != refstore.BatchStatusApplied { - t.Fatalf("results[1].Status = %v, want applied", results[1].Status) - } - - _, err = store.Resolve("refs/heads/main") - if err != nil { - t.Fatalf("Resolve(main): %v", err) - } - - _, err = store.Resolve("refs/heads/topic") - if err == nil { - t.Fatal("refs/heads/topic still exists") - } - }) -} - -func TestBatchApplyRejectsDuplicateQueuedRef(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - _, _, commitID := testRepo.MakeCommit(t, "base") - testRepo.UpdateRef(t, "refs/heads/main", commitID) - - store := openFilesStore(t, testRepo, algo) - - batch, err := store.BeginBatch() - if err != nil { - t.Fatalf("BeginBatch: %v", err) - } - - err = batch.Delete("refs/heads/main", commitID) - if err != nil { - t.Fatalf("Delete(main) queue: %v", err) - } - - err = batch.Verify("refs/heads/main", commitID) - if err != nil { - t.Fatalf("Verify(main) queue: %v", err) - } - - results, err := batch.Apply() - if err != nil { - t.Fatalf("Apply: %v", err) - } - - if len(results) != 2 { - t.Fatalf("len(results) = %d, want 2", len(results)) - } - - if results[0].Status != refstore.BatchStatusApplied { - t.Fatalf("results[0].Status = %v, want applied", results[0].Status) - } - - if results[1].Status != refstore.BatchStatusRejected { - t.Fatalf("results[1].Status = %v, want rejected", results[1].Status) - } - - if _, ok := errors.AsType[*refstore.DuplicateUpdateError](results[1].Error); !ok { - t.Fatalf("results[1].Error = %v, want duplicate update error", results[1].Error) - } - - _, err = store.Resolve("refs/heads/main") - if !errors.Is(err, refstore.ErrReferenceNotFound) { - t.Fatalf("Resolve(main): %v", err) - } - }) -} diff --git a/ref/store/files/broken_ref_error.go b/ref/store/files/broken_ref_error.go deleted file mode 100644 index daa40849..00000000 --- a/ref/store/files/broken_ref_error.go +++ /dev/null @@ -1,16 +0,0 @@ -package files - -import "fmt" - -type brokenRefError struct { - name string - err error -} - -func (err brokenRefError) Error() string { - return fmt.Sprintf("refstore/files: broken reference %q: %v", err.name, err.err) -} - -func (err brokenRefError) Unwrap() error { - return err.err -} diff --git a/ref/store/files/close.go b/ref/store/files/close.go deleted file mode 100644 index 84a69dab..00000000 --- a/ref/store/files/close.go +++ /dev/null @@ -1,8 +0,0 @@ -package files - -// Close releases resources associated with the store. -// -// Labels: MT-Unsafe. -func (store *Store) Close() error { - return store.commonRoot.Close() -} diff --git a/ref/store/files/helpers_test.go b/ref/store/files/helpers_test.go deleted file mode 100644 index c46cc9fc..00000000 --- a/ref/store/files/helpers_test.go +++ /dev/null @@ -1,150 +0,0 @@ -package files_test - -import ( - "os" - "slices" - "strings" - "testing" - "time" - - "codeberg.org/lindenii/furgit/internal/testgit" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/ref/store/files" -) - -const testPackedRefsTimeout = time.Second - -func openFilesStore(t *testing.T, testRepo *testgit.TestRepo, algo objectid.Algorithm) *files.Store { - t.Helper() - - root := testRepo.OpenGitRoot(t) - - store, err := files.New(root, algo, testPackedRefsTimeout) - if err != nil { - t.Fatalf("files.New: %v", err) - } - - return store -} - -func openFilesStoreAt(t *testing.T, root *os.Root, algo objectid.Algorithm) *files.Store { - t.Helper() - - store, err := files.New(root, algo, testPackedRefsTimeout) - if err != nil { - t.Fatalf("files.New: %v", err) - } - - return store -} - -func openGitRootUnder(t *testing.T, repoRoot *os.Root, worktreeName string) *os.Root { - t.Helper() - - worktreeRoot, err := repoRoot.OpenRoot(worktreeName) - if err != nil { - t.Fatalf("OpenRoot(%q): %v", worktreeName, err) - } - - t.Cleanup(func() { - _ = worktreeRoot.Close() - }) - - info, err := worktreeRoot.Stat(".git") - if err != nil { - t.Fatalf("stat %q: %v", worktreeName+"/.git", err) - } - - if info.IsDir() { - gitRoot, err := worktreeRoot.OpenRoot(".git") - if err != nil { - t.Fatalf("OpenRoot(.git): %v", err) - } - - t.Cleanup(func() { - _ = gitRoot.Close() - }) - - return gitRoot - } - - content, err := worktreeRoot.ReadFile(".git") - if err != nil { - t.Fatalf("read %q: %v", worktreeName+"/.git", err) - } - - gitDir := strings.TrimSpace(strings.TrimPrefix(string(content), "gitdir:")) - if gitDir == "" { - t.Fatalf("%q does not contain a gitdir path", worktreeName+"/.git") - } - - if strings.HasPrefix(gitDir, "/") { - gitRoot, err := os.OpenRoot(gitDir) - if err != nil { - t.Fatalf("os.OpenRoot(%q): %v", gitDir, err) - } - - t.Cleanup(func() { - _ = gitRoot.Close() - }) - - return gitRoot - } - - gitRoot, err := worktreeRoot.OpenRoot(gitDir) - if err != nil { - t.Fatalf("os.OpenRoot(%q): %v", gitDir, err) - } - - t.Cleanup(func() { - _ = gitRoot.Close() - }) - - return gitRoot -} - -func assertListMatchesGitForEachRef(t *testing.T, gitOut string, store *files.Store) { - t.Helper() - - listed, err := store.List("") - if err != nil { - t.Fatalf("List(\"\"): %v", err) - } - - gotNames := make([]string, 0, len(listed)) - for _, got := range listed { - if got.Name() == "HEAD" { - continue - } - - gotNames = append(gotNames, got.Name()) - } - - slices.Sort(gotNames) - - wantLines := strings.Split(strings.TrimSpace(gitOut), "\n") - wantNames := make([]string, 0, len(wantLines)) - - for _, line := range wantLines { - line = strings.TrimSpace(line) - if line == "" { - continue - } - - wantNames = append(wantNames, line) - } - - slices.Sort(wantNames) - - if !slices.Equal(gotNames, wantNames) { - t.Fatalf("List names = %v, want %v", gotNames, wantNames) - } -} - -func forEachRefLines(output string) []string { - if strings.TrimSpace(output) == "" { - return nil - } - - return strings.Split(strings.TrimSpace(output), "\n") -} diff --git a/ref/store/files/new.go b/ref/store/files/new.go deleted file mode 100644 index 391930fb..00000000 --- a/ref/store/files/new.go +++ /dev/null @@ -1,31 +0,0 @@ -package files - -import ( - "math/rand" - "os" - "time" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// New creates one files ref store rooted at one repository gitdir. -// -// Labels: Deps-Borrowed, Life-Parent. -func New(root *os.Root, algo objectid.Algorithm, packedRefsTimeout time.Duration) (*Store, error) { - if algo.Size() == 0 { - return nil, objectid.ErrInvalidAlgorithm - } - - commonRoot, err := openCommonRoot(root) - if err != nil { - return nil, err - } - - return &Store{ - gitRoot: root, - commonRoot: commonRoot, - algo: algo, - lockRand: rand.New(rand.NewSource(time.Now().UnixNano())), //nolint:gosec - packedRefsTimeout: packedRefsTimeout, - }, nil -} diff --git a/ref/store/files/packed_delete_test.go b/ref/store/files/packed_delete_test.go deleted file mode 100644 index 184eb79c..00000000 --- a/ref/store/files/packed_delete_test.go +++ /dev/null @@ -1,292 +0,0 @@ -package files_test - -import ( - "errors" - "os" - "slices" - "sync" - "testing" - "time" - - "codeberg.org/lindenii/furgit/internal/testgit" - objectid "codeberg.org/lindenii/furgit/object/id" - refstore "codeberg.org/lindenii/furgit/ref/store" -) - -func TestFilesTransactionPackedDeleteFailureLeavesRefsUnchanged(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - t.Run("packed-refs.lock held", func(t *testing.T) { - t.Parallel() - - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true, RefFormat: "files"}) - _, _, packedID := testRepo.MakeCommit(t, "packed") - _, _, looseID := testRepo.MakeCommit(t, "loose") - prefix := "refs/locked-packed-refs" - - testRepo.UpdateRef(t, prefix+"/foo", packedID) - testRepo.PackRefs(t, "--all", "--prune") - testRepo.UpdateRef(t, prefix+"/foo", looseID) - unchanged := forEachRefLines(testRepo.Run(t, "for-each-ref", "--format=%(objectname) %(refname)", prefix)) - testRepo.WriteFile(t, "packed-refs.lock", []byte{}, 0o644) - - store := openFilesStore(t, testRepo, algo) - - tx, err := store.BeginTransaction() - if err != nil { - t.Fatalf("BeginTransaction(lock held): %v", err) - } - - err = tx.Delete(prefix+"/foo", looseID) - if err != nil { - t.Fatalf("Delete(lock held) queue: %v", err) - } - - err = tx.Commit() - if err == nil { - t.Fatal("Commit(lock held) unexpectedly succeeded") - } - - actual := forEachRefLines(testRepo.Run(t, "for-each-ref", "--format=%(objectname) %(refname)", prefix)) - if !slices.Equal(actual, unchanged) { - t.Fatalf("ShowRef after failed delete = %v, want %v", actual, unchanged) - } - - got, err := store.ResolveToDetached(prefix + "/foo") - if err != nil { - t.Fatalf("ResolveToDetached(lock held): %v", err) - } - - if got.ID != looseID { - t.Fatalf("ResolveToDetached(lock held) = %s, want %s", got.ID, looseID) - } - - gitRoot := testRepo.OpenGitRoot(t) - - _, statErr := gitRoot.Stat(prefix + "/foo.lock") - if !errors.Is(statErr, os.ErrNotExist) { - t.Fatalf("unexpected leftover loose lock: %v", statErr) - } - }) - - t.Run("packed-refs.new exists", func(t *testing.T) { - t.Parallel() - - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true, RefFormat: "files"}) - _, _, packedID := testRepo.MakeCommit(t, "packed") - _, _, looseID := testRepo.MakeCommit(t, "loose") - prefix := "refs/failed-packed-refs" - - testRepo.UpdateRef(t, prefix+"/foo", packedID) - testRepo.PackRefs(t, "--all", "--prune") - testRepo.UpdateRef(t, prefix+"/foo", looseID) - unchanged := forEachRefLines(testRepo.Run(t, "for-each-ref", "--format=%(objectname) %(refname)", prefix)) - testRepo.WriteFile(t, "packed-refs.new", []byte{}, 0o644) - - store := openFilesStore(t, testRepo, algo) - - tx, err := store.BeginTransaction() - if err != nil { - t.Fatalf("BeginTransaction(new exists): %v", err) - } - - err = tx.Delete(prefix+"/foo", looseID) - if err != nil { - t.Fatalf("Delete(new exists) queue: %v", err) - } - - err = tx.Commit() - if err == nil { - t.Fatal("Commit(new exists) unexpectedly succeeded") - } - - actual := forEachRefLines(testRepo.Run(t, "for-each-ref", "--format=%(objectname) %(refname)", prefix)) - if !slices.Equal(actual, unchanged) { - t.Fatalf("ShowRef after failed delete = %v, want %v", actual, unchanged) - } - - got, err := store.ResolveToDetached(prefix + "/foo") - if err != nil { - t.Fatalf("ResolveToDetached(new exists): %v", err) - } - - if got.ID != looseID { - t.Fatalf("ResolveToDetached(new exists) = %s, want %s", got.ID, looseID) - } - }) - }) -} - -func TestFilesPackedRefDeleteDoesNotCreateDirectories(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true, RefFormat: "files"}) - _, _, commitID := testRepo.MakeCommit(t, "packed-only") - name := "refs/heads/d1/d2/r1" - - testRepo.UpdateRef(t, name, commitID) - testRepo.PackRefs(t, "--all", "--prune") - - gitRoot := testRepo.OpenGitRoot(t) - - _, err := gitRoot.Stat("refs/heads/d1/d2") - if !errors.Is(err, os.ErrNotExist) { - t.Fatalf("refs/heads/d1/d2 unexpectedly exists before delete: %v", err) - } - - store := openFilesStore(t, testRepo, algo) - - tx, err := store.BeginTransaction() - if err != nil { - t.Fatalf("BeginTransaction: %v", err) - } - - err = tx.Delete(name, commitID) - if err != nil { - t.Fatalf("Delete queue: %v", err) - } - - err = tx.Commit() - if err != nil { - t.Fatalf("Commit: %v", err) - } - - _, err = gitRoot.Stat("refs/heads/d1/d2") - if !errors.Is(err, os.ErrNotExist) { - t.Fatalf("refs/heads/d1/d2 unexpectedly exists after delete: %v", err) - } - - _, err = gitRoot.Stat("refs/heads/d1") - if !errors.Is(err, os.ErrNotExist) { - t.Fatalf("refs/heads/d1 unexpectedly exists after delete: %v", err) - } - }) -} - -func TestFilesPackedRefIgnoresEmptyDirectories(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true, RefFormat: "files"}) - _, _, commitID := testRepo.MakeCommit(t, "packed-visible") - prefix := "refs/e-for-each-ref" - name := prefix + "/foo" - - testRepo.UpdateRef(t, name, commitID) - expected := forEachRefLines(testRepo.Run(t, "for-each-ref", "--format=%(objectname) %(refname)", prefix)) - testRepo.PackRefs(t, "--all", "--prune") - testRepo.WriteFileAll(t, prefix+"/foo/bar/baz/.keep", []byte{}, 0o755, 0o644) - testRepo.Remove(t, prefix+"/foo/bar/baz/.keep") - - store := openFilesStore(t, testRepo, algo) - - got, err := store.ResolveToDetached(name) - if err != nil { - t.Fatalf("ResolveToDetached: %v", err) - } - - if got.ID != commitID { - t.Fatalf("ResolveToDetached = %s, want %s", got.ID, commitID) - } - - actual := make([]string, 0) - - listed, err := store.List(prefix + "/*") - if err != nil { - t.Fatalf("List: %v", err) - } - - for _, entry := range listed { - actual = append(actual, entry.Name()) - } - - fullActual := make([]string, 0, len(actual)) - for _, name := range actual { - refValue, resolveErr := store.ResolveToDetached(name) - if resolveErr != nil { - t.Fatalf("ResolveToDetached(%q): %v", name, resolveErr) - } - - fullActual = append(fullActual, refValue.ID.String()+" "+name) - } - - slices.Sort(fullActual) - - if !slices.Equal(fullActual, expected) { - t.Fatalf("for-each-ref view = %v, want %v", fullActual, expected) - } - }) -} - -func TestFilesDeleteWaitsForPackedRefsLockWithoutIntermediateState(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true, RefFormat: "files"}) - _, _, packedID := testRepo.MakeCommit(t, "packed") - _, _, looseID := testRepo.MakeCommit(t, "loose") - prefix := "refs/slow-transaction" - - testRepo.UpdateRef(t, prefix+"/foo", packedID) - testRepo.PackRefs(t, "--all", "--prune") - testRepo.UpdateRef(t, prefix+"/foo", looseID) - testRepo.WriteFile(t, "packed-refs.lock", []byte{}, 0o644) - - store := openFilesStore(t, testRepo, algo) - - tx, err := store.BeginTransaction() - if err != nil { - t.Fatalf("BeginTransaction: %v", err) - } - - err = tx.Delete(prefix+"/foo", looseID) - if err != nil { - t.Fatalf("Delete queue: %v", err) - } - - done := make(chan error, 1) - - var wg sync.WaitGroup - - wg.Go(func() { - done <- tx.Commit() - }) - - time.Sleep(75 * time.Millisecond) - - select { - case err := <-done: - t.Fatalf("Commit finished too early: %v", err) - default: - } - - got, err := store.ResolveToDetached(prefix + "/foo") - if err != nil { - t.Fatalf("ResolveToDetached while lock held: %v", err) - } - - if got.ID != looseID { - t.Fatalf("ResolveToDetached while lock held = %s, want %s", got.ID, looseID) - } - - testRepo.Remove(t, "packed-refs.lock") - - select { - case err := <-done: - if err != nil { - t.Fatalf("Commit after lock release: %v", err) - } - case <-time.After(2 * time.Second): - t.Fatal("Commit did not finish after lock release") - } - - wg.Wait() - - _, err = store.Resolve(prefix + "/foo") - if !errors.Is(err, refstore.ErrReferenceNotFound) { - t.Fatalf("Resolve after delete error = %v, want ErrReferenceNotFound", err) - } - }) -} diff --git a/ref/store/files/packed_parse.go b/ref/store/files/packed_parse.go deleted file mode 100644 index 3662f6ed..00000000 --- a/ref/store/files/packed_parse.go +++ /dev/null @@ -1,113 +0,0 @@ -package files - -import ( - "bufio" - "fmt" - "io" - "strings" - - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/ref" -) - -func parsePackedRefs(r io.Reader, algo objectid.Algorithm) (map[string]ref.Detached, []ref.Detached, error) { - byName := make(map[string]ref.Detached) - ordered := make([]ref.Detached, 0, 32) - - br := bufio.NewReader(r) - prev := -1 - lineNum := 0 - hexsz := algo.Size() * 2 - - for { - line, err := br.ReadString('\n') - if err != nil && err != io.EOF { - return nil, nil, err - } - - if line == "" && err == io.EOF { - break - } - - lineNum++ - hadNewline := strings.HasSuffix(line, "\n") - line = strings.TrimSuffix(line, "\n") - - if err == io.EOF && !hadNewline { - return nil, nil, fmt.Errorf("refstore/files: line %d: unterminated line", lineNum) - } - - if line == "" || strings.HasPrefix(line, "#") { - if err == io.EOF { - break - } - - continue - } - - if strings.HasPrefix(line, "^") { - if prev < 0 { - return nil, nil, fmt.Errorf("refstore/files: line %d: peeled line without preceding ref", lineNum) - } - - if len(line) != hexsz+1 { - return nil, nil, fmt.Errorf("refstore/files: line %d: malformed peeled line", lineNum) - } - - peeled, parseErr := objectid.ParseHex(algo, line[1:]) - if parseErr != nil { - return nil, nil, fmt.Errorf("refstore/files: line %d: invalid peeled oid: %w", lineNum, parseErr) - } - - peeledCopy := peeled - cur := ordered[prev] - cur.Peeled = &peeledCopy - ordered[prev] = cur - byName[cur.Name()] = cur - - if err == io.EOF { - break - } - - continue - } - - if len(line) < hexsz+2 { - return nil, nil, fmt.Errorf("refstore/files: line %d: malformed entry", lineNum) - } - - if line[hexsz] != ' ' { - return nil, nil, fmt.Errorf("refstore/files: line %d: malformed entry", lineNum) - } - - idText := line[:hexsz] - - name := line[hexsz+1:] - if name == "" { - return nil, nil, fmt.Errorf("refstore/files: line %d: empty ref name", lineNum) - } - - id, parseErr := objectid.ParseHex(algo, idText) - if parseErr != nil { - return nil, nil, fmt.Errorf("refstore/files: line %d: invalid oid: %w", lineNum, parseErr) - } - - if _, exists := byName[name]; exists { - return nil, nil, fmt.Errorf("refstore/files: line %d: duplicate ref %q", lineNum, name) - } - - detached := ref.Detached{ - RefName: name, - ID: id, - } - ordered = append(ordered, detached) - prev = len(ordered) - 1 - byName[name] = detached - - if err == io.EOF { - break - } - } - - return byName, ordered, nil -} diff --git a/ref/store/files/packed_read.go b/ref/store/files/packed_read.go deleted file mode 100644 index 20800709..00000000 --- a/ref/store/files/packed_read.go +++ /dev/null @@ -1,35 +0,0 @@ -package files - -import ( - "errors" - "fmt" - "os" - - "codeberg.org/lindenii/furgit/ref" -) - -func (store *Store) readPackedRefs() (*packedRefs, error) { - file, err := store.commonRoot.Open("packed-refs") - if err != nil { - if errors.Is(err, os.ErrNotExist) { - return &packedRefs{ - byName: make(map[string]ref.Detached), - ordered: nil, - }, nil - } - - return nil, fmt.Errorf("refstore/files: open packed-refs: %w", err) - } - - defer func() { _ = file.Close() }() - - byName, ordered, err := parsePackedRefs(file, store.algo) - if err != nil { - return nil, err - } - - return &packedRefs{ - byName: byName, - ordered: ordered, - }, nil -} diff --git a/ref/store/files/packed_refs.go b/ref/store/files/packed_refs.go deleted file mode 100644 index f3e91d83..00000000 --- a/ref/store/files/packed_refs.go +++ /dev/null @@ -1,10 +0,0 @@ -package files - -import ( - "codeberg.org/lindenii/furgit/ref" -) - -type packedRefs struct { - byName map[string]ref.Detached - ordered []ref.Detached -} diff --git a/ref/store/files/read_list.go b/ref/store/files/read_list.go deleted file mode 100644 index 5a828276..00000000 --- a/ref/store/files/read_list.go +++ /dev/null @@ -1,76 +0,0 @@ -package files - -import ( - "errors" - "path" - "slices" - - "codeberg.org/lindenii/furgit/ref" - refstore "codeberg.org/lindenii/furgit/ref/store" -) - -// List lists references from the visible files ref namespace. -func (store *Store) List(pattern string) ([]ref.Ref, error) { - matchAll := pattern == "" - if !matchAll { - _, err := path.Match(pattern, "HEAD") - if err != nil { - return nil, err - } - } - - looseNames, err := store.collectLooseRefNames() - if err != nil { - return nil, err - } - - packed, err := store.readPackedRefs() - if err != nil { - return nil, err - } - - byName := make(map[string]ref.Ref, len(looseNames)+len(packed.byName)) - for _, detached := range packed.ordered { - byName[detached.Name()] = detached - } - - for _, name := range looseNames { - resolved, resolveErr := store.readLooseRef(name) - if resolveErr != nil { - if errors.Is(resolveErr, refstore.ErrReferenceNotFound) { - delete(byName, name) - - continue - } - - return nil, resolveErr - } - - byName[name] = resolved - } - - names := make([]string, 0, len(byName)) - for name := range byName { - if !matchAll { - matched, matchErr := path.Match(pattern, name) - if matchErr != nil { - return nil, matchErr - } - - if !matched { - continue - } - } - - names = append(names, name) - } - - slices.Sort(names) - - refs := make([]ref.Ref, 0, len(names)) - for _, name := range names { - refs = append(refs, byName[name]) - } - - return refs, nil -} diff --git a/ref/store/files/read_list_collect.go b/ref/store/files/read_list_collect.go deleted file mode 100644 index f4e2cb69..00000000 --- a/ref/store/files/read_list_collect.go +++ /dev/null @@ -1,78 +0,0 @@ -package files - -import ( - "errors" - "os" - "path" - "strings" -) - -func (store *Store) collectLooseRefNames() ([]string, error) { - names := make([]string, 0, 16) - seen := make(map[string]struct{}, 16) - - _, err := store.gitRoot.Stat("HEAD") - if err == nil { - names = append(names, "HEAD") - seen["HEAD"] = struct{}{} - } else if !errors.Is(err, os.ErrNotExist) { - return nil, err - } - - var walk func(*os.Root, string) error - - walk = func(root *os.Root, dir string) error { - file, openErr := root.Open(dir) - if openErr != nil { - if errors.Is(openErr, os.ErrNotExist) { - return nil - } - - return openErr - } - - defer func() { _ = file.Close() }() - - entries, readErr := file.ReadDir(-1) - if readErr != nil { - return readErr - } - - for _, entry := range entries { - name := path.Join(dir, entry.Name()) - if entry.IsDir() { - err := walk(root, name) - if err != nil { - return err - } - - continue - } - - if strings.HasSuffix(name, ".lock") { - continue - } - - if _, ok := seen[name]; ok { - continue - } - - seen[name] = struct{}{} - names = append(names, name) - } - - return nil - } - - err = walk(store.commonRoot, "refs") - if err != nil { - return nil, err - } - - err = walk(store.gitRoot, "refs") - if err != nil { - return nil, err - } - - return names, nil -} diff --git a/ref/store/files/read_loose.go b/ref/store/files/read_loose.go deleted file mode 100644 index e9b1435a..00000000 --- a/ref/store/files/read_loose.go +++ /dev/null @@ -1,48 +0,0 @@ -package files - -import ( - "errors" - "fmt" - "os" - "strings" - - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/ref" - refstore "codeberg.org/lindenii/furgit/ref/store" -) - -func (store *Store) readLooseRef(name string) (ref.Ref, error) { //nolint:ireturn - refPath := store.loosePath(name) - - data, err := store.rootFor(refPath.root).ReadFile(refPath.path) - if err != nil { - if errors.Is(err, os.ErrNotExist) { - return nil, refstore.ErrReferenceNotFound - } - - return nil, err - } - - line := strings.TrimRightFunc(string(data), isRefWhitespace) - if strings.HasPrefix(line, "ref:") { - target := strings.TrimLeftFunc(line[len("ref:"):], isRefWhitespace) - if target == "" { - return nil, brokenRefError{name: name, err: fmt.Errorf("empty symbolic target")} - } - - return ref.Symbolic{ - RefName: name, - Target: target, - }, nil - } - - id, err := objectid.ParseHex(store.algo, line) - if err != nil { - return nil, brokenRefError{name: name, err: err} - } - - return ref.Detached{ - RefName: name, - ID: id, - }, nil -} diff --git a/ref/store/files/read_resolve.go b/ref/store/files/read_resolve.go deleted file mode 100644 index a998f970..00000000 --- a/ref/store/files/read_resolve.go +++ /dev/null @@ -1,41 +0,0 @@ -package files - -import ( - "errors" - - "codeberg.org/lindenii/furgit/ref" - refstore "codeberg.org/lindenii/furgit/ref/store" -) - -// Resolve resolves one reference name from the files store visible namespace. -func (store *Store) Resolve(name string) (ref.Ref, error) { //nolint:ireturn - if name == "" { - return nil, refstore.ErrReferenceNotFound - } - - resolved, err := store.readLooseRef(name) - if err == nil { - return resolved, nil - } - - if !errors.Is(err, refstore.ErrReferenceNotFound) { - refPath := store.loosePath(name) - - info, statErr := store.rootFor(refPath.root).Stat(refPath.path) - if statErr != nil || !info.IsDir() { - return nil, err - } - } - - packed, packedErr := store.readPackedRefs() - if packedErr != nil { - return nil, packedErr - } - - detached, ok := packed.byName[name] - if !ok { - return nil, refstore.ErrReferenceNotFound - } - - return detached, nil -} diff --git a/ref/store/files/read_resolve_fully.go b/ref/store/files/read_resolve_fully.go deleted file mode 100644 index de58eb6d..00000000 --- a/ref/store/files/read_resolve_fully.go +++ /dev/null @@ -1,42 +0,0 @@ -package files - -import ( - "fmt" - "strings" - - "codeberg.org/lindenii/furgit/ref" -) - -// ResolveToDetached resolves symbolic references through the visible files store -// namespace until one detached reference is reached. -func (store *Store) ResolveToDetached(name string) (ref.Detached, error) { - cur := name - seen := make(map[string]struct{}) - - for { - if _, ok := seen[cur]; ok { - return ref.Detached{}, fmt.Errorf("refstore/files: symbolic reference cycle at %q", cur) - } - - seen[cur] = struct{}{} - - resolved, err := store.Resolve(cur) - if err != nil { - return ref.Detached{}, err - } - - switch resolved := resolved.(type) { - case ref.Detached: - return resolved, nil - case ref.Symbolic: - target := strings.TrimSpace(resolved.Target) - if target == "" { - return ref.Detached{}, fmt.Errorf("refstore/files: symbolic reference %q has empty target", resolved.Name()) - } - - cur = target - default: - return ref.Detached{}, fmt.Errorf("refstore/files: unsupported reference type %T", resolved) - } - } -} diff --git a/ref/store/files/resolve_list_test.go b/ref/store/files/resolve_list_test.go deleted file mode 100644 index d52c5aa2..00000000 --- a/ref/store/files/resolve_list_test.go +++ /dev/null @@ -1,269 +0,0 @@ -package files_test - -import ( - "slices" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/ref" -) - -func TestFilesResolveAndListOverlay(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - _, _, packedID := testRepo.MakeCommit(t, "packed base") - _, _, looseID := testRepo.MakeCommit(t, "loose override") - testRepo.UpdateRef(t, "refs/heads/main", packedID) - testRepo.UpdateRef(t, "refs/tags/v1", packedID) - testRepo.SymbolicRef(t, "HEAD", "refs/heads/main") - testRepo.PackRefs(t, "--all", "--prune") - testRepo.UpdateRef(t, "refs/heads/main", looseID) - testRepo.UpdateRef(t, "refs/heads/dev", looseID) - - store := openFilesStore(t, testRepo, algo) - - resolvedMain, err := store.Resolve("refs/heads/main") - if err != nil { - t.Fatalf("Resolve(main): %v", err) - } - - mainDet, ok := resolvedMain.(ref.Detached) - if !ok { - t.Fatalf("Resolve(main) type = %T, want ref.Detached", resolvedMain) - } - - if mainDet.ID != looseID { - t.Fatalf("Resolve(main) id = %s, want %s", mainDet.ID, looseID) - } - - resolvedHead, err := store.Resolve("HEAD") - if err != nil { - t.Fatalf("Resolve(HEAD): %v", err) - } - - headSym, ok := resolvedHead.(ref.Symbolic) - if !ok { - t.Fatalf("Resolve(HEAD) type = %T, want ref.Symbolic", resolvedHead) - } - - if headSym.Target != "refs/heads/main" { - t.Fatalf("Resolve(HEAD) target = %q, want %q", headSym.Target, "refs/heads/main") - } - - fullHead, err := store.ResolveToDetached("HEAD") - if err != nil { - t.Fatalf("ResolveToDetached(HEAD): %v", err) - } - - if fullHead.ID != looseID { - t.Fatalf("ResolveToDetached(HEAD) = %s, want %s", fullHead.ID, looseID) - } - - allRefs, err := store.List("") - if err != nil { - t.Fatalf("List(\"\"): %v", err) - } - - names := make([]string, 0, len(allRefs)) - for _, entry := range allRefs { - names = append(names, entry.Name()) - } - - slices.Sort(names) - - want := []string{"HEAD", "refs/heads/dev", "refs/heads/main", "refs/tags/v1"} - if !slices.Equal(names, want) { - t.Fatalf("List(\"\") names = %v, want %v", names, want) - } - }) -} - -func TestFilesLooseRefParsingMatchesGit(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, RefFormat: "files"}) - oid := testRepo.HashObject(t, "blob", []byte("payload\n")) - - testRepo.WriteFileAll(t, ".git/refs/heads/no-lf", []byte(oid.String()), 0o755, 0o644) - testRepo.WriteFileAll(t, ".git/refs/heads/trailing-ws", []byte(oid.String()+" "), 0o755, 0o644) - testRepo.WriteFileAll(t, ".git/refs/heads/leading-ws", []byte(" "+oid.String()+"\n"), 0o755, 0o644) - testRepo.WriteFileAll(t, ".git/refs/heads/sym-trailing", []byte("ref: refs/heads/main "), 0o755, 0o644) - testRepo.WriteFileAll(t, ".git/refs/heads/sym-leading", []byte(" ref: refs/heads/main\n"), 0o755, 0o644) - - store := openFilesStore(t, testRepo, algo) - - got, err := store.ResolveToDetached("refs/heads/no-lf") - if err != nil { - t.Fatalf("ResolveToDetached(no-lf): %v", err) - } - - if got.ID != oid { - t.Fatalf("ResolveToDetached(no-lf) = %s, want %s", got.ID, oid) - } - - got, err = store.ResolveToDetached("refs/heads/trailing-ws") - if err != nil { - t.Fatalf("ResolveToDetached(trailing-ws): %v", err) - } - - if got.ID != oid { - t.Fatalf("ResolveToDetached(trailing-ws) = %s, want %s", got.ID, oid) - } - - _, err = store.Resolve("refs/heads/leading-ws") - if err == nil { - t.Fatal("Resolve(leading-ws) unexpectedly succeeded") - } - - resolved, err := store.Resolve("refs/heads/sym-trailing") - if err != nil { - t.Fatalf("Resolve(sym-trailing): %v", err) - } - - sym, ok := resolved.(ref.Symbolic) - if !ok { - t.Fatalf("Resolve(sym-trailing) type = %T, want ref.Symbolic", resolved) - } - - if sym.Target != "refs/heads/main" { - t.Fatalf("Resolve(sym-trailing) target = %q, want %q", sym.Target, "refs/heads/main") - } - - _, err = store.Resolve("refs/heads/sym-leading") - if err == nil { - t.Fatal("Resolve(sym-leading) unexpectedly succeeded") - } - }) -} - -func TestFilesRejectMalformedPackedRefs(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true, RefFormat: "files"}) - _, _, commitID := testRepo.MakeCommit(t, "packed") - testRepo.UpdateRef(t, "refs/heads/main", commitID) - testRepo.PackRefs(t, "--all", "--prune") - - hex := commitID.String() - - cases := []struct { - name string - content string - }{ - { - name: "unterminated line", - content: "# pack-refs with: peeled fully-peeled sorted\n" + hex + " refs/heads/main", - }, - { - name: "junk line", - content: "# pack-refs with: peeled fully-peeled sorted\nbogus content\n", - }, - { - name: "short oid", - content: "# pack-refs with: peeled fully-peeled sorted\n" + hex[:7] + " refs/heads/main\n", - }, - { - name: "trailing garbage after oid", - content: "# pack-refs with: peeled fully-peeled sorted\n" + hex + "xrefs/heads/main\n", - }, - { - name: "malformed peeled line", - content: "# pack-refs with: peeled fully-peeled sorted\n" + hex + " refs/tags/v1\n^" + hex + " garbage\n", - }, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - testRepo.WriteFile(t, "packed-refs", []byte(tc.content), 0o644) - store := openFilesStore(t, testRepo, algo) - - _, err := store.List("") - if err == nil { - t.Fatal("List unexpectedly succeeded") - } - }) - } - }) -} - -func TestFilesPackedRefsReadSemanticsMatchGit(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - t.Run("stale packed entry is still readable", func(t *testing.T) { - t.Parallel() - - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, RefFormat: "files"}) - testRepo.Run(t, "commit", "--allow-empty", "-m", "one") - - oneID, err := objectid.ParseHex(algo, testRepo.Run(t, "rev-parse", "HEAD")) - if err != nil { - t.Fatalf("ParseHex(one): %v", err) - } - - testRepo.Run(t, "tag", "-a", "v1.0", "-m", "v1.0", "HEAD") - testRepo.PackRefs(t, "--all", "--prune") - testRepo.Run(t, "checkout", "--orphan", "another") - testRepo.Run(t, "commit", "--allow-empty", "-m", "two") - testRepo.Run(t, "checkout", "-B", "main") - testRepo.Run(t, "branch", "-D", "another") - testRepo.Run(t, "reflog", "expire", "--expire=now", "--all") - testRepo.Run(t, "prune") - - store := openFilesStore(t, testRepo, algo) - - got, err := store.ResolveToDetached("refs/heads/main") - if err != nil { - t.Fatalf("ResolveToDetached(main): %v", err) - } - - if got.ID == oneID { - t.Fatalf("ResolveToDetached(main) unexpectedly returned stale packed id %s", oneID) - } - - tagRef, err := store.Resolve("refs/tags/v1.0") - if err != nil { - t.Fatalf("Resolve(tag): %v", err) - } - - tagDet, ok := tagRef.(ref.Detached) - if !ok { - t.Fatalf("Resolve(tag) type = %T, want ref.Detached", tagRef) - } - - if tagDet.ID.Algorithm().Size() == 0 { - t.Fatal("Resolve(tag) returned zero object id") - } - }) - - t.Run("exact unicode packed ref remains enumerable", func(t *testing.T) { - t.Parallel() - - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, RefFormat: "files"}) - _, _, commitID := testRepo.MakeCommit(t, "unicode") - testRepo.UpdateRef(t, "refs/heads/\ue43f", commitID) - testRepo.UpdateRef(t, "refs/heads/z", commitID) - testRepo.PackRefs(t, "--all", "--prune") - - store := openFilesStore(t, testRepo, algo) - - listed, err := store.List("refs/heads/z") - if err != nil { - t.Fatalf("List(refs/heads/z): %v", err) - } - - if len(listed) != 1 { - t.Fatalf("List(refs/heads/z) len = %d, want 1", len(listed)) - } - - if listed[0].Name() != "refs/heads/z" { - t.Fatalf("List(refs/heads/z)[0] = %q, want %q", listed[0].Name(), "refs/heads/z") - } - }) - }) -} diff --git a/ref/store/files/root_for.go b/ref/store/files/root_for.go deleted file mode 100644 index cb968ad9..00000000 --- a/ref/store/files/root_for.go +++ /dev/null @@ -1,13 +0,0 @@ -package files - -import ( - "os" -) - -func (store *Store) rootFor(kind rootKind) *os.Root { - if kind == rootCommon { - return store.commonRoot - } - - return store.gitRoot -} diff --git a/ref/store/files/root_kind.go b/ref/store/files/root_kind.go deleted file mode 100644 index d0ae8cf1..00000000 --- a/ref/store/files/root_kind.go +++ /dev/null @@ -1,8 +0,0 @@ -package files - -type rootKind uint8 - -const ( - rootGit rootKind = iota - rootCommon -) diff --git a/ref/store/files/root_loose_path.go b/ref/store/files/root_loose_path.go deleted file mode 100644 index 7764073b..00000000 --- a/ref/store/files/root_loose_path.go +++ /dev/null @@ -1,24 +0,0 @@ -package files - -import ( - "path" - - "codeberg.org/lindenii/furgit/ref/name" -) - -func (store *Store) loosePath(name string) refPath { - parsed := refname.ParseWorktree(name) - switch parsed.Type { - case refname.WorktreeCurrent: - return refPath{root: rootGit, path: parsed.BareRefName} - case refname.WorktreeMain, refname.WorktreeShared: - return refPath{root: rootCommon, path: parsed.BareRefName} - case refname.WorktreeOther: - return refPath{ - root: rootCommon, - path: path.Join("worktrees", parsed.WorktreeName, parsed.BareRefName), - } - default: - return refPath{root: rootCommon, path: name} - } -} diff --git a/ref/store/files/root_open_common.go b/ref/store/files/root_open_common.go deleted file mode 100644 index cac98cbc..00000000 --- a/ref/store/files/root_open_common.go +++ /dev/null @@ -1,31 +0,0 @@ -package files - -import ( - "errors" - "os" - "path/filepath" - "strings" -) - -func openCommonRoot(gitRoot *os.Root) (*os.Root, error) { - content, err := gitRoot.ReadFile("commondir") - if err != nil { - if errors.Is(err, os.ErrNotExist) { - return gitRoot.OpenRoot(".") - } - - return nil, err - } - - commonDir := strings.TrimSpace(string(content)) - if commonDir == "" { - return nil, os.ErrNotExist - } - - if filepath.IsAbs(commonDir) { - return os.OpenRoot(commonDir) - } - - // This is okay because that's how Git defines it anyway. - return os.OpenRoot(filepath.Join(gitRoot.Name(), commonDir)) -} diff --git a/ref/store/files/store.go b/ref/store/files/store.go deleted file mode 100644 index 66d46d06..00000000 --- a/ref/store/files/store.go +++ /dev/null @@ -1,31 +0,0 @@ -// Package files provides one Git files ref store with loose-over-packed reads -// and transaction-coordinated updates. -package files - -import ( - "math/rand" - "os" - "time" - - objectid "codeberg.org/lindenii/furgit/object/id" - refstore "codeberg.org/lindenii/furgit/ref/store" -) - -// Store reads and writes one Git files ref namespace rooted at one repository -// gitdir plus its commondir. -// -// Labels: Close-Caller. -type Store struct { - gitRoot *os.Root - commonRoot *os.Root - algo objectid.Algorithm - lockRand *rand.Rand - - packedRefsTimeout time.Duration -} - -var ( - _ refstore.Reader = (*Store)(nil) - _ refstore.Transactioner = (*Store)(nil) - _ refstore.Batcher = (*Store)(nil) -) diff --git a/ref/store/files/transaction.go b/ref/store/files/transaction.go deleted file mode 100644 index fec43e1d..00000000 --- a/ref/store/files/transaction.go +++ /dev/null @@ -1,13 +0,0 @@ -package files - -import ( - refstore "codeberg.org/lindenii/furgit/ref/store" -) - -// Transaction stages files-store updates for one atomic commit. -type Transaction struct { - store *Store - ops []queuedUpdate -} - -var _ refstore.Transaction = (*Transaction)(nil) diff --git a/ref/store/files/transaction_abort.go b/ref/store/files/transaction_abort.go deleted file mode 100644 index 4f8fed05..00000000 --- a/ref/store/files/transaction_abort.go +++ /dev/null @@ -1,4 +0,0 @@ -package files - -// Abort abandons the queued updates. -func (tx *Transaction) Abort() error { return nil } diff --git a/ref/store/files/transaction_begin.go b/ref/store/files/transaction_begin.go deleted file mode 100644 index cdd3bad1..00000000 --- a/ref/store/files/transaction_begin.go +++ /dev/null @@ -1,13 +0,0 @@ -package files - -import refstore "codeberg.org/lindenii/furgit/ref/store" - -// BeginTransaction creates one new files transaction. -// -//nolint:ireturn -func (store *Store) BeginTransaction() (refstore.Transaction, error) { - return &Transaction{ - store: store, - ops: make([]queuedUpdate, 0, 8), - }, nil -} diff --git a/ref/store/files/transaction_commit.go b/ref/store/files/transaction_commit.go deleted file mode 100644 index aeea497e..00000000 --- a/ref/store/files/transaction_commit.go +++ /dev/null @@ -1,13 +0,0 @@ -package files - -// Commit validates and applies the queued updates atomically. -func (tx *Transaction) Commit() error { - executor := &refUpdateExecutor{store: tx.store} - - prepared, err := executor.prepareUpdates(tx.ops) - if err != nil { - return err - } - - return executor.commitPreparedUpdates(prepared) -} diff --git a/ref/store/files/transaction_dirs_test.go b/ref/store/files/transaction_dirs_test.go deleted file mode 100644 index c010ae69..00000000 --- a/ref/store/files/transaction_dirs_test.go +++ /dev/null @@ -1,220 +0,0 @@ -package files_test - -import ( - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - objectid "codeberg.org/lindenii/furgit/object/id" -) - -func TestFilesTransactionEmptyDirectoriesDoNotBlock(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - _, _, oldID := testRepo.MakeCommit(t, "old") - _, _, newID := testRepo.MakeCommit(t, "new") - - testRepo.UpdateRef(t, "refs/e-verify/foo", oldID) - testRepo.PackRefs(t, "--all", "--prune") - testRepo.WriteFileAll(t, "refs/e-verify/foo/bar/baz/.keep", []byte{}, 0o755, 0o644) - testRepo.Remove(t, "refs/e-verify/foo/bar/baz/.keep") - - store := openFilesStore(t, testRepo, algo) - - tx, err := store.BeginTransaction() - if err != nil { - t.Fatalf("BeginTransaction(verify): %v", err) - } - - err = tx.Verify("refs/e-verify/foo", oldID) - if err != nil { - t.Fatalf("Verify with empty directories: %v", err) - } - - err = tx.Commit() - if err != nil { - t.Fatalf("Commit(verify with empty directories): %v", err) - } - - testRepo.UpdateRef(t, "refs/e-update/foo", oldID) - testRepo.PackRefs(t, "--all", "--prune") - testRepo.WriteFileAll(t, "refs/e-update/foo/bar/baz/.keep", []byte{}, 0o755, 0o644) - testRepo.Remove(t, "refs/e-update/foo/bar/baz/.keep") - - tx, err = store.BeginTransaction() - if err != nil { - t.Fatalf("BeginTransaction(update): %v", err) - } - - err = tx.Update("refs/e-update/foo", newID, oldID) - if err != nil { - t.Fatalf("Update with empty directories: %v", err) - } - - err = tx.Commit() - if err != nil { - t.Fatalf("Commit(update with empty directories): %v", err) - } - - got, err := store.ResolveToDetached("refs/e-update/foo") - if err != nil { - t.Fatalf("ResolveToDetached(updated foo): %v", err) - } - - if got.ID != newID { - t.Fatalf("updated foo = %s, want %s", got.ID, newID) - } - - testRepo.WriteFileAll(t, "refs/e-create/foo/bar/baz/.keep", []byte{}, 0o755, 0o644) - testRepo.Remove(t, "refs/e-create/foo/bar/baz/.keep") - - tx, err = store.BeginTransaction() - if err != nil { - t.Fatalf("BeginTransaction(create): %v", err) - } - - err = tx.Create("refs/e-create/foo", oldID) - if err != nil { - t.Fatalf("Create with empty directories: %v", err) - } - - err = tx.Commit() - if err != nil { - t.Fatalf("Commit(create with empty directories): %v", err) - } - - got, err = store.ResolveToDetached("refs/e-create/foo") - if err != nil { - t.Fatalf("ResolveToDetached(created foo): %v", err) - } - - if got.ID != oldID { - t.Fatalf("created foo = %s, want %s", got.ID, oldID) - } - - testRepo.UpdateRef(t, "refs/e-delete/foo", oldID) - testRepo.PackRefs(t, "--all", "--prune") - testRepo.WriteFileAll(t, "refs/e-delete/foo/bar/baz/.keep", []byte{}, 0o755, 0o644) - testRepo.Remove(t, "refs/e-delete/foo/bar/baz/.keep") - - tx, err = store.BeginTransaction() - if err != nil { - t.Fatalf("BeginTransaction(delete): %v", err) - } - - err = tx.Delete("refs/e-delete/foo", oldID) - if err != nil { - t.Fatalf("Delete with empty directories: %v", err) - } - - err = tx.Commit() - if err != nil { - t.Fatalf("Commit(delete with empty directories): %v", err) - } - }) -} - -func TestFilesTransactionNonEmptyDirectoryAndBrokenRefBlockCreate(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - _, _, commitID := testRepo.MakeCommit(t, "base") - store := openFilesStore(t, testRepo, algo) - - testRepo.WriteFileAll(t, "refs/ne-create/foo/bar/baz.lock", []byte(""), 0o755, 0o644) - - tx, err := store.BeginTransaction() - if err != nil { - t.Fatalf("BeginTransaction(non-empty dir): %v", err) - } - - err = tx.Create("refs/ne-create/foo", commitID) - if err != nil { - t.Fatalf("Create(non-empty dir) queue: %v", err) - } - - err = tx.Commit() - if err == nil { - t.Fatal("Commit(non-empty dir) unexpectedly succeeded") - } - - testRepo.WriteFileAll(t, "refs/broken/foo", []byte("gobbledigook\n"), 0o755, 0o644) - - tx, err = store.BeginTransaction() - if err != nil { - t.Fatalf("BeginTransaction(broken ref): %v", err) - } - - err = tx.Create("refs/broken/foo", commitID) - if err != nil { - t.Fatalf("Create(broken ref) queue: %v", err) - } - - err = tx.Commit() - if err == nil { - t.Fatal("Commit(broken ref) unexpectedly succeeded") - } - }) -} - -func TestFilesTransactionIndirectCreateMatchesGit(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - t.Run("non-empty directory blocks", func(t *testing.T) { - t.Parallel() - - repo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, RefFormat: "files"}) - _, _, innerID := repo.MakeCommit(t, "inner") - prefix := "refs/ne-indirect-create" - - repo.SymbolicRef(t, prefix+"/symref", prefix+"/foo") - repo.WriteFileAll(t, ".git/"+prefix+"/foo/bar/baz.lock", []byte{}, 0o755, 0o644) - store := openFilesStore(t, repo, algo) - - tx, err := store.BeginTransaction() - if err != nil { - t.Fatalf("BeginTransaction(non-empty): %v", err) - } - - err = tx.Create(prefix+"/symref", innerID) - if err != nil { - t.Fatalf("Create(non-empty) queue: %v", err) - } - - err = tx.Commit() - if err == nil { - t.Fatal("Commit(non-empty) unexpectedly succeeded") - } - }) - - t.Run("broken referent blocks", func(t *testing.T) { - t.Parallel() - - repo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, RefFormat: "files"}) - _, _, commitID := repo.MakeCommit(t, "broken") - prefix := "refs/broken-indirect-create" - - repo.SymbolicRef(t, prefix+"/symref", prefix+"/foo") - repo.WriteFileAll(t, ".git/"+prefix+"/foo", []byte("gobbledigook\n"), 0o755, 0o644) - store := openFilesStore(t, repo, algo) - - tx, err := store.BeginTransaction() - if err != nil { - t.Fatalf("BeginTransaction(broken): %v", err) - } - - err = tx.Create(prefix+"/symref", commitID) - if err != nil { - t.Fatalf("Create(broken) queue: %v", err) - } - - err = tx.Commit() - if err == nil { - t.Fatal("Commit(broken) unexpectedly succeeded") - } - }) - }) -} diff --git a/ref/store/files/transaction_names_test.go b/ref/store/files/transaction_names_test.go deleted file mode 100644 index b362cb08..00000000 --- a/ref/store/files/transaction_names_test.go +++ /dev/null @@ -1,188 +0,0 @@ -package files_test - -import ( - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/ref" - refstore "codeberg.org/lindenii/furgit/ref/store" -) - -func TestFilesTransactionValidateUpdateNames(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - _, _, commitID := testRepo.MakeCommit(t, "base") - - store := openFilesStore(t, testRepo, algo) - - tests := []struct { - name string - queue func(refstore.Transaction) error - wantErr bool - }{ - { - name: "create refs/heads/main", - queue: func(tx refstore.Transaction) error { - return tx.Create("refs/heads/main", commitID) - }, - }, - { - name: "create foo/bar", - queue: func(tx refstore.Transaction) error { - return tx.Create("foo/bar", commitID) - }, - }, - { - name: "create FETCH_HEAD", - queue: func(tx refstore.Transaction) error { - return tx.Create("FETCH_HEAD", commitID) - }, - wantErr: true, - }, - { - name: "create MERGE_HEAD", - queue: func(tx refstore.Transaction) error { - return tx.Create("MERGE_HEAD", commitID) - }, - wantErr: true, - }, - { - name: "create bad refname", - queue: func(tx refstore.Transaction) error { - return tx.Create("refs/heads/.bad", commitID) - }, - wantErr: true, - }, - { - name: "verify unsafe delete-style name", - queue: func(tx refstore.Transaction) error { - return tx.Verify("foo/bar", commitID) - }, - wantErr: true, - }, - { - name: "verify pseudoref-style name", - queue: func(tx refstore.Transaction) error { - return tx.Verify("PSEUDOREF", commitID) - }, - wantErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tx, err := store.BeginTransaction() - if err != nil { - t.Fatalf("BeginTransaction: %v", err) - } - - err = tt.queue(tx) - if (err != nil) != tt.wantErr { - t.Fatalf("queue err=%v, wantErr=%v", err, tt.wantErr) - } - - _ = tx.Abort() - }) - } - }) -} - -func TestFilesTransactionSymbolicTargetRules(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - _, _, mainID := testRepo.MakeCommit(t, "main") - testRepo.UpdateRef(t, "refs/heads/main", mainID) - testRepo.UpdateRef(t, "ORIG_HEAD", mainID) - - store := openFilesStore(t, testRepo, algo) - - tests := []struct { - name string - queue func(refstore.Transaction) error - wantErr bool - }{ - { - name: "head requires branch target", - queue: func(tx refstore.Transaction) error { - return tx.CreateSymbolic("HEAD", "foo") - }, - wantErr: true, - }, - { - name: "head accepts refs/heads target", - queue: func(tx refstore.Transaction) error { - return tx.CreateSymbolic("HEAD", "refs/heads/main") - }, - }, - { - name: "non-head allows top-level target", - queue: func(tx refstore.Transaction) error { - return tx.CreateSymbolic("refs/heads/top-level", "ORIG_HEAD") - }, - }, - { - name: "non-head rejects invalid target", - queue: func(tx refstore.Transaction) error { - return tx.CreateSymbolic("refs/heads/invalid", "foo..bar") - }, - wantErr: true, - }, - { - name: "non-head allows worktree target", - queue: func(tx refstore.Transaction) error { - return tx.CreateSymbolic("refs/heads/worktree-target", "worktrees/wt1/HEAD") - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tx, err := store.BeginTransaction() - if err != nil { - t.Fatalf("BeginTransaction: %v", err) - } - - err = tt.queue(tx) - if (err != nil) != tt.wantErr { - t.Fatalf("queue err=%v, wantErr=%v", err, tt.wantErr) - } - - _ = tx.Abort() - }) - } - - tx, err := store.BeginTransaction() - if err != nil { - t.Fatalf("BeginTransaction(final symbolic): %v", err) - } - - err = tx.CreateSymbolic("refs/heads/top-level", "ORIG_HEAD") - if err != nil { - t.Fatalf("CreateSymbolic(top-level): %v", err) - } - - err = tx.Commit() - if err != nil { - t.Fatalf("Commit(CreateSymbolic top-level): %v", err) - } - - got, err := store.Resolve("refs/heads/top-level") - if err != nil { - t.Fatalf("Resolve(top-level): %v", err) - } - - sym, ok := got.(ref.Symbolic) - if !ok { - t.Fatalf("Resolve(top-level) type = %T, want ref.Symbolic", got) - } - - if sym.Target != "ORIG_HEAD" { - t.Fatalf("top-level target = %q, want %q", sym.Target, "ORIG_HEAD") - } - }) -} diff --git a/ref/store/files/transaction_pseudoref_test.go b/ref/store/files/transaction_pseudoref_test.go deleted file mode 100644 index ea57e92f..00000000 --- a/ref/store/files/transaction_pseudoref_test.go +++ /dev/null @@ -1,106 +0,0 @@ -package files_test - -import ( - "errors" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - objectid "codeberg.org/lindenii/furgit/object/id" - refstore "codeberg.org/lindenii/furgit/ref/store" -) - -func TestFilesTransactionPseudorefLifecycle(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - _, _, aID := testRepo.MakeCommit(t, "A") - _, _, bID := testRepo.MakeCommit(t, "B") - _, _, cID := testRepo.MakeCommit(t, "C") - - store := openFilesStore(t, testRepo, algo) - - tx, err := store.BeginTransaction() - if err != nil { - t.Fatalf("BeginTransaction(create): %v", err) - } - - err = tx.Create("PSEUDOREF", aID) - if err != nil { - t.Fatalf("Create(PSEUDOREF): %v", err) - } - - err = tx.Commit() - if err != nil { - t.Fatalf("Commit(create PSEUDOREF): %v", err) - } - - got, err := store.ResolveToDetached("PSEUDOREF") - if err != nil { - t.Fatalf("ResolveToDetached(PSEUDOREF): %v", err) - } - - if got.ID != aID { - t.Fatalf("PSEUDOREF after create = %s, want %s", got.ID, aID) - } - - tx, err = store.BeginTransaction() - if err != nil { - t.Fatalf("BeginTransaction(update): %v", err) - } - - err = tx.Update("PSEUDOREF", bID, aID) - if err != nil { - t.Fatalf("Update(PSEUDOREF): %v", err) - } - - err = tx.Commit() - if err != nil { - t.Fatalf("Commit(update PSEUDOREF): %v", err) - } - - got, err = store.ResolveToDetached("PSEUDOREF") - if err != nil { - t.Fatalf("ResolveToDetached(PSEUDOREF) after update: %v", err) - } - - if got.ID != bID { - t.Fatalf("PSEUDOREF after update = %s, want %s", got.ID, bID) - } - - tx, err = store.BeginTransaction() - if err != nil { - t.Fatalf("BeginTransaction(stale update): %v", err) - } - - err = tx.Update("PSEUDOREF", cID, aID) - if err != nil { - t.Fatalf("queue stale update: %v", err) - } - - err = tx.Commit() - if err == nil { - t.Fatal("stale pseudoref update unexpectedly succeeded") - } - - tx, err = store.BeginTransaction() - if err != nil { - t.Fatalf("BeginTransaction(delete): %v", err) - } - - err = tx.Delete("PSEUDOREF", bID) - if err != nil { - t.Fatalf("Delete(PSEUDOREF): %v", err) - } - - err = tx.Commit() - if err != nil { - t.Fatalf("Commit(delete PSEUDOREF): %v", err) - } - - _, err = store.Resolve("PSEUDOREF") - if !errors.Is(err, refstore.ErrReferenceNotFound) { - t.Fatalf("Resolve(PSEUDOREF after delete) err=%v", err) - } - }) -} diff --git a/ref/store/files/transaction_queue.go b/ref/store/files/transaction_queue.go deleted file mode 100644 index aa2004c3..00000000 --- a/ref/store/files/transaction_queue.go +++ /dev/null @@ -1,12 +0,0 @@ -package files - -func (tx *Transaction) queue(op queuedUpdate) error { - err := (&refUpdateExecutor{store: tx.store}).validateQueuedUpdate(op) - if err != nil { - return err - } - - tx.ops = append(tx.ops, op) - - return nil -} diff --git a/ref/store/files/transaction_queue_ops.go b/ref/store/files/transaction_queue_ops.go deleted file mode 100644 index 63f48254..00000000 --- a/ref/store/files/transaction_queue_ops.go +++ /dev/null @@ -1,43 +0,0 @@ -package files - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// Create queues a detached reference creation. -func (tx *Transaction) Create(name string, newID objectid.ObjectID) error { - return tx.queue(queuedUpdate{name: name, kind: updateCreate, newID: newID}) -} - -// Update queues a detached reference update. -func (tx *Transaction) Update(name string, newID, oldID objectid.ObjectID) error { - return tx.queue(queuedUpdate{name: name, kind: updateReplace, newID: newID, oldID: oldID}) -} - -// Delete queues a detached reference deletion. -func (tx *Transaction) Delete(name string, oldID objectid.ObjectID) error { - return tx.queue(queuedUpdate{name: name, kind: updateDelete, oldID: oldID}) -} - -// Verify queues a detached reference verification. -func (tx *Transaction) Verify(name string, oldID objectid.ObjectID) error { - return tx.queue(queuedUpdate{name: name, kind: updateVerify, oldID: oldID}) -} - -// CreateSymbolic queues a symbolic reference creation. -func (tx *Transaction) CreateSymbolic(name, newTarget string) error { - return tx.queue(queuedUpdate{name: name, kind: updateCreateSymbolic, newTarget: newTarget}) -} - -// UpdateSymbolic queues a symbolic reference update. -func (tx *Transaction) UpdateSymbolic(name, newTarget, oldTarget string) error { - return tx.queue(queuedUpdate{name: name, kind: updateReplaceSymbolic, newTarget: newTarget, oldTarget: oldTarget}) -} - -// DeleteSymbolic queues a symbolic reference deletion. -func (tx *Transaction) DeleteSymbolic(name, oldTarget string) error { - return tx.queue(queuedUpdate{name: name, kind: updateDeleteSymbolic, oldTarget: oldTarget}) -} - -// VerifySymbolic queues a symbolic reference verification. -func (tx *Transaction) VerifySymbolic(name, oldTarget string) error { - return tx.queue(queuedUpdate{name: name, kind: updateVerifySymbolic, oldTarget: oldTarget}) -} diff --git a/ref/store/files/transaction_symbolic_test.go b/ref/store/files/transaction_symbolic_test.go deleted file mode 100644 index 360686d6..00000000 --- a/ref/store/files/transaction_symbolic_test.go +++ /dev/null @@ -1,154 +0,0 @@ -package files_test - -import ( - "errors" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - objectid "codeberg.org/lindenii/furgit/object/id" - refstore "codeberg.org/lindenii/furgit/ref/store" -) - -func TestFilesTransactionDirectSymbolicDeletes(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - _, _, mainID := testRepo.MakeCommit(t, "main") - testRepo.UpdateRef(t, "refs/heads/main", mainID) - - store := openFilesStore(t, testRepo, algo) - - tx, err := store.BeginTransaction() - if err != nil { - t.Fatalf("BeginTransaction(create symref): %v", err) - } - - err = tx.CreateSymbolic("SYMREF", "refs/heads/main") - if err != nil { - t.Fatalf("CreateSymbolic(SYMREF): %v", err) - } - - err = tx.Commit() - if err != nil { - t.Fatalf("Commit(CreateSymbolic SYMREF): %v", err) - } - - tx, err = store.BeginTransaction() - if err != nil { - t.Fatalf("BeginTransaction(delete symref): %v", err) - } - - err = tx.DeleteSymbolic("SYMREF", "refs/heads/main") - if err != nil { - t.Fatalf("DeleteSymbolic(SYMREF): %v", err) - } - - err = tx.Commit() - if err != nil { - t.Fatalf("Commit(DeleteSymbolic SYMREF): %v", err) - } - - _, err = store.Resolve("SYMREF") - if !errors.Is(err, refstore.ErrReferenceNotFound) { - t.Fatalf("Resolve(SYMREF after delete) err=%v", err) - } - - got, err := store.ResolveToDetached("refs/heads/main") - if err != nil { - t.Fatalf("ResolveToDetached(main): %v", err) - } - - if got.ID != mainID { - t.Fatalf("main after DeleteSymbolic = %s, want %s", got.ID, mainID) - } - }) -} - -func TestFilesTransactionSelfAndDanglingSymrefs(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - _, _, mainID := testRepo.MakeCommit(t, "main") - testRepo.UpdateRef(t, "refs/heads/main", mainID) - - store := openFilesStore(t, testRepo, algo) - - tx, err := store.BeginTransaction() - if err != nil { - t.Fatalf("BeginTransaction(create self): %v", err) - } - - err = tx.CreateSymbolic("refs/heads/self", "refs/heads/self") - if err != nil { - t.Fatalf("CreateSymbolic(self): %v", err) - } - - err = tx.Commit() - if err != nil { - t.Fatalf("Commit(CreateSymbolic self): %v", err) - } - - tx, err = store.BeginTransaction() - if err != nil { - t.Fatalf("BeginTransaction(delete logical self): %v", err) - } - - err = tx.Delete("refs/heads/self", mainID) - if err == nil { - err = tx.Commit() - } else { - _ = tx.Abort() - } - - if err == nil { - t.Fatal("Delete(self) unexpectedly succeeded") - } - - tx, err = store.BeginTransaction() - if err != nil { - t.Fatalf("BeginTransaction(delete symbolic self): %v", err) - } - - err = tx.DeleteSymbolic("refs/heads/self", "refs/heads/self") - if err != nil { - t.Fatalf("DeleteSymbolic(self): %v", err) - } - - err = tx.Commit() - if err != nil { - t.Fatalf("Commit(DeleteSymbolic self): %v", err) - } - - tx, err = store.BeginTransaction() - if err != nil { - t.Fatalf("BeginTransaction(create dangling): %v", err) - } - - err = tx.CreateSymbolic("refs/heads/dangling", "refs/heads/missing") - if err != nil { - t.Fatalf("CreateSymbolic(dangling): %v", err) - } - - err = tx.Commit() - if err != nil { - t.Fatalf("Commit(CreateSymbolic dangling): %v", err) - } - - tx, err = store.BeginTransaction() - if err != nil { - t.Fatalf("BeginTransaction(delete dangling): %v", err) - } - - err = tx.DeleteSymbolic("refs/heads/dangling", "refs/heads/missing") - if err != nil { - t.Fatalf("DeleteSymbolic(dangling): %v", err) - } - - err = tx.Commit() - if err != nil { - t.Fatalf("Commit(DeleteSymbolic dangling): %v", err) - } - }) -} diff --git a/ref/store/files/transaction_update_test.go b/ref/store/files/transaction_update_test.go deleted file mode 100644 index a29d586e..00000000 --- a/ref/store/files/transaction_update_test.go +++ /dev/null @@ -1,178 +0,0 @@ -package files_test - -import ( - "errors" - "strings" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/ref" - refstore "codeberg.org/lindenii/furgit/ref/store" -) - -func TestFilesTransactionPackedUpdateCreatesLooseOverride(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - _, _, oldID := testRepo.MakeCommit(t, "old packed") - _, _, newID := testRepo.MakeCommit(t, "new loose") - testRepo.UpdateRef(t, "refs/heads/main", oldID) - testRepo.PackRefs(t, "--all", "--prune") - - store := openFilesStore(t, testRepo, algo) - - tx, err := store.BeginTransaction() - if err != nil { - t.Fatalf("BeginTransaction: %v", err) - } - - err = tx.Update("refs/heads/main", newID, oldID) - if err != nil { - t.Fatalf("Update queue: %v", err) - } - - err = tx.Commit() - if err != nil { - t.Fatalf("Commit: %v", err) - } - - got, err := store.ResolveToDetached("refs/heads/main") - if err != nil { - t.Fatalf("ResolveToDetached(main): %v", err) - } - - if got.ID != newID { - t.Fatalf("ResolveToDetached(main) = %s, want %s", got.ID, newID) - } - - packedRefs := string(testRepo.ReadFile(t, "packed-refs")) - if !strings.Contains(packedRefs, oldID.String()+" refs/heads/main\n") { - t.Fatalf("packed-refs lost old packed main entry:\n%s", packedRefs) - } - - looseMain := string(testRepo.ReadFile(t, "refs/heads/main")) - if strings.TrimSpace(looseMain) != newID.String() { - t.Fatalf("loose refs/heads/main = %q, want %q", strings.TrimSpace(looseMain), newID.String()) - } - }) -} - -func TestFilesTransactionDeletesPackedAndLooseRefs(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - _, _, packedOnlyID := testRepo.MakeCommit(t, "packed only") - _, _, bothID := testRepo.MakeCommit(t, "both") - testRepo.UpdateRef(t, "refs/heads/packed", packedOnlyID) - testRepo.UpdateRef(t, "refs/heads/both", bothID) - testRepo.PackRefs(t, "--all", "--prune") - testRepo.UpdateRef(t, "refs/heads/both", bothID) - - store := openFilesStore(t, testRepo, algo) - - tx, err := store.BeginTransaction() - if err != nil { - t.Fatalf("BeginTransaction: %v", err) - } - - err = tx.Delete("refs/heads/packed", packedOnlyID) - if err != nil { - t.Fatalf("Delete(packed): %v", err) - } - - err = tx.Delete("refs/heads/both", bothID) - if err != nil { - t.Fatalf("Delete(both): %v", err) - } - - err = tx.Commit() - if err != nil { - t.Fatalf("Commit(delete): %v", err) - } - - _, err = store.Resolve("refs/heads/packed") - if !errors.Is(err, refstore.ErrReferenceNotFound) { - t.Fatalf("Resolve(packed after delete) error = %v", err) - } - - _, err = store.Resolve("refs/heads/both") - if !errors.Is(err, refstore.ErrReferenceNotFound) { - t.Fatalf("Resolve(both after delete) error = %v", err) - } - - packedRefs := string(testRepo.ReadFile(t, "packed-refs")) - if strings.Contains(packedRefs, "refs/heads/packed\n") || strings.Contains(packedRefs, "refs/heads/both\n") { - t.Fatalf("packed-refs still contains deleted refs:\n%s", packedRefs) - } - }) -} - -func TestFilesTransactionDerefAndDirectSymbolic(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - _, _, firstID := testRepo.MakeCommit(t, "first") - _, _, secondID := testRepo.MakeCommit(t, "second") - testRepo.UpdateRef(t, "refs/heads/main", firstID) - testRepo.SymbolicRef(t, "HEAD", "refs/heads/main") - - store := openFilesStore(t, testRepo, algo) - - tx, err := store.BeginTransaction() - if err != nil { - t.Fatalf("BeginTransaction(update): %v", err) - } - - err = tx.Update("HEAD", secondID, firstID) - if err != nil { - t.Fatalf("Update(HEAD): %v", err) - } - - err = tx.Commit() - if err != nil { - t.Fatalf("Commit(update HEAD): %v", err) - } - - mainRef, err := store.ResolveToDetached("refs/heads/main") - if err != nil { - t.Fatalf("ResolveToDetached(main): %v", err) - } - - if mainRef.ID != secondID { - t.Fatalf("main after Update(HEAD) = %s, want %s", mainRef.ID, secondID) - } - - tx, err = store.BeginTransaction() - if err != nil { - t.Fatalf("BeginTransaction(update symbolic): %v", err) - } - - err = tx.UpdateSymbolic("HEAD", "refs/heads/next", "refs/heads/main") - if err != nil { - t.Fatalf("UpdateSymbolic(HEAD): %v", err) - } - - err = tx.Commit() - if err != nil { - t.Fatalf("Commit(update symbolic HEAD): %v", err) - } - - headRef, err := store.Resolve("HEAD") - if err != nil { - t.Fatalf("Resolve(HEAD): %v", err) - } - - headSym, ok := headRef.(ref.Symbolic) - if !ok { - t.Fatalf("Resolve(HEAD) type = %T, want ref.Symbolic", headRef) - } - - if headSym.Target != "refs/heads/next" { - t.Fatalf("HEAD target = %q, want %q", headSym.Target, "refs/heads/next") - } - }) -} diff --git a/ref/store/files/trim.go b/ref/store/files/trim.go deleted file mode 100644 index 69a851dc..00000000 --- a/ref/store/files/trim.go +++ /dev/null @@ -1,10 +0,0 @@ -package files - -func isRefWhitespace(r rune) bool { - switch r { - case ' ', '\t', '\n', '\r', '\v', '\f': - return true - default: - return false - } -} diff --git a/ref/store/files/update_cleanup.go b/ref/store/files/update_cleanup.go deleted file mode 100644 index 5df2d967..00000000 --- a/ref/store/files/update_cleanup.go +++ /dev/null @@ -1,39 +0,0 @@ -package files - -import ( - "errors" - "os" - "slices" -) - -func (executor *refUpdateExecutor) cleanupPreparedUpdates(prepared []preparedUpdate) error { - var firstErr error - - lockNames := make([]string, 0, len(prepared)+1) - for _, item := range prepared { - lockNames = append(lockNames, updateTargetKey(item.target.loc)) - } - - lockNames = append(lockNames, updateTargetKey(refPath{root: rootCommon, path: "packed-refs"})) - slices.Sort(lockNames) - lockNames = slices.Compact(lockNames) - - for _, lockKey := range lockNames { - lockPath := refPathFromKey(lockKey) - lockName := lockPath.path + ".lock" - root := executor.store.rootFor(lockPath.root) - - err := root.Remove(lockName) - if err == nil || errors.Is(err, os.ErrNotExist) { - executor.tryRemoveEmptyParentPaths(lockPath.root, lockName) - - continue - } - - if firstErr == nil { - firstErr = err - } - } - - return firstErr -} diff --git a/ref/store/files/update_cleanup_parents.go b/ref/store/files/update_cleanup_parents.go deleted file mode 100644 index 5a994dcd..00000000 --- a/ref/store/files/update_cleanup_parents.go +++ /dev/null @@ -1,30 +0,0 @@ -package files - -import ( - "errors" - "os" - "path" -) - -func (executor *refUpdateExecutor) tryRemoveEmptyParents(name string) { - loc := executor.store.loosePath(name) - executor.tryRemoveEmptyParentPaths(loc.root, loc.path) -} - -func (executor *refUpdateExecutor) tryRemoveEmptyParentPaths(kind rootKind, name string) { - root := executor.store.rootFor(kind) - dir := path.Dir(name) - - for dir != "." && dir != "/" { - err := root.Remove(dir) - if err != nil { - if errors.Is(err, os.ErrNotExist) { - return - } - - return - } - - dir = path.Dir(dir) - } -} diff --git a/ref/store/files/update_commit.go b/ref/store/files/update_commit.go deleted file mode 100644 index 3d39e990..00000000 --- a/ref/store/files/update_commit.go +++ /dev/null @@ -1,25 +0,0 @@ -package files - -func (executor *refUpdateExecutor) commitPreparedUpdates(prepared []preparedUpdate) (err error) { - defer func() { - _ = executor.cleanupPreparedUpdates(prepared) - }() - - for _, item := range prepared { - if item.op.kind == updateDelete || item.op.kind == updateDeleteSymbolic || item.op.kind == updateVerify || item.op.kind == updateVerifySymbolic { - continue - } - - err = executor.writePreparedLooseUpdate(item) - if err != nil { - return wrapUpdateError(item.op.name, err) - } - } - - err = executor.applyPackedRefDeletes(prepared) - if err != nil { - return err - } - - return executor.removeDeletedLooseRefs(prepared) -} diff --git a/ref/store/files/update_commit_delete.go b/ref/store/files/update_commit_delete.go deleted file mode 100644 index 47a600fb..00000000 --- a/ref/store/files/update_commit_delete.go +++ /dev/null @@ -1,25 +0,0 @@ -package files - -import ( - "errors" - "os" -) - -func (executor *refUpdateExecutor) removeDeletedLooseRefs(prepared []preparedUpdate) error { - for _, item := range prepared { - switch item.op.kind { - case updateDelete, updateDeleteSymbolic: - if item.target.ref.isLoose { - err := executor.store.rootFor(item.target.loc.root).Remove(item.target.loc.path) - if err != nil && !errors.Is(err, os.ErrNotExist) { - return wrapUpdateError(item.op.name, err) - } - - executor.tryRemoveEmptyParents(item.target.name) - } - case updateCreate, updateReplace, updateVerify, updateCreateSymbolic, updateReplaceSymbolic, updateVerifySymbolic: - } - } - - return nil -} diff --git a/ref/store/files/update_dir_tree.go b/ref/store/files/update_dir_tree.go deleted file mode 100644 index 51fb5cfb..00000000 --- a/ref/store/files/update_dir_tree.go +++ /dev/null @@ -1,59 +0,0 @@ -package files - -import ( - "errors" - "fmt" - "os" - "path" -) - -func (executor *refUpdateExecutor) removeEmptyDirTree(name refPath) error { - root := executor.store.rootFor(name.root) - - info, err := root.Stat(name.path) - if err != nil { - if errors.Is(err, os.ErrNotExist) { - return nil - } - - return err - } - - if !info.IsDir() { - return nil - } - - return executor.removeEmptyDirTreeRecursive(name) -} - -func (executor *refUpdateExecutor) removeEmptyDirTreeRecursive(name refPath) error { - root := executor.store.rootFor(name.root) - - dir, err := root.Open(name.path) - if err != nil { - return err - } - - entries, err := dir.ReadDir(-1) - _ = dir.Close() - - if err != nil { - return err - } - - for _, entry := range entries { - if !entry.IsDir() { - return fmt.Errorf("refstore/files: non-empty directory blocks reference %q", name.path) - } - - err = executor.removeEmptyDirTreeRecursive(refPath{ - root: name.root, - path: path.Join(name.path, entry.Name()), - }) - if err != nil { - return err - } - } - - return root.Remove(name.path) -} diff --git a/ref/store/files/update_direct_read.go b/ref/store/files/update_direct_read.go deleted file mode 100644 index 50e15026..00000000 --- a/ref/store/files/update_direct_read.go +++ /dev/null @@ -1,76 +0,0 @@ -package files - -import ( - "errors" - "fmt" - - "codeberg.org/lindenii/furgit/ref" - "codeberg.org/lindenii/furgit/ref/name" - refstore "codeberg.org/lindenii/furgit/ref/store" -) - -func (executor *refUpdateExecutor) directRead(name string) (directRefState, error) { - loc := executor.store.loosePath(name) - hasPacked := false - - if loc.root == rootCommon && refname.ParseWorktree(name).Type == refname.WorktreeShared { - packed, packedErr := executor.store.readPackedRefs() - if packedErr != nil { - return directRefState{}, packedErr - } - - _, hasPacked = packed.byName[name] - } - - loose, err := executor.store.readLooseRef(name) - if err == nil { - switch loose := loose.(type) { - case ref.Detached: - return directRefState{ - kind: directDetached, - name: name, - id: loose.ID, - isLoose: true, - isPacked: hasPacked, - }, nil - case ref.Symbolic: - return directRefState{ - kind: directSymbolic, - name: name, - target: loose.Target, - isLoose: true, - isPacked: hasPacked, - }, nil - default: - return directRefState{}, fmt.Errorf("refstore/files: unsupported reference type %T", loose) - } - } - - if !errors.Is(err, refstore.ErrReferenceNotFound) { - info, statErr := executor.store.rootFor(loc.root).Stat(loc.path) - if statErr != nil || !info.IsDir() { - return directRefState{}, err - } - } - - if hasPacked { - packed, packedErr := executor.store.readPackedRefs() - if packedErr != nil { - return directRefState{}, packedErr - } - - detached := packed.byName[name] - - return directRefState{ - kind: directDetached, - name: name, - id: detached.ID, - isPacked: true, - }, nil - } - - return directRefState{ - kind: directMissing, - name: name, - }, nil -} diff --git a/ref/store/files/update_direct_ref.go b/ref/store/files/update_direct_ref.go deleted file mode 100644 index 3b429be0..00000000 --- a/ref/store/files/update_direct_ref.go +++ /dev/null @@ -1,20 +0,0 @@ -package files - -import objectid "codeberg.org/lindenii/furgit/object/id" - -type directRefKind uint8 - -const ( - directMissing directRefKind = iota - directDetached - directSymbolic -) - -type directRefState struct { - kind directRefKind - name string - id objectid.ObjectID - target string - isLoose bool - isPacked bool -} diff --git a/ref/store/files/update_error.go b/ref/store/files/update_error.go deleted file mode 100644 index d8841d44..00000000 --- a/ref/store/files/update_error.go +++ /dev/null @@ -1,28 +0,0 @@ -package files - -import "fmt" - -type updateContextError struct { - name string - err error -} - -func (err *updateContextError) Error() string { - return fmt.Sprintf("refstore/files: update %q: %v", err.name, err.err) -} - -func (err *updateContextError) Unwrap() error { - if err == nil { - return nil - } - - return err.err -} - -func wrapUpdateError(name string, err error) error { - if err == nil || name == "" { - return err - } - - return &updateContextError{name: name, err: err} -} diff --git a/ref/store/files/update_executor.go b/ref/store/files/update_executor.go deleted file mode 100644 index 749f7061..00000000 --- a/ref/store/files/update_executor.go +++ /dev/null @@ -1,5 +0,0 @@ -package files - -type refUpdateExecutor struct { - store *Store -} diff --git a/ref/store/files/update_kind.go b/ref/store/files/update_kind.go deleted file mode 100644 index f04719db..00000000 --- a/ref/store/files/update_kind.go +++ /dev/null @@ -1,14 +0,0 @@ -package files - -type updateKind uint8 - -const ( - updateCreate updateKind = iota - updateReplace - updateDelete - updateVerify - updateCreateSymbolic - updateReplaceSymbolic - updateDeleteSymbolic - updateVerifySymbolic -) diff --git a/ref/store/files/update_lock.go b/ref/store/files/update_lock.go deleted file mode 100644 index 1ce9adbb..00000000 --- a/ref/store/files/update_lock.go +++ /dev/null @@ -1,25 +0,0 @@ -package files - -import ( - "os" - "path" -) - -func (executor *refUpdateExecutor) createUpdateLock(name refPath) error { - root := executor.store.rootFor(name.root) - dir := path.Dir(name.path) - - if dir != "." { - err := root.MkdirAll(dir, 0o755) - if err != nil { - return err - } - } - - file, err := root.OpenFile(name.path+".lock", os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644) - if err != nil { - return err - } - - return file.Close() -} diff --git a/ref/store/files/update_lock_packed.go b/ref/store/files/update_lock_packed.go deleted file mode 100644 index f74a4f5e..00000000 --- a/ref/store/files/update_lock_packed.go +++ /dev/null @@ -1,44 +0,0 @@ -package files - -import ( - "errors" - "os" - "time" -) - -func (executor *refUpdateExecutor) createPackedRefsLock(timeout time.Duration) error { - const ( - initialBackoffMs = 1 - backoffMaxMultiplier = 1000 - ) - - deadline := time.Now().Add(timeout) - multiplier := 1 - n := 1 - - for { - file, err := executor.store.commonRoot.OpenFile("packed-refs.lock", os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644) - if err == nil { - return file.Close() - } - - if !errors.Is(err, os.ErrExist) { - return err - } - - if timeout == 0 || (timeout > 0 && time.Now().After(deadline)) { - return err - } - - backoffMs := multiplier * initialBackoffMs - waitMs := (750 + executor.store.lockRand.Intn(500)) * backoffMs / 1000 - time.Sleep(time.Duration(waitMs) * time.Millisecond) - - multiplier += 2*n + 1 - if multiplier > backoffMaxMultiplier { - multiplier = backoffMaxMultiplier - } else { - n++ - } - } -} diff --git a/ref/store/files/update_operation_prepared.go b/ref/store/files/update_operation_prepared.go deleted file mode 100644 index c50fea4e..00000000 --- a/ref/store/files/update_operation_prepared.go +++ /dev/null @@ -1,6 +0,0 @@ -package files - -type preparedUpdate struct { - op queuedUpdate - target resolvedUpdateTarget -} diff --git a/ref/store/files/update_operation_queue.go b/ref/store/files/update_operation_queue.go deleted file mode 100644 index ef7ced2f..00000000 --- a/ref/store/files/update_operation_queue.go +++ /dev/null @@ -1,12 +0,0 @@ -package files - -import objectid "codeberg.org/lindenii/furgit/object/id" - -type queuedUpdate struct { - name string - kind updateKind - newID objectid.ObjectID - oldID objectid.ObjectID - newTarget string - oldTarget string -} diff --git a/ref/store/files/update_path.go b/ref/store/files/update_path.go deleted file mode 100644 index 2bd42535..00000000 --- a/ref/store/files/update_path.go +++ /dev/null @@ -1,28 +0,0 @@ -package files - -import ( - "fmt" - "strings" -) - -type refPath struct { - root rootKind - path string -} - -func updateTargetKey(name refPath) string { - return fmt.Sprintf("%d:%s", name.root, name.path) -} - -func refPathFromKey(key string) refPath { - rootValue, pathValue, ok := strings.Cut(key, ":") - if !ok || rootValue == "" { - return refPath{root: rootCommon, path: key} - } - - if rootValue == "0" { - return refPath{root: rootGit, path: pathValue} - } - - return refPath{root: rootCommon, path: pathValue} -} diff --git a/ref/store/files/update_prepare.go b/ref/store/files/update_prepare.go deleted file mode 100644 index 035c0bc2..00000000 --- a/ref/store/files/update_prepare.go +++ /dev/null @@ -1,48 +0,0 @@ -package files - -func (executor *refUpdateExecutor) prepareUpdates(ops []queuedUpdate) (prepared []preparedUpdate, err error) { - defer func() { - if err != nil { - _ = executor.cleanupPreparedUpdates(prepared) - } - }() - - prepared, err = executor.resolvePreparedUpdates(ops) - if err != nil { - return prepared, err - } - - deleted, written := collectPreparedWrites(prepared) - - existing, err := executor.collectVisibleNames() - if err != nil { - return prepared, err - } - - for _, name := range written { - err = verifyRefnameAvailable(name, existing, written, deleted) - if err != nil { - return prepared, err - } - } - - err = executor.prepareUpdateLocks(prepared) - if err != nil { - return prepared, err - } - - hasDeletes := len(deleted) > 0 - if hasDeletes { - err = executor.createPackedRefsLock(executor.store.packedRefsTimeout) - if err != nil { - return prepared, err - } - } - - err = executor.verifyPreparedUpdates(prepared) - if err != nil { - return prepared, err - } - - return prepared, nil -} diff --git a/ref/store/files/update_prepare_lock.go b/ref/store/files/update_prepare_lock.go deleted file mode 100644 index 67db9628..00000000 --- a/ref/store/files/update_prepare_lock.go +++ /dev/null @@ -1,29 +0,0 @@ -package files - -import "slices" - -func (executor *refUpdateExecutor) prepareUpdateLocks(prepared []preparedUpdate) error { - lockNames := make([]string, 0, len(prepared)) - for _, item := range prepared { - lockNames = append(lockNames, updateTargetKey(item.target.loc)) - } - - slices.Sort(lockNames) - - for _, lockKey := range lockNames { - lockPath := refPathFromKey(lockKey) - - err := executor.createUpdateLock(lockPath) - if err != nil { - for _, item := range prepared { - if updateTargetKey(item.target.loc) == lockKey { - return wrapUpdateError(item.op.name, err) - } - } - - return err - } - } - - return nil -} diff --git a/ref/store/files/update_prepare_resolve.go b/ref/store/files/update_prepare_resolve.go deleted file mode 100644 index 19d209b0..00000000 --- a/ref/store/files/update_prepare_resolve.go +++ /dev/null @@ -1,43 +0,0 @@ -package files - -import refstore "codeberg.org/lindenii/furgit/ref/store" - -func (executor *refUpdateExecutor) resolvePreparedUpdates(ops []queuedUpdate) ([]preparedUpdate, error) { - prepared := make([]preparedUpdate, 0, len(ops)) - targets := make(map[string]struct{}, len(ops)) - - for _, op := range ops { - target, err := executor.resolveQueuedUpdateTarget(op) - if err != nil { - return prepared, err - } - - targetKey := updateTargetKey(target.loc) - if _, exists := targets[targetKey]; exists { - return prepared, wrapUpdateError(op.name, &refstore.DuplicateUpdateError{}) - } - - targets[targetKey] = struct{}{} - - prepared = append(prepared, preparedUpdate{op: op, target: target}) - } - - return prepared, nil -} - -func collectPreparedWrites(prepared []preparedUpdate) (deleted map[string]struct{}, written []string) { - deleted = make(map[string]struct{}) - written = make([]string, 0, len(prepared)) - - for _, item := range prepared { - switch item.op.kind { - case updateDelete, updateDeleteSymbolic: - deleted[item.target.name] = struct{}{} - case updateCreate, updateReplace, updateCreateSymbolic, updateReplaceSymbolic: - written = append(written, item.target.name) - case updateVerify, updateVerifySymbolic: - } - } - - return deleted, written -} diff --git a/ref/store/files/update_prepare_verify.go b/ref/store/files/update_prepare_verify.go deleted file mode 100644 index dcd14945..00000000 --- a/ref/store/files/update_prepare_verify.go +++ /dev/null @@ -1,21 +0,0 @@ -package files - -func (executor *refUpdateExecutor) verifyPreparedUpdates(prepared []preparedUpdate) error { - for i := range prepared { - item := &prepared[i] - - refState, err := executor.directRead(item.target.name) - if err != nil { - return wrapUpdateError(item.op.name, err) - } - - item.target.ref = refState - - err = executor.verifyPreparedUpdateCurrent(*item) - if err != nil { - return err - } - } - - return nil -} diff --git a/ref/store/files/update_resolve_target.go b/ref/store/files/update_resolve_target.go deleted file mode 100644 index 7cfb9aa1..00000000 --- a/ref/store/files/update_resolve_target.go +++ /dev/null @@ -1,21 +0,0 @@ -package files - -import "fmt" - -func (executor *refUpdateExecutor) resolveQueuedUpdateTarget(op queuedUpdate) (resolvedUpdateTarget, error) { - switch op.kind { - case updateCreate: - return executor.resolveOrdinaryTarget(op.name, true) - case updateReplace, updateDelete, updateVerify: - return executor.resolveOrdinaryTarget(op.name, false) - case updateCreateSymbolic, updateReplaceSymbolic, updateDeleteSymbolic, updateVerifySymbolic: - refState, err := executor.directRead(op.name) - if err != nil { - return resolvedUpdateTarget{}, err - } - - return resolvedUpdateTarget{name: op.name, loc: executor.store.loosePath(op.name), ref: refState}, nil - default: - return resolvedUpdateTarget{}, fmt.Errorf("refstore/files: unsupported update operation %d", op.kind) - } -} diff --git a/ref/store/files/update_resolve_target_ordinary.go b/ref/store/files/update_resolve_target_ordinary.go deleted file mode 100644 index cf8e1978..00000000 --- a/ref/store/files/update_resolve_target_ordinary.go +++ /dev/null @@ -1,48 +0,0 @@ -package files - -import ( - "fmt" - "strings" - - refstore "codeberg.org/lindenii/furgit/ref/store" -) - -func (executor *refUpdateExecutor) resolveOrdinaryTarget(name string, allowMissing bool) (resolvedUpdateTarget, error) { - cur := name - seen := make(map[string]struct{}) - - for { - if _, ok := seen[cur]; ok { - return resolvedUpdateTarget{}, fmt.Errorf("refstore/files: symbolic reference cycle at %q", cur) - } - - seen[cur] = struct{}{} - - refState, err := executor.directRead(cur) - if err != nil { - return resolvedUpdateTarget{}, err - } - - switch refState.kind { - case directMissing: - if !allowMissing { - return resolvedUpdateTarget{}, wrapUpdateError(name, refstore.ErrReferenceNotFound) - } - - return resolvedUpdateTarget{name: cur, loc: executor.store.loosePath(cur), ref: refState}, nil - case directDetached: - return resolvedUpdateTarget{name: cur, loc: executor.store.loosePath(cur), ref: refState}, nil - case directSymbolic: - target := strings.TrimSpace(refState.target) - if target == "" { - return resolvedUpdateTarget{}, wrapUpdateError(name, &refstore.InvalidValueError{ - Err: fmt.Errorf("symbolic reference has empty target"), - }) - } - - cur = target - default: - return resolvedUpdateTarget{}, fmt.Errorf("refstore/files: unsupported direct reference state %d", refState.kind) - } - } -} diff --git a/ref/store/files/update_target_resolved.go b/ref/store/files/update_target_resolved.go deleted file mode 100644 index c29e5938..00000000 --- a/ref/store/files/update_target_resolved.go +++ /dev/null @@ -1,7 +0,0 @@ -package files - -type resolvedUpdateTarget struct { - name string - loc refPath - ref directRefState -} diff --git a/ref/store/files/update_validate.go b/ref/store/files/update_validate.go deleted file mode 100644 index ac3429aa..00000000 --- a/ref/store/files/update_validate.go +++ /dev/null @@ -1,66 +0,0 @@ -package files - -import ( - "fmt" - "strings" - - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/ref/name" - refstore "codeberg.org/lindenii/furgit/ref/store" -) - -func (executor *refUpdateExecutor) validateQueuedUpdate(op queuedUpdate) error { - if op.name == "" { - return wrapUpdateError(op.name, &refstore.InvalidNameError{Err: fmt.Errorf("empty reference name")}) - } - - switch op.kind { - case updateCreate, updateReplace: - err := refname.ValidateUpdateName(op.name, true) - if err != nil { - return wrapUpdateError(op.name, &refstore.InvalidNameError{Err: err}) - } - - if op.newID.Algorithm().Size() == 0 { - return wrapUpdateError(op.name, &refstore.InvalidValueError{Err: objectid.ErrInvalidAlgorithm}) - } - case updateDelete, updateVerify: - err := refname.ValidateUpdateName(op.name, false) - if err != nil { - return wrapUpdateError(op.name, &refstore.InvalidNameError{Err: err}) - } - - if op.oldID.Algorithm().Size() == 0 { - return wrapUpdateError(op.name, &refstore.InvalidValueError{Err: objectid.ErrInvalidAlgorithm}) - } - case updateCreateSymbolic, updateReplaceSymbolic: - err := refname.ValidateUpdateName(op.name, true) - if err != nil { - return wrapUpdateError(op.name, &refstore.InvalidNameError{Err: err}) - } - - if strings.TrimSpace(op.newTarget) == "" { - return wrapUpdateError(op.name, &refstore.InvalidValueError{Err: fmt.Errorf("empty symbolic target")}) - } - - err = refname.ValidateSymbolicTarget(op.name, strings.TrimSpace(op.newTarget)) - if err != nil { - return wrapUpdateError(op.name, &refstore.InvalidValueError{Err: err}) - } - case updateDeleteSymbolic, updateVerifySymbolic: - err := refname.ValidateUpdateName(op.name, false) - if err != nil { - return wrapUpdateError(op.name, &refstore.InvalidNameError{Err: err}) - } - default: - return fmt.Errorf("refstore/files: unsupported update operation %d", op.kind) - } - - if op.kind == updateReplaceSymbolic || op.kind == updateDeleteSymbolic || op.kind == updateVerifySymbolic { - if strings.TrimSpace(op.oldTarget) == "" { - return wrapUpdateError(op.name, &refstore.InvalidValueError{Err: fmt.Errorf("empty symbolic old target")}) - } - } - - return nil -} diff --git a/ref/store/files/update_verify_current.go b/ref/store/files/update_verify_current.go deleted file mode 100644 index 51ed1b42..00000000 --- a/ref/store/files/update_verify_current.go +++ /dev/null @@ -1,60 +0,0 @@ -package files - -import ( - "strings" - - refstore "codeberg.org/lindenii/furgit/ref/store" -) - -func (executor *refUpdateExecutor) verifyPreparedUpdateCurrent(item preparedUpdate) error { - switch item.op.kind { - case updateCreate: - if item.target.ref.kind != directMissing { - return wrapUpdateError(item.op.name, &refstore.CreateExistsError{}) - } - - return nil - case updateReplace, updateDelete, updateVerify: - if item.target.ref.kind == directMissing { - return wrapUpdateError(item.op.name, refstore.ErrReferenceNotFound) - } - - if item.target.ref.kind != directDetached { - return wrapUpdateError(item.op.name, &refstore.ExpectedDetachedError{}) - } - - if item.target.ref.id != item.op.oldID { - return wrapUpdateError(item.op.name, &refstore.IncorrectOldValueError{ - Actual: item.target.ref.id.String(), - Expected: item.op.oldID.String(), - }) - } - - return nil - case updateCreateSymbolic: - if item.target.ref.kind != directMissing { - return wrapUpdateError(item.op.name, &refstore.CreateExistsError{}) - } - - return nil - case updateReplaceSymbolic, updateDeleteSymbolic, updateVerifySymbolic: - if item.target.ref.kind == directMissing { - return wrapUpdateError(item.op.name, refstore.ErrReferenceNotFound) - } - - if item.target.ref.kind != directSymbolic { - return wrapUpdateError(item.op.name, &refstore.ExpectedSymbolicError{}) - } - - if strings.TrimSpace(item.target.ref.target) != strings.TrimSpace(item.op.oldTarget) { - return wrapUpdateError(item.op.name, &refstore.IncorrectOldValueError{ - Actual: strings.TrimSpace(item.target.ref.target), - Expected: strings.TrimSpace(item.op.oldTarget), - }) - } - - return nil - } - - return nil -} diff --git a/ref/store/files/update_verify_refnames.go b/ref/store/files/update_verify_refnames.go deleted file mode 100644 index 8bc34a62..00000000 --- a/ref/store/files/update_verify_refnames.go +++ /dev/null @@ -1,41 +0,0 @@ -package files - -import ( - "strings" - - refstore "codeberg.org/lindenii/furgit/ref/store" -) - -func verifyRefnameAvailable(name string, existing map[string]struct{}, writes []string, deleted map[string]struct{}) error { - for existingName := range existing { - if existingName == name { - continue - } - - if _, skip := deleted[existingName]; skip { - continue - } - - if refnamesConflict(name, existingName) { - return wrapUpdateError(name, &refstore.NameConflictError{Other: existingName}) - } - } - - for _, other := range writes { - if other == name { - continue - } - - if refnamesConflict(name, other) { - return wrapUpdateError(name, &refstore.NameConflictError{Other: other}) - } - } - - return nil -} - -func refnamesConflict(left, right string) bool { - return left == right || - strings.HasPrefix(left, right+"/") || - strings.HasPrefix(right, left+"/") -} diff --git a/ref/store/files/update_visible_names.go b/ref/store/files/update_visible_names.go deleted file mode 100644 index f5792f93..00000000 --- a/ref/store/files/update_visible_names.go +++ /dev/null @@ -1,29 +0,0 @@ -package files - -func (executor *refUpdateExecutor) collectVisibleNames() (map[string]struct{}, error) { - names := make(map[string]struct{}) - - looseNames, err := executor.store.collectLooseRefNames() - if err != nil { - return nil, err - } - - for _, name := range looseNames { - names[name] = struct{}{} - } - - packed, err := executor.store.readPackedRefs() - if err != nil { - return nil, err - } - - for name := range packed.byName { - if _, exists := names[name]; exists { - continue - } - - names[name] = struct{}{} - } - - return names, nil -} diff --git a/ref/store/files/update_write_loose.go b/ref/store/files/update_write_loose.go deleted file mode 100644 index 212be9a8..00000000 --- a/ref/store/files/update_write_loose.go +++ /dev/null @@ -1,59 +0,0 @@ -package files - -import ( - "fmt" - "os" - "path" - "strings" -) - -func (executor *refUpdateExecutor) writePreparedLooseUpdate(item preparedUpdate) error { - root := executor.store.rootFor(item.target.loc.root) - lockName := item.target.loc.path + ".lock" - - lock, err := root.OpenFile(lockName, os.O_WRONLY|os.O_TRUNC, 0o644) - if err != nil { - return err - } - - var content string - - switch item.op.kind { - case updateCreate, updateReplace: - content = item.op.newID.String() + "\n" - case updateCreateSymbolic, updateReplaceSymbolic: - content = "ref: " + strings.TrimSpace(item.op.newTarget) + "\n" - case updateDelete, updateVerify, updateDeleteSymbolic, updateVerifySymbolic: - default: - _ = lock.Close() - - return fmt.Errorf("refstore/files: unsupported write operation %d", item.op.kind) - } - - _, err = lock.WriteString(content) - if err != nil { - _ = lock.Close() - - return err - } - - err = lock.Close() - if err != nil { - return err - } - - dir := path.Dir(item.target.loc.path) - if dir != "." { - err = root.MkdirAll(dir, 0o755) - if err != nil { - return err - } - } - - err = executor.removeEmptyDirTree(item.target.loc) - if err != nil { - return err - } - - return root.Rename(lockName, item.target.loc.path) -} diff --git a/ref/store/files/update_write_packed_refs.go b/ref/store/files/update_write_packed_refs.go deleted file mode 100644 index c7eea780..00000000 --- a/ref/store/files/update_write_packed_refs.go +++ /dev/null @@ -1,98 +0,0 @@ -package files - -import ( - "errors" - "os" -) - -func (executor *refUpdateExecutor) applyPackedRefDeletes(prepared []preparedUpdate) error { - _, err := executor.store.commonRoot.Stat("packed-refs.lock") - if err != nil { - if errors.Is(err, os.ErrNotExist) { - return nil - } - - return err - } - - packed, err := executor.store.readPackedRefs() - if err != nil { - return err - } - - deleted := make(map[string]struct{}) - needed := false - - for _, item := range prepared { - if item.op.kind != updateDelete && item.op.kind != updateDeleteSymbolic { - continue - } - - deleted[item.target.name] = struct{}{} - if item.target.ref.isPacked { - needed = true - } - } - - if !needed { - return nil - } - - lock, err := executor.store.commonRoot.OpenFile("packed-refs.new", os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644) - if err != nil { - return err - } - - createdTemp := true - - defer func() { - if !createdTemp { - return - } - - _ = executor.store.commonRoot.Remove("packed-refs.new") - }() - - _, err = lock.WriteString("# pack-refs with: peeled fully-peeled sorted\n") - if err != nil { - _ = lock.Close() - - return err - } - - for _, entry := range packed.ordered { - if _, skip := deleted[entry.Name()]; skip { - continue - } - - _, err = lock.WriteString(entry.ID.String() + " " + entry.Name() + "\n") - if err != nil { - _ = lock.Close() - - return err - } - - if entry.Peeled != nil { - _, err = lock.WriteString("^" + entry.Peeled.String() + "\n") - if err != nil { - _ = lock.Close() - - return err - } - } - } - - err = lock.Close() - if err != nil { - return err - } - - err = executor.store.commonRoot.Rename("packed-refs.new", "packed-refs") - if err != nil { - return err - } - - createdTemp = false - - return nil -} diff --git a/ref/store/files/worktree_test.go b/ref/store/files/worktree_test.go deleted file mode 100644 index ae3dc299..00000000 --- a/ref/store/files/worktree_test.go +++ /dev/null @@ -1,206 +0,0 @@ -package files_test - -import ( - "errors" - "slices" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - objectid "codeberg.org/lindenii/furgit/object/id" - refstore "codeberg.org/lindenii/furgit/ref/store" -) - -func TestFilesWorktreeRefsMatchGit(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, RefFormat: "files"}) - - testRepo.Run(t, "commit", "--allow-empty", "-m", "initial") - - initialID, err := objectid.ParseHex(algo, testRepo.Run(t, "rev-parse", "HEAD")) - if err != nil { - t.Fatalf("ParseHex(initial HEAD): %v", err) - } - - testRepo.Run(t, "branch", "wt1", initialID.String()) - testRepo.Run(t, "branch", "wt2", initialID.String()) - testRepo.Run(t, "worktree", "add", "wt1", "wt1") - testRepo.Run(t, "worktree", "add", "wt2", "wt2") - - testRepo.Run(t, "-C", "wt1", "commit", "--allow-empty", "-m", "wt1") - testRepo.Run(t, "-C", "wt2", "commit", "--allow-empty", "-m", "wt2") - - wt1ID, err := objectid.ParseHex(algo, testRepo.Run(t, "-C", "wt1", "rev-parse", "HEAD")) - if err != nil { - t.Fatalf("ParseHex(wt1 HEAD): %v", err) - } - - wt2ID, err := objectid.ParseHex(algo, testRepo.Run(t, "-C", "wt2", "rev-parse", "HEAD")) - if err != nil { - t.Fatalf("ParseHex(wt2 HEAD): %v", err) - } - - testRepo.UpdateRef(t, "refs/worktree/foo", initialID) - testRepo.Run(t, "-C", "wt1", "update-ref", "refs/worktree/foo", wt1ID.String()) - testRepo.Run(t, "-C", "wt2", "update-ref", "refs/worktree/foo", wt2ID.String()) - - mainStore := openFilesStore(t, testRepo, algo) - repoRoot := testRepo.OpenRoot(t) - wt1Store := openFilesStoreAt(t, openGitRootUnder(t, repoRoot, "wt1"), algo) - wt2Store := openFilesStoreAt(t, openGitRootUnder(t, repoRoot, "wt2"), algo) - - got, err := mainStore.ResolveToDetached("refs/worktree/foo") - if err != nil { - t.Fatalf("ResolveToDetached(main refs/worktree/foo): %v", err) - } - - if got.ID != initialID { - t.Fatalf("ResolveToDetached(main refs/worktree/foo) = %s, want %s", got.ID, initialID) - } - - got, err = wt1Store.ResolveToDetached("refs/worktree/foo") - if err != nil { - t.Fatalf("ResolveToDetached(wt1 refs/worktree/foo): %v", err) - } - - if got.ID != wt1ID { - t.Fatalf("ResolveToDetached(wt1 refs/worktree/foo) = %s, want %s", got.ID, wt1ID) - } - - got, err = wt2Store.ResolveToDetached("refs/worktree/foo") - if err != nil { - t.Fatalf("ResolveToDetached(wt2 refs/worktree/foo): %v", err) - } - - if got.ID != wt2ID { - t.Fatalf("ResolveToDetached(wt2 refs/worktree/foo) = %s, want %s", got.ID, wt2ID) - } - - got, err = wt1Store.ResolveToDetached("main-worktree/HEAD") - if err != nil { - t.Fatalf("ResolveToDetached(wt1 main-worktree/HEAD): %v", err) - } - - if got.ID != initialID { - t.Fatalf("ResolveToDetached(wt1 main-worktree/HEAD) = %s, want %s", got.ID, initialID) - } - - got, err = mainStore.ResolveToDetached("worktrees/wt1/HEAD") - if err != nil { - t.Fatalf("ResolveToDetached(main worktrees/wt1/HEAD): %v", err) - } - - if got.ID != wt1ID { - t.Fatalf("ResolveToDetached(main worktrees/wt1/HEAD) = %s, want %s", got.ID, wt1ID) - } - - got, err = wt2Store.ResolveToDetached("worktrees/wt1/HEAD") - if err != nil { - t.Fatalf("ResolveToDetached(wt2 worktrees/wt1/HEAD): %v", err) - } - - if got.ID != wt1ID { - t.Fatalf("ResolveToDetached(wt2 worktrees/wt1/HEAD) = %s, want %s", got.ID, wt1ID) - } - - assertListMatchesGitForEachRef(t, testRepo.Run(t, "for-each-ref", "--format=%(refname)"), mainStore) - assertListMatchesGitForEachRef(t, testRepo.Run(t, "-C", "wt1", "for-each-ref", "--format=%(refname)"), wt1Store) - }) -} - -func TestFilesTransactionPerWorktreeRefsMatchGit(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, RefFormat: "files"}) - testRepo.Run(t, "commit", "--allow-empty", "-m", "initial") - testRepo.Run(t, "branch", "wt1", "HEAD") - testRepo.Run(t, "worktree", "add", "wt1", "wt1") - - mainID, err := objectid.ParseHex(algo, testRepo.Run(t, "rev-parse", "HEAD")) - if err != nil { - t.Fatalf("ParseHex(main HEAD): %v", err) - } - - testRepo.Run(t, "-C", "wt1", "commit", "--allow-empty", "-m", "wt1") - - wt1ID, err := objectid.ParseHex(algo, testRepo.Run(t, "-C", "wt1", "rev-parse", "HEAD")) - if err != nil { - t.Fatalf("ParseHex(wt1 HEAD): %v", err) - } - - mainStore := openFilesStore(t, testRepo, algo) - repoRoot := testRepo.OpenRoot(t) - wt1Store := openFilesStoreAt(t, openGitRootUnder(t, repoRoot, "wt1"), algo) - - mainTx, err := mainStore.BeginTransaction() - if err != nil { - t.Fatalf("BeginTransaction(main): %v", err) - } - - err = mainTx.Create("refs/bisect/main-only", mainID) - if err != nil { - t.Fatalf("Create(main-only) queue: %v", err) - } - - err = mainTx.Commit() - if err != nil { - t.Fatalf("Commit(main-only): %v", err) - } - - wtTx, err := wt1Store.BeginTransaction() - if err != nil { - t.Fatalf("BeginTransaction(wt1): %v", err) - } - - err = wtTx.Create("refs/bisect/wt-only", wt1ID) - if err != nil { - t.Fatalf("Create(wt-only) queue: %v", err) - } - - err = wtTx.Commit() - if err != nil { - t.Fatalf("Commit(wt-only): %v", err) - } - - got, err := mainStore.ResolveToDetached("refs/bisect/main-only") - if err != nil { - t.Fatalf("ResolveToDetached(main-only): %v", err) - } - - if got.ID != mainID { - t.Fatalf("ResolveToDetached(main-only) = %s, want %s", got.ID, mainID) - } - - got, err = wt1Store.ResolveToDetached("refs/bisect/wt-only") - if err != nil { - t.Fatalf("ResolveToDetached(wt-only): %v", err) - } - - if got.ID != wt1ID { - t.Fatalf("ResolveToDetached(wt-only) = %s, want %s", got.ID, wt1ID) - } - - _, err = mainStore.Resolve("refs/bisect/wt-only") - if !errors.Is(err, refstore.ErrReferenceNotFound) { - t.Fatalf("Resolve(main sees wt-only) error = %v, want ErrReferenceNotFound", err) - } - - _, err = wt1Store.Resolve("refs/bisect/main-only") - if !errors.Is(err, refstore.ErrReferenceNotFound) { - t.Fatalf("Resolve(wt sees main-only) error = %v, want ErrReferenceNotFound", err) - } - - mainRefs := forEachRefLines(testRepo.Run(t, "for-each-ref", "--format=%(refname)", "refs/bisect")) - - wtRefs := forEachRefLines(testRepo.Run(t, "-C", "wt1", "for-each-ref", "--format=%(refname)", "refs/bisect")) - if !slices.Equal(mainRefs, []string{"refs/bisect/main-only"}) { - t.Fatalf("main for-each-ref refs/bisect = %v", mainRefs) - } - - if !slices.Equal(wtRefs, []string{"refs/bisect/wt-only"}) { - t.Fatalf("wt1 for-each-ref refs/bisect = %v", wtRefs) - } - }) -} diff --git a/ref/store/reading.go b/ref/store/reading.go deleted file mode 100644 index a9e2ad49..00000000 --- a/ref/store/reading.go +++ /dev/null @@ -1,34 +0,0 @@ -package refstore - -import "codeberg.org/lindenii/furgit/ref" - -// Reader reads Git references. -// -// Labels: MT-Safe. -type Reader interface { - // Resolve resolves a reference name to either a symbolic or detached ref. - // - // Implementations should return value forms ([ref.Detached] or [ref.Symbolic]), - // not pointer forms. If the reference does not exist, implementations should - // return [ErrReferenceNotFound]. - // - // Labels: Life-Parent. - Resolve(name string) (ref.Ref, error) - // ResolveToDetached resolves a reference name to a detached object ID. - // - // Implementations may use backend-local lookup semantics for symbolic hops. - // Callers that need cross-backend symbolic resolution (for example in a - // chain of stores) should prefer repeatedly calling Resolve. - // - // ResolveToDetached resolves symbolic references only. It does not imply peeling - // annotated tag objects. - // - // Labels: Life-Parent. - ResolveToDetached(name string) (ref.Detached, error) - // List returns references matching pattern. - // - // The exact pattern language is backend-defined. - // - // Labels: Life-Parent. - List(pattern string) ([]ref.Ref, error) -} diff --git a/ref/store/transaction.go b/ref/store/transaction.go deleted file mode 100644 index 30f6ab50..00000000 --- a/ref/store/transaction.go +++ /dev/null @@ -1,52 +0,0 @@ -package refstore - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// Transaction stages reference updates for one atomic commit. -// -// A transaction borrows its underlying store and is invalid after that store -// is closed. -// -// Ordinary methods operate in dereference mode if name resolves to -// a symbolic ref, the operation applies to the final referent rather -// than to the symbolic ref itself. -// -// Symbolic methods operate on the named reference directly, without -// dereferencing symbolic refs. -// -// Labels: MT-Unsafe. -type Transaction interface { - // Create creates one detached reference, requiring that the logical - // reference does not already exist. - Create(name string, newID objectid.ObjectID) error - // Update updates one detached reference, requiring that the current logical - // reference value matches oldID. - Update(name string, newID, oldID objectid.ObjectID) error - // Delete deletes one detached reference, requiring that the current logical - // reference value matches oldID. - Delete(name string, oldID objectid.ObjectID) error - // Verify verifies that the current logical reference value matches oldID. - Verify(name string, oldID objectid.ObjectID) error - - // CreateSymbolic creates one symbolic reference, requiring that the named - // reference does not already exist. - CreateSymbolic(name, newTarget string) error - // UpdateSymbolic updates one symbolic reference directly, requiring that its - // current target matches oldTarget. - UpdateSymbolic(name, newTarget, oldTarget string) error - // DeleteSymbolic deletes one symbolic reference directly, requiring that its - // current target matches oldTarget. - DeleteSymbolic(name, oldTarget string) error - // VerifySymbolic verifies that the named symbolic reference currently points - // at oldTarget. - VerifySymbolic(name, oldTarget string) error - - // Commit validates and applies all queued operations atomically. - // - // Commit invalidates the receiver. - Commit() error - // Abort abandons the transaction and releases any resources it holds. - // - // Abort invalidates the receiver. - Abort() error -} diff --git a/ref/store/transactional_store.go b/ref/store/transactional_store.go deleted file mode 100644 index 10f4543a..00000000 --- a/ref/store/transactional_store.go +++ /dev/null @@ -1,13 +0,0 @@ -package refstore - -// Transactioner begins atomic reference transactions. -// -// Not every readable reference store is writable. Implementations should only -// satisfy Transactioner when they can stage and commit reference updates -// atomically within that backend. -type Transactioner interface { - // BeginTransaction creates one new mutable transaction. - // - // Labels: Life-Parent. - BeginTransaction() (Transaction, error) -} diff --git a/ref/store/update_errors.go b/ref/store/update_errors.go deleted file mode 100644 index f05f37d2..00000000 --- a/ref/store/update_errors.go +++ /dev/null @@ -1,110 +0,0 @@ -package refstore - -import "fmt" - -// InvalidNameError indicates that one requested reference name is invalid. -type InvalidNameError struct { - Err error -} - -func (err *InvalidNameError) Error() string { - if err == nil || err.Err == nil { - return "invalid reference name" - } - - return fmt.Sprintf("invalid reference name: %v", err.Err) -} - -func (err *InvalidNameError) Unwrap() error { - if err == nil { - return nil - } - - return err.Err -} - -// InvalidValueError indicates that one requested reference value is invalid. -type InvalidValueError struct { - Err error -} - -func (err *InvalidValueError) Error() string { - if err == nil || err.Err == nil { - return "invalid reference value" - } - - return fmt.Sprintf("invalid reference value: %v", err.Err) -} - -func (err *InvalidValueError) Unwrap() error { - if err == nil { - return nil - } - - return err.Err -} - -// DuplicateUpdateError indicates that one batch or transaction includes a -// duplicate update target. -type DuplicateUpdateError struct{} - -func (err *DuplicateUpdateError) Error() string { - return "duplicate reference update" -} - -// CreateExistsError indicates that one create operation targeted an existing -// reference. -type CreateExistsError struct{} - -func (err *CreateExistsError) Error() string { - return "reference already exists" -} - -// IncorrectOldValueError indicates that one operation's expected old value did -// not match the current reference value. -type IncorrectOldValueError struct { - Actual string - Expected string -} - -func (err *IncorrectOldValueError) Error() string { - if err == nil { - return "incorrect old value provided" - } - - if err.Actual == "" && err.Expected == "" { - return "incorrect old value provided" - } - - return fmt.Sprintf("incorrect old value provided: got %q, expected %q", err.Actual, err.Expected) -} - -// ExpectedDetachedError indicates that one operation required a detached -// reference but found a different kind. -type ExpectedDetachedError struct{} - -func (err *ExpectedDetachedError) Error() string { - return "expected detached reference" -} - -// ExpectedSymbolicError indicates that one operation required a symbolic -// reference but found a different kind. -type ExpectedSymbolicError struct{} - -func (err *ExpectedSymbolicError) Error() string { - return "expected symbolic reference" -} - -// NameConflictError indicates that one reference name conflicts with another -// visible or queued reference name. -type NameConflictError struct { - Other string -} - -func (err *NameConflictError) Error() string { - if err == nil || err.Other == "" { - return "reference name conflict" - } - - return fmt.Sprintf("reference name conflict with %q", err.Other) -} diff --git a/ref/symbolic.go b/ref/symbolic.go deleted file mode 100644 index af9f9e84..00000000 --- a/ref/symbolic.go +++ /dev/null @@ -1,14 +0,0 @@ -package ref - -// Symbolic points to another reference name. -type Symbolic struct { - RefName string - Target string -} - -// Name returns the fully-qualified reference name. -func (ref Symbolic) Name() string { - return ref.RefName -} - -func (Symbolic) isRef() {} diff --git a/repository/algorithm.go b/repository/algorithm.go deleted file mode 100644 index 1098f0b8..00000000 --- a/repository/algorithm.go +++ /dev/null @@ -1,29 +0,0 @@ -package repository - -import ( - "fmt" - - "codeberg.org/lindenii/furgit/config" - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// detectObjectAlgorithm uses a repository's configuration to detect -// the expected Object ID hashing algorithm. -func detectObjectAlgorithm(cfg *config.Config) (objectid.Algorithm, error) { - algoName := cfg.Lookup("extensions", "", "objectformat").Value - if algoName == "" { - algoName = objectid.AlgorithmSHA1.String() - } - - algo, ok := objectid.ParseAlgorithm(algoName) - if !ok { - return objectid.AlgorithmUnknown, fmt.Errorf("repository: unsupported object format %q", algoName) - } - - return algo, nil -} - -// Algorithm returns the repository object ID algorithm. -func (repo *Repository) Algorithm() objectid.Algorithm { - return repo.algo -} diff --git a/repository/close.go b/repository/close.go deleted file mode 100644 index c1261821..00000000 --- a/repository/close.go +++ /dev/null @@ -1,54 +0,0 @@ -package repository - -import "errors" - -// Close closes repository-owned stores and filesystem roots. -// -// Labels: MT-Unsafe. -func (repo *Repository) Close() error { - var errs []error - - if repo.commitGraph != nil { - err := repo.commitGraph.Close() - if err != nil { - errs = append(errs, err) - } - } - - if repo.objectsPacked != nil { - err := repo.objectsPacked.Close() - if err != nil { - errs = append(errs, err) - } - } - - if repo.objectsLoose != nil { - err := repo.objectsLoose.Close() - if err != nil { - errs = append(errs, err) - } - } - - if repo.objectsPackRoot != nil { - err := repo.objectsPackRoot.Close() - if err != nil { - errs = append(errs, err) - } - } - - if repo.objectsRoot != nil { - err := repo.objectsRoot.Close() - if err != nil { - errs = append(errs, err) - } - } - - if repo.refRoot != nil { - err := repo.refRoot.Close() - if err != nil { - errs = append(errs, err) - } - } - - return errors.Join(errs...) -} diff --git a/repository/commit_graph.go b/repository/commit_graph.go deleted file mode 100644 index 4f210b56..00000000 --- a/repository/commit_graph.go +++ /dev/null @@ -1,42 +0,0 @@ -package repository - -import ( - "errors" - "os" - - commitgraphread "codeberg.org/lindenii/furgit/format/commitgraph/read" - objectid "codeberg.org/lindenii/furgit/object/id" -) - -func openCommitGraph(root *os.Root, algo objectid.Algorithm) (*commitgraphread.Reader, error) { - reader, err := commitgraphread.Open(root, algo, commitgraphread.OpenChain) - if err == nil { - return reader, nil - } - - var malformed *commitgraphread.MalformedError - if errors.As(err, &malformed) && - malformed.Path == "info/commit-graphs/commit-graph-chain" && - malformed.Reason == "missing commit-graph-chain" { - reader, err = commitgraphread.Open(root, algo, commitgraphread.OpenSingle) - if errors.Is(err, os.ErrNotExist) { - return nil, nil //nolint:nilnil - } - - return reader, err - } - - return nil, err -} - -// CommitGraph returns the configured commit-graph reader, if available. -// -// Not all repositories have a commit-graph, so CommitGraph may return nil. -// Most callers should prefer [Repository.CommitQueries] or -// [Repository.Reachability] unless they specifically need direct -// commit-graph access. -// -// Labels: Life-Parent, Close-No. -func (repo *Repository) CommitGraph() *commitgraphread.Reader { - return repo.commitGraph -} diff --git a/repository/commit_queries.go b/repository/commit_queries.go deleted file mode 100644 index bd004418..00000000 --- a/repository/commit_queries.go +++ /dev/null @@ -1,13 +0,0 @@ -package repository - -import "codeberg.org/lindenii/furgit/commitquery" - -// CommitQueries returns commit queries backed by the repository's object store -// and optional commit-graph. -// -// Use CommitQueries for ancestor checks and merge-base computation. -// -// Labels: Life-Parent. -func (repo *Repository) CommitQueries() *commitquery.Queries { - return repo.commitQueries -} diff --git a/repository/config.go b/repository/config.go deleted file mode 100644 index a6cf28e3..00000000 --- a/repository/config.go +++ /dev/null @@ -1,34 +0,0 @@ -package repository - -import ( - "fmt" - "os" - - "codeberg.org/lindenii/furgit/config" -) - -// parseRepositoryConfig loads the configuration of the repository through -// finding the config file in the repo root, and parses the config. -func parseRepositoryConfig(root *os.Root) (*config.Config, error) { - configFile, err := root.Open("config") - if err != nil { - return nil, fmt.Errorf("repository: open config: %w", err) - } - - defer func() { _ = configFile.Close() }() - - cfg, err := config.ParseConfig(configFile) - if err != nil { - return nil, fmt.Errorf("repository: parse config: %w", err) - } - - return cfg, nil -} - -// Config returns the parsed repository configuration snapshot. -// -// The returned pointer is owned by Repository. Callers should treat it as -// read-only. -func (repo *Repository) Config() *config.Config { - return repo.config -} diff --git a/repository/fetcher.go b/repository/fetcher.go deleted file mode 100644 index 9cb0133c..00000000 --- a/repository/fetcher.go +++ /dev/null @@ -1,14 +0,0 @@ -package repository - -import "codeberg.org/lindenii/furgit/object/fetch" - -// Fetcher returns an object fetcher backed by the repository's object store. -// -// Use Fetcher when you want typed commits, trees, blobs, or tags, when you -// need to peel through annotated tags, or when you want path-based access -// within trees. -// -// Labels: Life-Parent. -func (repo *Repository) Fetcher() *fetch.Fetcher { - return repo.fetcher -} diff --git a/repository/objects.go b/repository/objects.go deleted file mode 100644 index 2527363a..00000000 --- a/repository/objects.go +++ /dev/null @@ -1,100 +0,0 @@ -package repository - -import ( - "fmt" - "os" - - objectid "codeberg.org/lindenii/furgit/object/id" - objectstore "codeberg.org/lindenii/furgit/object/store" - objectdual "codeberg.org/lindenii/furgit/object/store/dual" - objectloose "codeberg.org/lindenii/furgit/object/store/loose" - objectpacked "codeberg.org/lindenii/furgit/object/store/packed" -) - -// openObjectStore opens the roots and object stores of both -// the loose and packed stores for a particular repo root and -// object ID hashing algorithm. -// -// Since real object store implementations do not take ownership of -// the roots given to them, and since composite object stores do not -// take ownership of the object stores that they consist of, all -// of them are returned and should be closed by the caller. -// -//nolint:ireturn -func openObjectStore( - root *os.Root, - algo objectid.Algorithm, -) ( - objects *objectdual.Dual, - objectsRoot *os.Root, - objectsPackRoot *os.Root, - objectsLoose *objectloose.Store, - objectsPacked *objectpacked.Store, - err error, -) { - objectsRoot, err = root.OpenRoot("objects") - if err != nil { - return nil, nil, nil, nil, nil, fmt.Errorf("repository: open objects: %w", err) - } - - objectsLoose, err = objectloose.New(objectsRoot, algo) - if err != nil { - _ = objectsRoot.Close() - - return nil, nil, nil, nil, nil, err - } - - err = objectsRoot.Mkdir("pack", 0o755) - if err != nil && !os.IsExist(err) { - _ = objectsLoose.Close() - _ = objectsRoot.Close() - - return nil, nil, nil, nil, nil, fmt.Errorf("repository: create objects/pack: %w", err) - } - - objectsPackRoot, err = objectsRoot.OpenRoot("pack") - if err != nil { - _ = objectsLoose.Close() - _ = objectsRoot.Close() - - return nil, nil, nil, nil, nil, fmt.Errorf("repository: open objects/pack: %w", err) - } - - objectsPacked, err = objectpacked.New( - objectsPackRoot, - algo, - objectpacked.Options{ - RefreshPolicy: objectpacked.RefreshPolicyNever, - WriteRev: true, - }, - ) - if err != nil { - _ = objectsPackRoot.Close() - _ = objectsLoose.Close() - _ = objectsRoot.Close() - - return nil, nil, nil, nil, nil, err - } - - objects = objectdual.New(objectsLoose, objectsPacked) - - return objects, objectsRoot, objectsPackRoot, objectsLoose, objectsPacked, nil -} - -// Objects returns the configured object store. -// -// Use Objects for direct object-ID lookups, object headers, sizes, raw object -// bytes, streamed object contents, object writes, pack ingestion, and -// coordinated quarantines. Callers who want typed object values should usually -// prefer [Repository.Fetcher]. -// -// Labels: Life-Parent. -// -//nolint:ireturn -func (repo *Repository) Objects() interface { - objectstore.Reader - objectstore.Writer - objectstore.Quarantiner -} { - return repo.objects -} diff --git a/repository/open.go b/repository/open.go deleted file mode 100644 index 04aaa00c..00000000 --- a/repository/open.go +++ /dev/null @@ -1,78 +0,0 @@ -package repository - -import ( - "fmt" - "os" - - "codeberg.org/lindenii/furgit/commitquery" - "codeberg.org/lindenii/furgit/object/fetch" - reffiles "codeberg.org/lindenii/furgit/ref/store/files" -) - -// Open opens a repository and wires its stores and helpers from the on-disk -// repository format. -// -// root must refer to the Git directory itself: -// a bare repository root or a non-bare ".git" directory. -// -// Labels: Deps-Borrowed. -func Open(root *os.Root) (repo *Repository, err error) { - repo = &Repository{} - - defer func() { - if err != nil { - _ = repo.Close() - } - }() - - cfg, err := parseRepositoryConfig(root) - if err != nil { - return nil, err - } - - repo.config = cfg - - algo, err := detectObjectAlgorithm(cfg) - if err != nil { - return nil, err - } - - repo.algo = algo - - objects, objectsRoot, objectsPackRoot, objectsLoose, objectsPacked, err := openObjectStore(root, algo) - if err != nil { - return nil, err - } - - repo.objects = objects - repo.fetcher = fetch.New(objects) - repo.objectsRoot = objectsRoot - repo.objectsPackRoot = objectsPackRoot - repo.objectsLoose = objectsLoose - repo.objectsPacked = objectsPacked - - commitGraph, err := openCommitGraph(objectsRoot, algo) - if err != nil { - return nil, err - } - - repo.commitGraph = commitGraph - repo.commitQueries = commitquery.New(repo.fetcher, commitGraph) - - refRoot, err := root.OpenRoot(".") - if err != nil { - return nil, fmt.Errorf("repository: open root for refs: %w", err) - } - - refs, err := reffiles.New(refRoot, algo, detectPackedRefsTimeout(cfg)) - if err != nil { - _ = refRoot.Close() - - return nil, err - } - - repo.refs = refs - repo.refRoot = refRoot - - return repo, nil -} diff --git a/repository/reachability.go b/repository/reachability.go deleted file mode 100644 index d9ac9faf..00000000 --- a/repository/reachability.go +++ /dev/null @@ -1,14 +0,0 @@ -package repository - -import "codeberg.org/lindenii/furgit/reachability" - -// Reachability returns graph traversal helpers backed by the repository's -// object store and optional commit-graph. -// -// Use Reachability to walk reachable commits or objects and to perform -// connectivity checks. -// -// Labels: Life-Parent. -func (repo *Repository) Reachability() *reachability.Reachability { - return reachability.New(repo.fetcher, repo.commitGraph) -} diff --git a/repository/refs.go b/repository/refs.go deleted file mode 100644 index d66ae752..00000000 --- a/repository/refs.go +++ /dev/null @@ -1,20 +0,0 @@ -package repository - -import refstore "codeberg.org/lindenii/furgit/ref/store" - -// Refs returns the configured ref store. -// -// Use Refs when starting from branch names, tags, HEAD, or other references. -// A common pattern is to resolve a reference first and then pass the resulting -// object ID to [Repository.Fetcher] or [Repository.Objects]. -// -// Labels: Life-Parent. -// -//nolint:ireturn -func (repo *Repository) Refs() interface { - refstore.Reader - refstore.Transactioner - refstore.Batcher -} { - return repo.refs -} diff --git a/repository/refs_test.go b/repository/refs_test.go deleted file mode 100644 index d01dda19..00000000 --- a/repository/refs_test.go +++ /dev/null @@ -1,113 +0,0 @@ -package repository_test - -import ( - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" - "codeberg.org/lindenii/furgit/ref" -) - -func TestOpenFilesRefFormat(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - repoHarness := testgit.NewRepo(t, testgit.RepoOptions{ - ObjectFormat: algo, - Bare: true, - RefFormat: "files", - }) - - _, _, commitID := repoHarness.MakeCommit(t, "files refs") - repoHarness.UpdateRef(t, "refs/heads/main", commitID) - repoHarness.SymbolicRef(t, "HEAD", "refs/heads/main") - - repo := repoHarness.OpenRepository(t) - - if repo.Algorithm() != algo { - t.Fatalf("Algorithm = %v, want %v", repo.Algorithm(), algo) - } - - headerType, headerSize, err := repo.Objects().ReadHeader(commitID) - if err != nil { - t.Fatalf("ReadHeader(commit): %v", err) - } - - if headerType != objecttype.TypeCommit { - t.Fatalf("ReadHeader(commit) type = %v, want %v", headerType, objecttype.TypeCommit) - } - - if headerSize <= 0 { - t.Fatalf("ReadHeader(commit) size = %d, want > 0", headerSize) - } - - resolved, err := repo.Refs().Resolve("refs/heads/main") - if err != nil { - t.Fatalf("Resolve(refs/heads/main): %v", err) - } - - detached, ok := resolved.(ref.Detached) - if !ok { - t.Fatalf("Resolve(refs/heads/main) type = %T, want ref.Detached", resolved) - } - - if detached.ID != commitID { - t.Fatalf("Resolve(refs/heads/main) id = %s, want %s", detached.ID, commitID) - } - - head, err := repo.Refs().ResolveToDetached("HEAD") - if err != nil { - t.Fatalf("ResolveToDetached(HEAD): %v", err) - } - - if head.ID != commitID { - t.Fatalf("ResolveToDetached(HEAD) id = %s, want %s", head.ID, commitID) - } - }) -} - -func TestOpenFilesWithPackedRefs(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - repoHarness := newRepoForRefs(t, algo, "files") - commitID := writeMainAndHead(t, repoHarness) - repoHarness.PackRefs(t, "--all", "--prune") - assertResolveToDetached(t, repoHarness, "refs/heads/main", commitID) - }) -} - -func newRepoForRefs(t *testing.T, algo objectid.Algorithm, refFormat string) *testgit.TestRepo { - t.Helper() - - return testgit.NewRepo(t, testgit.RepoOptions{ - ObjectFormat: algo, - Bare: true, - RefFormat: refFormat, - }) -} - -func writeMainAndHead(t *testing.T, repoHarness *testgit.TestRepo) objectid.ObjectID { - t.Helper() - _, _, commitID := repoHarness.MakeCommit(t, "refs") - repoHarness.UpdateRef(t, "refs/heads/main", commitID) - repoHarness.SymbolicRef(t, "HEAD", "refs/heads/main") - - return commitID -} - -func assertResolveToDetached(t *testing.T, repoHarness *testgit.TestRepo, name string, want objectid.ObjectID) { - t.Helper() - - repo := repoHarness.OpenRepository(t) - - resolved, err := repo.Refs().ResolveToDetached(name) - if err != nil { - t.Fatalf("ResolveToDetached(%s): %v", name, err) - } - - if resolved.ID != want { - t.Fatalf("ResolveToDetached(%s) id = %s, want %s", name, resolved.ID, want) - } -} diff --git a/repository/refs_timeout.go b/repository/refs_timeout.go deleted file mode 100644 index 275613ba..00000000 --- a/repository/refs_timeout.go +++ /dev/null @@ -1,16 +0,0 @@ -package repository - -import ( - "time" - - "codeberg.org/lindenii/furgit/config" -) - -func detectPackedRefsTimeout(cfg *config.Config) time.Duration { - timeoutValue, err := cfg.Lookup("core", "", "packedrefstimeout").Int() - if err != nil { - return time.Second - } - - return time.Duration(timeoutValue) * time.Millisecond -} diff --git a/repository/repository.go b/repository/repository.go deleted file mode 100644 index bfba5008..00000000 --- a/repository/repository.go +++ /dev/null @@ -1,49 +0,0 @@ -// Package repository opens a typical on-disk Git repository and exposes its -// main stores and helpers. -// -// Start with [Open] when working with a bare repository root or a non-bare -// ".git" directory. [Repository] then provides access to ref storage, object -// storage, typed object fetching, commit queries, reachability helpers, and -// optional commit-graph access. -package repository - -import ( - "os" - - "codeberg.org/lindenii/furgit/commitquery" - "codeberg.org/lindenii/furgit/config" - commitgraphread "codeberg.org/lindenii/furgit/format/commitgraph/read" - "codeberg.org/lindenii/furgit/object/fetch" - objectid "codeberg.org/lindenii/furgit/object/id" - objectdual "codeberg.org/lindenii/furgit/object/store/dual" - objectloose "codeberg.org/lindenii/furgit/object/store/loose" - objectpacked "codeberg.org/lindenii/furgit/object/store/packed" - refstore "codeberg.org/lindenii/furgit/ref/store" -) - -// Repository represents a typical on-disk Git repository by composing its -// stores and helpers together for access. -// -// Open expects a root for the Git directory itself: -// a bare repository root or a non-bare ".git" directory. -// -// Labels: MT-Safe, Close-Caller. -type Repository struct { - config *config.Config - algo objectid.Algorithm - - objects *objectdual.Dual - fetcher *fetch.Fetcher - objectsRoot *os.Root - objectsPackRoot *os.Root - objectsLoose *objectloose.Store - objectsPacked *objectpacked.Store - commitGraph *commitgraphread.Reader - commitQueries *commitquery.Queries - refRoot *os.Root - refs interface { - refstore.Reader - refstore.Transactioner - refstore.Batcher - } -} diff --git a/repository/stored_test.go b/repository/stored_test.go deleted file mode 100644 index 5604b418..00000000 --- a/repository/stored_test.go +++ /dev/null @@ -1,234 +0,0 @@ -package repository_test - -import ( - "fmt" - "strings" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/tree" -) - -func TestReadStoredTyped(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - repoHarness := testgit.NewRepo(t, testgit.RepoOptions{ - ObjectFormat: algo, - Bare: true, - RefFormat: "files", - }) - - blobID, treeID, commitID := repoHarness.MakeCommit(t, "stored types") - - repo := repoHarness.OpenRepository(t) - - blob, err := repo.Fetcher().ExactBlob(blobID) - if err != nil { - t.Fatalf("ExactBlob: %v", err) - } - - if blob.ID() != blobID { - t.Fatalf("blob ID = %s, want %s", blob.ID(), blobID) - } - - if string(blob.Object().Data) != "commit-body\n" { - t.Fatalf("blob body = %q, want %q", blob.Object().Data, "commit-body\n") - } - - tree, err := repo.Fetcher().ExactTree(treeID) - if err != nil { - t.Fatalf("ExactTree: %v", err) - } - - if tree.ID() != treeID { - t.Fatalf("tree ID = %s, want %s", tree.ID(), treeID) - } - - if len(tree.Object().Entries) != 1 { - t.Fatalf("tree entries = %d, want 1", len(tree.Object().Entries)) - } - - commit, err := repo.Fetcher().ExactCommit(commitID) - if err != nil { - t.Fatalf("ExactCommit: %v", err) - } - - if commit.ID() != commitID { - t.Fatalf("commit ID = %s, want %s", commit.ID(), commitID) - } - - if commit.Object().Tree != treeID { - t.Fatalf("commit tree = %s, want %s", commit.Object().Tree, treeID) - } - }) -} - -func TestResolverPath(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - repoHarness := testgit.NewRepo(t, testgit.RepoOptions{ - ObjectFormat: algo, - Bare: true, - RefFormat: "files", - }) - - blobID := repoHarness.HashObject(t, "blob", []byte("nested-file\n")) - childTreeID := repoHarness.Mktree(t, fmt.Sprintf("100644 blob %s\tleaf.txt\n", blobID)) - rootTreeID := repoHarness.Mktree(t, fmt.Sprintf("040000 tree %s\tdir\n", childTreeID)) - - repo := repoHarness.OpenRepository(t) - - entry, err := repo.Fetcher().Path(rootTreeID, [][]byte{[]byte("dir"), []byte("leaf.txt")}) - if err != nil { - t.Fatalf("Path: %v", err) - } - - if entry.Mode != tree.FileModeRegular { - t.Fatalf("Path mode = %o, want %o", entry.Mode, tree.FileModeRegular) - } - - if entry.ID != blobID { - t.Fatalf("Path id = %s, want %s", entry.ID, blobID) - } - }) -} - -func TestResolverPathErrors(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - t.Run("missing path component", func(t *testing.T) { - t.Parallel() - repoHarness := testgit.NewRepo(t, testgit.RepoOptions{ - ObjectFormat: algo, - Bare: true, - RefFormat: "files", - }) - blobID := repoHarness.HashObject(t, "blob", []byte("body\n")) - rootTreeID := repoHarness.Mktree(t, fmt.Sprintf("100644 blob %s\tfile.txt\n", blobID)) - - repo := repoHarness.OpenRepository(t) - - _, err := repo.Fetcher().Path(rootTreeID, [][]byte{[]byte("missing")}) - if err == nil || !strings.Contains(err.Error(), "not found") { - t.Fatalf("Path missing: err = %v, want not found error", err) - } - }) - - t.Run("non-tree intermediate", func(t *testing.T) { - t.Parallel() - repoHarness := testgit.NewRepo(t, testgit.RepoOptions{ - ObjectFormat: algo, - Bare: true, - RefFormat: "files", - }) - blobID := repoHarness.HashObject(t, "blob", []byte("body\n")) - rootTreeID := repoHarness.Mktree(t, fmt.Sprintf("100644 blob %s\tdir\n", blobID)) - - repo := repoHarness.OpenRepository(t) - - _, err := repo.Fetcher().Path(rootTreeID, [][]byte{[]byte("dir"), []byte("leaf")}) - if err == nil || !strings.Contains(err.Error(), "is not a tree") { - t.Fatalf("Path non-tree: err = %v, want non-tree error", err) - } - }) - }) -} - -func TestResolverPathDeepPath(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - const depth = 50 - - repoHarness := testgit.NewRepo(t, testgit.RepoOptions{ - ObjectFormat: algo, - Bare: true, - RefFormat: "files", - }) - - leafBlobID := repoHarness.HashObject(t, "blob", []byte("deep-content\n")) - currentTree := repoHarness.Mktree(t, fmt.Sprintf("100644 blob %s\tleaf.txt\n", leafBlobID)) - - parts := make([][]byte, 0, depth+1) - for i := depth - 1; i >= 0; i-- { - name := fmt.Sprintf("level%02d", i) - currentTree = repoHarness.Mktree(t, fmt.Sprintf("040000 tree %s\t%s\n", currentTree, name)) - parts = append([][]byte{[]byte(name)}, parts...) - } - - parts = append(parts, []byte("leaf.txt")) - - repo := repoHarness.OpenRepository(t) - - entry, err := repo.Fetcher().Path(currentTree, parts) - if err != nil { - t.Fatalf("Path(deep): %v", err) - } - - if entry.Mode != tree.FileModeRegular { - t.Fatalf("Path(deep) mode = %o, want %o", entry.Mode, tree.FileModeRegular) - } - - if entry.ID != leafBlobID { - t.Fatalf("Path(deep) id = %s, want %s", entry.ID, leafBlobID) - } - }) -} - -func TestReadStoredTreeMixedModes(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - repoHarness := testgit.NewRepo(t, testgit.RepoOptions{ - ObjectFormat: algo, - Bare: true, - RefFormat: "files", - }) - - normalID := repoHarness.HashObject(t, "blob", []byte("normal-file\n")) - execID := repoHarness.HashObject(t, "blob", []byte("#!/bin/sh\necho hi\n")) - symID := repoHarness.HashObject(t, "blob", []byte("normal.txt")) - nestedBlobID := repoHarness.HashObject(t, "blob", []byte("nested\n")) - nestedTreeID := repoHarness.Mktree(t, fmt.Sprintf("100644 blob %s\tleaf.txt\n", nestedBlobID)) - - rootTreeID := repoHarness.Mktree(t, - fmt.Sprintf( - "100644 blob %s\tnormal.txt\n100755 blob %s\trun.sh\n120000 blob %s\tlink.txt\n040000 tree %s\tdir\n", - normalID, - execID, - symID, - nestedTreeID, - ), - ) - - repo := repoHarness.OpenRepository(t) - - rootTree, err := repo.Fetcher().ExactTree(rootTreeID) - if err != nil { - t.Fatalf("ExactTree(root): %v", err) - } - - expect := map[string]tree.FileMode{ - "normal.txt": tree.FileModeRegular, - "run.sh": tree.FileModeExecutable, - "link.txt": tree.FileModeSymlink, - "dir": tree.FileModeDir, - } - - for name, wantMode := range expect { - entry := rootTree.Object().Entry([]byte(name)) - - if entry == nil { - t.Fatalf("Entry(%q) returned nil", name) - } - - if entry.Mode != wantMode { - t.Fatalf("Entry(%q) mode = %o, want %o", name, entry.Mode, wantMode) - } - } - }) -} diff --git a/repository/traversal_test.go b/repository/traversal_test.go deleted file mode 100644 index 99a8faaf..00000000 --- a/repository/traversal_test.go +++ /dev/null @@ -1,210 +0,0 @@ -package repository_test - -import ( - "fmt" - "os" - "path/filepath" - "strings" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - "codeberg.org/lindenii/furgit/object/blob" - "codeberg.org/lindenii/furgit/object/commit" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/tag" - "codeberg.org/lindenii/furgit/object/tree" - "codeberg.org/lindenii/furgit/repository" -) - -func TestRepositoryDepthFirstEnumerationFromHEAD(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - repoHarness := testgit.NewRepo(t, testgit.RepoOptions{ - ObjectFormat: algo, - Bare: true, - RefFormat: "files", - }) - - _, _, commit1 := repoHarness.MakeCommit(t, "walk-one") - blob2, tree2 := repoHarness.MakeSingleFileTree(t, "second.txt", []byte("second\n")) - commit2 := repoHarness.CommitTree(t, tree2, "walk-two", commit1) - _ = blob2 - - repoHarness.UpdateRef(t, "refs/heads/main", commit2) - repoHarness.SymbolicRef(t, "HEAD", "refs/heads/main") - - root := repoHarness.OpenGitRoot(t) - walkRepositoryFromRoot(t, root, "test repo") - }) -} - -func TestRepositoryDepthFirstEnumerationCurrentWorktree(t *testing.T) { - t.Parallel() - - worktreeRoot := filepath.Clean("..") - - worktreeFS, err := os.OpenRoot(worktreeRoot) - if err != nil { - t.Fatalf("os.OpenRoot(%q): %v", worktreeRoot, err) - } - - defer func() { _ = worktreeFS.Close() }() - - info, err := worktreeFS.Stat(".git") - if err != nil { - t.Fatalf("stat %q: %v", filepath.Join(worktreeRoot, ".git"), err) - } - - if info.IsDir() { - gitRoot, err := worktreeFS.OpenRoot(".git") - if err != nil { - t.Fatalf("OpenRoot(.git): %v", err) - } - - defer func() { _ = gitRoot.Close() }() - - walkRepositoryFromRoot(t, gitRoot, filepath.Join(worktreeRoot, ".git")) - - return - } - - if !info.Mode().IsRegular() { - t.Fatalf("%q is neither a directory nor a regular file", filepath.Join(worktreeRoot, ".git")) - } - - content, err := worktreeFS.ReadFile(".git") - if err != nil { - t.Fatalf("read %q: %v", filepath.Join(worktreeRoot, ".git"), err) - } - - line := strings.TrimSpace(string(content)) - - prefix := "gitdir: " - if !strings.HasPrefix(line, prefix) { - t.Fatalf("%q file does not begin with %q", filepath.Join(worktreeRoot, ".git"), prefix) - } - - gitdirRel := strings.TrimSpace(line[len(prefix):]) - if gitdirRel == "" { - t.Fatalf("%q contains empty gitdir path", filepath.Join(worktreeRoot, ".git")) - } - - gitdirPath := gitdirRel - if !filepath.IsAbs(gitdirPath) { - gitdirPath = filepath.Join(worktreeRoot, gitdirPath) - } - - gitRoot, err := os.OpenRoot(gitdirPath) - if err != nil { - t.Fatalf("os.OpenRoot(%q): %v", gitdirPath, err) - } - - defer func() { _ = gitRoot.Close() }() - - commondirContent, err := gitRoot.ReadFile("commondir") - if err != nil { - t.Fatalf("read %q: %v", filepath.Join(gitdirPath, "commondir"), err) - } - - repoPath := strings.TrimSpace(string(commondirContent)) - if repoPath == "" { - t.Fatalf("%q contains empty repo path", filepath.Join(gitdirPath, "commondir")) - } - - if filepath.IsAbs(repoPath) { - repoRoot, err := os.OpenRoot(repoPath) - if err != nil { - t.Fatalf("os.OpenRoot(%q): %v", repoPath, err) - } - - defer func() { _ = repoRoot.Close() }() - - walkRepositoryFromRoot(t, repoRoot, repoPath) - - return - } - - repoPath = filepath.Join(gitdirPath, repoPath) - - repoRoot, err := os.OpenRoot(repoPath) - if err != nil { - t.Fatalf("os.OpenRoot(%q): %v", repoPath, err) - } - - defer func() { _ = repoRoot.Close() }() - - walkRepositoryFromRoot(t, repoRoot, repoPath) -} - -func walkRepositoryFromRoot(t *testing.T, root *os.Root, label string) { - t.Helper() - - repo, err := repository.Open(root) - if err != nil { - t.Fatalf("repository.Open(root for %q): %v", label, err) - } - - defer func() { _ = repo.Close() }() - - head, err := repo.Refs().ResolveToDetached("HEAD") - if err != nil { - t.Fatalf("ResolveRefFully(HEAD): %v", err) - } - - objectsRead, err := traverseReachableIter(repo, head.ID) - if err != nil { - t.Fatalf("traverseReachableIter(%s): %v", head.ID, err) - } - - if objectsRead <= 0 { - t.Fatalf("no objects were enumerated from HEAD (%s)", fmt.Sprintf("%q", label)) - } -} - -func traverseReachableIter(repo *repository.Repository, root objectid.ObjectID) (int, error) { - stack := []objectid.ObjectID{root} - visited := make(map[objectid.ObjectID]struct{}) - total := 0 - - for len(stack) > 0 { - id := stack[len(stack)-1] - stack = stack[:len(stack)-1] - - _, ok := visited[id] - if ok { - continue - } - - visited[id] = struct{}{} - - stored, err := repo.Fetcher().ExactObject(id) - if err != nil { - return 0, err - } - - total++ - - switch obj := stored.Object().(type) { - case *commit.Commit: - stack = append(stack, obj.Tree) - stack = append(stack, obj.Parents...) - case *tree.Tree: - for i := len(obj.Entries) - 1; i >= 0; i-- { - entry := obj.Entries[i] - if entry.Mode == tree.FileModeGitlink { - continue - } - - stack = append(stack, entry.ID) - } - case *tag.Tag: - stack = append(stack, obj.Target) - case *blob.Blob: - default: - // Unknown parsed object variants are treated as leaves. - } - } - - return total, nil -} diff --git a/repository/write_loose_test.go b/repository/write_loose_test.go deleted file mode 100644 index d1fa479c..00000000 --- a/repository/write_loose_test.go +++ /dev/null @@ -1,113 +0,0 @@ -package repository_test - -import ( - "bytes" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -func TestWriteLooseBytesContent(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - repoHarness := testgit.NewRepo(t, testgit.RepoOptions{ - ObjectFormat: algo, - Bare: true, - RefFormat: "files", - }) - - repo := repoHarness.OpenRepository(t) - - content := []byte("write-loose-bytes-content\n") - - gotID, err := repo.Objects().WriteBytesContent(objecttype.TypeBlob, content) - if err != nil { - t.Fatalf("WriteLooseBytesContent: %v", err) - } - - wantID := repoHarness.HashObject(t, "blob", content) - if gotID != wantID { - t.Fatalf("WriteLooseBytesContent id = %s, want %s", gotID, wantID) - } - - ty, gotContent, err := repo.Objects().ReadBytesContent(gotID) - if err != nil { - t.Fatalf("ReadStoredBytesContent: %v", err) - } - - if ty != objecttype.TypeBlob { - t.Fatalf("ReadStoredBytesContent type = %v, want %v", ty, objecttype.TypeBlob) - } - - if !bytes.Equal(gotContent, content) { - t.Fatalf("ReadStoredBytesContent content mismatch") - } - }) -} - -func TestWriteLooseReaderContent(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - repoHarness := testgit.NewRepo(t, testgit.RepoOptions{ - ObjectFormat: algo, - Bare: true, - RefFormat: "files", - }) - - repo := repoHarness.OpenRepository(t) - - content := []byte("write-loose-reader-content\n") - - gotID, err := repo.Objects().WriteReaderContent(objecttype.TypeBlob, int64(len(content)), bytes.NewReader(content)) - if err != nil { - t.Fatalf("WriteLooseReaderContent: %v", err) - } - - wantID := repoHarness.HashObject(t, "blob", content) - if gotID != wantID { - t.Fatalf("WriteLooseReaderContent id = %s, want %s", gotID, wantID) - } - }) -} - -func TestWriteLooseFull(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - repoHarness := testgit.NewRepo(t, testgit.RepoOptions{ - ObjectFormat: algo, - Bare: true, - RefFormat: "files", - }) - _, _, commitID := repoHarness.MakeCommit(t, "write-loose-full") - - repo := repoHarness.OpenRepository(t) - - raw, err := repo.Objects().ReadBytesFull(commitID) - if err != nil { - t.Fatalf("ReadStoredBytesFull: %v", err) - } - - idFromBytes, err := repo.Objects().WriteBytesFull(raw) - if err != nil { - t.Fatalf("WriteLooseBytesFull: %v", err) - } - - if idFromBytes != commitID { - t.Fatalf("WriteLooseBytesFull id = %s, want %s", idFromBytes, commitID) - } - - idFromReader, err := repo.Objects().WriteReaderFull(bytes.NewReader(raw)) - if err != nil { - t.Fatalf("WriteLooseReaderFull: %v", err) - } - - if idFromReader != commitID { - t.Fatalf("WriteLooseReaderFull id = %s, want %s", idFromReader, commitID) - } - }) -} diff --git a/research/dynamic_packfiles.txt b/research/dynamic_packfiles.txt deleted file mode 100644 index e6671521..00000000 --- a/research/dynamic_packfiles.txt +++ /dev/null @@ -1,173 +0,0 @@ -dynamic packfiles to append objects - -gc/refcount process punches page-sized holes in them for pages fully -within the space of unwanted objects, after setting a tombstone mark - -holes are recorded in an index and re-used - -then, if desired, the repack process removes all the punched holes -and anything surrounding from unwanted objects that are slightly out -of the page boundary - -repack is not really git's repack algorithm, it's bascially just -defragmentation. - -genreational bloom filters - -idx design -========== - -so, let's first get our invariants and patterns clear. - -* fixed-length cryptographic object IDs -* essentially uniform key distribution -* exact lookup only, no range scans, no ordered iteration requirements -* reads are extremely important -* writes are mostly append-like -* deletes/tombstones may happen later but are secondary - -1st design ----------- - -* mutable front index -* immutable base index -* period merge/compaction into a new base generation - - - -upload-pack/send-pack/defrag -============================ - -take current pack, remove dead objects/holes, filter objects out, record -offsets and adjust ofs_deltas since they always go backwards, write the pack -back; then stream written pack to client. two-step necessary because pack -header includes object count; could have a custom new protocol that doesn't do -so. - -random chat log dump -==================== -<~runxiyu> ori: actually. i think my hashtable-ish .idx scheme doesn't work really well with e.g. "user provided us a small part of the hash" -<~runxiyu> and when using the Git CLI, abbreviated hashes are extremely common.... -<~runxiyu> not lik ei'd need them in a *forge* -<~runxiyu> but ugh -<~runxiyu> i guess i'm going with some sort of b-tree :(( -<~runxiyu> ~~maybe i should just port gefs to git~~ -<&ori> runxiyu: why not? you should be able to pick the pages based on the prefix and then scan, no? -<~rx> ori: i need to somehow munge the has to prevent page directory explosions -<~rx> the hash* -<~rx> e.g. siphash(objectid, secret) -<~rx> otherwise an attacker could give you 10M objects that start with 00000 and whatnot -<&ori> what's the worst case that would happen there, and is it exponentially worse than giving you 10M objects that start with anything? -<&ori> I'm thinking that you can't generate a case worse than 256/nobject extra table lookups, assuming one bit per fanout.. -<~runxiyu> ori: for extendible hashing, yes, definitely worse -<~runxiyu> the directory will expand a lot for no good reason -<&ori> yes, but you have 256 bits of hash -<&ori> how much is a lot worse? -<&ori> what's the worst an attacker can do, and how is the impact worse than uploading 10M giant objects? -<&ori> also, spotted a bag of kuai kuai keeping the cash register working today at a tea shop -<~runxiyu> waitt -<~runxiyu> hmmm - * runxiyu looks agagin if it's O(N) or O(2^N) -<~runxiyu> well -<~runxiyu> i think it should be a O(2^n) directory size when the attacker can control n bits prefix -<&ori> what's the 'n' here? -<~runxiyu> > can control n bits prefix -<&ori> yeah, you run out of prefix pretty quickly, though -<&ori> I'm not seeing how you could get an exponential blowup if you share pages -<&ori> may be missing something, though -<~runxiyu> hm -<&ori> oh, wait, I see -<&ori> no, wait -<~runxiyu> i think im confusing myself too to some extent but something doesn't feel right -<~runxiyu> urgh -<~runxiyu> okay, rethinking this -<~runxiyu> d is the global depth -<~runxiyu> diretory size is 2^d -<~runxiyu> B records per bucket -<~runxiyu> whatever happens inside the bucket idc, let's say it's a linked list -<~runxiyu> whatever happens inside the bucket idc, let's say it's an array* (linked lists suck) -<~runxiyu> l <= d -<~runxiyu> (l being the local depth of a bucket) -<~runxiyu> normal: d = log^2(N/B) -<&ori> ahh, I see. -<~runxiyu> N is the object count -<&ori> yes, so what if you binary searched the page directory, or made it multi-level -<~runxiyu> an attacker could grab a giant repo and find commonly-prefixed objects, they don't need to brute force their own -<~runxiyu> ori: remember we're trying to do something easy to add new objects into -<~runxiyu> how'd you do that with a binary search? -<~runxiyu> not sure what you mean by multi-level yet here -<~runxiyu> well, it could just turn into a b+tree... -<~runxiyu> hm -<&ori> multilevel -- you have pd[0] using bits 0..n -<~runxiyu> maybe an lmdb object store isn't too bad after all -<&ori> pd[0][1] using bits n...m -<&ori> etc -<&ori> and the reason I was a bit confused was that I had thought the directory was a trie -<&ori> rather than just an expanding top level directory -<~runxiyu> ah -<&ori> so, yeah, I was thinking you could make the page directory an actual trie -<~runxiyu> sigh -<~runxiyu> i guess abbreviated object IDs is something i can't really skip. -<~runxiyu> ori: ill look into radix trees and LSM trees too -<~runxiyu> well, you're basically suggesting a radix tree i guess -<~runxiyu> well actually -<~runxiyu> radix might not necessarily be the best trie here -<~runxiyu> idk -<~runxiyu> hm -<~runxiyu> firstly im really heavy on reads -<~runxiyu> and random keys with no sequential access -<~runxiyu> ok LSM makes no sense -<&hax[xor]> > O(2^N) -<~runxiyu> ori: thoughts on how to make tries reasonable to use on disks? -<&hax[xor]> that sounds like something is already very broken -<~runxiyu> hax[xor]: wdym -<&hax[xor]> directory size should absolutely not scale like that -<~runxiyu> hax[xor]: maybe read up on how extendible hsahing works again? -<&hax[xor]> probably but if that's how it scales it still sounds verybroken -<~runxiyu> n is not the amount of objects -<~runxiyu> it's a pathlogic condition caused by chosne-prefix keys -<~runxiyu> (your keys are usually supposed to be hashed into something the attacker can't predict) -<&hax[xor]> if you mean the directory size scales linearly with the number of objects the attacker puts in it... that sounds perfectly normal? -<&ori> runxiyu: same as extendible hashing, just after you extend to, say, 8 bits, you stop splitting the page directory, and have subdirectories -<~runxiyu> ori: that could make senes -<~runxiyu> haven't thought it through -<~runxiyu> directory size is 2^d, d being the global depth -<~runxiyu> urgh i need to review for exams -<~runxiyu> okay -<~runxiyu> write amplification issue -<~runxiyu> im not sure how significant this is for realistic git workloads -<~runxiyu> i haven't counted, but there should be many, many, many more reads than writes -<~runxiyu> if write amplification is really an issue -<&ori> I may go wander around a bit. -<~runxiyu> then ill just port gefs -<~runxiyu> ori: do you mean IRL, or over dynamic pack data structures- -<&ori> irl. -<~runxiyu> alright that makes more sense :P -<&ori> tomorrow I think I check out Jiufen -<~runxiyu> frick i want to be able to type epsilon with compose -<&ori> is that not possible? -<~runxiyu> i don't seem to be able to -<~runxiyu> but idk the compose tables on my system -<~runxiyu> ε -<~runxiyu> well -<~runxiyu> unicode hex input always works :/ -<~runxiyu> OKAY FUCK -<~runxiyu> I keep getting distracted by interesting things -<~runxiyu> I need to review for my fucking exams --- Mode #chat [-q runxiyu] by runxiyu --- Mode #chat [-a runxiyu] by runxiyu --- #chat: You must be a channel halfop or higher to set channel mode b (ban). --- Mode #chat [+b mute:account:runxiyu] by runxiyu --- #chat: You cannot send messages to this channel whilst a m: (mute) extban is set matching you. --- #chat: You cannot send messages to this channel whilst a m: (mute) extban is set matching you. -<&f_> does that even work? -<&ori> for 9front, *e gives ε -<&ori> but, don't remember the compose map -<&ori> thought that there was a similar thing for all greek letters - - -See also: -https://github.com/inkandswitch/darn -https://www.youtube.com/watch?v=nk4nefmguZk -https://crates.io/crates/iroh-blobs -https://crates.io/crates/bao-tree diff --git a/research/packfile_bloom.txt b/research/packfile_bloom.txt deleted file mode 100644 index 6f7dccf2..00000000 --- a/research/packfile_bloom.txt +++ /dev/null @@ -1,142 +0,0 @@ -Packfile bloom filter RFC -========================= - -Problem -------- - -Especially for server-side usages, repacking is extremely expensive, and -creating multi-pack-indexes is still rather expensive. Incremental MIDX -partially solves this, but would defeat the purpose of MIDX when there are too -many of them, as Git would still have to walk the MIDXes in order while -performing expensive indexing queries. - -Idea ----- - -Each MIDX layer, and each non-MIDX index, comes with a bloom filter. MIDXes and -ordinary .idx files are still traversed in their usual order, but the first -step when traversing them, is to check whether that index could possibly have -the desired object, through a bloom filter. - -We will want the filters to be mmaped, and we want the lookup cost to be -dominated by one cache-line read rather than using many scattered reads. -Therefore, a blocked bloom filter is likely the right direction here. The steps -are as follows: - -1. Split the filter into 64-octet buckets, since 64 octets is the most common - cache-line size. -2. Use some bits of the object ID to choose the bucket. -3. Use the rest of the key to choose several bit positions inside that bucket. -4. A lookup thus reads one 64-octet bucket and checks whether all required bits - are set. - -Note on Object IDs ------------------- - -Git object IDs are cryptographic hashes (e.g., currently either SHA-256 -or SHA-1), and are thus uniformly distributed in non-pathological scenarios. -See also the "Security considerations" section. - -Definitions ------------ - -Let: - - B := number of buckets - K := number of bits set and tested per object ID - -* All integers here are big endian. -* The OID is to be interpreted as a big-endian bitstring, where bit offset 0 - is the most significant bit of octet 0. -* log2(B) + 9K <= hash length in bits. - -File layout ------------ - -* 4-octet signature: {'I', 'D', 'B', 'L'} -* 4-octet version identifier (= 1) -* 4-octet object hash algorithm identifier (= 1 for SHA-1, 2 for SHA-256) -* 4-octet B (number of buckets) -* 2-octet K (number of bits set and tested per object ID) -* 46-octet padding (must be all zeros) -* B buckets of 64 octets each. - -Validation ----------- - -* Matching signature -* Supported version (the rest of the rules are for this version) -* Hash function identifier must be recognized -* B must be nonzero and a power of two -* K must be nonzero -* log2(B) + 9K <= hash length in bits -* Padding must be all zero -* File size must be 64 + 64 * B octets - -Lookup procedure ----------------- - -1. Let b be the unsigned integer encoded by the most significant log2(B) bits - of OID. B is a power of two, and 0 <= b < B. -2. Select and read bucket b. -3. For each 0 <= i < K: - 1. Start immediately after the most significant log2(B) bits of OID, let the - i-th 9-bit field be the bits at offset 9 * i through 9 * i + 8 within the - next 9 * K bits of the OID. - 2. Let pi be the unsigned integer encoded by that 9-bit field. - Then, 0 <= pi < 512. - 3. Compute wi := pi >> 6, and bi := pi & 63. - Thus, wi identifies one of the 8 64-bit words in bucket b, and bi - identifies one bit within that word. - 4. Test whether bi is set in the word wi of bucket b. (Within each 64-bit - word, bit index 0 denotes the most significant bit, and bit index 63 - denotes the least significant bit.) - -If any test fails, the OID is definitely not in the relevant idx. -If all tests succeed, the OID may be in the relevant idx. - -Note that two of the K 9-bit fields can decode to the same pi, which means an -insertion may set fewer than K distinct bits. - -Worked example --------------- - -Let: - - B = 1 << 15 = 32768 - K = 8 - -Then, log2(B) = 15. Each lookup thus uses 15 bits to choose the bucket -and 8 * 9 = 72 bits to choose the in-bucket positions, for a total of -87 bits taken from the object ID. - -1. Read the first 15 bits of OID and interpret them as b, where - 0 <= b < 32768. -2. Read bucket b. -3. For each 0 <= i < 8: - 1. Read the i-th 9-bit field from the next 72 bits of OID and interpret it - as pi, where 0 <= pi < 512. - 2. Compute: wi = pi >> 6, bi = pi & 63. - 3. Test whether bit bi is set in the word wi of bucket b. - -Security considerations ------------------------- - -An adversarial packfile where objects are (computationally intensive, even for -SHA-1 as vulnerable as it is) constructed to have the same prefix for the -relevant object format hash algorithm could be used to fill up the bloom -filters, rendering some buckets useless. In the worst case, if they somehow -fill all filters, this proposal's optimizations become useless, but would not -be a significant DoS vector. - -TODOs ------ - -* Consider dropping mmap (page read vs cachline read) -* How should B and K be chosen? -* How does creation/insert work? Note that packfiles and `.idx`es are immutable. -* What are the sizes? -* What are the false positive rates? -* How are benchmarks? - -* Switch to XOR filters since those are immutable sets anyway. -- cgit v1.3.1-10-gc9f91