git-remote-ops 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +177 -0
- package/LICENSE +21 -0
- package/README.md +247 -0
- package/esm/_dnt.shims.js +72 -0
- package/esm/cli.js +217 -0
- package/esm/client.js +439 -0
- package/esm/deps/jsr.io/@cliffy/command/1.1.0/_argument_types.js +1 -0
- package/esm/deps/jsr.io/@cliffy/command/1.1.0/_errors.js +133 -0
- package/esm/deps/jsr.io/@cliffy/command/1.1.0/_spread.js +1 -0
- package/esm/deps/jsr.io/@cliffy/command/1.1.0/_type_utils.js +1 -0
- package/esm/deps/jsr.io/@cliffy/command/1.1.0/_utils.js +141 -0
- package/esm/deps/jsr.io/@cliffy/command/1.1.0/command.js +1861 -0
- package/esm/deps/jsr.io/@cliffy/command/1.1.0/help/_help_generator.js +357 -0
- package/esm/deps/jsr.io/@cliffy/command/1.1.0/mod.js +13 -0
- package/esm/deps/jsr.io/@cliffy/command/1.1.0/type.js +27 -0
- package/esm/deps/jsr.io/@cliffy/command/1.1.0/types/action_list.js +16 -0
- package/esm/deps/jsr.io/@cliffy/command/1.1.0/types/boolean.js +13 -0
- package/esm/deps/jsr.io/@cliffy/command/1.1.0/types/child_command.js +14 -0
- package/esm/deps/jsr.io/@cliffy/command/1.1.0/types/command.js +9 -0
- package/esm/deps/jsr.io/@cliffy/command/1.1.0/types/enum.js +24 -0
- package/esm/deps/jsr.io/@cliffy/command/1.1.0/types/file.js +12 -0
- package/esm/deps/jsr.io/@cliffy/command/1.1.0/types/integer.js +9 -0
- package/esm/deps/jsr.io/@cliffy/command/1.1.0/types/number.js +9 -0
- package/esm/deps/jsr.io/@cliffy/command/1.1.0/types/secret.js +7 -0
- package/esm/deps/jsr.io/@cliffy/command/1.1.0/types/string.js +9 -0
- package/esm/deps/jsr.io/@cliffy/command/1.1.0/types.js +2 -0
- package/esm/deps/jsr.io/@cliffy/command/1.1.0/upgrade/_check_version.js +26 -0
- package/esm/deps/jsr.io/@cliffy/flags/1.1.0/_errors.js +129 -0
- package/esm/deps/jsr.io/@cliffy/flags/1.1.0/_utils.js +100 -0
- package/esm/deps/jsr.io/@cliffy/flags/1.1.0/_validate_flags.js +166 -0
- package/esm/deps/jsr.io/@cliffy/flags/1.1.0/flags.js +750 -0
- package/esm/deps/jsr.io/@cliffy/flags/1.1.0/mod.js +55 -0
- package/esm/deps/jsr.io/@cliffy/flags/1.1.0/types/boolean.js +11 -0
- package/esm/deps/jsr.io/@cliffy/flags/1.1.0/types/integer.js +9 -0
- package/esm/deps/jsr.io/@cliffy/flags/1.1.0/types/number.js +11 -0
- package/esm/deps/jsr.io/@cliffy/flags/1.1.0/types/string.js +4 -0
- package/esm/deps/jsr.io/@cliffy/flags/1.1.0/types.js +1 -0
- package/esm/deps/jsr.io/@cliffy/internal/1.1.0/runtime/exit.js +16 -0
- package/esm/deps/jsr.io/@cliffy/internal/1.1.0/runtime/get_args.js +11 -0
- package/esm/deps/jsr.io/@cliffy/internal/1.1.0/runtime/get_columns.js +25 -0
- package/esm/deps/jsr.io/@cliffy/internal/1.1.0/runtime/get_env.js +18 -0
- package/esm/deps/jsr.io/@cliffy/internal/1.1.0/runtime/inspect.js +11 -0
- package/esm/deps/jsr.io/@cliffy/table/1.1.0/_layout.js +616 -0
- package/esm/deps/jsr.io/@cliffy/table/1.1.0/_utils.js +79 -0
- package/esm/deps/jsr.io/@cliffy/table/1.1.0/border.js +18 -0
- package/esm/deps/jsr.io/@cliffy/table/1.1.0/cell.js +190 -0
- package/esm/deps/jsr.io/@cliffy/table/1.1.0/column.js +117 -0
- package/esm/deps/jsr.io/@cliffy/table/1.1.0/consume_words.js +64 -0
- package/esm/deps/jsr.io/@cliffy/table/1.1.0/mod.js +42 -0
- package/esm/deps/jsr.io/@cliffy/table/1.1.0/row.js +82 -0
- package/esm/deps/jsr.io/@cliffy/table/1.1.0/table.js +341 -0
- package/esm/deps/jsr.io/@cliffy/table/1.1.0/unicode_width.js +101 -0
- package/esm/deps/jsr.io/@std/crypto/1.1.0/_types.js +2 -0
- package/esm/deps/jsr.io/@std/crypto/1.1.0/_wasm/lib/deno_std_wasm_crypto.internal.js +237 -0
- package/esm/deps/jsr.io/@std/crypto/1.1.0/_wasm/lib/deno_std_wasm_crypto.js +2277 -0
- package/esm/deps/jsr.io/@std/crypto/1.1.0/_wasm/mod.js +46 -0
- package/esm/deps/jsr.io/@std/crypto/1.1.0/aes_gcm.js +132 -0
- package/esm/deps/jsr.io/@std/crypto/1.1.0/crypto.js +270 -0
- package/esm/deps/jsr.io/@std/crypto/1.1.0/mod.js +23 -0
- package/esm/deps/jsr.io/@std/crypto/1.1.0/timing_safe_equal.js +61 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.10/_common16.js +51 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.10/_common_detach.js +13 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.10/_types.js +2 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.10/hex.js +87 -0
- package/esm/deps/jsr.io/@std/fmt/1.0.10/colors.js +903 -0
- package/esm/deps/jsr.io/@std/text/1.0.18/closest_string.js +46 -0
- package/esm/deps/jsr.io/@std/text/1.0.18/levenshtein_distance.js +127 -0
- package/esm/errors.js +38 -0
- package/esm/index.js +10 -0
- package/esm/logger.js +216 -0
- package/esm/objects/commit.js +47 -0
- package/esm/objects/index.js +2 -0
- package/esm/objects/tree.js +149 -0
- package/esm/pack/delta.js +179 -0
- package/esm/pack/index.js +3 -0
- package/esm/pack/objects.js +72 -0
- package/esm/pack/parser.js +304 -0
- package/esm/package.json +3 -0
- package/esm/protocol/index.js +3 -0
- package/esm/protocol/pkt_line.js +103 -0
- package/esm/protocol/refs.js +100 -0
- package/esm/protocol/upload_pack.js +259 -0
- package/esm/transport.js +128 -0
- package/esm/types.js +8 -0
- package/package.json +50 -0
- package/types/_dnt.shims.d.ts +16 -0
- package/types/_dnt.shims.d.ts.map +1 -0
- package/types/cli.d.ts +3 -0
- package/types/cli.d.ts.map +1 -0
- package/types/client.d.ts +108 -0
- package/types/client.d.ts.map +1 -0
- package/types/deps/jsr.io/@cliffy/command/1.1.0/_argument_types.d.ts +163 -0
- package/types/deps/jsr.io/@cliffy/command/1.1.0/_argument_types.d.ts.map +1 -0
- package/types/deps/jsr.io/@cliffy/command/1.1.0/_errors.d.ts +71 -0
- package/types/deps/jsr.io/@cliffy/command/1.1.0/_errors.d.ts.map +1 -0
- package/types/deps/jsr.io/@cliffy/command/1.1.0/_spread.d.ts +16 -0
- package/types/deps/jsr.io/@cliffy/command/1.1.0/_spread.d.ts.map +1 -0
- package/types/deps/jsr.io/@cliffy/command/1.1.0/_type_utils.d.ts +15 -0
- package/types/deps/jsr.io/@cliffy/command/1.1.0/_type_utils.d.ts.map +1 -0
- package/types/deps/jsr.io/@cliffy/command/1.1.0/_utils.d.ts +38 -0
- package/types/deps/jsr.io/@cliffy/command/1.1.0/_utils.d.ts.map +1 -0
- package/types/deps/jsr.io/@cliffy/command/1.1.0/command.d.ts +1086 -0
- package/types/deps/jsr.io/@cliffy/command/1.1.0/command.d.ts.map +1 -0
- package/types/deps/jsr.io/@cliffy/command/1.1.0/help/_help_generator.d.ts +33 -0
- package/types/deps/jsr.io/@cliffy/command/1.1.0/help/_help_generator.d.ts.map +1 -0
- package/types/deps/jsr.io/@cliffy/command/1.1.0/mod.d.ts +78 -0
- package/types/deps/jsr.io/@cliffy/command/1.1.0/mod.d.ts.map +1 -0
- package/types/deps/jsr.io/@cliffy/command/1.1.0/type.d.ts +51 -0
- package/types/deps/jsr.io/@cliffy/command/1.1.0/type.d.ts.map +1 -0
- package/types/deps/jsr.io/@cliffy/command/1.1.0/types/action_list.d.ts +10 -0
- package/types/deps/jsr.io/@cliffy/command/1.1.0/types/action_list.d.ts.map +1 -0
- package/types/deps/jsr.io/@cliffy/command/1.1.0/types/boolean.d.ts +10 -0
- package/types/deps/jsr.io/@cliffy/command/1.1.0/types/boolean.d.ts.map +1 -0
- package/types/deps/jsr.io/@cliffy/command/1.1.0/types/child_command.d.ts +10 -0
- package/types/deps/jsr.io/@cliffy/command/1.1.0/types/child_command.d.ts.map +1 -0
- package/types/deps/jsr.io/@cliffy/command/1.1.0/types/command.d.ts +8 -0
- package/types/deps/jsr.io/@cliffy/command/1.1.0/types/command.d.ts.map +1 -0
- package/types/deps/jsr.io/@cliffy/command/1.1.0/types/enum.d.ts +11 -0
- package/types/deps/jsr.io/@cliffy/command/1.1.0/types/enum.d.ts.map +1 -0
- package/types/deps/jsr.io/@cliffy/command/1.1.0/types/file.d.ts +6 -0
- package/types/deps/jsr.io/@cliffy/command/1.1.0/types/file.d.ts.map +1 -0
- package/types/deps/jsr.io/@cliffy/command/1.1.0/types/integer.d.ts +8 -0
- package/types/deps/jsr.io/@cliffy/command/1.1.0/types/integer.d.ts.map +1 -0
- package/types/deps/jsr.io/@cliffy/command/1.1.0/types/number.d.ts +8 -0
- package/types/deps/jsr.io/@cliffy/command/1.1.0/types/number.d.ts.map +1 -0
- package/types/deps/jsr.io/@cliffy/command/1.1.0/types/secret.d.ts +6 -0
- package/types/deps/jsr.io/@cliffy/command/1.1.0/types/secret.d.ts.map +1 -0
- package/types/deps/jsr.io/@cliffy/command/1.1.0/types/string.d.ts +8 -0
- package/types/deps/jsr.io/@cliffy/command/1.1.0/types/string.d.ts.map +1 -0
- package/types/deps/jsr.io/@cliffy/command/1.1.0/types.d.ts +161 -0
- package/types/deps/jsr.io/@cliffy/command/1.1.0/types.d.ts.map +1 -0
- package/types/deps/jsr.io/@cliffy/command/1.1.0/upgrade/_check_version.d.ts +4 -0
- package/types/deps/jsr.io/@cliffy/command/1.1.0/upgrade/_check_version.d.ts.map +1 -0
- package/types/deps/jsr.io/@cliffy/flags/1.1.0/_errors.d.ts +67 -0
- package/types/deps/jsr.io/@cliffy/flags/1.1.0/_errors.d.ts.map +1 -0
- package/types/deps/jsr.io/@cliffy/flags/1.1.0/_utils.d.ts +17 -0
- package/types/deps/jsr.io/@cliffy/flags/1.1.0/_utils.d.ts.map +1 -0
- package/types/deps/jsr.io/@cliffy/flags/1.1.0/_validate_flags.d.ts +11 -0
- package/types/deps/jsr.io/@cliffy/flags/1.1.0/_validate_flags.d.ts.map +1 -0
- package/types/deps/jsr.io/@cliffy/flags/1.1.0/flags.d.ts +154 -0
- package/types/deps/jsr.io/@cliffy/flags/1.1.0/flags.d.ts.map +1 -0
- package/types/deps/jsr.io/@cliffy/flags/1.1.0/mod.d.ts +57 -0
- package/types/deps/jsr.io/@cliffy/flags/1.1.0/mod.d.ts.map +1 -0
- package/types/deps/jsr.io/@cliffy/flags/1.1.0/types/boolean.d.ts +4 -0
- package/types/deps/jsr.io/@cliffy/flags/1.1.0/types/boolean.d.ts.map +1 -0
- package/types/deps/jsr.io/@cliffy/flags/1.1.0/types/integer.d.ts +4 -0
- package/types/deps/jsr.io/@cliffy/flags/1.1.0/types/integer.d.ts.map +1 -0
- package/types/deps/jsr.io/@cliffy/flags/1.1.0/types/number.d.ts +4 -0
- package/types/deps/jsr.io/@cliffy/flags/1.1.0/types/number.d.ts.map +1 -0
- package/types/deps/jsr.io/@cliffy/flags/1.1.0/types/string.d.ts +4 -0
- package/types/deps/jsr.io/@cliffy/flags/1.1.0/types/string.d.ts.map +1 -0
- package/types/deps/jsr.io/@cliffy/flags/1.1.0/types.d.ts +170 -0
- package/types/deps/jsr.io/@cliffy/flags/1.1.0/types.d.ts.map +1 -0
- package/types/deps/jsr.io/@cliffy/internal/1.1.0/runtime/exit.d.ts +8 -0
- package/types/deps/jsr.io/@cliffy/internal/1.1.0/runtime/exit.d.ts.map +1 -0
- package/types/deps/jsr.io/@cliffy/internal/1.1.0/runtime/get_args.d.ts +7 -0
- package/types/deps/jsr.io/@cliffy/internal/1.1.0/runtime/get_args.d.ts.map +1 -0
- package/types/deps/jsr.io/@cliffy/internal/1.1.0/runtime/get_columns.d.ts +7 -0
- package/types/deps/jsr.io/@cliffy/internal/1.1.0/runtime/get_columns.d.ts.map +1 -0
- package/types/deps/jsr.io/@cliffy/internal/1.1.0/runtime/get_env.d.ts +8 -0
- package/types/deps/jsr.io/@cliffy/internal/1.1.0/runtime/get_env.d.ts.map +1 -0
- package/types/deps/jsr.io/@cliffy/internal/1.1.0/runtime/inspect.d.ts +7 -0
- package/types/deps/jsr.io/@cliffy/internal/1.1.0/runtime/inspect.d.ts.map +1 -0
- package/types/deps/jsr.io/@cliffy/table/1.1.0/_layout.d.ts +108 -0
- package/types/deps/jsr.io/@cliffy/table/1.1.0/_layout.d.ts.map +1 -0
- package/types/deps/jsr.io/@cliffy/table/1.1.0/_utils.d.ts +26 -0
- package/types/deps/jsr.io/@cliffy/table/1.1.0/_utils.d.ts.map +1 -0
- package/types/deps/jsr.io/@cliffy/table/1.1.0/border.d.ts +21 -0
- package/types/deps/jsr.io/@cliffy/table/1.1.0/border.d.ts.map +1 -0
- package/types/deps/jsr.io/@cliffy/table/1.1.0/cell.d.ts +155 -0
- package/types/deps/jsr.io/@cliffy/table/1.1.0/cell.d.ts.map +1 -0
- package/types/deps/jsr.io/@cliffy/table/1.1.0/column.d.ts +97 -0
- package/types/deps/jsr.io/@cliffy/table/1.1.0/column.d.ts.map +1 -0
- package/types/deps/jsr.io/@cliffy/table/1.1.0/consume_words.d.ts +30 -0
- package/types/deps/jsr.io/@cliffy/table/1.1.0/consume_words.d.ts.map +1 -0
- package/types/deps/jsr.io/@cliffy/table/1.1.0/mod.d.ts +43 -0
- package/types/deps/jsr.io/@cliffy/table/1.1.0/mod.d.ts.map +1 -0
- package/types/deps/jsr.io/@cliffy/table/1.1.0/row.d.ts +67 -0
- package/types/deps/jsr.io/@cliffy/table/1.1.0/row.d.ts.map +1 -0
- package/types/deps/jsr.io/@cliffy/table/1.1.0/table.d.ts +235 -0
- package/types/deps/jsr.io/@cliffy/table/1.1.0/table.d.ts.map +1 -0
- package/types/deps/jsr.io/@cliffy/table/1.1.0/unicode_width.d.ts +40 -0
- package/types/deps/jsr.io/@cliffy/table/1.1.0/unicode_width.d.ts.map +1 -0
- package/types/deps/jsr.io/@std/crypto/1.1.0/_types.d.ts +9 -0
- package/types/deps/jsr.io/@std/crypto/1.1.0/_types.d.ts.map +1 -0
- package/types/deps/jsr.io/@std/crypto/1.1.0/_wasm/lib/deno_std_wasm_crypto.d.ts +2 -0
- package/types/deps/jsr.io/@std/crypto/1.1.0/_wasm/lib/deno_std_wasm_crypto.d.ts.map +1 -0
- package/types/deps/jsr.io/@std/crypto/1.1.0/_wasm/lib/deno_std_wasm_crypto.internal.d.ts +69 -0
- package/types/deps/jsr.io/@std/crypto/1.1.0/_wasm/lib/deno_std_wasm_crypto.internal.d.ts.map +1 -0
- package/types/deps/jsr.io/@std/crypto/1.1.0/_wasm/mod.d.ts +13 -0
- package/types/deps/jsr.io/@std/crypto/1.1.0/_wasm/mod.d.ts.map +1 -0
- package/types/deps/jsr.io/@std/crypto/1.1.0/aes_gcm.d.ts +76 -0
- package/types/deps/jsr.io/@std/crypto/1.1.0/aes_gcm.d.ts.map +1 -0
- package/types/deps/jsr.io/@std/crypto/1.1.0/crypto.d.ts +149 -0
- package/types/deps/jsr.io/@std/crypto/1.1.0/crypto.d.ts.map +1 -0
- package/types/deps/jsr.io/@std/crypto/1.1.0/mod.d.ts +22 -0
- package/types/deps/jsr.io/@std/crypto/1.1.0/mod.d.ts.map +1 -0
- package/types/deps/jsr.io/@std/crypto/1.1.0/timing_safe_equal.d.ts +40 -0
- package/types/deps/jsr.io/@std/crypto/1.1.0/timing_safe_equal.d.ts.map +1 -0
- package/types/deps/jsr.io/@std/encoding/1.0.10/_common16.d.ts +23 -0
- package/types/deps/jsr.io/@std/encoding/1.0.10/_common16.d.ts.map +1 -0
- package/types/deps/jsr.io/@std/encoding/1.0.10/_common_detach.d.ts +4 -0
- package/types/deps/jsr.io/@std/encoding/1.0.10/_common_detach.d.ts.map +1 -0
- package/types/deps/jsr.io/@std/encoding/1.0.10/_types.d.ts +9 -0
- package/types/deps/jsr.io/@std/encoding/1.0.10/_types.d.ts.map +1 -0
- package/types/deps/jsr.io/@std/encoding/1.0.10/hex.d.ts +39 -0
- package/types/deps/jsr.io/@std/encoding/1.0.10/hex.d.ts.map +1 -0
- package/types/deps/jsr.io/@std/fmt/1.0.10/colors.d.ts +700 -0
- package/types/deps/jsr.io/@std/fmt/1.0.10/colors.d.ts.map +1 -0
- package/types/deps/jsr.io/@std/text/1.0.18/closest_string.d.ts +42 -0
- package/types/deps/jsr.io/@std/text/1.0.18/closest_string.d.ts.map +1 -0
- package/types/deps/jsr.io/@std/text/1.0.18/levenshtein_distance.d.ts +23 -0
- package/types/deps/jsr.io/@std/text/1.0.18/levenshtein_distance.d.ts.map +1 -0
- package/types/errors.d.ts +73 -0
- package/types/errors.d.ts.map +1 -0
- package/types/index.d.ts +14 -0
- package/types/index.d.ts.map +1 -0
- package/types/logger.d.ts +70 -0
- package/types/logger.d.ts.map +1 -0
- package/types/objects/commit.d.ts +23 -0
- package/types/objects/commit.d.ts.map +1 -0
- package/types/objects/index.d.ts +3 -0
- package/types/objects/index.d.ts.map +1 -0
- package/types/objects/tree.d.ts +49 -0
- package/types/objects/tree.d.ts.map +1 -0
- package/types/pack/delta.d.ts +47 -0
- package/types/pack/delta.d.ts.map +1 -0
- package/types/pack/index.d.ts +4 -0
- package/types/pack/index.d.ts.map +1 -0
- package/types/pack/objects.d.ts +53 -0
- package/types/pack/objects.d.ts.map +1 -0
- package/types/pack/parser.d.ts +61 -0
- package/types/pack/parser.d.ts.map +1 -0
- package/types/protocol/index.d.ts +4 -0
- package/types/protocol/index.d.ts.map +1 -0
- package/types/protocol/pkt_line.d.ts +44 -0
- package/types/protocol/pkt_line.d.ts.map +1 -0
- package/types/protocol/refs.d.ts +40 -0
- package/types/protocol/refs.d.ts.map +1 -0
- package/types/protocol/upload_pack.d.ts +45 -0
- package/types/protocol/upload_pack.d.ts.map +1 -0
- package/types/transport.d.ts +24 -0
- package/types/transport.d.ts.map +1 -0
- package/types/types.d.ts +121 -0
- package/types/types.d.ts.map +1 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module pack-delta
|
|
3
|
+
*
|
|
4
|
+
* Pack format varint decoders and the Git delta reconstruction routine.
|
|
5
|
+
*
|
|
6
|
+
* Two varint flavours live in pack files. The "big-endian" form used by
|
|
7
|
+
* `OBJ_OFS_DELTA` headers is decoded by {@link readVarintBe}; the
|
|
8
|
+
* little-endian form used inside the delta opcode stream is handled inline
|
|
9
|
+
* by {@link applyDelta}.
|
|
10
|
+
*
|
|
11
|
+
* Delta format spec: https://git-scm.com/docs/pack-format#_deltified_representation
|
|
12
|
+
*/
|
|
13
|
+
import { Result } from "better-result";
|
|
14
|
+
import { PackParseError } from "../errors.js";
|
|
15
|
+
import { VARINT_CONTINUE, VARINT_VALUE_MASK } from "./objects.js";
|
|
16
|
+
/** Bits of payload per varint byte. */
|
|
17
|
+
const VARINT_VALUE_BITS = 7;
|
|
18
|
+
/** High bit on a delta opcode byte → COPY instruction; otherwise INSERT. */
|
|
19
|
+
const DELTA_COPY_FLAG = 0x80;
|
|
20
|
+
/** Up to 4 bytes encode the copy offset (one per set bit in opcode low nibble). */
|
|
21
|
+
const DELTA_COPY_OFFSET_BYTES = 4;
|
|
22
|
+
/** Up to 3 bytes encode the copy size (one per set bit in opcode high nibble). */
|
|
23
|
+
const DELTA_COPY_SIZE_BYTES = 3;
|
|
24
|
+
/** Bit position of the first copy-size selector inside the opcode byte. */
|
|
25
|
+
const DELTA_COPY_SIZE_SHIFT = 4;
|
|
26
|
+
/** Special case: a copy with all size bytes absent means "copy 64 KiB". */
|
|
27
|
+
const DELTA_DEFAULT_COPY_SIZE = 0x10000;
|
|
28
|
+
/**
|
|
29
|
+
* Decode Git's "offset-encoded" big-endian varint (used for `OBJ_OFS_DELTA`
|
|
30
|
+
* base offsets).
|
|
31
|
+
*
|
|
32
|
+
* Each continuation byte adds an implicit `+1` before shifting, which lets the
|
|
33
|
+
* encoding represent more values in the same number of bytes than a plain MSB
|
|
34
|
+
* varint would.
|
|
35
|
+
*
|
|
36
|
+
* @returns `{ value, offset }` where `offset` points just past the last byte
|
|
37
|
+
* consumed, or {@link PackParseError} if the buffer ends mid-varint.
|
|
38
|
+
*/
|
|
39
|
+
export function readVarintBe(data, offset) {
|
|
40
|
+
if (offset >= data.length) {
|
|
41
|
+
return Result.err(new PackParseError({
|
|
42
|
+
reason: "truncated-varint",
|
|
43
|
+
message: "truncated varint",
|
|
44
|
+
offset,
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
let byte = data[offset++];
|
|
48
|
+
let value = byte & VARINT_VALUE_MASK;
|
|
49
|
+
while ((byte & VARINT_CONTINUE) !== 0) {
|
|
50
|
+
if (offset >= data.length) {
|
|
51
|
+
return Result.err(new PackParseError({
|
|
52
|
+
reason: "truncated-varint",
|
|
53
|
+
message: "truncated varint",
|
|
54
|
+
offset,
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
value += 1;
|
|
58
|
+
byte = data[offset++];
|
|
59
|
+
value = (value << VARINT_VALUE_BITS) | (byte & VARINT_VALUE_MASK);
|
|
60
|
+
}
|
|
61
|
+
return Result.ok({ value, offset });
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Reconstruct an object body from a delta against `base`.
|
|
65
|
+
*
|
|
66
|
+
* The delta stream begins with two little-endian varints (source size, target
|
|
67
|
+
* size). Then a sequence of opcodes follows, each either:
|
|
68
|
+
*
|
|
69
|
+
* - **COPY** (high bit set): copy a range from `base` into the output. The
|
|
70
|
+
* low nibble's set bits select which of up to 4 offset bytes follow; the
|
|
71
|
+
* next 3 bits select up to 3 size bytes. A size of 0 means 64 KiB.
|
|
72
|
+
* - **INSERT** (high bit clear, opcode `1..0x7f`): emit the next `opcode`
|
|
73
|
+
* bytes of the delta stream verbatim into the output.
|
|
74
|
+
* - opcode `0` is reserved and invalid.
|
|
75
|
+
*
|
|
76
|
+
* The source size is validated implicitly by every COPY's bounds check.
|
|
77
|
+
*
|
|
78
|
+
* @returns The fully reconstructed object, or {@link PackParseError} on any
|
|
79
|
+
* truncation, invalid opcode, or size mismatch.
|
|
80
|
+
*/
|
|
81
|
+
export function applyDelta(base, delta) {
|
|
82
|
+
let offset = 0;
|
|
83
|
+
function readVarint() {
|
|
84
|
+
let result = 0;
|
|
85
|
+
let shift = 0;
|
|
86
|
+
while (true) {
|
|
87
|
+
if (offset >= delta.length) {
|
|
88
|
+
return Result.err(new PackParseError({
|
|
89
|
+
reason: "truncated-delta-varint",
|
|
90
|
+
message: "truncated delta varint",
|
|
91
|
+
offset,
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
const byte = delta[offset++];
|
|
95
|
+
result |= (byte & VARINT_VALUE_MASK) << shift;
|
|
96
|
+
if ((byte & VARINT_CONTINUE) === 0)
|
|
97
|
+
return Result.ok(result);
|
|
98
|
+
shift += VARINT_VALUE_BITS;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const sourceSize = readVarint();
|
|
102
|
+
if (sourceSize.isErr())
|
|
103
|
+
return Result.err(sourceSize.error);
|
|
104
|
+
const targetSize = readVarint();
|
|
105
|
+
if (targetSize.isErr())
|
|
106
|
+
return Result.err(targetSize.error);
|
|
107
|
+
const out = new Uint8Array(targetSize.value);
|
|
108
|
+
let outOffset = 0;
|
|
109
|
+
while (offset < delta.length) {
|
|
110
|
+
const cmdOffset = offset;
|
|
111
|
+
const cmd = delta[offset++];
|
|
112
|
+
if ((cmd & DELTA_COPY_FLAG) !== 0) {
|
|
113
|
+
let copyOffset = 0;
|
|
114
|
+
let size = 0;
|
|
115
|
+
for (let i = 0; i < DELTA_COPY_OFFSET_BYTES; i++) {
|
|
116
|
+
if ((cmd & (1 << i)) !== 0) {
|
|
117
|
+
if (offset >= delta.length) {
|
|
118
|
+
return Result.err(new PackParseError({
|
|
119
|
+
reason: "truncated-copy-offset",
|
|
120
|
+
message: "truncated delta copy offset",
|
|
121
|
+
offset,
|
|
122
|
+
}));
|
|
123
|
+
}
|
|
124
|
+
copyOffset |= delta[offset++] << (i * 8);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
for (let i = 0; i < DELTA_COPY_SIZE_BYTES; i++) {
|
|
128
|
+
if ((cmd & (1 << (DELTA_COPY_SIZE_SHIFT + i))) !== 0) {
|
|
129
|
+
if (offset >= delta.length) {
|
|
130
|
+
return Result.err(new PackParseError({
|
|
131
|
+
reason: "truncated-copy-size",
|
|
132
|
+
message: "truncated delta copy size",
|
|
133
|
+
offset,
|
|
134
|
+
}));
|
|
135
|
+
}
|
|
136
|
+
size |= delta[offset++] << (i * 8);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (size === 0)
|
|
140
|
+
size = DELTA_DEFAULT_COPY_SIZE;
|
|
141
|
+
if (copyOffset + size > base.length || outOffset + size > out.length) {
|
|
142
|
+
return Result.err(new PackParseError({
|
|
143
|
+
reason: "delta-copy-out-of-bounds",
|
|
144
|
+
message: "delta copy exceeds source or target size",
|
|
145
|
+
offset: cmdOffset,
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
148
|
+
out.set(base.subarray(copyOffset, copyOffset + size), outOffset);
|
|
149
|
+
outOffset += size;
|
|
150
|
+
}
|
|
151
|
+
else if (cmd !== 0) {
|
|
152
|
+
if (offset + cmd > delta.length || outOffset + cmd > out.length) {
|
|
153
|
+
return Result.err(new PackParseError({
|
|
154
|
+
reason: "delta-insert-out-of-bounds",
|
|
155
|
+
message: "delta insert exceeds source or target size",
|
|
156
|
+
offset: cmdOffset,
|
|
157
|
+
}));
|
|
158
|
+
}
|
|
159
|
+
out.set(delta.subarray(offset, offset + cmd), outOffset);
|
|
160
|
+
outOffset += cmd;
|
|
161
|
+
offset += cmd;
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
return Result.err(new PackParseError({
|
|
165
|
+
reason: "invalid-delta-opcode",
|
|
166
|
+
message: "invalid delta opcode 0",
|
|
167
|
+
offset: cmdOffset,
|
|
168
|
+
}));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (outOffset !== out.length) {
|
|
172
|
+
return Result.err(new PackParseError({
|
|
173
|
+
reason: "delta-target-size-mismatch",
|
|
174
|
+
message: `delta produced ${outOffset} bytes, expected ${out.length}`,
|
|
175
|
+
offset,
|
|
176
|
+
}));
|
|
177
|
+
}
|
|
178
|
+
return Result.ok(out);
|
|
179
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module pack-objects
|
|
3
|
+
*
|
|
4
|
+
* Shared constants and the SHA-1 helper used by the packfile parser. Object
|
|
5
|
+
* type codes, varint flags, and pack-format magic live here so that
|
|
6
|
+
* {@link parser} and {@link delta} both reach for the same source of truth.
|
|
7
|
+
*
|
|
8
|
+
* Pack format reference:
|
|
9
|
+
* https://git-scm.com/docs/pack-format
|
|
10
|
+
*/
|
|
11
|
+
import { Result } from "better-result";
|
|
12
|
+
import { crypto } from "../deps/jsr.io/@std/crypto/1.1.0/mod.js";
|
|
13
|
+
import { encodeHex } from "../deps/jsr.io/@std/encoding/1.0.10/hex.js";
|
|
14
|
+
import { PackParseError } from "../errors.js";
|
|
15
|
+
/** Magic bytes opening every packfile: ASCII `"PACK"`. */
|
|
16
|
+
export const PACK_SIGNATURE = new Uint8Array([0x50, 0x41, 0x43, 0x4b]);
|
|
17
|
+
/** Bytes consumed by the fixed pack header (signature + version + count). */
|
|
18
|
+
export const PACK_HEADER_SIZE = 12;
|
|
19
|
+
/** Size of {@link PACK_SIGNATURE} in bytes. */
|
|
20
|
+
export const PACK_SIGNATURE_SIZE = 4;
|
|
21
|
+
/** Pack format versions this parser accepts. v4 exists but is not produced by mainline Git. */
|
|
22
|
+
export const SUPPORTED_PACK_VERSIONS = new Set([2, 3]);
|
|
23
|
+
/** Trailing SHA-1 over the rest of the pack — sits at the very end of the buffer. */
|
|
24
|
+
export const PACK_TRAILER_SIZE = 20;
|
|
25
|
+
/** High bit set on a varint byte means "another byte follows". */
|
|
26
|
+
export const VARINT_CONTINUE = 0x80;
|
|
27
|
+
/** Low 7 bits of a varint byte hold value bits. */
|
|
28
|
+
export const VARINT_VALUE_MASK = 0x7f;
|
|
29
|
+
/** Packfile object type code: commit. */
|
|
30
|
+
export const OBJ_COMMIT = 1;
|
|
31
|
+
/** Packfile object type code: tree. */
|
|
32
|
+
export const OBJ_TREE = 2;
|
|
33
|
+
/** Packfile object type code: blob. */
|
|
34
|
+
export const OBJ_BLOB = 3;
|
|
35
|
+
/** Packfile object type code: annotated tag. */
|
|
36
|
+
export const OBJ_TAG = 4;
|
|
37
|
+
/** Delta object whose base is referenced by negative offset within the same pack. */
|
|
38
|
+
export const OBJ_OFS_DELTA = 6;
|
|
39
|
+
/** Delta object whose base is referenced by 20-byte SHA-1 (may be outside the pack). */
|
|
40
|
+
export const OBJ_REF_DELTA = 7;
|
|
41
|
+
/** Pack type code → loose-object type name. Excludes delta codes by design. */
|
|
42
|
+
export const OBJ_NAMES = new Map([
|
|
43
|
+
[OBJ_COMMIT, "commit"],
|
|
44
|
+
[OBJ_TREE, "tree"],
|
|
45
|
+
[OBJ_BLOB, "blob"],
|
|
46
|
+
[OBJ_TAG, "tag"],
|
|
47
|
+
]);
|
|
48
|
+
/** Reverse of {@link OBJ_NAMES} — type name → pack type code. */
|
|
49
|
+
export const OBJ_TYPES = new Map([...OBJ_NAMES.entries()].map(([num, name]) => [name, num]));
|
|
50
|
+
const encoder = new TextEncoder();
|
|
51
|
+
/**
|
|
52
|
+
* Compute the canonical Git SHA-1 of an object — `sha1("<type> <size>\0" || content)`.
|
|
53
|
+
*
|
|
54
|
+
* @param type Either a pack type code (1–4) or the loose-object type name.
|
|
55
|
+
* @param content The uncompressed object body.
|
|
56
|
+
* @returns Lowercase hex SHA-1, or {@link PackParseError} when `type` is a
|
|
57
|
+
* delta code or otherwise unknown.
|
|
58
|
+
*/
|
|
59
|
+
export function sha1OfObject(type, content) {
|
|
60
|
+
const name = typeof type === "number" ? OBJ_NAMES.get(type) : type;
|
|
61
|
+
if (!name) {
|
|
62
|
+
return Result.err(new PackParseError({
|
|
63
|
+
reason: "unknown-object-type",
|
|
64
|
+
message: `unknown object type ${type}`,
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
const header = encoder.encode(`${name} ${content.length}\0`);
|
|
68
|
+
const input = new Uint8Array(header.length + content.length);
|
|
69
|
+
input.set(header);
|
|
70
|
+
input.set(content, header.length);
|
|
71
|
+
return Result.ok(encodeHex(new Uint8Array(crypto.subtle.digestSync("SHA-1", input))));
|
|
72
|
+
}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module pack-parser
|
|
3
|
+
*
|
|
4
|
+
* Decode an in-memory packfile into a `sha → GitObject` map.
|
|
5
|
+
*
|
|
6
|
+
* The parser handles the four loose-object types directly and resolves both
|
|
7
|
+
* delta encodings (`OBJ_OFS_DELTA`, `OBJ_REF_DELTA`) by applying
|
|
8
|
+
* {@link applyDelta} against bases discovered earlier in the same pack. Ref
|
|
9
|
+
* deltas whose bases haven't appeared yet are deferred and retried in fixed
|
|
10
|
+
* point until every delta resolves or progress stalls.
|
|
11
|
+
*
|
|
12
|
+
* Optimization: passing a `targets` set lets the parser bail out as soon as
|
|
13
|
+
* every requested SHA has been materialized — useful for "fetch one object"
|
|
14
|
+
* paths where the server may have sent a much larger pack.
|
|
15
|
+
*
|
|
16
|
+
* zlib note: we reach into `node:zlib`'s `_processChunk` to learn exactly how
|
|
17
|
+
* many compressed input bytes were consumed. The public Streams API doesn't
|
|
18
|
+
* expose that, and parsing the next object requires knowing where the current
|
|
19
|
+
* one's compressed run ends.
|
|
20
|
+
*/
|
|
21
|
+
import { Result } from "better-result";
|
|
22
|
+
import zlib from "node:zlib";
|
|
23
|
+
import { Buffer } from "node:buffer";
|
|
24
|
+
import { encodeHex } from "../deps/jsr.io/@std/encoding/1.0.10/hex.js";
|
|
25
|
+
import { PackParseError } from "../errors.js";
|
|
26
|
+
import { applyDelta, readVarintBe } from "./delta.js";
|
|
27
|
+
import { OBJ_NAMES, OBJ_OFS_DELTA, OBJ_REF_DELTA, OBJ_TYPES, PACK_HEADER_SIZE, PACK_SIGNATURE, PACK_SIGNATURE_SIZE, PACK_TRAILER_SIZE, sha1OfObject, SUPPORTED_PACK_VERSIONS, VARINT_CONTINUE, VARINT_VALUE_MASK, } from "./objects.js";
|
|
28
|
+
/** Mask for the 3-bit object type field in the first header byte. */
|
|
29
|
+
const TYPE_FIELD_MASK = 0x07;
|
|
30
|
+
/** Shift to bring the type field into the low bits. */
|
|
31
|
+
const TYPE_FIELD_SHIFT = 4;
|
|
32
|
+
/** Mask for the size bits in the first header byte (bottom 4 bits). */
|
|
33
|
+
const SIZE_LOW_NIBBLE_MASK = 0x0f;
|
|
34
|
+
/** Bit width of those size bits. */
|
|
35
|
+
const SIZE_LOW_NIBBLE_BITS = 4;
|
|
36
|
+
/** Bit width of each subsequent varint byte's payload. */
|
|
37
|
+
const VARINT_VALUE_BITS = 7;
|
|
38
|
+
/** Raw 20-byte SHA-1 inline-prefixed on every `OBJ_REF_DELTA`. */
|
|
39
|
+
const REF_DELTA_SHA_SIZE = 20;
|
|
40
|
+
/** Offset of the big-endian pack version word inside the 12-byte header. */
|
|
41
|
+
const VERSION_OFFSET = 4;
|
|
42
|
+
/** Offset of the big-endian object count word inside the 12-byte header. */
|
|
43
|
+
const COUNT_OFFSET = 8;
|
|
44
|
+
function bytesEqual(a, b) {
|
|
45
|
+
if (a.length !== b.length)
|
|
46
|
+
return false;
|
|
47
|
+
for (let i = 0; i < a.length; i++)
|
|
48
|
+
if (a[i] !== b[i])
|
|
49
|
+
return false;
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
function typeNumberOf(type) {
|
|
53
|
+
const num = OBJ_TYPES.get(type);
|
|
54
|
+
if (num === undefined) {
|
|
55
|
+
return Result.err(new PackParseError({
|
|
56
|
+
reason: "unknown-base-object-type",
|
|
57
|
+
message: `unknown base object type ${type}`,
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
return Result.ok(num);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Decode a single pack-entry header starting at `offset`.
|
|
64
|
+
*
|
|
65
|
+
* Format: one byte holds `[MSB | type:3 | size_lo:4]`. While MSB is set,
|
|
66
|
+
* each additional byte contributes 7 more size bits. Returns the parsed
|
|
67
|
+
* type code, uncompressed size, and the offset just past the header.
|
|
68
|
+
*/
|
|
69
|
+
export function readPackObjectHeader(data, offset) {
|
|
70
|
+
if (offset >= data.length) {
|
|
71
|
+
return Result.err(new PackParseError({
|
|
72
|
+
reason: "truncated-object-header",
|
|
73
|
+
message: "truncated pack object header",
|
|
74
|
+
offset,
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
let byte = data[offset++];
|
|
78
|
+
const type = (byte >> TYPE_FIELD_SHIFT) & TYPE_FIELD_MASK;
|
|
79
|
+
let size = byte & SIZE_LOW_NIBBLE_MASK;
|
|
80
|
+
let shift = SIZE_LOW_NIBBLE_BITS;
|
|
81
|
+
while ((byte & VARINT_CONTINUE) !== 0) {
|
|
82
|
+
if (offset >= data.length) {
|
|
83
|
+
return Result.err(new PackParseError({
|
|
84
|
+
reason: "truncated-object-header",
|
|
85
|
+
message: "truncated pack object header",
|
|
86
|
+
offset,
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
byte = data[offset++];
|
|
90
|
+
size |= (byte & VARINT_VALUE_MASK) << shift;
|
|
91
|
+
shift += VARINT_VALUE_BITS;
|
|
92
|
+
}
|
|
93
|
+
return Result.ok({ type, size, offset });
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Inflate one zlib stream embedded in the pack at `offset`.
|
|
97
|
+
*
|
|
98
|
+
* The pack's 20-byte trailer is sliced off before inflation so the deflate
|
|
99
|
+
* engine never sees it. `bytesWritten` on the `Inflate` engine reports how
|
|
100
|
+
* many input bytes were consumed, which is the only reliable way to advance
|
|
101
|
+
* `offset` to the next entry — multiple deflate streams sit back-to-back
|
|
102
|
+
* inside the pack and there's no length prefix at this layer.
|
|
103
|
+
*
|
|
104
|
+
* @param expectedSize Uncompressed size reported by the object header. When
|
|
105
|
+
* provided, used to right-size the inflate chunk buffer and to assert the
|
|
106
|
+
* output length post-hoc.
|
|
107
|
+
*/
|
|
108
|
+
export function decompressAt(data, offset, expectedSize) {
|
|
109
|
+
const slice = data.subarray(offset, data.length - PACK_TRAILER_SIZE);
|
|
110
|
+
const input = Buffer.from(slice);
|
|
111
|
+
const engine = new zlib.Inflate({
|
|
112
|
+
chunkSize: expectedSize !== undefined ? Math.max(expectedSize + 16, 1024) : 16384,
|
|
113
|
+
});
|
|
114
|
+
let output;
|
|
115
|
+
try {
|
|
116
|
+
output = engine._processChunk(input, zlib.constants.Z_FINISH);
|
|
117
|
+
}
|
|
118
|
+
catch (cause) {
|
|
119
|
+
engine.close();
|
|
120
|
+
return Result.err(new PackParseError({
|
|
121
|
+
reason: "inflate-failed",
|
|
122
|
+
message: `failed to inflate pack object: ${cause?.message ?? cause}`,
|
|
123
|
+
offset,
|
|
124
|
+
cause,
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
127
|
+
const consumed = engine.bytesWritten;
|
|
128
|
+
engine.close();
|
|
129
|
+
if (expectedSize !== undefined && output.length !== expectedSize) {
|
|
130
|
+
return Result.err(new PackParseError({
|
|
131
|
+
reason: "inflated-size-mismatch",
|
|
132
|
+
message: `object inflated to ${output.length} bytes, expected ${expectedSize}`,
|
|
133
|
+
offset,
|
|
134
|
+
}));
|
|
135
|
+
}
|
|
136
|
+
const value = new Uint8Array(output.buffer, output.byteOffset, output.byteLength);
|
|
137
|
+
return Result.ok({ value, offset: offset + consumed });
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Walk every entry in `pack` and return a map of resolved objects keyed by
|
|
141
|
+
* SHA-1.
|
|
142
|
+
*
|
|
143
|
+
* For non-delta entries, the inflated bytes are hashed and stored. For
|
|
144
|
+
* `OBJ_OFS_DELTA` the base is looked up via `byOffset` (always present, since
|
|
145
|
+
* pack ordering guarantees the base appears earlier in the stream). For
|
|
146
|
+
* `OBJ_REF_DELTA` the base may be either earlier or later — unresolved ones
|
|
147
|
+
* accumulate in `pendingRefDeltas` and are retried until either all resolve
|
|
148
|
+
* or a pass makes no progress (cyclic / out-of-pack reference).
|
|
149
|
+
*
|
|
150
|
+
* @param targets Optional set of "wanted" SHAs. If supplied, the parser
|
|
151
|
+
* returns as soon as every target has been materialized — even mid-pack.
|
|
152
|
+
*/
|
|
153
|
+
export function parsePackfile(pack, targets) {
|
|
154
|
+
if (pack.length < PACK_HEADER_SIZE + PACK_TRAILER_SIZE) {
|
|
155
|
+
return Result.err(new PackParseError({
|
|
156
|
+
reason: "truncated-packfile",
|
|
157
|
+
message: "packfile too small",
|
|
158
|
+
offset: 0,
|
|
159
|
+
}));
|
|
160
|
+
}
|
|
161
|
+
const signature = pack.subarray(0, PACK_SIGNATURE_SIZE);
|
|
162
|
+
if (!bytesEqual(signature, PACK_SIGNATURE)) {
|
|
163
|
+
return Result.err(new PackParseError({
|
|
164
|
+
reason: "invalid-signature",
|
|
165
|
+
message: `not a packfile, starts with ${Array.from(signature).join(",")}`,
|
|
166
|
+
offset: 0,
|
|
167
|
+
}));
|
|
168
|
+
}
|
|
169
|
+
const view = new DataView(pack.buffer, pack.byteOffset, pack.byteLength);
|
|
170
|
+
const version = view.getUint32(VERSION_OFFSET);
|
|
171
|
+
const count = view.getUint32(COUNT_OFFSET);
|
|
172
|
+
if (!SUPPORTED_PACK_VERSIONS.has(version)) {
|
|
173
|
+
return Result.err(new PackParseError({
|
|
174
|
+
reason: "unsupported-version",
|
|
175
|
+
message: `unsupported pack version ${version}`,
|
|
176
|
+
offset: VERSION_OFFSET,
|
|
177
|
+
}));
|
|
178
|
+
}
|
|
179
|
+
const bySha = new Map();
|
|
180
|
+
const byOffset = new Map();
|
|
181
|
+
let offset = PACK_HEADER_SIZE;
|
|
182
|
+
let pendingRefDeltas = [];
|
|
183
|
+
function store(typeNumber, content, objectOffset) {
|
|
184
|
+
const type = OBJ_NAMES.get(typeNumber);
|
|
185
|
+
if (!type) {
|
|
186
|
+
return Result.err(new PackParseError({
|
|
187
|
+
reason: "unknown-object-type",
|
|
188
|
+
message: `unknown object type ${typeNumber}`,
|
|
189
|
+
offset: objectOffset,
|
|
190
|
+
}));
|
|
191
|
+
}
|
|
192
|
+
const sha = sha1OfObject(typeNumber, content);
|
|
193
|
+
if (sha.isErr())
|
|
194
|
+
return Result.err(sha.error);
|
|
195
|
+
const object = { type, content };
|
|
196
|
+
bySha.set(sha.value, object);
|
|
197
|
+
byOffset.set(objectOffset, object);
|
|
198
|
+
return Result.ok(sha.value);
|
|
199
|
+
}
|
|
200
|
+
function foundTargets() {
|
|
201
|
+
return targets !== undefined && [...targets].every((sha) => bySha.has(sha));
|
|
202
|
+
}
|
|
203
|
+
for (let i = 0; i < count; i++) {
|
|
204
|
+
const objectOffset = offset;
|
|
205
|
+
const header = readPackObjectHeader(pack, offset);
|
|
206
|
+
if (header.isErr())
|
|
207
|
+
return Result.err(header.error);
|
|
208
|
+
offset = header.value.offset;
|
|
209
|
+
if (OBJ_NAMES.has(header.value.type)) {
|
|
210
|
+
const result = decompressAt(pack, offset, header.value.size);
|
|
211
|
+
if (result.isErr())
|
|
212
|
+
return Result.err(result.error);
|
|
213
|
+
const stored = store(header.value.type, result.value.value, objectOffset);
|
|
214
|
+
if (stored.isErr())
|
|
215
|
+
return Result.err(stored.error);
|
|
216
|
+
if (foundTargets())
|
|
217
|
+
return Result.ok(bySha);
|
|
218
|
+
offset = result.value.offset;
|
|
219
|
+
}
|
|
220
|
+
else if (header.value.type === OBJ_OFS_DELTA) {
|
|
221
|
+
const varint = readVarintBe(pack, offset);
|
|
222
|
+
if (varint.isErr())
|
|
223
|
+
return Result.err(varint.error);
|
|
224
|
+
offset = varint.value.offset;
|
|
225
|
+
const base = byOffset.get(objectOffset - varint.value.value);
|
|
226
|
+
if (!base) {
|
|
227
|
+
return Result.err(new PackParseError({
|
|
228
|
+
reason: "unresolved-ofs-delta-base",
|
|
229
|
+
message: "unresolved ofs-delta base object",
|
|
230
|
+
offset: objectOffset,
|
|
231
|
+
}));
|
|
232
|
+
}
|
|
233
|
+
const result = decompressAt(pack, offset, header.value.size);
|
|
234
|
+
if (result.isErr())
|
|
235
|
+
return Result.err(result.error);
|
|
236
|
+
const applied = applyDelta(base.content, result.value.value);
|
|
237
|
+
if (applied.isErr())
|
|
238
|
+
return Result.err(applied.error);
|
|
239
|
+
const typeNumber = typeNumberOf(base.type);
|
|
240
|
+
if (typeNumber.isErr())
|
|
241
|
+
return Result.err(typeNumber.error);
|
|
242
|
+
const stored = store(typeNumber.value, applied.value, objectOffset);
|
|
243
|
+
if (stored.isErr())
|
|
244
|
+
return Result.err(stored.error);
|
|
245
|
+
if (foundTargets())
|
|
246
|
+
return Result.ok(bySha);
|
|
247
|
+
offset = result.value.offset;
|
|
248
|
+
}
|
|
249
|
+
else if (header.value.type === OBJ_REF_DELTA) {
|
|
250
|
+
if (offset + REF_DELTA_SHA_SIZE > pack.length) {
|
|
251
|
+
return Result.err(new PackParseError({
|
|
252
|
+
reason: "truncated-ref-delta-base",
|
|
253
|
+
message: "truncated ref-delta base sha",
|
|
254
|
+
offset,
|
|
255
|
+
}));
|
|
256
|
+
}
|
|
257
|
+
const baseSha = encodeHex(pack.subarray(offset, offset + REF_DELTA_SHA_SIZE));
|
|
258
|
+
offset += REF_DELTA_SHA_SIZE;
|
|
259
|
+
const result = decompressAt(pack, offset, header.value.size);
|
|
260
|
+
if (result.isErr())
|
|
261
|
+
return Result.err(result.error);
|
|
262
|
+
pendingRefDeltas.push({ offset: objectOffset, baseSha, delta: result.value.value });
|
|
263
|
+
offset = result.value.offset;
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
return Result.err(new PackParseError({
|
|
267
|
+
reason: "unknown-object-type",
|
|
268
|
+
message: `unknown object type ${header.value.type}`,
|
|
269
|
+
offset: objectOffset,
|
|
270
|
+
}));
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
while (pendingRefDeltas.length > 0) {
|
|
274
|
+
const remaining = [];
|
|
275
|
+
let resolved = 0;
|
|
276
|
+
for (const delta of pendingRefDeltas) {
|
|
277
|
+
const base = bySha.get(delta.baseSha);
|
|
278
|
+
if (!base) {
|
|
279
|
+
remaining.push(delta);
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
const applied = applyDelta(base.content, delta.delta);
|
|
283
|
+
if (applied.isErr())
|
|
284
|
+
return Result.err(applied.error);
|
|
285
|
+
const typeNumber = typeNumberOf(base.type);
|
|
286
|
+
if (typeNumber.isErr())
|
|
287
|
+
return Result.err(typeNumber.error);
|
|
288
|
+
const stored = store(typeNumber.value, applied.value, delta.offset);
|
|
289
|
+
if (stored.isErr())
|
|
290
|
+
return Result.err(stored.error);
|
|
291
|
+
if (foundTargets())
|
|
292
|
+
return Result.ok(bySha);
|
|
293
|
+
resolved++;
|
|
294
|
+
}
|
|
295
|
+
if (resolved === 0) {
|
|
296
|
+
return Result.err(new PackParseError({
|
|
297
|
+
reason: "unresolved-ref-delta-base",
|
|
298
|
+
message: `unresolved ref-delta base object(s): ${remaining.map((item) => item.baseSha).slice(0, 3).join(", ")}`,
|
|
299
|
+
}));
|
|
300
|
+
}
|
|
301
|
+
pendingRefDeltas = remaining;
|
|
302
|
+
}
|
|
303
|
+
return Result.ok(bySha);
|
|
304
|
+
}
|
package/esm/package.json
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module pkt-line
|
|
3
|
+
*
|
|
4
|
+
* Encode/decode Git's framing format. A pkt-line is a 4-byte ASCII-hex length
|
|
5
|
+
* prefix followed by `length - 4` payload bytes. Three reserved length values
|
|
6
|
+
* carry no payload and act as control markers (flush / delim / response-end).
|
|
7
|
+
*
|
|
8
|
+
* Spec: https://git-scm.com/docs/protocol-common
|
|
9
|
+
*/
|
|
10
|
+
import { Result } from "better-result";
|
|
11
|
+
import { PktLineError } from "../errors.js";
|
|
12
|
+
const encoder = new TextEncoder();
|
|
13
|
+
const decoder = new TextDecoder();
|
|
14
|
+
/** Width of the ASCII-hex length prefix that opens every pkt-line. */
|
|
15
|
+
const PKT_LENGTH_PREFIX_SIZE = 4;
|
|
16
|
+
/** Numeric base for the length prefix (lowercase hex). */
|
|
17
|
+
const PKT_LENGTH_HEX_BASE = 16;
|
|
18
|
+
/** Hard cap on total pkt-line size (length prefix + payload) per the spec. */
|
|
19
|
+
const MAX_PKT_LINE_LENGTH = 0xffff;
|
|
20
|
+
/** Sentinel length signalling end-of-section. Payload is empty. */
|
|
21
|
+
const FLUSH_PKT_LENGTH = 0;
|
|
22
|
+
/** Sentinel length separating header from body in protocol v2 requests. */
|
|
23
|
+
const DELIM_PKT_LENGTH = 1;
|
|
24
|
+
/** Sentinel length marking the end of a v2 response stream. */
|
|
25
|
+
const RESPONSE_END_PKT_LENGTH = 2;
|
|
26
|
+
/** Encoded `"0000"` flush packet — closes a section in v0 and v2. */
|
|
27
|
+
export const FLUSH_PKT = encoder.encode("0000");
|
|
28
|
+
/** Encoded `"0001"` delim packet — only valid in protocol v2. */
|
|
29
|
+
export const DELIM_PKT = encoder.encode("0001");
|
|
30
|
+
/** Encoded `"0002"` response-end packet — only valid in protocol v2. */
|
|
31
|
+
export const RESPONSE_END_PKT = encoder.encode("0002");
|
|
32
|
+
/**
|
|
33
|
+
* Wrap `payload` in a pkt-line frame.
|
|
34
|
+
*
|
|
35
|
+
* An empty payload returns the flush packet rather than a 4-byte zero-length
|
|
36
|
+
* frame, matching `git`'s convention.
|
|
37
|
+
*
|
|
38
|
+
* @param payload Raw bytes to frame. Pass any trailing `\n` you want preserved.
|
|
39
|
+
* @returns The framed bytes, or {@link PktLineError} if the total would exceed
|
|
40
|
+
* {@link MAX_PKT_LINE_LENGTH}.
|
|
41
|
+
*/
|
|
42
|
+
export function pktLine(payload) {
|
|
43
|
+
if (payload.length === 0) {
|
|
44
|
+
return Result.ok(FLUSH_PKT);
|
|
45
|
+
}
|
|
46
|
+
const length = payload.length + PKT_LENGTH_PREFIX_SIZE;
|
|
47
|
+
if (length > MAX_PKT_LINE_LENGTH) {
|
|
48
|
+
return Result.err(new PktLineError({
|
|
49
|
+
reason: "pkt-line-too-long",
|
|
50
|
+
message: `pkt-line too long: ${length}`,
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
const prefix = encoder.encode(length.toString(PKT_LENGTH_HEX_BASE).padStart(PKT_LENGTH_PREFIX_SIZE, "0"));
|
|
54
|
+
const out = new Uint8Array(length);
|
|
55
|
+
out.set(prefix, 0);
|
|
56
|
+
out.set(payload, PKT_LENGTH_PREFIX_SIZE);
|
|
57
|
+
return Result.ok(out);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Decode a sequence of pkt-lines from `buf`.
|
|
61
|
+
*
|
|
62
|
+
* Returns one entry per frame. Control packets (flush/delim/response-end) have
|
|
63
|
+
* `payload: null`. Data packets expose `payload` as a subarray view into `buf` —
|
|
64
|
+
* no bytes are copied. Trailing `\n` bytes inside payloads are preserved; callers
|
|
65
|
+
* that want them stripped must do so explicitly.
|
|
66
|
+
*
|
|
67
|
+
* @param buf Bytes containing zero or more concatenated pkt-lines.
|
|
68
|
+
* @param offset Starting byte index in `buf` (default `0`).
|
|
69
|
+
* @returns The decoded frames in order, or {@link PktLineError} on a bad length
|
|
70
|
+
* prefix or a frame that runs past `buf`'s end.
|
|
71
|
+
*/
|
|
72
|
+
export function parsePktLines(buf, offset = 0) {
|
|
73
|
+
const lines = [];
|
|
74
|
+
while (offset + PKT_LENGTH_PREFIX_SIZE <= buf.length) {
|
|
75
|
+
const lengthOffset = offset;
|
|
76
|
+
const lengthHex = decoder.decode(buf.subarray(offset, offset + PKT_LENGTH_PREFIX_SIZE));
|
|
77
|
+
const length = Number.parseInt(lengthHex, PKT_LENGTH_HEX_BASE);
|
|
78
|
+
if (!Number.isFinite(length) || !/^[0-9a-fA-F]{4}$/.test(lengthHex)) {
|
|
79
|
+
return Result.err(new PktLineError({
|
|
80
|
+
reason: "invalid-length-prefix",
|
|
81
|
+
message: `invalid pkt-line length: ${lengthHex}`,
|
|
82
|
+
offset: lengthOffset,
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
if (length === FLUSH_PKT_LENGTH || length === DELIM_PKT_LENGTH ||
|
|
86
|
+
length === RESPONSE_END_PKT_LENGTH) {
|
|
87
|
+
offset += PKT_LENGTH_PREFIX_SIZE;
|
|
88
|
+
lines.push({ offset, payload: null });
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (length < PKT_LENGTH_PREFIX_SIZE || offset + length > buf.length) {
|
|
92
|
+
return Result.err(new PktLineError({
|
|
93
|
+
reason: "truncated-pkt-line",
|
|
94
|
+
message: `truncated pkt-line length ${length}`,
|
|
95
|
+
offset: lengthOffset,
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
const payload = buf.subarray(offset + PKT_LENGTH_PREFIX_SIZE, offset + length);
|
|
99
|
+
offset += length;
|
|
100
|
+
lines.push({ offset, payload });
|
|
101
|
+
}
|
|
102
|
+
return Result.ok(lines);
|
|
103
|
+
}
|