gix-object-0.60.0/.cargo_vcs_info.json0000644000000001501046102023000131700ustar { "git": { "sha1": "53f880c7604232c367870088176e42efd8a5b783" }, "path_in_vcs": "gix-object" }gix-object-0.60.0/Cargo.lock0000644000001062711046102023000111560ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "aho-corasick" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] [[package]] name = "alloca" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4" dependencies = [ "cc", ] [[package]] name = "anes" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstyle" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anyhow" version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "bitflags" version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "block-buffer" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] [[package]] name = "bstr" version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" dependencies = [ "memchr", "regex-automata", "serde", ] [[package]] name = "bumpalo" version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cast" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" version = "1.2.60" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" dependencies = [ "find-msvc-tools", "shlex", ] [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "ciborium" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" dependencies = [ "ciborium-io", "ciborium-ll", "serde", ] [[package]] name = "ciborium-io" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" [[package]] name = "ciborium-ll" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" dependencies = [ "ciborium-io", "half", ] [[package]] name = "clap" version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstyle", "clap_lex", ] [[package]] name = "clap_lex" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "console" version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" dependencies = [ "encode_unicode", "libc", "windows-sys", ] [[package]] name = "cpufeatures" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] [[package]] name = "criterion" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3" dependencies = [ "alloca", "anes", "cast", "ciborium", "clap", "criterion-plot", "itertools", "num-traits", "oorandom", "page_size", "plotters", "rayon", "regex", "serde", "serde_json", "tinytemplate", "walkdir", ] [[package]] name = "criterion-plot" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea" dependencies = [ "cast", "itertools", ] [[package]] name = "crossbeam-deque" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-common" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", ] [[package]] name = "diff" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", ] [[package]] name = "document-features" version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" dependencies = [ "litrs", ] [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "encode_unicode" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", "windows-sys", ] [[package]] name = "faster-hex" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7223ae2d2f179b803433d9c830478527e92b8117eab39460edae7f1614d9fb73" dependencies = [ "heapless", "serde", ] [[package]] name = "fastrand" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "foldhash" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "generic-array" version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", ] [[package]] name = "getrandom" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", "r-efi", "wasip2", "wasip3", ] [[package]] name = "gix-actor" version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "272916673b83714734b15d4ef3c8b5f1ccddb15fea8ff548430b97c1ab7b7ed8" dependencies = [ "bstr", "gix-date", "gix-error", "serde", ] [[package]] name = "gix-date" version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b94cdae4eb4b0f4136e3d9b3aa2d2cd03cfb5bb9b636b31263aea2df86d41543" dependencies = [ "bstr", "gix-error", "itoa", "jiff", "serde", "smallvec", ] [[package]] name = "gix-error" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e207b971746ab724fccdfced2e4e19e854744611904a0195d3aa8fda8a110613" dependencies = [ "bstr", ] [[package]] name = "gix-features" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af375693ad5333d0a2c66b4c5b2cbe9ccc38e34f8e8bf24e4ae42c12307fdc4f" dependencies = [ "gix-trace", "libc", "prodash", ] [[package]] name = "gix-hash" version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcf70d1e252337eed16360f8b8ebb71865ece58eab7954b39ce38b420de703d2" dependencies = [ "faster-hex", "gix-features", "serde", "sha1-checked", "thiserror", ] [[package]] name = "gix-hashtable" version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d33b455e07b3c16d3b2eeebc7b38d2dafcbf8a653de1138ef55d4c2a1fd0b08b" dependencies = [ "gix-hash", "hashbrown 0.16.1", "parking_lot", ] [[package]] name = "gix-object" version = "0.60.0" dependencies = [ "bstr", "criterion", "document-features", "gix-actor", "gix-date", "gix-features", "gix-hash", "gix-hashtable", "gix-utils", "gix-validate", "insta", "itoa", "pretty_assertions", "serde", "smallvec", "termtree", "thiserror", ] [[package]] name = "gix-trace" version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f23569e55f2ffaf958617353b9734a7d52a7c19c439eeaa5e3efc217fd2270e" [[package]] name = "gix-utils" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e477b4f07a6e8da4ba791c53c858102959703c60d70f199932010d5b94adb2c" dependencies = [ "fastrand", "unicode-normalization", ] [[package]] name = "gix-validate" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e26ac2602b43eadfdca0560b81d3341944162a3c9f64ccdeef8fc501ad80dad5" dependencies = [ "bstr", ] [[package]] name = "half" version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ "cfg-if", "crunchy", "zerocopy", ] [[package]] name = "hash32" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" dependencies = [ "byteorder", ] [[package]] name = "hashbrown" version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "foldhash", ] [[package]] name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "hashbrown" version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" [[package]] name = "heapless" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" dependencies = [ "hash32", "stable_deref_trait", ] [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "id-arena" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" [[package]] name = "indexmap" version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", "hashbrown 0.17.0", "serde", "serde_core", ] [[package]] name = "insta" version = "1.47.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b4a6248eb93a4401ed2f37dfe8ea592d3cf05b7cf4f8efa867b6895af7e094e" dependencies = [ "console", "once_cell", "similar", "tempfile", ] [[package]] name = "itertools" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] [[package]] name = "itoa" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" dependencies = [ "jiff-static", "jiff-tzdb-platform", "log", "portable-atomic", "portable-atomic-util", "serde_core", "windows-sys", ] [[package]] name = "jiff-static" version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "jiff-tzdb" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c900ef84826f1338a557697dc8fc601df9ca9af4ac137c7fb61d4c6f2dfd3076" [[package]] name = "jiff-tzdb-platform" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" dependencies = [ "jiff-tzdb", ] [[package]] name = "js-sys" version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" dependencies = [ "once_cell", "wasm-bindgen", ] [[package]] name = "leb128fmt" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" version = "0.2.185" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" [[package]] name = "linux-raw-sys" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litrs" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" [[package]] name = "lock_api" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ "scopeguard", ] [[package]] name = "log" version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "memchr" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "once_cell" version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "oorandom" version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" [[package]] name = "page_size" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" dependencies = [ "libc", "winapi", ] [[package]] name = "parking_lot" version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", "windows-link", ] [[package]] name = "plotters" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" dependencies = [ "num-traits", "plotters-backend", "plotters-svg", "wasm-bindgen", "web-sys", ] [[package]] name = "plotters-backend" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" [[package]] name = "plotters-svg" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" dependencies = [ "plotters-backend", ] [[package]] name = "portable-atomic" version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" dependencies = [ "portable-atomic", ] [[package]] name = "pretty_assertions" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" dependencies = [ "diff", "yansi", ] [[package]] name = "prettyplease" version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", "syn", ] [[package]] name = "proc-macro2" version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "prodash" version = "31.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "962200e2d7d551451297d9fdce85138374019ada198e30ea9ede38034e27604c" dependencies = [ "parking_lot", ] [[package]] name = "quote" version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rayon" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ "either", "rayon-core", ] [[package]] name = "rayon-core" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", ] [[package]] name = "redox_syscall" version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ "bitflags", ] [[package]] name = "regex" version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "rustix" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", "windows-sys", ] [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "same-file" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ "winapi-util", ] [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "semver" version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", "serde_derive", ] [[package]] name = "serde_core" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", "serde", "serde_core", "zmij", ] [[package]] name = "sha1" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "sha1-checked" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89f599ac0c323ebb1c6082821a54962b839832b03984598375bff3975b804423" dependencies = [ "digest", "sha1", ] [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "similar" version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" dependencies = [ "serde", ] [[package]] name = "stable_deref_trait" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "syn" version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "tempfile" version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", "getrandom", "once_cell", "rustix", "windows-sys", ] [[package]] name = "termtree" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4d1330fe7f7f872cd05165130b10602d667b205fd85be09be2814b115d4ced9" [[package]] name = "thiserror" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tinytemplate" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" dependencies = [ "serde", "serde_json", ] [[package]] name = "tinyvec" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] [[package]] name = "tinyvec_macros" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "typenum" version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-normalization" version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" dependencies = [ "tinyvec", ] [[package]] name = "unicode-xid" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "walkdir" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", ] [[package]] name = "wasip2" version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ "wit-bindgen 0.57.1", ] [[package]] name = "wasip3" version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ "wit-bindgen 0.51.0", ] [[package]] name = "wasm-bindgen" version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" dependencies = [ "bumpalo", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" dependencies = [ "unicode-ident", ] [[package]] name = "wasm-encoder" version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" dependencies = [ "leb128fmt", "wasmparser", ] [[package]] name = "wasm-metadata" version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", "indexmap", "wasm-encoder", "wasmparser", ] [[package]] name = "wasmparser" version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ "bitflags", "hashbrown 0.15.5", "indexmap", "semver", ] [[package]] name = "web-sys" version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ "windows-sys", ] [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-sys" version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", ] [[package]] name = "wit-bindgen" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" dependencies = [ "wit-bindgen-rust-macro", ] [[package]] name = "wit-bindgen" version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] name = "wit-bindgen-core" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" dependencies = [ "anyhow", "heck", "wit-parser", ] [[package]] name = "wit-bindgen-rust" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", "heck", "indexmap", "prettyplease", "syn", "wasm-metadata", "wit-bindgen-core", "wit-component", ] [[package]] name = "wit-bindgen-rust-macro" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" dependencies = [ "anyhow", "prettyplease", "proc-macro2", "quote", "syn", "wit-bindgen-core", "wit-bindgen-rust", ] [[package]] name = "wit-component" version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", "bitflags", "indexmap", "log", "serde", "serde_derive", "serde_json", "wasm-encoder", "wasm-metadata", "wasmparser", "wit-parser", ] [[package]] name = "wit-parser" version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ "anyhow", "id-arena", "indexmap", "log", "semver", "serde", "serde_derive", "serde_json", "unicode-xid", "wasmparser", ] [[package]] name = "yansi" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] name = "zerocopy" version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" gix-object-0.60.0/Cargo.toml0000644000000105151046102023000111740ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.82" name = "gix-object" version = "0.60.0" authors = ["Sebastian Thiel "] build = false include = [ "/src/**/*", "/LICENSE-*", ] autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "Immutable and mutable git objects with decoding and encoding support" readme = false license = "MIT OR Apache-2.0" repository = "https://github.com/GitoxideLabs/gitoxide" [package.metadata.docs.rs] all-features = true features = [ "sha1", "document-features", ] [features] serde = [ "dep:serde", "bstr/serde", "smallvec/serde", "gix-hash/serde", "gix-actor/serde", ] sha1 = ["gix-hash/sha1"] [lib] name = "gix_object" path = "src/lib.rs" doctest = true [dependencies.bstr] version = "1.12.0" features = [ "std", "unicode", ] default-features = false [dependencies.document-features] version = "0.2.0" optional = true [dependencies.gix-actor] version = "^0.41.0" [dependencies.gix-date] version = "^0.15.3" [dependencies.gix-features] version = "^0.48.0" features = ["progress"] [dependencies.gix-hash] version = "^0.25.0" [dependencies.gix-hashtable] version = "^0.15.0" [dependencies.gix-utils] version = "^0.3.2" [dependencies.gix-validate] version = "^0.11.1" [dependencies.itoa] version = "1.0.17" [dependencies.serde] version = "1.0.114" features = ["derive"] optional = true default-features = false [dependencies.smallvec] version = "1.15.1" features = ["write"] [dependencies.thiserror] version = "2.0.18" [dev-dependencies.criterion] version = "0.8.2" [dev-dependencies.insta] version = "1.46.3" [dev-dependencies.pretty_assertions] version = "1.0.0" [dev-dependencies.termtree] version = "1.0.0" [lints.clippy] bool_to_int_with_if = "allow" borrow_as_ptr = "allow" cast_lossless = "allow" cast_possible_truncation = "allow" cast_possible_wrap = "allow" cast_precision_loss = "allow" cast_sign_loss = "allow" checked_conversions = "allow" copy_iterator = "allow" default_trait_access = "allow" doc_markdown = "allow" empty_docs = "allow" enum_glob_use = "allow" explicit_deref_methods = "allow" explicit_into_iter_loop = "allow" explicit_iter_loop = "allow" filter_map_next = "allow" fn_params_excessive_bools = "allow" from_iter_instead_of_collect = "allow" if_not_else = "allow" ignored_unit_patterns = "allow" implicit_clone = "allow" inconsistent_struct_constructor = "allow" inefficient_to_string = "allow" inline_always = "allow" items_after_statements = "allow" iter_not_returning_iterator = "allow" iter_without_into_iter = "allow" large_enum_variant = "allow" large_stack_arrays = "allow" manual_assert = "allow" manual_is_variant_and = "allow" manual_let_else = "allow" manual_string_new = "allow" many_single_char_names = "allow" match_bool = "allow" match_same_arms = "allow" match_wild_err_arm = "allow" match_wildcard_for_single_variants = "allow" missing_errors_doc = "allow" missing_panics_doc = "allow" module_name_repetitions = "allow" must_use_candidate = "allow" mut_mut = "allow" naive_bytecount = "allow" needless_continue = "allow" needless_for_each = "allow" needless_pass_by_value = "allow" needless_raw_string_hashes = "allow" no_effect_underscore_binding = "allow" option_option = "allow" range_plus_one = "allow" redundant_else = "allow" result_large_err = "allow" return_self_not_must_use = "allow" should_panic_without_expect = "allow" similar_names = "allow" single_match_else = "allow" stable_sort_primitive = "allow" struct_excessive_bools = "allow" struct_field_names = "allow" too_long_first_doc_paragraph = "allow" too_many_lines = "allow" transmute_ptr_to_ptr = "allow" trivially_copy_pass_by_ref = "allow" unnecessary_join = "allow" unnecessary_wraps = "allow" unreadable_literal = "allow" unused_self = "allow" used_underscore_binding = "allow" wildcard_imports = "allow" [lints.clippy.pedantic] level = "warn" priority = -1 [lints.rust] gix-object-0.60.0/Cargo.toml.orig000064400000000000000000000040151046102023000146310ustar 00000000000000lints.workspace = true [package] name = "gix-object" version = "0.60.0" description = "Immutable and mutable git objects with decoding and encoding support" authors = ["Sebastian Thiel "] repository = "https://github.com/GitoxideLabs/gitoxide" license = "MIT OR Apache-2.0" edition = "2021" include = ["/src/**/*", "/LICENSE-*"] rust-version = "1.82" [lib] doctest = true [[bench]] name = "decode-objects" harness = false path = "./benches/decode_objects.rs" [[bench]] name = "edit-tree" harness = false path = "./benches/edit_tree.rs" [features] ## Enable support for the SHA-1 hash by enabling the respective feature in the `gix-hash` crate. sha1 = ["gix-hash/sha1"] ## Data structures implement `serde::Serialize` and `serde::Deserialize`. serde = [ "dep:serde", "bstr/serde", "smallvec/serde", "gix-hash/serde", "gix-actor/serde", ] [dependencies] gix-features = { version = "^0.48.0", path = "../gix-features", features = [ "progress", ] } gix-hash = { version = "^0.25.0", path = "../gix-hash" } gix-hashtable = { version = "^0.15.0", path = "../gix-hashtable" } gix-validate = { version = "^0.11.1", path = "../gix-validate" } gix-actor = { version = "^0.41.0", path = "../gix-actor" } gix-date = { version = "^0.15.3", path = "../gix-date" } gix-utils = { version = "^0.3.2", path = "../gix-utils" } itoa = "1.0.17" thiserror = "2.0.18" bstr = { version = "1.12.0", default-features = false, features = [ "std", "unicode", ] } smallvec = { version = "1.15.1", features = ["write"] } serde = { version = "1.0.114", optional = true, default-features = false, features = [ "derive", ] } document-features = { version = "0.2.0", optional = true } [dev-dependencies] criterion = "0.8.2" pretty_assertions = "1.0.0" insta = "1.46.3" gix-testtools = { path = "../tests/tools" } gix-odb = { path = "../gix-odb" } gix-hash = { path = "../gix-hash", features = ["sha1", "sha256"] } termtree = "1.0.0" [package.metadata.docs.rs] all-features = true features = ["sha1", "document-features"] gix-object-0.60.0/LICENSE-APACHE000064400000000000000000000236761046102023000137040ustar 00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS gix-object-0.60.0/LICENSE-MIT000064400000000000000000000017771046102023000134120ustar 00000000000000Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. gix-object-0.60.0/src/blob.rs000064400000000000000000000022641046102023000140210ustar 00000000000000use std::{convert::Infallible, io}; use crate::{Blob, BlobRef, Kind}; impl crate::WriteTo for BlobRef<'_> { /// Write the blobs data to `out` verbatim. fn write_to(&self, out: &mut dyn io::Write) -> io::Result<()> { out.write_all(self.data) } fn kind(&self) -> Kind { Kind::Blob } fn size(&self) -> u64 { self.data.len() as u64 } } impl crate::WriteTo for Blob { /// Write the blobs data to `out` verbatim. fn write_to(&self, out: &mut dyn io::Write) -> io::Result<()> { self.to_ref().write_to(out) } fn kind(&self) -> Kind { Kind::Blob } fn size(&self) -> u64 { self.to_ref().size() } } impl Blob { /// Provide a `BlobRef` to this owned blob pub fn to_ref(&self) -> BlobRef<'_> { BlobRef { data: &self.data } } } impl BlobRef<'_> { /// Instantiate a `Blob` from the given `data`, which is used as-is. pub fn from_bytes(data: &[u8]) -> Result, Infallible> { Ok(BlobRef { data }) } /// Clone the data in this instance by allocating a new vector for a fully owned blob. pub fn into_owned(self) -> Blob { self.into() } } gix-object-0.60.0/src/commit/decode.rs000064400000000000000000000062541046102023000156210ustar 00000000000000use std::borrow::Cow; use smallvec::SmallVec; use crate::{parse, parse::ParseResult, BStr, ByteSlice, CommitRef}; /// Parse the commit message after the header/message separator. /// /// Typical input starts with the blank-line separator before the message, for /// example `\nsubject\n\nbody\n`. The returned message excludes that first /// separator newline and borrows all remaining bytes from `i`. /// /// On success, `i` is advanced to the empty suffix as commits end with a message. pub fn message<'a>(i: &mut &'a [u8]) -> ParseResult<&'a BStr> { if let Some(rest) = i.strip_prefix(parse::NL) { *i = &[]; Ok(rest.as_bstr()) } else { Err(crate::decode::Error) } } /// Parse a complete commit object body. /// /// Typical input starts with `tree \n`, followed by zero or more /// `parent \n` headers, then `author \n` and /// `committer \n`. An optional `encoding \n` header and any /// number of extra single-line or multi-line headers may follow. The headers /// are terminated by a blank line, after which all remaining bytes are the /// commit message. /// /// On success, the returned [`CommitRef`] borrows fields from `i` where /// possible, and `i` is advanced to the empty suffix. Extra single-line header /// values are borrowed, while multi-line header values are unfolded into owned /// buffers. /// /// This parser is not transactional as a whole: if a later required field or /// the final message parse fails, `i` may already have been advanced past /// earlier successfully parsed fields. pub fn commit<'a>(i: &mut &'a [u8], hash_kind: gix_hash::Kind) -> ParseResult> { let tree = parse::header_field(i, b"tree", |value| parse::hex_hash(value, hash_kind))?; let mut parents = SmallVec::new(); loop { let before = *i; match parse::header_field(i, b"parent", |value| parse::hex_hash(value, hash_kind)) { Ok(parent) => parents.push(parent), Err(_) => { *i = before; break; } } } let author = parse::header_field(i, b"author", parse::signature_raw)?; let committer = parse::header_field(i, b"committer", parse::signature_raw)?; let before = *i; let encoding = match parse::header_field(i, b"encoding", Ok) { Ok(encoding) => Some(encoding.as_bstr()), Err(_) => { *i = before; None } }; let mut extra_headers = Vec::new(); loop { let before = *i; match parse::any_header_field_multi_line(i) .map(|(k, v)| (k.as_bstr(), Cow::Owned(v))) .or_else(|_| { *i = before; parse::any_header_field(i).map(|(k, v)| (k.as_bstr(), Cow::Borrowed(v.as_bstr()))) }) { Ok(header) => extra_headers.push(header), Err(_) => { *i = before; break; } } } let message = message(i)?; if !i.is_empty() { return Err(crate::decode::Error); } Ok(CommitRef { tree, parents, author, committer, encoding, message, extra_headers, }) } gix-object-0.60.0/src/commit/message/body.rs000064400000000000000000000350041046102023000167520ustar 00000000000000use std::{borrow::Cow, ops::Deref}; use crate::{ bstr::{BStr, BString, ByteSlice, ByteVec}, commit::message::BodyRef, }; /// An iterator over trailers as parsed from a commit message body. /// /// lines with parsing failures will be skipped pub struct Trailers<'a> { pub(crate) cursor: &'a [u8], } /// A trailer as parsed from the commit message body. #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct TrailerRef<'a> { /// The name of the trailer, like "Signed-off-by", up to the separator `: `. #[cfg_attr(feature = "serde", serde(borrow))] pub token: &'a BStr, /// The value right after the separator `: `, with leading and trailing whitespace trimmed. /// Multi-line values are unfolded to match `git interpret-trailers --parse`, which is when /// this field is [`Cow::Owned`]. #[cfg_attr(feature = "serde", serde(borrow))] pub value: Cow<'a, BStr>, } // Git treats these as built-in, recognized trailer prefixes when deciding whether a // trailing paragraph is a trailer block at all. The cherry-pick marker is special in // that it is not a `token: value` trailer, but it still contributes to Git's // recognized-prefix / 25% heuristic in `interpret-trailers`. const GIT_GENERATED_PREFIXES: [&[u8]; 2] = [b"Signed-off-by: ", b"(cherry picked from commit "]; #[derive(Clone, Copy)] /// A physical line in the original message body. /// /// `text` has its trailing line ending removed for parsing, while `start` /// points to the first byte of that line in the original `body` slice. struct Line<'a> { /// The line contents without a trailing `\n` or `\r\n`. text: &'a [u8], /// Byte offset of the start of this line in the original body buffer. start: usize, } /// Windows or linux line endings are supported here. fn trim_line_ending(mut line: &[u8]) -> &[u8] { if let Some(stripped) = line.strip_suffix(b"\n") { line = stripped; if let Some(stripped) = line.strip_suffix(b"\r") { line = stripped; } } else if let Some(stripped) = line.strip_suffix(b"\r") { line = stripped; } line } /// Split `input` into physical lines while keeping enough information to map /// parser decisions back to the original byte slice. /// /// This is different from using plain `.lines()` because trailer block detection /// needs normalized line contents for parsing *and* exact byte offsets to slice /// the original body at the eventual trailer boundary. fn lines(input: &[u8]) -> Vec> { let mut start = 0; input .lines_with_terminator() .map(|raw| { let line = Line { text: trim_line_ending(raw), start, }; start += raw.len(); line }) .collect() } /// Find the byte position of a Git trailer separator in `line`. /// /// This recognizes the `:` that terminates a trailer token like `Acked-by: Alice` /// as well as the looser Git form with optional whitespace before the separator, /// like `Acked-by : Alice`. fn find_separator(line: &[u8]) -> Option { let mut whitespace_found = false; for (idx, byte) in line.iter().copied().enumerate() { if byte == b':' { return Some(idx); } if !whitespace_found && (byte.is_ascii_alphanumeric() || byte == b'-') { continue; } if idx != 0 && matches!(byte, b' ' | b'\t') { whitespace_found = true; continue; } break; } None } /// Parse a single physical trailer line. /// /// Returns `None` if `line` is not a valid trailer line at all. /// /// Returns `Some((token, separator_offset))` if parsing succeeds, where `token` /// is the normalized trailer token as a `BStr` and `separator_offset` is the /// byte offset of the `:` separator in the original `line`. Callers use that /// offset to slice out the raw value bytes, potentially including following /// continuation lines. fn parse_trailer_line(line: &[u8]) -> Option<(&BStr, usize)> { if line.first().is_some_and(u8::is_ascii_whitespace) { return None; } let separator = find_separator(line)?; (separator > 0).then_some((line[..separator].trim().as_bstr(), separator)) } fn is_blank_line(line: &[u8]) -> bool { line.iter().all(u8::is_ascii_whitespace) } fn is_recognized_prefix(line: &[u8]) -> bool { GIT_GENERATED_PREFIXES.iter().any(|prefix| line.starts_with(prefix)) } /// Turn a raw trailer value, possibly spanning multiple physical lines, into /// the unfolded value Git would expose for parsing. /// /// A single-line value is returned borrowed. If continuation lines are present, /// embedded newlines and leading continuation whitespace are collapsed into /// single spaces and the unfolded value is returned owned. fn unfold_value(value: &[u8]) -> Cow<'_, BStr> { let mut physical_lines = value.lines().peekable(); let Some(first_line) = physical_lines.next() else { return Cow::Borrowed(b"".as_bstr()); }; if physical_lines.peek().is_none() { return Cow::Borrowed(first_line.trim().as_bstr()); } let mut out = BString::from(first_line.trim()); for line in physical_lines { let line = line.trim(); if line.is_empty() { continue; } if !out.is_empty() { out.push_byte(b' '); } out.extend_from_slice(line); } Cow::Owned(out) } /// Find the byte offset at which the trailer block begins in `body`. /// /// Returns `None` if the trailing paragraph does not satisfy Git's trailer-block /// heuristic. Returns `Some(offset)` if it does, where `offset` points into the /// original `body` slice at the first byte that belongs to the trailer block, /// including the separating blank line when one is present. /// /// Internally this mirrors Git's backward scan: count trailer lines, /// non-trailer lines, continuation lines, and whether a recognized built-in /// prefix was seen, then apply the "all trailers" or recognized-prefix / 25% /// rule to the last paragraph of the body. fn trailer_block_start(body: &[u8]) -> Option { /// Git accepts the trailing paragraph either if it is made entirely of /// trailers, or if it contains at least one recognized built-in trailer /// prefix and at least 25% of the paragraph consists of trailer lines. fn accepts_as_trailer_block(recognized_prefix: bool, trailer_lines: usize, non_trailer_lines: usize) -> bool { (trailer_lines > 0 && non_trailer_lines == 0) || (recognized_prefix && trailer_lines * 3 >= non_trailer_lines) } let lines = lines(body); let mut recognized_prefix = false; let mut trailer_lines = 0usize; let mut non_trailer_lines = 0usize; let mut possible_continuation_lines = 0usize; let mut saw_non_blank_line = false; for idx in (0..lines.len()).rev() { let line = &lines[idx]; if is_blank_line(line.text) { if !saw_non_blank_line { continue; } non_trailer_lines += possible_continuation_lines; return accepts_as_trailer_block(recognized_prefix, trailer_lines, non_trailer_lines).then_some( idx.checked_sub(1) .map_or(0, |prev| lines[prev].start + lines[prev].text.len()), ); } saw_non_blank_line = true; if is_recognized_prefix(line.text) { trailer_lines += 1; possible_continuation_lines = 0; recognized_prefix = true; continue; } if parse_trailer_line(line.text).is_some() { trailer_lines += 1; possible_continuation_lines = 0; continue; } if line.text.first().is_some_and(u8::is_ascii_whitespace) { possible_continuation_lines += 1; continue; } non_trailer_lines += 1 + possible_continuation_lines; possible_continuation_lines = 0; } non_trailer_lines += possible_continuation_lines; accepts_as_trailer_block(recognized_prefix, trailer_lines, non_trailer_lines).then_some(0) } impl<'a> Iterator for Trailers<'a> { type Item = TrailerRef<'a>; fn next(&mut self) -> Option { if self.cursor.is_empty() { return None; } while let Some(line) = self.cursor.lines_with_terminator().next() { let line = trim_line_ending(line); let consumed = self.cursor.lines_with_terminator().next().map_or(0, <[u8]>::len); if let Some((token, separator)) = parse_trailer_line(line) { let mut trailer_len = consumed; let mut cursor = &self.cursor[consumed..]; while let Some(next_line) = cursor.lines_with_terminator().next() { let next_text = trim_line_ending(next_line); if is_blank_line(next_text) || !next_text.first().is_some_and(u8::is_ascii_whitespace) { break; } trailer_len += next_line.len(); cursor = &cursor[next_line.len()..]; } let value = unfold_value(&self.cursor[separator + 1..trailer_len]); self.cursor = &self.cursor[trailer_len..]; return Some(TrailerRef { token, value }); } self.cursor = &self.cursor[consumed..]; } None } } impl<'a> BodyRef<'a> { /// Parse `body` bytes into the trailer and the actual body. pub fn from_bytes(body: &'a [u8]) -> Self { trailer_block_start(body).map_or( BodyRef { body_without_trailer: body.as_bstr(), start_of_trailer: &[], }, |start| BodyRef { body_without_trailer: body[..start].as_bstr(), start_of_trailer: &body[start..], }, ) } /// Returns the body with the trailers stripped. /// /// You can iterate trailers with the [`trailers()`][BodyRef::trailers()] method. pub fn without_trailer(&self) -> &'a BStr { self.body_without_trailer } /// Return an iterator over the trailers parsed from the last paragraph of the body. Maybe empty. pub fn trailers(&self) -> Trailers<'a> { Trailers { cursor: self.start_of_trailer, } } } impl AsRef for BodyRef<'_> { fn as_ref(&self) -> &BStr { self.body_without_trailer } } impl Deref for BodyRef<'_> { type Target = BStr; fn deref(&self) -> &Self::Target { self.body_without_trailer } } /// Convenience methods impl TrailerRef<'_> { /// Check if this trailer is a `Signed-off-by` trailer (case-insensitive). pub fn is_signed_off_by(&self) -> bool { self.token.eq_ignore_ascii_case(b"Signed-off-by") } /// Check if this trailer is a `Co-authored-by` trailer (case-insensitive). pub fn is_co_authored_by(&self) -> bool { self.token.eq_ignore_ascii_case(b"Co-authored-by") } /// Check if this trailer is an `Acked-by` trailer (case-insensitive). pub fn is_acked_by(&self) -> bool { self.token.eq_ignore_ascii_case(b"Acked-by") } /// Check if this trailer is a `Reviewed-by` trailer (case-insensitive). pub fn is_reviewed_by(&self) -> bool { self.token.eq_ignore_ascii_case(b"Reviewed-by") } /// Check if this trailer is a `Tested-by` trailer (case-insensitive). pub fn is_tested_by(&self) -> bool { self.token.eq_ignore_ascii_case(b"Tested-by") } /// Check if this trailer represents any kind of authorship or attribution /// (`Signed-off-by`, `Co-authored-by`, etc.). pub fn is_attribution(&self) -> bool { self.is_signed_off_by() || self.is_co_authored_by() || self.is_acked_by() || self.is_reviewed_by() || self.is_tested_by() } } /// Convenience methods impl<'a> Trailers<'a> { /// Filter trailers to only include `Signed-off-by` entries. pub fn signed_off_by(self) -> impl Iterator> { self.filter(TrailerRef::is_signed_off_by) } /// Filter trailers to only include `Co-authored-by` entries. pub fn co_authored_by(self) -> impl Iterator> { self.filter(TrailerRef::is_co_authored_by) } /// Filter trailers to only include attribution-related entries. /// (`Signed-off-by`, `Co-authored-by`, `Acked-by`, `Reviewed-by`, `Tested-by`). pub fn attributions(self) -> impl Iterator> { self.filter(TrailerRef::is_attribution) } /// Filter trailers to only include authors from `Signed-off-by` and `Co-authored-by` entries. pub fn authors(self) -> impl Iterator> { self.filter(|trailer| trailer.is_signed_off_by() || trailer.is_co_authored_by()) } } #[cfg(test)] mod test_parse_trailer { use super::*; fn parse(input: &str) -> TrailerRef<'_> { Trailers { cursor: input.as_bytes(), } .next() .expect("a trailer to be parsed") } #[test] fn simple_newline() { assert_eq!( parse("foo: bar\n"), TrailerRef { token: "foo".into(), value: b"bar".as_bstr().into() } ); } #[test] fn whitespace_around_separator_is_normalized() { assert_eq!( parse("foo : bar"), TrailerRef { token: "foo".into(), value: b"bar".as_bstr().into() } ); } #[test] fn trailing_whitespace_after_value_is_trimmed() { assert_eq!( parse("hello-foo: bar there \n"), TrailerRef { token: "hello-foo".into(), value: b"bar there".as_bstr().into() } ); } #[test] fn invalid_token_is_not_a_trailer() { assert_eq!( Trailers { cursor: "🤗: 🎉".as_bytes() } .next(), None ); } #[test] fn simple_newline_windows() { assert_eq!( parse("foo: bar\r\n"), TrailerRef { token: "foo".into(), value: b"bar".as_bstr().into() } ); } #[test] fn folded_value_is_unfolded() { assert_eq!( parse("foo: bar\n continued\r\n here"), TrailerRef { token: "foo".into(), value: b"bar continued here".as_bstr().into() } ); } } gix-object-0.60.0/src/commit/message/decode.rs000064400000000000000000000014111046102023000172330ustar 00000000000000use crate::bstr::{BStr, ByteSlice}; /// Returns title and body, without separator pub fn message_title_and_body(input: &[u8]) -> (&BStr, Option<&BStr>) { let mut pos = 0; while pos < input.len() { if let Some(first_len) = newline_len(&input[pos..]) { if let Some(second_len) = newline_len(&input[pos + first_len..]) { let body = &input[pos + first_len + second_len..]; return (input[..pos].as_bstr(), (!body.is_empty()).then(|| body.as_bstr())); } } pos += 1; } (input.as_bstr(), None) } fn newline_len(input: &[u8]) -> Option { if input.starts_with(b"\r\n") { Some(2) } else if input.starts_with(b"\n") { Some(1) } else { None } } gix-object-0.60.0/src/commit/message/mod.rs000064400000000000000000000122201046102023000165670ustar 00000000000000use std::borrow::Cow; use crate::{ bstr::{BStr, BString, ByteSlice, ByteVec}, commit::MessageRef, CommitRef, }; /// pub mod body; mod decode; impl<'a> CommitRef<'a> { /// Return exactly the same message as [`MessageRef::summary()`]. pub fn message_summary(&self) -> Cow<'a, BStr> { summary(self.message) } /// Return an iterator over message trailers as obtained from the last paragraph of the commit message. /// Maybe empty. pub fn message_trailers(&self) -> body::Trailers<'a> { MessageRef::from_bytes(self.message) .body() .map_or(body::Trailers { cursor: &[] }, |body| body.trailers()) } } /// Convenience methods impl<'a> CommitRef<'a> { /// Get an iterator over all `Signed-off-by` trailers in the commit message. /// This is useful for finding who signed off on the commit. pub fn signed_off_by_trailers(&self) -> impl Iterator> { self.message_trailers().signed_off_by() } /// Get an iterator over `Co-authored-by` trailers in the commit message. /// This is useful for squashed commits that contain multiple authors. pub fn co_authored_by_trailers(&self) -> impl Iterator> { self.message_trailers().co_authored_by() } /// Get all authors mentioned in `Signed-off-by` and `Co-authored-by` trailers. /// This is useful for squashed commits that contain multiple authors. /// Returns a Vec of author strings that can include both signers and co-authors. pub fn author_trailers(&self) -> impl Iterator> { self.message_trailers().authors() } /// Get an iterator over all attribution-related trailers /// (`Signed-off-by,` `Co-authored-by`, `Acked-by`, `Reviewed-by`, `Tested-by`). /// This provides a comprehensive view of everyone who contributed to or reviewed the commit. /// Note that the same name may occur multiple times, it's not a unified list. pub fn attribution_trailers(&self) -> impl Iterator> { self.message_trailers().attributions() } } impl<'a> MessageRef<'a> { /// Parse the given `input` as a message. /// /// Note that this cannot fail as everything will be interpreted as title if there is no body separator. pub fn from_bytes(input: &'a [u8]) -> Self { let (title, body) = decode::message_title_and_body(input); MessageRef { title, body } } /// Produce a short commit summary for the message title. /// /// This means the following /// /// * Take the subject line which is delimited by two newlines (\n\n) /// * transform intermediate consecutive whitespace including \r into one space /// /// The resulting summary will have folded whitespace before a newline into spaces and stopped that process /// once two consecutive newlines are encountered. pub fn summary(&self) -> Cow<'a, BStr> { summary(self.title) } /// Further parse the body into non-trailer and trailers, which can be iterated from the returned [`BodyRef`]. pub fn body(&self) -> Option> { self.body.map(|b| BodyRef::from_bytes(b)) } } pub(crate) fn summary(message: &BStr) -> Cow<'_, BStr> { let message = message.trim(); match message.find_byte(b'\n') { Some(mut pos) => { let mut out = BString::default(); let mut previous_pos = None; loop { if let Some(previous_pos) = previous_pos { if previous_pos + 1 == pos { let len_after_trim = out.trim_end().len(); out.resize(len_after_trim, 0); break out.into(); } } let message_to_newline = &message[previous_pos.map_or(0, |p| p + 1)..pos]; if let Some(pos_before_whitespace) = message_to_newline.rfind_not_byteset(b"\t\n\x0C\r ") { out.extend_from_slice(&message_to_newline[..=pos_before_whitespace]); } out.push_byte(b' '); previous_pos = Some(pos); match message.get(pos + 1..).and_then(|i| i.find_byte(b'\n')) { Some(next_nl_pos) => pos += next_nl_pos + 1, None => { if let Some(slice) = message.get((pos + 1)..) { out.extend_from_slice(slice); } break out.into(); } } } } None => message.as_bstr().into(), } } /// A reference to a message body, further parsed to only contain the non-trailer parts. /// /// See [git-interpret-trailers](https://git-scm.com/docs/git-interpret-trailers) for more information /// on what constitutes trailers and not that this implementation is only good for typical sign-off footer or key-value parsing. /// /// Note that we only parse trailers from the bottom of the body. #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] pub struct BodyRef<'a> { body_without_trailer: &'a BStr, start_of_trailer: &'a [u8], } gix-object-0.60.0/src/commit/mod.rs000064400000000000000000000157771046102023000151670ustar 00000000000000use std::ops::Range; use bstr::{BStr, BString, ByteSlice}; use crate::parse::parse_signature; use crate::{Commit, CommitRef, TagRef}; /// The well-known field name for gpg signatures. pub const SIGNATURE_FIELD_NAME: &str = "gpgsig"; mod decode; /// pub mod message; /// A parsed commit message that assumes a title separated from the body by two consecutive newlines. /// /// Titles can have any amount of whitespace #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct MessageRef<'a> { /// The title of the commit, as separated from the body with two consecutive newlines. The newlines are not included. #[cfg_attr(feature = "serde", serde(borrow))] pub title: &'a BStr, /// All bytes not consumed by the title, excluding the separating newlines. /// /// The body is `None` if there was now title separation or the body was empty after the separator. pub body: Option<&'a BStr>, } /// The raw commit data, parseable by [`CommitRef`] or [`Commit`], which was fed into a program to produce a signature. /// /// See [`extract_signature()`](crate::CommitRefIter::signature()) for how to obtain it. // TODO: implement `std::io::Read` to avoid allocations #[derive(PartialEq, Eq, Debug, Hash, Clone)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct SignedData<'a> { /// The raw commit data that includes the signature. data: &'a [u8], /// The byte range at which we find the signature. All but the signature is the data that was signed. signature_range: Range, } impl SignedData<'_> { /// Convenience method to obtain a copy of the signed data. pub fn to_bstring(&self) -> BString { let mut buf = BString::from(&self.data[..self.signature_range.start]); buf.extend_from_slice(&self.data[self.signature_range.end..]); buf } } impl From> for BString { fn from(value: SignedData<'_>) -> Self { value.to_bstring() } } /// pub mod ref_iter; mod write; /// Lifecycle impl<'a> CommitRef<'a> { /// Deserialize a commit from the given `data` bytes while avoiding most allocations, using `hash_kind` to know /// what kind of hash to expect for validation. pub fn from_bytes(mut data: &'a [u8], hash_kind: gix_hash::Kind) -> Result, crate::decode::Error> { let input = &mut data; match decode::commit(input, hash_kind) { Ok(tag) => Ok(tag), Err(err) => Err(err), } } } /// Access impl<'a> CommitRef<'a> { /// Return the `tree` fields hash digest. pub fn tree(&self) -> gix_hash::ObjectId { gix_hash::ObjectId::from_hex(self.tree).expect("prior validation of tree hash during parsing") } /// Returns an iterator of parent object ids pub fn parents(&self) -> impl Iterator + '_ { self.parents .iter() .map(|hex_hash| gix_hash::ObjectId::from_hex(hex_hash).expect("prior validation of hashes during parsing")) } /// Returns a convenient iterator over all extra headers. pub fn extra_headers(&self) -> ExtraHeaders> { ExtraHeaders::new( self.extra_headers.iter().map(|(k, v)| (*k, v.as_ref())), self.tree().kind(), ) } /// Return the author, with whitespace trimmed. /// /// This is different from the `author` field which may contain whitespace. pub fn author(&self) -> Result, crate::decode::Error> { parse_signature(self.author).map(|signature| signature.trim()) } /// Return the committer, with whitespace trimmed. /// /// This is different from the `committer` field which may contain whitespace. pub fn committer(&self) -> Result, crate::decode::Error> { parse_signature(self.committer).map(|signature| signature.trim()) } /// Returns a partially parsed message from which more information can be derived. pub fn message(&self) -> MessageRef<'a> { MessageRef::from_bytes(self.message) } /// Returns the time at which this commit was created, or a default time if it could not be parsed. pub fn time(&self) -> Result { parse_signature(self.committer).map(|signature| signature.time().unwrap_or_default()) } } /// Conversion impl CommitRef<'_> { /// Copy all fields of this instance into a fully owned commit, consuming this instance. pub fn into_owned(self) -> Result { self.try_into() } /// Copy all fields of this instance into a fully owned commit, internally cloning this instance. pub fn to_owned(self) -> Result { self.try_into() } } impl Commit { /// Returns a convenient iterator over all extra headers. pub fn extra_headers(&self) -> ExtraHeaders> { ExtraHeaders::new( self.extra_headers.iter().map(|(k, v)| (k.as_bstr(), v.as_bstr())), self.tree.kind(), ) } } /// An iterator over extra headers in [owned][crate::Commit] and [borrowed][crate::CommitRef] commits. pub struct ExtraHeaders { inner: I, hash_kind: gix_hash::Kind, } /// Instantiation and convenience. impl<'a, I> ExtraHeaders where I: Iterator, { /// Create a new instance from an iterator over tuples of (name, value) pairs. pub fn new(iter: I, hash_kind: gix_hash::Kind) -> Self { ExtraHeaders { inner: iter, hash_kind } } /// Find the _value_ of the _first_ header with the given `name`. pub fn find(mut self, name: &str) -> Option<&'a BStr> { self.inner .find_map(move |(k, v)| if k == name.as_bytes().as_bstr() { Some(v) } else { None }) } /// Find the entry index with the given name, or return `None` if unavailable. pub fn find_pos(self, name: &str) -> Option { self.inner .enumerate() .find_map(|(pos, (field, _value))| (field == name).then_some(pos)) } /// Return an iterator over all _values_ of headers with the given `name`. pub fn find_all(self, name: &'a str) -> impl Iterator { self.inner .filter_map(move |(k, v)| if k == name.as_bytes().as_bstr() { Some(v) } else { None }) } /// Return an iterator over all git mergetags. /// /// A merge tag is a tag object embedded within the respective header field of a commit, making /// it a child object of sorts. pub fn mergetags(self) -> impl Iterator, crate::decode::Error>> { let hash_kind = self.hash_kind; self.find_all("mergetag").map(move |b| TagRef::from_bytes(b, hash_kind)) } /// Return the cryptographic signature provided by gpg/pgp verbatim. pub fn pgp_signature(self) -> Option<&'a BStr> { self.find(SIGNATURE_FIELD_NAME) } } gix-object-0.60.0/src/commit/ref_iter.rs000064400000000000000000000275341046102023000162010ustar 00000000000000use std::{borrow::Cow, ops::Range}; use bstr::BStr; use gix_hash::{oid, ObjectId}; use crate::{ bstr::ByteSlice, commit::{decode, SignedData, SIGNATURE_FIELD_NAME}, parse, CommitRefIter, }; #[derive(Copy, Clone)] pub(crate) enum SignatureKind { Author, Committer, } #[derive(Default, Copy, Clone)] pub(crate) enum State { #[default] Tree, Parents, Signature { of: SignatureKind, }, Encoding, ExtraHeaders, Message, } /// Lifecycle impl<'a> CommitRefIter<'a> { /// Create a commit iterator from the given `data`, using `hash_kind` to know /// what kind of hash to expect for validation. pub fn from_bytes(data: &'a [u8], hash_kind: gix_hash::Kind) -> CommitRefIter<'a> { CommitRefIter { data, state: State::default(), hash_kind, } } } /// Access impl<'a> CommitRefIter<'a> { /// Parse `data` as commit and return its PGP signature, along with *all non-signature* data as [`SignedData`], or `None` /// if the commit isn't signed. All hashes in `data` are parsed as `hash_kind`. /// /// This allows the caller to validate the signature by passing the signed data along with the signature back to the program /// that created it. pub fn signature( data: &'a [u8], hash_kind: gix_hash::Kind, ) -> Result, SignedData<'a>)>, crate::decode::Error> { let mut signature_and_range = None; let raw_tokens = CommitRefIterRaw { data, state: State::default(), offset: 0, hash_kind, }; for token in raw_tokens { let token = token?; if let Token::ExtraHeader((name, value)) = &token.token { if *name == SIGNATURE_FIELD_NAME { // keep track of the signature range alongside the signature data, // because all but the signature is the signed data. signature_and_range = Some((value.clone(), token.token_range)); break; } } } Ok(signature_and_range.map(|(sig, signature_range)| (sig, SignedData { data, signature_range }))) } /// Returns the object id of this commits tree if it is the first function called and if there is no error in decoding /// the data. /// /// Note that this method must only be called once or else will always return None while consuming a single token. /// Errors are coerced into options, hiding whether there was an error or not. The caller should assume an error if they /// call the method as intended. Such a squelched error cannot be recovered unless the objects data is retrieved and parsed again. /// `next()`. pub fn tree_id(&mut self) -> Result { let tree_id = self.next().ok_or_else(missing_field)??; Token::try_into_id(tree_id).ok_or_else(missing_field) } /// Return all `parent_ids` as iterator. /// /// Parsing errors are ignored quietly. pub fn parent_ids(self) -> impl Iterator + 'a { self.filter_map(|t| match t { Ok(Token::Parent { id }) => Some(id), _ => None, }) } /// Returns all signatures, first the author, then the committer, if there is no decoding error. /// /// Errors are coerced into options, hiding whether there was an error or not. The caller knows if there was an error or not /// if not exactly two signatures were iterable. /// Errors are not the common case - if an error needs to be detectable, use this instance as iterator. pub fn signatures(self) -> impl Iterator> + 'a { self.filter_map(|t| match t { Ok(Token::Author { signature } | Token::Committer { signature }) => Some(signature), _ => None, }) } /// Returns the committer signature if there is no decoding error. pub fn committer(mut self) -> Result, crate::decode::Error> { self.find_map(|t| match t { Ok(Token::Committer { signature }) => Some(Ok(signature)), Err(err) => Some(Err(err)), _ => None, }) .ok_or_else(missing_field)? } /// Returns the author signature if there is no decoding error. /// /// It may contain white space surrounding it, and is exactly as parsed. pub fn author(mut self) -> Result, crate::decode::Error> { self.find_map(|t| match t { Ok(Token::Author { signature }) => Some(Ok(signature)), Err(err) => Some(Err(err)), _ => None, }) .ok_or_else(missing_field)? } /// Returns the message if there is no decoding error. /// /// It may contain white space surrounding it, and is exactly as // parsed. pub fn message(mut self) -> Result<&'a BStr, crate::decode::Error> { self.find_map(|t| match t { Ok(Token::Message(msg)) => Some(Ok(msg)), Err(err) => Some(Err(err)), _ => None, }) .transpose() .map(Option::unwrap_or_default) } } fn missing_field() -> crate::decode::Error { crate::decode::empty_error() } impl<'a> CommitRefIter<'a> { #[inline] fn next_inner( mut i: &'a [u8], state: &mut State, hash_kind: gix_hash::Kind, ) -> Result<(&'a [u8], Token<'a>), crate::decode::Error> { let input = &mut i; match Self::next_inner_(input, state, hash_kind) { Ok(token) => Ok((*input, token)), Err(err) => Err(err), } } fn next_inner_( input: &mut &'a [u8], state: &mut State, hash_kind: gix_hash::Kind, ) -> Result, crate::decode::Error> { use State::*; Ok(match state { Tree => { let tree = parse::header_field(input, b"tree", |value| parse::hex_hash(value, hash_kind))?; *state = State::Parents; Token::Tree { id: ObjectId::from_hex(tree).expect("parsing validation"), } } Parents => { if input.starts_with(b"parent ") { let parent = parse::header_field(input, b"parent", |value| parse::hex_hash(value, hash_kind))?; Token::Parent { id: ObjectId::from_hex(parent).expect("parsing validation"), } } else { *state = State::Signature { of: SignatureKind::Author, }; Self::next_inner_(input, state, hash_kind)? } } Signature { ref mut of } => { let who = *of; let field_name = match of { SignatureKind::Author => { *of = SignatureKind::Committer; &b"author"[..] } SignatureKind::Committer => { *state = State::Encoding; &b"committer"[..] } }; let signature = parse::header_field(input, field_name, parse::signature)?; match who { SignatureKind::Author => Token::Author { signature }, SignatureKind::Committer => Token::Committer { signature }, } } Encoding => { *state = State::ExtraHeaders; if input.starts_with(b"encoding ") { let encoding = parse::header_field(input, b"encoding", Ok)?; Token::Encoding(encoding.as_bstr()) } else { Self::next_inner_(input, state, hash_kind)? } } ExtraHeaders => { if input.starts_with(b"\n") { *state = State::Message; Self::next_inner_(input, state, hash_kind)? } else { let before = *input; match parse::any_header_field_multi_line(input) .map(|(k, o)| (k.as_bstr(), Cow::Owned(o))) .or_else(|_| { *input = before; parse::any_header_field(input).map(|(k, o)| (k.as_bstr(), Cow::Borrowed(o.as_bstr()))) }) { Ok(extra_header) => Token::ExtraHeader(extra_header), Err(err) => return Err(err), } } } Message => { let message = decode::message(input)?; debug_assert!( input.is_empty(), "we should have consumed all data - otherwise iter may go forever" ); Token::Message(message) } }) } } impl<'a> Iterator for CommitRefIter<'a> { type Item = Result, crate::decode::Error>; fn next(&mut self) -> Option { if self.data.is_empty() { return None; } match Self::next_inner(self.data, &mut self.state, self.hash_kind) { Ok((data, token)) => { self.data = data; Some(Ok(token)) } Err(err) => { self.data = &[]; Some(Err(err)) } } } } /// A variation of [`CommitRefIter`] that return's [`RawToken`]s instead. struct CommitRefIterRaw<'a> { data: &'a [u8], state: State, offset: usize, hash_kind: gix_hash::Kind, } impl<'a> Iterator for CommitRefIterRaw<'a> { type Item = Result, crate::decode::Error>; fn next(&mut self) -> Option { if self.data.is_empty() { return None; } match CommitRefIter::next_inner(self.data, &mut self.state, self.hash_kind) { Ok((remaining, token)) => { let consumed = self.data.len() - remaining.len(); let start = self.offset; let end = start + consumed; self.offset = end; self.data = remaining; Some(Ok(RawToken { token, token_range: start..end, })) } Err(err) => { self.data = &[]; Some(Err(err)) } } } } /// A combination of a parsed [`Token`] as well as the range of bytes that were consumed to parse it. struct RawToken<'a> { /// The parsed token. token: Token<'a>, token_range: Range, } /// A token returned by the [commit iterator][CommitRefIter]. #[allow(missing_docs)] #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] pub enum Token<'a> { Tree { id: ObjectId, }, Parent { id: ObjectId, }, /// A person who authored the content of the commit. Author { signature: gix_actor::SignatureRef<'a>, }, /// A person who committed the authors work to the repository. Committer { signature: gix_actor::SignatureRef<'a>, }, Encoding(&'a BStr), ExtraHeader((&'a BStr, Cow<'a, BStr>)), Message(&'a BStr), } impl Token<'_> { /// Return the object id of this token if it's a [tree][Token::Tree] or a [parent commit][Token::Parent]. pub fn id(&self) -> Option<&oid> { match self { Token::Tree { id } | Token::Parent { id } => Some(id.as_ref()), _ => None, } } /// Return the owned object id of this token if it's a [tree][Token::Tree] or a [parent commit][Token::Parent]. pub fn try_into_id(self) -> Option { match self { Token::Tree { id } | Token::Parent { id } => Some(id), _ => None, } } } gix-object-0.60.0/src/commit/write.rs000064400000000000000000000100661046102023000155240ustar 00000000000000use std::io; use bstr::ByteSlice; use crate::{encode, encode::NL, Commit, CommitRef, Kind}; impl crate::WriteTo for Commit { /// Serializes this instance to `out` in the git serialization format. fn write_to(&self, mut out: &mut dyn io::Write) -> io::Result<()> { encode::trusted_header_id(b"tree", &self.tree, &mut out)?; for parent in &self.parents { encode::trusted_header_id(b"parent", parent, &mut out)?; } let mut buf = gix_date::parse::TimeBuf::default(); encode::trusted_header_signature(b"author", &self.author.to_ref(&mut buf), &mut out)?; encode::trusted_header_signature(b"committer", &self.committer.to_ref(&mut buf), &mut out)?; if let Some(encoding) = self.encoding.as_ref() { encode::header_field(b"encoding", encoding, &mut out)?; } for (name, value) in &self.extra_headers { encode::header_field_multi_line(name, value, &mut out)?; } out.write_all(NL)?; out.write_all(&self.message) } fn kind(&self) -> Kind { Kind::Commit } fn size(&self) -> u64 { let hash_in_hex = self.tree.kind().len_in_hex(); (b"tree".len() + 1 /*space*/ + hash_in_hex + 1 /* nl */ + self.parents.iter().count() * (b"parent".len() + 1 + hash_in_hex + 1) + b"author".len() + 1 /* space */ + self.author.size() + 1 /* nl */ + b"committer".len() + 1 /* space */ + self.committer.size() + 1 /* nl */ + self .encoding .as_ref() .map_or(0, |e| b"encoding".len() + 1 /* space */ + e.len() + 1 /* nl */) + self .extra_headers .iter() .map(|(name, value)| { // each header *value* is preceded by a space, and it starts right after the name. name.len() + value.lines_with_terminator().map(|s| s.len() + 1).sum::() + usize::from(!value.ends_with_str(b"\n")) }) .sum::() + 1 /* nl */ + self.message.len()) as u64 } } impl crate::WriteTo for CommitRef<'_> { /// Serializes this instance to `out` in the git serialization format. fn write_to(&self, mut out: &mut dyn io::Write) -> io::Result<()> { encode::trusted_header_id(b"tree", &self.tree(), &mut out)?; for parent in self.parents() { encode::trusted_header_id(b"parent", &parent, &mut out)?; } encode::trusted_header_field(b"author", self.author.as_ref(), &mut out)?; encode::trusted_header_field(b"committer", self.committer.as_ref(), &mut out)?; if let Some(encoding) = self.encoding.as_ref() { encode::header_field(b"encoding", encoding, &mut out)?; } for (name, value) in &self.extra_headers { encode::header_field_multi_line(name, value, &mut out)?; } out.write_all(NL)?; out.write_all(self.message) } fn kind(&self) -> Kind { Kind::Commit } fn size(&self) -> u64 { let hash_in_hex = self.tree().kind().len_in_hex(); (b"tree".len() + 1 /* space */ + hash_in_hex + 1 /* nl */ + self.parents.iter().count() * (b"parent".len() + 1 /* space */ + hash_in_hex + 1 /* nl */) + b"author".len() + 1 /* space */ + self.author.len() + 1 /* nl */ + b"committer".len() + 1 /* space */ + self.committer.len() + 1 /* nl */ + self .encoding .as_ref() .map_or(0, |e| b"encoding".len() + 1 /* space */ + e.len() + 1 /* nl */) + self .extra_headers .iter() .map(|(name, value)| { // each header *value* is preceded by a space, and it starts right after the name. name.len() + value.lines_with_terminator().map(|s| s.len() + 1).sum::() + usize::from(!value.ends_with_str(b"\n")) }) .sum::() + 1 /* nl */ + self.message.len()) as u64 } } gix-object-0.60.0/src/data.rs000064400000000000000000000067241046102023000140210ustar 00000000000000//! Contains a borrowed Object bound to a buffer holding its decompressed data. use crate::{BlobRef, CommitRef, CommitRefIter, Data, Kind, ObjectRef, TagRef, TagRefIter, TreeRef, TreeRefIter}; impl<'a> Data<'a> { /// Constructs a new data object from `data`, `kind` and `hash_kind`. pub fn new(data: &'a [u8], kind: Kind, hash_kind: gix_hash::Kind) -> Data<'a> { Data { kind, hash_kind, data } } /// Decodes the data in the backing slice into a [`ObjectRef`], allowing to access all of its data /// conveniently. The cost of parsing an object is negligible. /// /// **Note** that [mutable, decoded objects][crate::Object] can be created from [`Data`] /// using [`crate::ObjectRef::into_owned()`]. pub fn decode(&self) -> Result, crate::decode::Error> { Ok(match self.kind { Kind::Tree => ObjectRef::Tree(TreeRef::from_bytes(self.data, self.hash_kind)?), Kind::Blob => ObjectRef::Blob(BlobRef { data: self.data }), Kind::Commit => ObjectRef::Commit(CommitRef::from_bytes(self.data, self.hash_kind)?), Kind::Tag => ObjectRef::Tag(TagRef::from_bytes(self.data, self.hash_kind)?), }) } /// Returns this object as tree iterator to parse entries one at a time to avoid allocations, or /// `None` if this is not a tree object. pub fn try_into_tree_iter(self) -> Option> { match self.kind { Kind::Tree => Some(TreeRefIter::from_bytes(self.data, self.hash_kind)), _ => None, } } /// Returns this object as commit iterator to parse tokens one at a time to avoid allocations, or /// `None` if this is not a commit object. pub fn try_into_commit_iter(self) -> Option> { match self.kind { Kind::Commit => Some(CommitRefIter::from_bytes(self.data, self.hash_kind)), _ => None, } } /// Returns this object as tag iterator to parse tokens one at a time to avoid allocations, or /// `None` if this is not a tag object. pub fn try_into_tag_iter(self) -> Option> { match self.kind { Kind::Tag => Some(TagRefIter::from_bytes(self.data, self.hash_kind)), _ => None, } } } /// Types supporting object hash verification pub mod verify { /// Returned by [`crate::Data::verify_checksum()`] #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum Error { #[error("Failed to hash object")] Hasher(#[from] gix_hash::hasher::Error), #[error(transparent)] Verify(#[from] gix_hash::verify::Error), } impl crate::Data<'_> { /// Compute the checksum of `self` and compare it with the `expected` hash. /// If the hashes do not match, an [`Error`] is returned, containing the actual /// hash of `self`. pub fn verify_checksum(&self, expected: &gix_hash::oid) -> Result { let actual = crate::compute_hash(expected.kind(), self.kind, self.data)?; actual.verify(expected)?; Ok(actual) } } } #[cfg(test)] mod tests { use super::*; #[test] fn size_of_object() { #[cfg(target_pointer_width = "64")] assert_eq!(std::mem::size_of::>(), 24, "this shouldn't change unnoticed"); #[cfg(target_pointer_width = "32")] assert_eq!(std::mem::size_of::>(), 12, "this shouldn't change unnoticed"); } } gix-object-0.60.0/src/encode.rs000064400000000000000000000050161046102023000143360ustar 00000000000000//! Encoding utilities use std::io::{self, Write}; use bstr::{BString, ByteSlice}; /// An error returned when object encoding fails. #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum Error { #[error("Newlines are not allowed in header values: {value:?}")] NewlineInHeaderValue { value: BString }, #[error("Header values must not be empty")] EmptyValue, } macro_rules! check { ($e: expr) => { $e.expect("Writing to a Vec should never fail.") }; } /// Generates a loose header buffer pub fn loose_header(kind: crate::Kind, size: u64) -> smallvec::SmallVec<[u8; 28]> { let mut v = smallvec::SmallVec::new(); check!(v.write_all(kind.as_bytes())); check!(v.write_all(SPACE)); check!(v.write_all(itoa::Buffer::new().format(size).as_bytes())); check!(v.write_all(b"\0")); v } impl From for io::Error { fn from(other: Error) -> io::Error { io::Error::other(other) } } pub(crate) fn header_field_multi_line(name: &[u8], value: &[u8], out: &mut dyn io::Write) -> io::Result<()> { let mut lines = value.as_bstr().lines_with_terminator(); out.write_all(name)?; out.write_all(SPACE)?; if let Some(line) = lines.next() { out.write_all(line)?; } for line in lines { out.write_all(SPACE)?; out.write_all(line)?; } if !value.ends_with_str(b"\n") { out.write_all(NL)?; } Ok(()) } pub(crate) fn trusted_header_field(name: &[u8], value: &[u8], out: &mut dyn io::Write) -> io::Result<()> { out.write_all(name)?; out.write_all(SPACE)?; out.write_all(value)?; out.write_all(NL) } pub(crate) fn trusted_header_signature( name: &[u8], value: &gix_actor::SignatureRef<'_>, out: &mut dyn io::Write, ) -> io::Result<()> { out.write_all(name)?; out.write_all(SPACE)?; value.write_to(out)?; out.write_all(NL) } pub(crate) fn trusted_header_id( name: &[u8], value: &gix_hash::ObjectId, mut out: &mut dyn io::Write, ) -> io::Result<()> { out.write_all(name)?; out.write_all(SPACE)?; value.write_hex_to(&mut out)?; out.write_all(NL) } pub(crate) fn header_field(name: &[u8], value: &[u8], out: &mut dyn io::Write) -> io::Result<()> { if value.is_empty() { return Err(Error::EmptyValue.into()); } if value.find(NL).is_some() { return Err(Error::NewlineInHeaderValue { value: value.into() }.into()); } trusted_header_field(name, value, out) } pub(crate) const NL: &[u8; 1] = b"\n"; pub(crate) const SPACE: &[u8; 1] = b" "; gix-object-0.60.0/src/find.rs000064400000000000000000000047131046102023000140240ustar 00000000000000/// The error type returned by the [`Find`](crate::Find) trait. pub type Error = Box; /// pub mod existing { use gix_hash::ObjectId; /// The error returned by the [`find(…)`][crate::FindExt::find()] trait methods. #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum Error { #[error(transparent)] Find(crate::find::Error), #[error("An object with id {} could not be found", .oid)] NotFound { oid: ObjectId }, } } /// pub mod existing_object { use gix_hash::ObjectId; /// The error returned by the various [`find_*()`][crate::FindExt::find_commit()] trait methods. #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum Error { #[error(transparent)] Find(crate::find::Error), #[error("Could not decode object at {oid}")] Decode { oid: ObjectId, source: crate::decode::Error, }, #[error("An object with id {oid} could not be found")] NotFound { oid: ObjectId }, #[error("Expected object of kind {expected} but got {actual} at {oid}")] ObjectKind { oid: ObjectId, actual: crate::Kind, expected: crate::Kind, }, } } /// pub mod existing_iter { use gix_hash::ObjectId; /// The error returned by the various [`find_*_iter()`][crate::FindExt::find_commit_iter()] trait methods. #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum Error { #[error(transparent)] Find(crate::find::Error), #[error("An object with id {oid} could not be found")] NotFound { oid: ObjectId }, #[error("Expected object of kind {expected} but got {actual} at {oid}")] ObjectKind { oid: ObjectId, actual: crate::Kind, expected: crate::Kind, }, } } /// An implementation of all traits that never fails, but also never finds anything. #[derive(Debug, Copy, Clone)] pub struct Never; impl super::FindHeader for Never { fn try_header(&self, _id: &gix_hash::oid) -> Result, Error> { Ok(None) } } impl super::Find for Never { fn try_find<'a>(&self, _id: &gix_hash::oid, _buffer: &'a mut Vec) -> Result>, Error> { Ok(None) } } impl super::Exists for Never { fn exists(&self, _id: &gix_hash::oid) -> bool { false } } gix-object-0.60.0/src/kind.rs000064400000000000000000000033261046102023000140300ustar 00000000000000use std::fmt; use crate::Kind; /// The Error used in [`Kind::from_bytes()`]. #[derive(Debug, Clone, thiserror::Error)] #[allow(missing_docs)] pub enum Error { #[error("Unknown object kind: {kind:?}")] InvalidObjectKind { kind: bstr::BString }, } /// Initialization impl Kind { /// Parse a `Kind` from its serialized loose git objects. pub fn from_bytes(s: &[u8]) -> Result { Ok(match s { b"tree" => Kind::Tree, b"blob" => Kind::Blob, b"commit" => Kind::Commit, b"tag" => Kind::Tag, _ => return Err(Error::InvalidObjectKind { kind: s.into() }), }) } } /// Access impl Kind { /// Return the name of `self` for use in serialized loose git objects. pub fn as_bytes(&self) -> &[u8] { match self { Kind::Tree => b"tree", Kind::Commit => b"commit", Kind::Blob => b"blob", Kind::Tag => b"tag", } } /// Returns `true` if this instance is representing a commit. pub fn is_commit(&self) -> bool { matches!(self, Kind::Commit) } /// Returns `true` if this instance is representing a tree. pub fn is_tree(&self) -> bool { matches!(self, Kind::Tree) } /// Returns `true` if this instance is representing a tag. pub fn is_tag(&self) -> bool { matches!(self, Kind::Tag) } /// Returns `true` if this instance is representing a blob. pub fn is_blob(&self) -> bool { matches!(self, Kind::Blob) } } impl fmt::Display for Kind { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(std::str::from_utf8(self.as_bytes()).expect("Converting Kind name to utf8")) } } gix-object-0.60.0/src/lib.rs000064400000000000000000000372121046102023000136520ustar 00000000000000//! This crate provides types for [read-only git objects][crate::ObjectRef] backed by bytes provided in git's serialization format //! as well as [mutable versions][Object] of these. Both types of objects can be encoded. //! //! ## Decode Borrowed Objects //! //! ``` //! let object = gix_object::ObjectRef::from_loose(b"blob 5\0hello", gix_hash::Kind::Sha1).unwrap(); //! let blob = object.as_blob().unwrap(); //! //! assert_eq!(blob.data, b"hello"); //! assert_eq!(object.kind(), gix_object::Kind::Blob); //! ``` //! //! ## Mutate And Encode Owned Objects //! //! ``` //! use gix_object::WriteTo; //! //! let object = gix_object::ObjectRef::from_loose(b"blob 5\0hello", gix_hash::Kind::Sha1) //! .unwrap() //! .into_owned() //! .unwrap(); //! let mut blob = object.into_blob(); //! blob.data.extend_from_slice(b" world"); //! //! let mut out = Vec::new(); //! blob.write_to(&mut out).unwrap(); //! assert_eq!(out, b"hello world"); //! assert_eq!(blob.loose_header().as_slice(), b"blob 11\0"); //! ``` //! ## Feature Flags #![cfg_attr( all(doc, feature = "document-features"), doc = ::document_features::document_features!() )] #![cfg_attr(all(doc, feature = "document-features"), feature(doc_cfg))] #![deny(missing_docs, rust_2018_idioms)] #![forbid(unsafe_code)] use std::borrow::Cow; /// For convenience to allow using `bstr` without adding it to own cargo manifest. pub use bstr; use bstr::{BStr, BString, ByteSlice}; /// For convenience to allow using `gix-date` without adding it to own cargo manifest. pub use gix_date as date; use smallvec::SmallVec; /// pub mod commit; mod object; /// pub mod tag; /// pub mod tree; mod blob; /// pub mod data; /// pub mod find; /// pub mod write { /// The error type returned by the [`Write`](crate::Write) trait. pub type Error = Box; } mod traits; pub use traits::{Exists, Find, FindExt, FindObjectOrHeader, Header as FindHeader, HeaderExt, Write, WriteTo}; pub mod encode; pub(crate) mod parse; /// pub mod kind; /// The four types of objects that git differentiates. #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] #[allow(missing_docs)] pub enum Kind { Tree, Blob, Commit, Tag, } /// A chunk of any [`data`](BlobRef::data). #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct BlobRef<'a> { /// The bytes themselves. pub data: &'a [u8], } /// A mutable chunk of any [`data`](Blob::data). #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Blob { /// The data itself. pub data: Vec, } /// A git commit parsed using [`from_bytes()`](CommitRef::from_bytes()). /// /// A commit encapsulates information about a point in time at which the state of the repository is recorded, usually after a /// change which is documented in the commit `message`. #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct CommitRef<'a> { /// HEX hash of tree object we point to. /// /// Use [`tree()`](CommitRef::tree()) to obtain a decoded version of it. #[cfg_attr(feature = "serde", serde(borrow))] pub tree: &'a BStr, /// HEX hash of each parent commit. Empty for first commit in repository. pub parents: SmallVec<[&'a BStr; 1]>, /// The raw author header value as encountered during parsing. /// /// Use the [`author()`](CommitRef::author()) method to obtain a parsed version of it. #[cfg_attr(feature = "serde", serde(borrow))] pub author: &'a BStr, /// The raw committer header value as encountered during parsing. /// /// Use the [`committer()`](CommitRef::committer()) method to obtain a parsed version of it. #[cfg_attr(feature = "serde", serde(borrow))] pub committer: &'a BStr, /// The name of the message encoding, otherwise [UTF-8 should be assumed](https://github.com/git/git/blob/e67fbf927dfdf13d0b21dc6ea15dc3c7ef448ea0/commit.c#L1493:L1493). pub encoding: Option<&'a BStr>, /// The commit message documenting the change. pub message: &'a BStr, /// Extra header fields, in order of them being encountered, made accessible with the iterator returned by [`extra_headers()`](CommitRef::extra_headers()). pub extra_headers: Vec<(&'a BStr, Cow<'a, BStr>)>, } /// Like [`CommitRef`], but as `Iterator` to support (up to) entirely allocation free parsing. /// It's particularly useful to traverse the commit graph without ever allocating arrays for parents. #[derive(Copy, Clone)] pub struct CommitRefIter<'a> { data: &'a [u8], state: commit::ref_iter::State, hash_kind: gix_hash::Kind, } /// A mutable git commit, representing an annotated state of a working tree along with a reference to its historical commits. #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Commit { /// The hash of recorded working tree state. pub tree: gix_hash::ObjectId, /// Hash of each parent commit. Empty for the first commit in repository. pub parents: SmallVec<[gix_hash::ObjectId; 1]>, /// Who wrote this commit. pub author: gix_actor::Signature, /// Who committed this commit. /// /// This may be different from the `author` in case the author couldn't write to the repository themselves and /// is commonly encountered with contributed commits. pub committer: gix_actor::Signature, /// The name of the message encoding, otherwise [UTF-8 should be assumed](https://github.com/git/git/blob/e67fbf927dfdf13d0b21dc6ea15dc3c7ef448ea0/commit.c#L1493:L1493). pub encoding: Option, /// The commit message documenting the change. pub message: BString, /// Extra header fields, in order of them being encountered, made accessible with the iterator returned /// by [`extra_headers()`](Commit::extra_headers()). pub extra_headers: Vec<(BString, BString)>, } /// Represents a git tag, commonly indicating a software release. #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct TagRef<'a> { /// The hash in hexadecimal being the object this tag points to. Use [`target()`](TagRef::target()) to obtain a byte representation. #[cfg_attr(feature = "serde", serde(borrow))] pub target: &'a BStr, /// The kind of object that `target` points to. pub target_kind: Kind, /// The name of the tag, e.g. "v1.0". pub name: &'a BStr, /// The raw tagger header value as encountered during parsing. /// /// Use the [`tagger()`](TagRef::tagger()) method to obtain a parsed version of it. #[cfg_attr(feature = "serde", serde(borrow))] pub tagger: Option<&'a BStr>, /// The message describing this release. pub message: &'a BStr, /// A cryptographic signature over the entire content of the serialized tag object thus far. pub pgp_signature: Option<&'a BStr>, } /// Like [`TagRef`], but as `Iterator` to support entirely allocation free parsing. /// It's particularly useful to dereference only the target chain. #[derive(Copy, Clone)] pub struct TagRefIter<'a> { data: &'a [u8], state: tag::ref_iter::State, hash_kind: gix_hash::Kind, } /// A mutable git tag. #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Tag { /// The hash this tag is pointing to. pub target: gix_hash::ObjectId, /// The kind of object this tag is pointing to. pub target_kind: Kind, /// The name of the tag, e.g. "v1.0". pub name: BString, /// The tags author. pub tagger: Option, /// The message describing the tag. pub message: BString, /// A pgp signature over all bytes of the encoded tag, excluding the pgp signature itself. pub pgp_signature: Option, } /// Immutable objects are read-only structures referencing most data from [a byte slice](ObjectRef::from_bytes()). /// /// Immutable objects are expected to be deserialized from bytes that acts as backing store, and they /// cannot be mutated or serialized. Instead, one will [convert](ObjectRef::into_owned()) them into their [`mutable`](Object) counterparts /// which support mutation and serialization. /// /// An `ObjectRef` is representing [`Trees`](TreeRef), [`Blobs`](BlobRef), [`Commits`](CommitRef), or [`Tags`](TagRef). #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[allow(missing_docs)] pub enum ObjectRef<'a> { #[cfg_attr(feature = "serde", serde(borrow))] Tree(TreeRef<'a>), Blob(BlobRef<'a>), Commit(CommitRef<'a>), Tag(TagRef<'a>), } /// Mutable objects with each field being separately allocated and changeable. /// /// Mutable objects are Commits, Trees, Blobs and Tags that can be changed and serialized. /// /// They either created using object [construction](Object) or by [deserializing existing objects](ObjectRef::from_bytes()) /// and converting these [into mutable copies](ObjectRef::into_owned()) for adjustments. /// /// An `Object` is representing [`Trees`](Tree), [`Blobs`](Blob), [`Commits`](Commit), or [`Tags`](Tag). #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[allow(clippy::large_enum_variant, missing_docs)] pub enum Object { Tree(Tree), Blob(Blob), Commit(Commit), Tag(Tag), } /// A directory snapshot containing files (blobs), directories (trees) and submodules (commits). #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct TreeRef<'a> { /// The directories and files contained in this tree. /// /// Beware that the sort order isn't *quite* by name, so one may bisect only with a [`tree::EntryRef`] to handle ordering correctly. #[cfg_attr(feature = "serde", serde(borrow))] pub entries: Vec>, } /// A directory snapshot containing files (blobs), directories (trees) and submodules (commits), lazily evaluated. #[derive(Default, PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] pub struct TreeRefIter<'a> { /// The hash kind to use for parsing this tree. hash_kind: gix_hash::Kind, /// The directories and files contained in this tree. data: &'a [u8], } /// A mutable Tree, containing other trees, blobs or commits. #[derive(Default, PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Tree { /// The directories and files contained in this tree. They must be and remain sorted by [`filename`][tree::Entry::filename]. /// /// Beware that the sort order isn't *quite* by name, so one may bisect only with a [`tree::Entry`] to handle ordering correctly. pub entries: Vec, } impl Tree { /// Return an empty tree which serializes to a well-known hash pub fn empty() -> Self { Tree { entries: Vec::new() } } } /// A borrowed object using a slice as backing buffer, or in other words a bytes buffer that knows the kind of object it represents. #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] pub struct Data<'a> { /// kind of object pub kind: Kind, /// The hash kind to use for parsing this data. pub hash_kind: gix_hash::Kind, /// decoded, decompressed data, owned by a backing store. pub data: &'a [u8], } /// Information about an object, which includes its kind and the amount of bytes it would have when obtained. #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] pub struct Header { /// The kind of object. pub kind: Kind, /// The object's size in bytes, or the size of the buffer when it's retrieved in full. pub size: u64, } /// pub mod decode { mod error { pub(crate) fn empty_error() -> Error { Error } /// A type to indicate any error occurred during parsing. #[derive(Debug, Clone, Copy, Default)] pub struct Error; impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str("object parsing failed") } } impl std::error::Error for Error {} } pub(crate) use error::empty_error; pub use error::Error; /// Returned by [`loose_header()`] #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum LooseHeaderDecodeError { #[error("{message}: {number:?}")] ParseIntegerError { source: gix_utils::btoi::ParseIntegerError, message: &'static str, number: bstr::BString, }, #[error("{message}")] InvalidHeader { message: &'static str }, #[error("The object header contained an unknown object kind.")] ObjectHeader(#[from] super::kind::Error), } use bstr::ByteSlice; /// Decode a loose object header, being ` \0`, returns /// ([`kind`](super::Kind), `size`, `consumed bytes`). /// /// `size` is the uncompressed size of the payload in bytes. pub fn loose_header(input: &[u8]) -> Result<(super::Kind, u64, usize), LooseHeaderDecodeError> { use LooseHeaderDecodeError::*; let kind_end = input.find_byte(0x20).ok_or(InvalidHeader { message: "Expected ' '", })?; let kind = super::Kind::from_bytes(&input[..kind_end])?; let size_end = input.find_byte(0x0).ok_or(InvalidHeader { message: "Did not find 0 byte in header", })?; let size_bytes = &input[kind_end + 1..size_end]; let size = gix_utils::btoi::to_signed(size_bytes).map_err(|source| ParseIntegerError { source, message: "Object size in header could not be parsed", number: size_bytes.into(), })?; Ok((kind, size, size_end + 1)) } } fn object_hasher(hash_kind: gix_hash::Kind, object_kind: Kind, object_size: u64) -> gix_hash::Hasher { let mut hasher = gix_hash::hasher(hash_kind); hasher.update(&encode::loose_header(object_kind, object_size)); hasher } /// A function to compute a hash of kind `hash_kind` for an object of `object_kind` and its `data`. #[doc(alias = "hash_object", alias = "git2")] pub fn compute_hash( hash_kind: gix_hash::Kind, object_kind: Kind, data: &[u8], ) -> Result { let mut hasher = object_hasher(hash_kind, object_kind, data.len() as u64); hasher.update(data); hasher.try_finalize() } /// A function to compute a hash of kind `hash_kind` for an object of `object_kind` and its data read from `stream` /// which has to yield exactly `stream_len` bytes. /// Use `progress` to learn about progress in bytes processed and `should_interrupt` to be able to abort the operation /// if set to `true`. #[doc(alias = "hash_file", alias = "git2")] pub fn compute_stream_hash( hash_kind: gix_hash::Kind, object_kind: Kind, stream: &mut dyn std::io::Read, stream_len: u64, progress: &mut dyn gix_features::progress::Progress, should_interrupt: &std::sync::atomic::AtomicBool, ) -> Result { let hasher = object_hasher(hash_kind, object_kind, stream_len); gix_hash::bytes_with_hasher(stream, stream_len, hasher, progress, should_interrupt) } gix-object-0.60.0/src/object/convert.rs000064400000000000000000000142351046102023000160320ustar 00000000000000use std::convert::TryFrom; use crate::parse::parse_signature; use crate::{tree, Blob, BlobRef, Commit, CommitRef, Object, ObjectRef, Tag, TagRef, Tree, TreeRef}; impl TryFrom> for Tag { type Error = crate::decode::Error; fn try_from(other: TagRef<'_>) -> Result { let TagRef { target, name, target_kind, message, tagger, pgp_signature, } = other; let untrimmed_tagger = tagger.map(parse_signature).transpose()?.map(Into::into); Ok(Tag { target: gix_hash::ObjectId::from_hex(target).expect("prior parser validation"), name: name.to_owned(), target_kind, message: message.to_owned(), tagger: untrimmed_tagger, pgp_signature: pgp_signature.map(ToOwned::to_owned), }) } } impl TryFrom> for Commit { type Error = crate::decode::Error; fn try_from(other: CommitRef<'_>) -> Result { let CommitRef { tree, parents, author, committer, encoding, message, extra_headers, } = other; let untrimmed_author = parse_signature(author)?; let untrimmed_committer = parse_signature(committer)?; Ok(Commit { tree: gix_hash::ObjectId::from_hex(tree).expect("prior parser validation"), parents: parents .iter() .map(|parent| gix_hash::ObjectId::from_hex(parent).expect("prior parser validation")) .collect(), author: untrimmed_author.into(), committer: untrimmed_committer.into(), encoding: encoding.map(ToOwned::to_owned), message: message.to_owned(), extra_headers: extra_headers .into_iter() .map(|(k, v)| (k.into(), v.into_owned())) .collect(), }) } } impl<'a> From> for Blob { fn from(v: BlobRef<'a>) -> Self { Blob { data: v.data.to_owned(), } } } impl From> for Tree { fn from(other: TreeRef<'_>) -> Tree { let TreeRef { entries } = other; Tree { entries: entries.into_iter().map(Into::into).collect(), } } } impl From> for tree::Entry { fn from(other: tree::EntryRef<'_>) -> tree::Entry { let tree::EntryRef { mode, filename, oid } = other; tree::Entry { mode, filename: filename.to_owned(), oid: oid.into(), } } } impl<'a> From<&'a tree::Entry> for tree::EntryRef<'a> { fn from(other: &'a tree::Entry) -> tree::EntryRef<'a> { let tree::Entry { mode, filename, oid } = other; tree::EntryRef { mode: *mode, filename: filename.as_ref(), oid, } } } impl TryFrom> for Object { type Error = crate::decode::Error; fn try_from(v: ObjectRef<'_>) -> Result { Ok(match v { ObjectRef::Tree(v) => Object::Tree(v.into()), ObjectRef::Blob(v) => Object::Blob(v.into()), ObjectRef::Commit(v) => Object::Commit(v.try_into()?), ObjectRef::Tag(v) => Object::Tag(v.try_into()?), }) } } impl From for Object { fn from(v: Tag) -> Self { Object::Tag(v) } } impl From for Object { fn from(v: Commit) -> Self { Object::Commit(v) } } impl From for Object { fn from(v: Tree) -> Self { Object::Tree(v) } } impl From for Object { fn from(v: Blob) -> Self { Object::Blob(v) } } impl TryFrom for Tag { type Error = Object; fn try_from(value: Object) -> Result { Ok(match value { Object::Tag(v) => v, _ => return Err(value), }) } } impl TryFrom for Commit { type Error = Object; fn try_from(value: Object) -> Result { Ok(match value { Object::Commit(v) => v, _ => return Err(value), }) } } impl TryFrom for Tree { type Error = Object; fn try_from(value: Object) -> Result { Ok(match value { Object::Tree(v) => v, _ => return Err(value), }) } } impl TryFrom for Blob { type Error = Object; fn try_from(value: Object) -> Result { Ok(match value { Object::Blob(v) => v, _ => return Err(value), }) } } impl<'a> From> for ObjectRef<'a> { fn from(v: TagRef<'a>) -> Self { ObjectRef::Tag(v) } } impl<'a> From> for ObjectRef<'a> { fn from(v: CommitRef<'a>) -> Self { ObjectRef::Commit(v) } } impl<'a> From> for ObjectRef<'a> { fn from(v: TreeRef<'a>) -> Self { ObjectRef::Tree(v) } } impl<'a> From> for ObjectRef<'a> { fn from(v: BlobRef<'a>) -> Self { ObjectRef::Blob(v) } } impl<'a> TryFrom> for TagRef<'a> { type Error = ObjectRef<'a>; fn try_from(value: ObjectRef<'a>) -> Result { Ok(match value { ObjectRef::Tag(v) => v, _ => return Err(value), }) } } impl<'a> TryFrom> for CommitRef<'a> { type Error = ObjectRef<'a>; fn try_from(value: ObjectRef<'a>) -> Result { Ok(match value { ObjectRef::Commit(v) => v, _ => return Err(value), }) } } impl<'a> TryFrom> for TreeRef<'a> { type Error = ObjectRef<'a>; fn try_from(value: ObjectRef<'a>) -> Result { Ok(match value { ObjectRef::Tree(v) => v, _ => return Err(value), }) } } impl<'a> TryFrom> for BlobRef<'a> { type Error = ObjectRef<'a>; fn try_from(value: ObjectRef<'a>) -> Result { Ok(match value { ObjectRef::Blob(v) => v, _ => return Err(value), }) } } gix-object-0.60.0/src/object/mod.rs000064400000000000000000000220331046102023000151240ustar 00000000000000use crate::{Blob, Commit, Object, Tag, Tree}; mod convert; mod write { use std::io; use crate::{Kind, Object, ObjectRef, WriteTo}; /// Serialization impl WriteTo for ObjectRef<'_> { /// Write the contained object to `out` in the git serialization format. fn write_to(&self, out: &mut dyn io::Write) -> io::Result<()> { use crate::ObjectRef::*; match self { Tree(v) => v.write_to(out), Blob(v) => v.write_to(out), Commit(v) => v.write_to(out), Tag(v) => v.write_to(out), } } fn kind(&self) -> Kind { self.kind() } fn size(&self) -> u64 { use crate::ObjectRef::*; match self { Tree(v) => v.size(), Blob(v) => v.size(), Commit(v) => v.size(), Tag(v) => v.size(), } } } /// Serialization impl WriteTo for Object { /// Write the contained object to `out` in the git serialization format. fn write_to(&self, out: &mut dyn io::Write) -> io::Result<()> { use crate::Object::*; match self { Tree(v) => v.write_to(out), Blob(v) => v.write_to(out), Commit(v) => v.write_to(out), Tag(v) => v.write_to(out), } } fn kind(&self) -> Kind { self.kind() } fn size(&self) -> u64 { use crate::Object::*; match self { Tree(v) => v.size(), Blob(v) => v.size(), Commit(v) => v.size(), Tag(v) => v.size(), } } } } /// Convenient extraction of typed object. impl Object { /// Turns this instance into a [`Blob`], panic otherwise. pub fn into_blob(self) -> Blob { match self { Object::Blob(v) => v, _ => panic!("BUG: not a blob"), } } /// Turns this instance into a [`Commit`] panic otherwise. pub fn into_commit(self) -> Commit { match self { Object::Commit(v) => v, _ => panic!("BUG: not a commit"), } } /// Turns this instance into a [`Tree`] panic otherwise. pub fn into_tree(self) -> Tree { match self { Object::Tree(v) => v, _ => panic!("BUG: not a tree"), } } /// Turns this instance into a [`Tag`] panic otherwise. pub fn into_tag(self) -> Tag { match self { Object::Tag(v) => v, _ => panic!("BUG: not a tag"), } } /// Turns this instance into a [`Blob`] if it is one. #[allow(clippy::result_large_err)] pub fn try_into_blob(self) -> Result { match self { Object::Blob(v) => Ok(v), _ => Err(self), } } /// Turns this instance into a [`BlobRef`] if it is a blob. pub fn try_into_blob_ref(&self) -> Option> { match self { Object::Blob(v) => Some(v.to_ref()), _ => None, } } /// Turns this instance into a [`Commit`] if it is one. #[allow(clippy::result_large_err)] pub fn try_into_commit(self) -> Result { match self { Object::Commit(v) => Ok(v), _ => Err(self), } } /// Turns this instance into a [`Tree`] if it is one. #[allow(clippy::result_large_err)] pub fn try_into_tree(self) -> Result { match self { Object::Tree(v) => Ok(v), _ => Err(self), } } /// Turns this instance into a [`Tag`] if it is one. #[allow(clippy::result_large_err)] pub fn try_into_tag(self) -> Result { match self { Object::Tag(v) => Ok(v), _ => Err(self), } } /// Returns a [`Blob`] if it is one. pub fn as_blob(&self) -> Option<&Blob> { match self { Object::Blob(v) => Some(v), _ => None, } } /// Returns a [`Commit`] if it is one. pub fn as_commit(&self) -> Option<&Commit> { match self { Object::Commit(v) => Some(v), _ => None, } } /// Returns a [`Tree`] if it is one. pub fn as_tree(&self) -> Option<&Tree> { match self { Object::Tree(v) => Some(v), _ => None, } } /// Returns a [`Tag`] if it is one. pub fn as_tag(&self) -> Option<&Tag> { match self { Object::Tag(v) => Some(v), _ => None, } } /// Returns the kind of object stored in this instance. pub fn kind(&self) -> crate::Kind { match self { Object::Tree(_) => crate::Kind::Tree, Object::Blob(_) => crate::Kind::Blob, Object::Commit(_) => crate::Kind::Commit, Object::Tag(_) => crate::Kind::Tag, } } } use crate::{ decode::{loose_header, Error as DecodeError, LooseHeaderDecodeError}, BlobRef, CommitRef, Kind, ObjectRef, TagRef, TreeRef, }; #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum LooseDecodeError { #[error(transparent)] InvalidHeader(#[from] LooseHeaderDecodeError), #[error(transparent)] InvalidContent(#[from] DecodeError), #[error("Object sized {size} does not fit into memory - this can happen on 32 bit systems")] OutOfMemory { size: u64 }, } impl<'a> ObjectRef<'a> { /// Deserialize an object from a loose serialisation given `data`, parsing with the provided `hash_kind`. pub fn from_loose(data: &'a [u8], hash_kind: gix_hash::Kind) -> Result, LooseDecodeError> { let (kind, size, offset) = loose_header(data)?; let body = &data[offset..] .get(..size.try_into().map_err(|_| LooseDecodeError::OutOfMemory { size })?) .ok_or(LooseHeaderDecodeError::InvalidHeader { message: "object data was shorter than its size declared in the header", })?; Ok(Self::from_bytes(body, kind, hash_kind)?) } /// Deserialize an object of `kind` from the given `data`, using `hash_kind`. pub fn from_bytes( data: &'a [u8], kind: Kind, hash_kind: gix_hash::Kind, ) -> Result, crate::decode::Error> { Ok(match kind { Kind::Tree => ObjectRef::Tree(TreeRef::from_bytes(data, hash_kind)?), Kind::Blob => ObjectRef::Blob(BlobRef { data }), Kind::Commit => ObjectRef::Commit(CommitRef::from_bytes(data, hash_kind)?), Kind::Tag => ObjectRef::Tag(TagRef::from_bytes(data, hash_kind)?), }) } /// Convert the immutable object into a mutable version, consuming the source in the process. /// /// Note that this is an expensive operation. pub fn into_owned(self) -> Result { self.try_into() } /// Convert this immutable object into its mutable counterpart. /// /// Note that this is an expensive operation. pub fn to_owned(&self) -> Result { self.clone().try_into() } } /// Convenient access to contained objects. impl<'a> ObjectRef<'a> { /// Interpret this object as blob. pub fn as_blob(&self) -> Option<&BlobRef<'a>> { match self { ObjectRef::Blob(v) => Some(v), _ => None, } } /// Interpret this object as blob, chainable. pub fn into_blob(self) -> Option> { match self { ObjectRef::Blob(v) => Some(v), _ => None, } } /// Interpret this object as commit. pub fn as_commit(&self) -> Option<&CommitRef<'a>> { match self { ObjectRef::Commit(v) => Some(v), _ => None, } } /// Interpret this object as commit, chainable. pub fn into_commit(self) -> Option> { match self { ObjectRef::Commit(v) => Some(v), _ => None, } } /// Interpret this object as tree. pub fn as_tree(&self) -> Option<&TreeRef<'a>> { match self { ObjectRef::Tree(v) => Some(v), _ => None, } } /// Interpret this object as tree, chainable pub fn into_tree(self) -> Option> { match self { ObjectRef::Tree(v) => Some(v), _ => None, } } /// Interpret this object as tag. pub fn as_tag(&self) -> Option<&TagRef<'a>> { match self { ObjectRef::Tag(v) => Some(v), _ => None, } } /// Interpret this object as tag, chainable. pub fn into_tag(self) -> Option> { match self { ObjectRef::Tag(v) => Some(v), _ => None, } } /// Return the kind of object. pub fn kind(&self) -> Kind { match self { ObjectRef::Tree(_) => Kind::Tree, ObjectRef::Blob(_) => Kind::Blob, ObjectRef::Commit(_) => Kind::Commit, ObjectRef::Tag(_) => Kind::Tag, } } } gix-object-0.60.0/src/parse.rs000064400000000000000000000125141046102023000142140ustar 00000000000000use bstr::{BStr, BString, ByteSlice, ByteVec}; pub(crate) const NL: &[u8] = b"\n"; pub(crate) const SPACE: &[u8] = b" "; const SPACE_OR_NL: &[u8] = b" \n"; /// The result type shared by object parsers. pub(crate) type ParseResult = Result; /// Parse any multi-line object header field. /// /// Typical input is `gpgsig -----BEGIN...\n \nnext ...`, where the /// field name is followed by a space, an initial value line, and at least one /// continuation line starting with a space. /// /// The returned tuple contains the field name and the unfolded value, /// with the leading space removed from each continuation line. /// /// On success, `i` is advanced to the first byte after the final continuation /// line. pub(crate) fn any_header_field_multi_line<'a>(i: &mut &'a [u8]) -> ParseResult<(&'a [u8], BString)> { let mut c = *i; let input = c; let name_end = c .find_byteset(SPACE_OR_NL) .filter(|pos| *pos > 0) .ok_or(crate::decode::Error)?; if c.get(name_end) != Some(&b' ') { return Err(crate::decode::Error); } c = &c[name_end + 1..]; let first_line_end = c.find_byte(b'\n').ok_or(crate::decode::Error)?; c = &c[first_line_end + 1..]; let mut continuation_end = name_end + 1 + first_line_end + 1; let mut continuation_count = 0usize; while c.first() == Some(&b' ') { let line_end = c.find_byte(b'\n').ok_or(crate::decode::Error)?; continuation_end += line_end + 1; c = &c[line_end + 1..]; continuation_count += 1; } if continuation_count == 0 { return Err(crate::decode::Error); } let bytes = input[name_end + 1..continuation_end].as_bstr(); let mut out = BString::from(Vec::with_capacity(bytes.len())); let mut lines = bytes.lines_with_terminator(); out.push_str(lines.next().expect("first line")); for line in lines { out.push_str(&line[1..]); } *i = &input[continuation_end..]; Ok((input[..name_end].as_bstr(), out)) } /// Parse a specific single-line object header field. /// /// `name` is the header name without its trailing space, for example `b"tree"` /// or `b"author"`. Typical input is ` \n...`. The value bytes, /// excluding the header name, separator, and trailing newline, are passed to /// `parse_value`. /// /// On success, `i` is advanced past the entire header line and the parsed value /// is returned. pub(crate) fn header_field<'a, T>( i: &mut &'a [u8], name: &'static [u8], parse_value: impl FnOnce(&'a [u8]) -> ParseResult, ) -> ParseResult { let c = *i; let Some(rest) = c.strip_prefix(name).and_then(|rest| rest.strip_prefix(SPACE)) else { return Err(crate::decode::Error); }; let Some(nl) = rest.find_byte(b'\n') else { return Err(crate::decode::Error); }; let value = parse_value(&rest[..nl])?; *i = &rest[nl + 1..]; Ok(value) } /// Parse any single-line object header field and return its raw value. /// /// Typical input is ` \n...`. The returned tuple contains the /// field name and the value bytes without the trailing newline. /// /// On success, `i` is advanced past the newline. pub(crate) fn any_header_field<'a>(i: &mut &'a [u8]) -> ParseResult<(&'a [u8], &'a [u8])> { let mut c = *i; let input = c; let name_end = c .find_byteset(SPACE_OR_NL) .filter(|pos| *pos > 0) .ok_or(crate::decode::Error)?; if c.get(name_end) != Some(&b' ') { return Err(crate::decode::Error); } c = &c[name_end + 1..]; if let Some(value_end) = c.find_byte(b'\n') { let value = &c[..value_end]; let rest = &c[value_end + 1..]; *i = rest; Ok((&input[..name_end], value)) } else { Err(crate::decode::Error) } } /// Parse a complete hexadecimal object id of the given `hash_kind`. /// /// Typical input is a 40-byte SHA-1 hex id or a 64-byte SHA-256 hex id, /// depending on `hash_kind`. The entire input slice must be ASCII hex and /// match the expected object hash length. pub fn hex_hash(i: &[u8], hash_kind: gix_hash::Kind) -> ParseResult<&BStr> { if i.len() != hash_kind.len_in_hex() || !i.iter().all(u8::is_ascii_hexdigit) { return Err(crate::decode::Error); } Ok(i.as_bstr()) } /// Parse a complete actor signature. /// /// Typical input is `Name 1700000000 +0000`. /// The entire input slice must be consumed by /// `gix_actor`'s signature parser; trailing bytes cause an error. pub(crate) fn signature(mut i: &[u8]) -> ParseResult> { let signature = gix_actor::SignatureRef::from_bytes_consuming(&mut i).map_err(|_| crate::decode::Error)?; if i.is_empty() { Ok(signature) } else { Err(crate::decode::Error) } } /// Validate a complete actor signature and return its raw bytes. /// /// Typical input is `Name 1700000000 +0000`. On success, the /// returned [`BStr`] borrows all of `i`. pub(crate) fn signature_raw(i: &[u8]) -> ParseResult<&BStr> { signature(i).map(|_| i.as_bstr()) } /// Parse a complete actor signature from a [`BStr`]. /// /// This is a convenience wrapper around [`signature`] for callers that already /// hold byte-string data. pub(crate) fn parse_signature(raw: &BStr) -> Result, crate::decode::Error> { signature(raw.as_ref()) } gix-object-0.60.0/src/tag/decode.rs000064400000000000000000000144751046102023000151100ustar 00000000000000use bstr::ByteSlice; use crate::{parse, parse::ParseResult, BStr, Kind, TagRef}; /// Parse a complete annotated tag object body. /// /// Typical input starts with `object \n`, followed by `type \n`, /// `tag \n`, an optional `tagger \n`, and a message separated /// from the headers by a blank line. On success, the returned [`TagRef`] borrows /// all fields from `i` and `i` is advanced to the empty suffix, and it expects /// to see an entire, fully consumable tag in `i` without any unconsumed content /// after parsing. /// /// This parser is not transactional as a whole: if a later field fails, `i` may /// already have been advanced past earlier successfully parsed fields. Individual /// field parsers document their own cursor behaviour. pub fn git_tag<'a>(i: &mut &'a [u8], hash_kind: gix_hash::Kind) -> ParseResult> { let target = target(i, hash_kind)?; let kind = kind(i)?; let tag_version = name(i)?; let tagger = tagger_raw(i)?; let (message, pgp_signature) = message(i)?; if !i.is_empty() { return Err(crate::decode::Error); } Ok(TagRef { target, name: tag_version.as_bstr(), target_kind: kind, message, tagger, pgp_signature, }) } /// Parse the `object \n` header and return the object id as bytes. /// /// Typical input is `object 0123456789012345678901234567890123456789\n`. /// The hash must match `hash_kind`. Uppercase ASCII hex is also valid. /// On success, `i` is advanced past the entire header line. pub(crate) fn target<'a>(i: &mut &'a [u8], hash_kind: gix_hash::Kind) -> ParseResult<&'a BStr> { parse::header_field(i, b"object", |value| parse::hex_hash(value, hash_kind)) } /// Parse the `type \n` header and return the object kind. /// /// Typical inputs are `type commit\n`, `type tree\n`, `type blob\n`, and /// `type tag\n`. On success, `i` is advanced past the entire header line. pub(crate) fn kind(i: &mut &[u8]) -> ParseResult { parse::header_field(i, b"type", |value| { Kind::from_bytes(value).map_err(|_| crate::decode::Error) }) } /// Parse the `tag \n` header and return the tag name. /// /// A typical input is `tag v1.0.0\n`. The returned name excludes both the /// `tag ` prefix and the trailing newline, and must be non-empty. On success, /// `i` is advanced past the entire header line. pub(crate) fn name<'a>(i: &mut &'a [u8]) -> ParseResult<&'a BStr> { parse::header_field(i, b"tag", |value| { (!value.is_empty()).then(|| value.as_bstr()).ok_or(crate::decode::Error) }) } /// Parse an optional `tagger \n` header and return its raw signature. /// /// A typical input is `tagger Name 1700000000 +0000\n`. If /// the `tagger ` prefix is absent, this returns `Ok(None)`. On success, it /// returns the signature bytes without the prefix or newline and advances `i` /// past the entire header line. pub(crate) fn tagger_raw<'a>(i: &mut &'a [u8]) -> ParseResult> { if !i.starts_with(b"tagger ") { return Ok(None); } parse::header_field(i, b"tagger", |raw| { let mut sig = raw; gix_actor::SignatureRef::from_bytes_consuming(&mut sig).map_err(|_| crate::decode::Error)?; Ok(raw.as_bstr()) }) .map(Some) } /// Parse an optional `tagger \n` header and return the decoded signature. /// /// A typical input is `tagger Name 1700000000 +0000\n`. If /// the `tagger ` prefix is absent, this returns `Ok(None)`. On success, it /// returns the parsed [`gix_actor::SignatureRef`] and advances `i` past the /// entire header line. pub(crate) fn tagger<'a>(i: &mut &'a [u8]) -> ParseResult>> { if !i.starts_with(b"tagger ") { return Ok(None); } parse::header_field(i, b"tagger", |i| { let mut sig = i; let signature = gix_actor::SignatureRef::from_bytes_consuming(&mut sig).map_err(|_| crate::decode::Error)?; Ok(signature) }) .map(Some) } /// Parse the tag message and its optional PGP signature block. /// /// Typical input starts with the blank-line separator before the message, for /// example `\nrelease notes`. A signed input looks like /// `\nrelease notes\n-----BEGIN PGP SIGNATURE-----\n...\n-----END PGP SIGNATURE-----`. /// On success, `i` is always advanced to the empty suffix. The returned tuple /// contains the message and, if a PGP signature marker is found at the /// beginning of a line, all bytes from that marker to the end of the input, /// and notably the end-of-signature marker isn't required. /// /// An input consisting only of newlines is accepted as an empty-header message /// and consumed entirely. In that case, the newlines are returned as part of /// the message to preserve roundtrips for tags whose body is only the /// header/message separator. pub fn message<'a>(i: &mut &'a [u8]) -> ParseResult<(&'a BStr, Option<&'a BStr>)> { const PGP_SIGNATURE_BEGIN: &[u8] = b"-----BEGIN PGP SIGNATURE-----"; if i.iter().all(|b| *b == b'\n') { let message = i.as_bstr(); *i = &[]; return Ok((message, None)); } let Some(rest) = i.strip_prefix(parse::NL) else { return Err(crate::decode::Error); }; *i = &[]; if let Some(sig_start) = find_pgp_signature(rest, PGP_SIGNATURE_BEGIN) { // Truncate newline off the message end. let message_end = if sig_start > 0 && rest[sig_start - 1] == b'\n' { sig_start - 1 } else { sig_start }; let message = rest[..message_end].as_bstr(); let signature = &rest[sig_start..]; return Ok((message, (!signature.is_empty()).then(|| signature.as_bstr()))); } Ok((rest.as_bstr(), None)) } /// Find a PGP signature marker that starts at a line boundary. /// /// `haystack` is usually the tag message body and `needle` is the marker to /// search for. On success, the returned index is the marker itself. fn find_pgp_signature(haystack: &[u8], needle: &[u8]) -> Option { if haystack.starts_with(needle) { return Some(0); } let mut offset = 0; while let Some(pos) = haystack.get(offset..)?.find_byte(b'\n') { let found = offset + pos + 1; if haystack[found..].starts_with(needle) { return Some(found); } offset = found; } None } gix-object-0.60.0/src/tag/mod.rs000064400000000000000000000020611046102023000144300ustar 00000000000000use crate::parse::parse_signature; use crate::TagRef; mod decode; /// pub mod write; /// pub mod ref_iter; impl<'a> TagRef<'a> { /// Deserialize a tag from `data`. pub fn from_bytes(mut data: &'a [u8], hash_kind: gix_hash::Kind) -> Result, crate::decode::Error> { let input = &mut data; match decode::git_tag(input, hash_kind) { Ok(tag) => Ok(tag), Err(err) => Err(err), } } /// The object this tag points to as `Id`. pub fn target(&self) -> gix_hash::ObjectId { gix_hash::ObjectId::from_hex(self.target).expect("prior validation") } /// Return the tagger, if present. pub fn tagger(&self) -> Result>, crate::decode::Error> { Ok(self .tagger .map(parse_signature) .transpose()? .map(|signature| signature.trim())) } /// Copy all data into a fully-owned instance. pub fn into_owned(self) -> Result { self.try_into() } } gix-object-0.60.0/src/tag/ref_iter.rs000064400000000000000000000116711046102023000154570ustar 00000000000000use bstr::BStr; use gix_hash::{oid, ObjectId}; use crate::{bstr::ByteSlice, tag::decode, Kind, TagRefIter}; #[derive(Default, Copy, Clone)] pub(crate) enum State { #[default] Target, TargetKind, Name, Tagger, Message, } impl<'a> TagRefIter<'a> { /// Create a tag iterator from `data`, parsing hashes as `hash_kind`. pub fn from_bytes(data: &'a [u8], hash_kind: gix_hash::Kind) -> TagRefIter<'a> { TagRefIter { data, state: State::default(), hash_kind, } } /// Returns the target id of this tag if it is the first function called and if there is no error in decoding /// the data. /// /// Note that this method must only be called once or else will always return None while consuming a single token. /// Errors are coerced into options, hiding whether there was an error or not. The caller should assume an error if they /// call the method as intended. Such a squelched error cannot be recovered unless the objects data is retrieved and parsed again. /// `next()`. pub fn target_id(mut self) -> Result { let token = self.next().ok_or_else(missing_field)??; Token::into_id(token).ok_or_else(missing_field) } /// Returns the taggers signature if there is no decoding error, and if this field exists. /// Errors are coerced into options, hiding whether there was an error or not. The caller knows if there was an error or not. pub fn tagger(mut self) -> Result>, crate::decode::Error> { self.find_map(|t| match t { Ok(Token::Tagger(signature)) => Some(Ok(signature)), Err(err) => Some(Err(err)), _ => None, }) .ok_or_else(missing_field)? } } fn missing_field() -> crate::decode::Error { crate::decode::empty_error() } impl<'a> TagRefIter<'a> { #[inline] fn next_inner( mut i: &'a [u8], state: &mut State, hash_kind: gix_hash::Kind, ) -> Result<(&'a [u8], Token<'a>), crate::decode::Error> { let input = &mut i; match Self::next_inner_(input, state, hash_kind) { Ok(token) => Ok((*input, token)), Err(err) => Err(err), } } fn next_inner_( input: &mut &'a [u8], state: &mut State, hash_kind: gix_hash::Kind, ) -> Result, crate::decode::Error> { use State::*; Ok(match state { Target => { let target = decode::target(input, hash_kind)?; *state = TargetKind; Token::Target { id: ObjectId::from_hex(target).expect("parsing validation"), } } TargetKind => { let kind = decode::kind(input)?; *state = Name; Token::TargetKind(kind) } Name => { let tag_version = decode::name(input)?; *state = Tagger; Token::Name(tag_version.as_bstr()) } Tagger => { *state = Message; let signature = decode::tagger(input)?; Token::Tagger(signature) } Message => { let (message, pgp_signature) = decode::message(input)?; debug_assert!( input.is_empty(), "we should have consumed all data - otherwise iter may go forever" ); Token::Body { message, pgp_signature } } }) } } impl<'a> Iterator for TagRefIter<'a> { type Item = Result, crate::decode::Error>; fn next(&mut self) -> Option { if self.data.is_empty() { return None; } match Self::next_inner(self.data, &mut self.state, self.hash_kind) { Ok((data, token)) => { self.data = data; Some(Ok(token)) } Err(err) => { self.data = &[]; Some(Err(err)) } } } } /// A token returned by the [tag iterator][TagRefIter]. #[allow(missing_docs)] #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] pub enum Token<'a> { Target { id: ObjectId, }, TargetKind(Kind), Name(&'a BStr), Tagger(Option>), Body { message: &'a BStr, pgp_signature: Option<&'a BStr>, }, } impl Token<'_> { /// Return the object id of this token if its a [Target][Token::Target]. pub fn id(&self) -> Option<&oid> { match self { Token::Target { id } => Some(id.as_ref()), _ => None, } } /// Return the owned object id of this token if its a [Target][Token::Target]. pub fn into_id(self) -> Option { match self { Token::Target { id } => Some(id), _ => None, } } } gix-object-0.60.0/src/tag/write/tests.rs000064400000000000000000000012021046102023000161410ustar 00000000000000mod validated_name { mod invalid { use bstr::ByteSlice; use super::super::super::*; #[test] fn only_dash() { assert!(validated_name(b"-".as_bstr()).is_err()); } #[test] fn leading_dash() { assert!(validated_name(b"-hello".as_bstr()).is_err()); } } mod valid { use bstr::ByteSlice; use super::super::super::*; #[test] fn version() { for version in &["v1.0.0", "0.2.1", "0-alpha1"] { assert!(validated_name(version.as_bytes().as_bstr()).is_ok()); } } } } gix-object-0.60.0/src/tag/write.rs000064400000000000000000000072031046102023000150060ustar 00000000000000use std::io; use bstr::BStr; use gix_date::parse::TimeBuf; use crate::{encode, encode::NL, Kind, Tag, TagRef}; /// An Error used in [`Tag::write_to()`][crate::WriteTo::write_to()]. #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum Error { #[error("Tags must not start with a dash: '-'")] StartsWithDash, #[error("The tag name was no valid reference name")] InvalidRefName(#[from] gix_validate::tag::name::Error), } impl From for io::Error { fn from(err: Error) -> Self { io::Error::other(err) } } impl crate::WriteTo for Tag { fn write_to(&self, out: &mut dyn io::Write) -> io::Result<()> { encode::trusted_header_id(b"object", &self.target, out)?; encode::trusted_header_field(b"type", self.target_kind.as_bytes(), out)?; encode::header_field(b"tag", validated_name(self.name.as_ref())?, out)?; if let Some(tagger) = &self.tagger { let mut buf = TimeBuf::default(); encode::trusted_header_signature(b"tagger", &tagger.to_ref(&mut buf), out)?; } if !self.message.iter().all(|b| *b == b'\n') { out.write_all(NL)?; } out.write_all(self.message.as_ref())?; if let Some(message) = &self.pgp_signature { out.write_all(NL)?; out.write_all(message.as_ref())?; } Ok(()) } fn kind(&self) -> Kind { Kind::Tag } fn size(&self) -> u64 { (b"object".len() + 1 /* space */ + self.target.kind().len_in_hex() + 1 /* nl */ + b"type".len() + 1 /* space */ + self.target_kind.as_bytes().len() + 1 /* nl */ + b"tag".len() + 1 /* space */ + self.name.len() + 1 /* nl */ + self .tagger .as_ref() .map_or(0, |t| b"tagger".len() + 1 /* space */ + t.size() + 1 /* nl */) + if self.message.iter().all(|b| *b == b'\n') { 0 } else { 1 /* nl */ } + self.message.len() + self.pgp_signature.as_ref().map_or(0, |m| 1 /* nl */ + m.len())) as u64 } } impl crate::WriteTo for TagRef<'_> { fn write_to(&self, mut out: &mut dyn io::Write) -> io::Result<()> { encode::trusted_header_field(b"object", self.target, &mut out)?; encode::trusted_header_field(b"type", self.target_kind.as_bytes(), &mut out)?; encode::header_field(b"tag", validated_name(self.name)?, &mut out)?; if let Some(tagger) = self.tagger { encode::trusted_header_field(b"tagger", tagger.as_ref(), &mut out)?; } if !self.message.iter().all(|b| *b == b'\n') { out.write_all(NL)?; } out.write_all(self.message)?; if let Some(message) = self.pgp_signature { out.write_all(NL)?; out.write_all(message)?; } Ok(()) } fn kind(&self) -> Kind { Kind::Tag } fn size(&self) -> u64 { (b"object".len() + 1 /* space */ + self.target().kind().len_in_hex() + 1 /* nl */ + b"type".len() + 1 /* space */ + self.target_kind.as_bytes().len() + 1 /* nl */ + b"tag".len() + 1 /* space */ + self.name.len() + 1 /* nl */ + self .tagger .map_or(0, |raw| b"tagger".len() + 1 /* space */ + raw.len() + 1 /* nl */) + if self.message.iter().all(|b| *b == b'\n') { 0 } else { 1 /* nl */ } + self.message.len() + self.pgp_signature.as_ref().map_or(0, |m| 1 /* nl */ + m.len())) as u64 } } fn validated_name(name: &BStr) -> Result<&BStr, Error> { gix_validate::tag::name(name)?; if name[0] == b'-' { return Err(Error::StartsWithDash); } Ok(name) } #[cfg(test)] mod tests; gix-object-0.60.0/src/traits/_impls.rs000064400000000000000000000036141046102023000156740ustar 00000000000000use std::{io::Read, ops::Deref, rc::Rc, sync::Arc}; use gix_hash::ObjectId; use crate::{Kind, WriteTo}; impl crate::Write for &T where T: crate::Write, { fn write(&self, object: &dyn WriteTo) -> Result { (*self).write(object) } fn write_buf(&self, object: Kind, from: &[u8]) -> Result { (*self).write_buf(object, from) } fn write_stream(&self, kind: Kind, size: u64, from: &mut dyn Read) -> Result { (*self).write_stream(kind, size, from) } } impl crate::Write for Arc where T: crate::Write, { fn write(&self, object: &dyn WriteTo) -> Result { self.deref().write(object) } fn write_buf(&self, object: Kind, from: &[u8]) -> Result { self.deref().write_buf(object, from) } fn write_stream(&self, kind: Kind, size: u64, from: &mut dyn Read) -> Result { self.deref().write_stream(kind, size, from) } } impl crate::Write for Rc where T: crate::Write, { fn write(&self, object: &dyn WriteTo) -> Result { self.deref().write(object) } fn write_buf(&self, object: Kind, from: &[u8]) -> Result { self.deref().write_buf(object, from) } fn write_stream(&self, kind: Kind, size: u64, from: &mut dyn Read) -> Result { self.deref().write_stream(kind, size, from) } } impl WriteTo for &T where T: WriteTo, { fn write_to(&self, out: &mut dyn std::io::Write) -> std::io::Result<()> { ::write_to(self, out) } fn kind(&self) -> Kind { ::kind(self) } fn size(&self) -> u64 { ::size(self) } } gix-object-0.60.0/src/traits/find.rs000064400000000000000000000263621046102023000153360ustar 00000000000000use crate::find; /// Check if an object is present in an object store. pub trait Exists { /// Returns `true` if the object exists in the database. fn exists(&self, id: &gix_hash::oid) -> bool; } /// Find an object in the object store. /// /// ## Notes /// /// Find effectively needs [generic associated types][issue] to allow a trait for the returned object type. /// Until then, we will have to make due with explicit types and give them the potentially added features we want. /// /// [issue]: https://github.com/rust-lang/rust/issues/44265 pub trait Find { /// Find an object matching `id` in the database while placing its raw, possibly encoded data into `buffer`. /// /// Returns `Some` object if it was present in the database, or the error that occurred during lookup or object /// retrieval. fn try_find<'a>(&self, id: &gix_hash::oid, buffer: &'a mut Vec) -> Result>, find::Error>; } /// Find the header of an object in the object store. pub trait Header { /// Find the header of the object matching `id` in the database. /// /// Returns `Some` header if it was present, or the error that occurred during lookup. fn try_header(&self, id: &gix_hash::oid) -> Result, find::Error>; } /// A combination of [`Find`] and [`Header`] traits to help with `dyn` trait objects. pub trait FindObjectOrHeader: Find + Header {} mod _impls { use std::{ops::Deref, rc::Rc, sync::Arc}; use gix_hash::oid; use crate::Data; impl crate::Exists for &T where T: crate::Exists, { fn exists(&self, id: &oid) -> bool { (*self).exists(id) } } impl crate::FindObjectOrHeader for T where T: crate::Find + crate::FindHeader {} impl crate::Find for &T where T: crate::Find, { fn try_find<'a>(&self, id: &oid, buffer: &'a mut Vec) -> Result>, crate::find::Error> { (*self).try_find(id, buffer) } } impl crate::FindHeader for &T where T: crate::FindHeader, { fn try_header(&self, id: &gix_hash::oid) -> Result, crate::find::Error> { (*self).try_header(id) } } impl crate::Exists for Box where T: crate::Exists, { fn exists(&self, id: &oid) -> bool { self.deref().exists(id) } } impl crate::Exists for Rc where T: crate::Exists, { fn exists(&self, id: &oid) -> bool { self.deref().exists(id) } } impl crate::Find for Rc where T: crate::Find, { fn try_find<'a>(&self, id: &oid, buffer: &'a mut Vec) -> Result>, crate::find::Error> { self.deref().try_find(id, buffer) } } impl crate::FindHeader for Rc where T: crate::FindHeader, { fn try_header(&self, id: &gix_hash::oid) -> Result, crate::find::Error> { self.deref().try_header(id) } } impl crate::Find for Box where T: crate::Find, { fn try_find<'a>(&self, id: &oid, buffer: &'a mut Vec) -> Result>, crate::find::Error> { self.deref().try_find(id, buffer) } } impl crate::FindHeader for Box where T: crate::FindHeader, { fn try_header(&self, id: &gix_hash::oid) -> Result, crate::find::Error> { self.deref().try_header(id) } } impl crate::Exists for Arc where T: crate::Exists, { fn exists(&self, id: &oid) -> bool { self.deref().exists(id) } } impl crate::Find for Arc where T: crate::Find, { fn try_find<'a>(&self, id: &oid, buffer: &'a mut Vec) -> Result>, crate::find::Error> { self.deref().try_find(id, buffer) } } impl crate::FindHeader for Arc where T: crate::FindHeader, { fn try_header(&self, id: &gix_hash::oid) -> Result, crate::find::Error> { self.deref().try_header(id) } } } mod ext { use crate::{find, BlobRef, CommitRef, CommitRefIter, Kind, ObjectRef, TagRef, TagRefIter, TreeRef, TreeRefIter}; macro_rules! make_obj_lookup { ($method:ident, $object_variant:path, $object_kind:path, $object_type:ty) => { /// Like [`find(…)`][Self::find()], but flattens the `Result>` into a single `Result` making a non-existing object an error /// while returning the desired object type. fn $method<'a>( &self, id: &gix_hash::oid, buffer: &'a mut Vec, ) -> Result<$object_type, find::existing_object::Error> { self.try_find(id, buffer) .map_err(find::existing_object::Error::Find)? .ok_or_else(|| find::existing_object::Error::NotFound { oid: id.as_ref().to_owned(), }) .and_then(|o| { o.decode().map_err(|err| find::existing_object::Error::Decode { source: err, oid: id.as_ref().to_owned(), }) }) .and_then(|o| match o { $object_variant(o) => return Ok(o), o => Err(find::existing_object::Error::ObjectKind { oid: id.as_ref().to_owned(), actual: o.kind(), expected: $object_kind, }), }) } }; } macro_rules! make_iter_lookup { ($method:ident, $object_kind:path, $object_type:ty, $into_iter:tt) => { /// Like [`find(…)`][Self::find()], but flattens the `Result>` into a single `Result` making a non-existing object an error /// while returning the desired iterator type. fn $method<'a>( &self, id: &gix_hash::oid, buffer: &'a mut Vec, ) -> Result<$object_type, find::existing_iter::Error> { self.try_find(id, buffer) .map_err(find::existing_iter::Error::Find)? .ok_or_else(|| find::existing_iter::Error::NotFound { oid: id.as_ref().to_owned(), }) .and_then(|o| { o.$into_iter() .ok_or_else(|| find::existing_iter::Error::ObjectKind { oid: id.as_ref().to_owned(), actual: o.kind, expected: $object_kind, }) }) } }; } /// An extension trait with convenience functions. pub trait HeaderExt: super::Header { /// Like [`try_header(…)`](super::Header::try_header()), but flattens the `Result>` into a single `Result` making a non-existing header an error. fn header(&self, id: &gix_hash::oid) -> Result { self.try_header(id) .map_err(find::existing::Error::Find)? .ok_or_else(|| find::existing::Error::NotFound { oid: id.to_owned() }) } } /// An extension trait with convenience functions. pub trait FindExt: super::Find { /// Like [`try_find(…)`](super::Find::try_find()), but flattens the `Result>` into a single `Result` making a non-existing object an error. fn find<'a>( &self, id: &gix_hash::oid, buffer: &'a mut Vec, ) -> Result, find::existing::Error> { self.try_find(id, buffer) .map_err(find::existing::Error::Find)? .ok_or_else(|| find::existing::Error::NotFound { oid: id.to_owned() }) } /// Like [`find(…)`][Self::find()], but flattens the `Result>` into a single `Result` making a non-existing object an error /// while returning the desired object type. fn find_blob<'a>( &self, id: &gix_hash::oid, buffer: &'a mut Vec, ) -> Result, find::existing_object::Error> { if id == gix_hash::ObjectId::empty_blob(id.kind()) { return Ok(BlobRef { data: &[] }); } self.try_find(id, buffer) .map_err(find::existing_object::Error::Find)? .ok_or_else(|| find::existing_object::Error::NotFound { oid: id.as_ref().to_owned(), }) .and_then(|o| { o.decode().map_err(|err| find::existing_object::Error::Decode { source: err, oid: id.as_ref().to_owned(), }) }) .and_then(|o| match o { ObjectRef::Blob(o) => Ok(o), o => Err(find::existing_object::Error::ObjectKind { oid: id.as_ref().to_owned(), actual: o.kind(), expected: Kind::Blob, }), }) } /// Like [`find(…)`][Self::find()], but flattens the `Result>` into a single `Result` making a non-existing object an error /// while returning the desired object type. fn find_tree<'a>( &self, id: &gix_hash::oid, buffer: &'a mut Vec, ) -> Result, find::existing_object::Error> { if id == gix_hash::ObjectId::empty_tree(id.kind()) { return Ok(TreeRef { entries: Vec::new() }); } self.try_find(id, buffer) .map_err(find::existing_object::Error::Find)? .ok_or_else(|| find::existing_object::Error::NotFound { oid: id.as_ref().to_owned(), }) .and_then(|o| { o.decode().map_err(|err| find::existing_object::Error::Decode { source: err, oid: id.as_ref().to_owned(), }) }) .and_then(|o| match o { ObjectRef::Tree(o) => Ok(o), o => Err(find::existing_object::Error::ObjectKind { oid: id.as_ref().to_owned(), actual: o.kind(), expected: Kind::Tree, }), }) } make_obj_lookup!(find_commit, ObjectRef::Commit, Kind::Commit, CommitRef<'a>); make_obj_lookup!(find_tag, ObjectRef::Tag, Kind::Tag, TagRef<'a>); make_iter_lookup!(find_commit_iter, Kind::Commit, CommitRefIter<'a>, try_into_commit_iter); make_iter_lookup!(find_tree_iter, Kind::Tree, TreeRefIter<'a>, try_into_tree_iter); make_iter_lookup!(find_tag_iter, Kind::Tag, TagRefIter<'a>, try_into_tag_iter); } impl FindExt for T {} } pub use ext::{FindExt, HeaderExt}; gix-object-0.60.0/src/traits/mod.rs000064400000000000000000000041171046102023000151670ustar 00000000000000use std::io; use crate::Kind; /// Describe the capability to write git objects into an object store. pub trait Write { /// Write objects using the intrinsic kind of [`hash`](gix_hash::Kind) into the database, /// returning id to reference it in subsequent reads. fn write(&self, object: &dyn WriteTo) -> Result { let mut buf = Vec::with_capacity(2048); object.write_to(&mut buf)?; self.write_stream(object.kind(), buf.len() as u64, &mut buf.as_slice()) } /// As [`write`](Write::write), but takes an [`object` kind](Kind) along with its encoded bytes. fn write_buf(&self, object: crate::Kind, mut from: &[u8]) -> Result { self.write_stream(object, from.len() as u64, &mut from) } /// As [`write`](Write::write), but takes an input stream. /// This is commonly used for writing blobs directly without reading them to memory first. fn write_stream( &self, kind: crate::Kind, size: u64, from: &mut dyn io::Read, ) -> Result; } /// Writing of objects to a `Write` implementation pub trait WriteTo { /// Write a representation of this instance to `out`. fn write_to(&self, out: &mut dyn std::io::Write) -> std::io::Result<()>; /// Returns the type of this object. fn kind(&self) -> Kind; /// Returns the size of this object's representation (the amount /// of data which would be written by [`write_to`](Self::write_to)). /// /// [`size`](Self::size)'s value has no bearing on the validity of /// the object, as such it's possible for [`size`](Self::size) to /// return a sensible value but [`write_to`](Self::write_to) to /// fail because the object was not actually valid in some way. fn size(&self) -> u64; /// Returns a loose object header based on the object's data fn loose_header(&self) -> smallvec::SmallVec<[u8; 28]> { crate::encode::loose_header(self.kind(), self.size()) } } mod _impls; mod find; pub use find::*; gix-object-0.60.0/src/tree/editor.rs000064400000000000000000000512241046102023000153300ustar 00000000000000use std::{ cmp::Ordering, collections::{hash_map, HashMap}, fmt::Formatter, }; use bstr::{BStr, BString, ByteSlice, ByteVec}; use gix_hash::ObjectId; use crate::{ tree, tree::{Editor, EntryKind}, Tree, }; /// A way to constrain all [tree-edits](Editor) to a given subtree. pub struct Cursor<'a, 'find> { /// The underlying editor parent: &'a mut Editor<'find>, /// Our own location, used as prefix for all operations. /// Note that it's assumed to always contain a tree. prefix: BString, } impl std::fmt::Debug for Editor<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("Editor") .field("object_hash", &self.object_hash) .field("path_buf", &self.path_buf) .field("trees", &self.trees) .finish() } } /// The error returned by [Editor] or [Cursor] edit operation. #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum Error { #[error("Empty path components are not allowed")] EmptyPathComponent, #[error(transparent)] FindExistingObject(#[from] crate::find::existing_object::Error), } /// Lifecycle impl<'a> Editor<'a> { /// Create a new editor that uses `root` as base for all edits. Use `find` to lookup existing /// trees when edits are made. Each tree will only be looked-up once and then edited in place from /// that point on. /// `object_hash` denotes the kind of hash to create. pub fn new(root: Tree, find: &'a dyn crate::FindExt, object_hash: gix_hash::Kind) -> Self { Editor { find, object_hash, trees: HashMap::from_iter(Some((empty_path(), root))), path_buf: BString::from(Vec::with_capacity(256)).into(), tree_buf: Vec::with_capacity(512), } } } /// Operations impl Editor<'_> { /// Write the entire in-memory state of all changed trees (and only changed trees) to `out`, and remove /// written portions from our state except for the root tree, which affects [`get()`](Editor::get()). /// Note that the returned object id *can* be the empty tree if everything was removed or if nothing /// was added to the tree. /// /// The last call to `out` will be the changed root tree, whose object-id will also be returned. /// `out` is free to do any kind of additional validation, like to assure that all entries in the tree exist. /// We don't assure that as there is no validation that inserted entries are valid object ids. /// /// Future calls to [`upsert`](Self::upsert) or similar will keep working on the last seen state of the /// just-written root-tree. /// If this is not desired, use [set_root()](Self::set_root()). /// /// ### Validation /// /// Note that no additional validation is performed to assure correctness of entry-names. /// It is absolutely and intentionally possible to write out invalid trees with this method. /// Higher layers are expected to perform detailed validation. pub fn write(&mut self, out: impl FnMut(&Tree) -> Result) -> Result { self.path_buf.borrow_mut().clear(); self.write_at_pathbuf(out, WriteMode::Normal) } /// Remove the entry at `rela_path`, loading all trees on the path accordingly. /// It's no error if the entry doesn't exist, or if `rela_path` doesn't lead to an existing entry at all. /// /// Note that trying to remove a path with an empty component is also forbidden. pub fn remove(&mut self, rela_path: I) -> Result<&mut Self, Error> where I: IntoIterator, C: AsRef, { self.path_buf.borrow_mut().clear(); self.upsert_or_remove_at_pathbuf(rela_path, None) } /// Obtain the entry at `rela_path` or return `None` if none was found, or the tree wasn't yet written /// to that point. /// Note that after [writing](Self::write) only the root path remains, all other intermediate trees are removed. /// The entry can be anything that can be stored in a tree, but may have a null-id if it's a newly /// inserted tree. Also, ids of trees might not be accurate as they may have been changed in memory. pub fn get(&self, rela_path: I) -> Option<&tree::Entry> where I: IntoIterator, C: AsRef, { self.path_buf.borrow_mut().clear(); self.get_inner(rela_path) } /// Insert a new entry of `kind` with `id` at `rela_path`, an iterator over each path component in the tree, /// like `a/b/c`. Names are matched case-sensitively. /// /// Existing leaf-entries will be overwritten unconditionally, and it is assumed that `id` is available in the object database /// or will be made available at a later point to assure the integrity of the produced tree. /// /// Intermediate trees will be created if they don't exist in the object database, otherwise they will be loaded and entries /// will be inserted into them instead. /// /// Note that `id` can be [null](ObjectId::null()) to create a placeholder. These will not be written, and paths leading /// through them will not be considered a problem. /// /// `id` can also be an empty tree, along with [the respective `kind`](EntryKind::Tree), even though that's normally not allowed /// in Git trees. pub fn upsert(&mut self, rela_path: I, kind: EntryKind, id: ObjectId) -> Result<&mut Self, Error> where I: IntoIterator, C: AsRef, { self.path_buf.borrow_mut().clear(); self.upsert_or_remove_at_pathbuf(rela_path, Some((kind, id, UpsertMode::Normal))) } fn get_inner(&self, rela_path: I) -> Option<&tree::Entry> where I: IntoIterator, C: AsRef, { let mut path_buf = self.path_buf.borrow_mut(); let mut cursor = self.trees.get(path_buf.as_bstr()).expect("root is always present"); let mut rela_path = rela_path.into_iter().peekable(); while let Some(name) = rela_path.next() { let name = name.as_ref(); let is_last = rela_path.peek().is_none(); match cursor .entries .binary_search_by(|e| cmp_entry_with_name(e, name, true)) .or_else(|_| cursor.entries.binary_search_by(|e| cmp_entry_with_name(e, name, false))) { Ok(idx) if is_last => return Some(&cursor.entries[idx]), Ok(idx) => { if cursor.entries[idx].mode.is_tree() { push_path_component(&mut path_buf, name); cursor = self.trees.get(path_buf.as_bstr())?; } else { break; } } Err(_) => break, } } None } fn write_at_pathbuf( &mut self, mut out: impl FnMut(&Tree) -> Result, mode: WriteMode, ) -> Result { assert_ne!(self.trees.len(), 0, "there is at least the root tree"); // back is for children, front is for parents. let path_buf = self.path_buf.borrow_mut(); let mut parents = vec![( None::, path_buf.clone(), self.trees .remove(path_buf.as_bstr()) .expect("root tree is always present"), )]; let mut children = Vec::new(); while let Some((parent_idx, mut rela_path, mut tree)) = children.pop().or_else(|| parents.pop()) { let mut all_entries_unchanged_or_written = true; for entry in &tree.entries { if entry.mode.is_tree() { let prev_len = push_path_component(&mut rela_path, &entry.filename); if let Some(sub_tree) = self.trees.remove(&rela_path) { all_entries_unchanged_or_written = false; let next_parent_idx = parents.len(); children.push((Some(next_parent_idx), rela_path.clone(), sub_tree)); } rela_path.truncate(prev_len); } } if all_entries_unchanged_or_written { tree.entries.retain(|e| !e.oid.is_null()); if let Some((_, _, parent_to_adjust)) = parent_idx.map(|idx| parents.get_mut(idx).expect("always present, pointing towards zero")) { let name = filename(rela_path.as_bstr()); let entry_idx = parent_to_adjust .entries .binary_search_by(|e| cmp_entry_with_name(e, name, true)) .expect("the parent always knows us by name"); if tree.entries.is_empty() { parent_to_adjust.entries.remove(entry_idx); } else { match out(&tree) { Ok(id) => { parent_to_adjust.entries[entry_idx].oid = id; } Err(err) => { let root_tree = parents.into_iter().next().expect("root wasn't consumed yet"); self.trees.insert(root_tree.1, root_tree.2); return Err(err); } } } } else if parents.is_empty() { debug_assert!(children.is_empty(), "we consume children before parents"); debug_assert_eq!(rela_path, **path_buf, "this should always be the root tree"); // There may be left-over trees if they are replaced with blobs for example. match out(&tree) { Ok(id) => { let root_tree_id = id; match mode { WriteMode::Normal => { self.trees.clear(); } WriteMode::FromCursor => {} } self.trees.insert(rela_path, tree); return Ok(root_tree_id); } Err(err) => { self.trees.insert(rela_path, tree); return Err(err); } } } else if !tree.entries.is_empty() { out(&tree)?; } } else { parents.push((parent_idx, rela_path, tree)); } } unreachable!("we exit as soon as everything is consumed") } fn upsert_or_remove_at_pathbuf( &mut self, rela_path: I, kind_and_id: Option<(EntryKind, ObjectId, UpsertMode)>, ) -> Result<&mut Self, Error> where I: IntoIterator, C: AsRef, { let mut path_buf = self.path_buf.borrow_mut(); let mut cursor = self.trees.get_mut(path_buf.as_bstr()).expect("root is always present"); let mut rela_path = rela_path.into_iter().peekable(); let new_kind_is_tree = kind_and_id.is_some_and(|(kind, _, _)| kind == EntryKind::Tree); while let Some(name) = rela_path.next() { let name = name.as_ref(); if name.is_empty() { return Err(Error::EmptyPathComponent); } let is_last = rela_path.peek().is_none(); let mut needs_sorting = false; let current_level_must_be_tree = !is_last || new_kind_is_tree; let check_type_change = |entry: &tree::Entry| entry.mode.is_tree() != current_level_must_be_tree; let tree_to_lookup = match cursor .entries .binary_search_by(|e| cmp_entry_with_name(e, name, false)) .or_else(|file_insertion_idx| { cursor .entries .binary_search_by(|e| cmp_entry_with_name(e, name, true)) .map_err(|dir_insertion_index| { if current_level_must_be_tree { dir_insertion_index } else { file_insertion_idx } }) }) { Ok(idx) => { match kind_and_id { None => { if is_last { cursor.entries.remove(idx); break; } else { let entry = &cursor.entries[idx]; if entry.mode.is_tree() { Some(entry.oid) } else { break; } } } Some((kind, id, _mode)) => { let entry = &mut cursor.entries[idx]; if is_last { // unconditionally overwrite what's there. entry.oid = id; needs_sorting = check_type_change(entry); entry.mode = kind.into(); None } else if entry.mode.is_tree() { // Possibly lookup the existing tree on our way down the path. Some(entry.oid) } else { // it is no tree, but we are traversing a path, so turn it into one. entry.oid = id.kind().null(); needs_sorting = check_type_change(entry); entry.mode = EntryKind::Tree.into(); None } } } } Err(insertion_idx) => match kind_and_id { None => break, Some((kind, id, _mode)) => { cursor.entries.insert( insertion_idx, tree::Entry { filename: name.into(), mode: if is_last { kind.into() } else { EntryKind::Tree.into() }, oid: if is_last { id } else { id.kind().null() }, }, ); None } }, }; if needs_sorting { cursor.entries.sort(); } if is_last && kind_and_id.is_some_and(|(_, _, mode)| mode == UpsertMode::Normal) { break; } push_path_component(&mut path_buf, name); cursor = match self.trees.entry(path_buf.clone()) { hash_map::Entry::Occupied(e) => e.into_mut(), hash_map::Entry::Vacant(e) => e.insert( if let Some(tree_id) = tree_to_lookup.filter(|tree_id| !tree_id.is_empty_tree()) { self.find.find_tree(&tree_id, &mut self.tree_buf)?.into() } else { Tree::default() }, ), }; } drop(path_buf); Ok(self) } /// Set the root tree of the modification to `root`, assuring it has a well-known state. /// /// Note that this erases all previous edits. /// /// This is useful if the same editor is re-used for various trees. pub fn set_root(&mut self, root: Tree) -> &mut Self { self.trees.clear(); self.trees.insert(empty_path(), root); self } } mod cursor { use bstr::{BStr, BString}; use gix_hash::ObjectId; use crate::{ tree, tree::{ editor::{Cursor, UpsertMode, WriteMode}, Editor, EntryKind, }, Tree, }; /// Cursor handling impl<'a> Editor<'a> { /// Turn ourselves as a cursor, which points to the same tree as the editor. /// /// This is useful if a method takes a [`Cursor`], not an [`Editor`]. pub fn to_cursor(&mut self) -> Cursor<'_, 'a> { Cursor { parent: self, prefix: BString::default(), } } /// Create a cursor at the given `rela_path`, which must be a tree or is turned into a tree as its own edit. /// /// The returned cursor will then allow applying edits to the tree at `rela_path` as root. /// If `rela_path` is a single empty string, it is equivalent to using the current instance itself. pub fn cursor_at(&mut self, rela_path: I) -> Result, super::Error> where I: IntoIterator, C: AsRef, { self.path_buf.borrow_mut().clear(); self.upsert_or_remove_at_pathbuf( rela_path, Some((EntryKind::Tree, self.object_hash.null(), UpsertMode::AssureTreeOnly)), )?; let prefix = self.path_buf.borrow_mut().clone(); Ok(Cursor { prefix, /* set during the upsert call */ parent: self, }) } } impl Cursor<'_, '_> { /// Obtain the entry at `rela_path` or return `None` if none was found, or the tree wasn't yet written /// to that point. /// Note that after [writing](Self::write) only the root path remains, all other intermediate trees are removed. /// The entry can be anything that can be stored in a tree, but may have a null-id if it's a newly /// inserted tree. Also, ids of trees might not be accurate as they may have been changed in memory. pub fn get(&self, rela_path: I) -> Option<&tree::Entry> where I: IntoIterator, C: AsRef, { self.parent.path_buf.borrow_mut().clone_from(&self.prefix); self.parent.get_inner(rela_path) } /// Like [`Editor::upsert()`], but with the constraint of only editing in this cursor's tree. pub fn upsert(&mut self, rela_path: I, kind: EntryKind, id: ObjectId) -> Result<&mut Self, super::Error> where I: IntoIterator, C: AsRef, { self.parent.path_buf.borrow_mut().clone_from(&self.prefix); self.parent .upsert_or_remove_at_pathbuf(rela_path, Some((kind, id, UpsertMode::Normal)))?; Ok(self) } /// Like [`Editor::remove()`], but with the constraint of only editing in this cursor's tree. pub fn remove(&mut self, rela_path: I) -> Result<&mut Self, super::Error> where I: IntoIterator, C: AsRef, { self.parent.path_buf.borrow_mut().clone_from(&self.prefix); self.parent.upsert_or_remove_at_pathbuf(rela_path, None)?; Ok(self) } /// Like [`Editor::write()`], but will write only the subtree of the cursor. pub fn write(&mut self, out: impl FnMut(&Tree) -> Result) -> Result { self.parent.path_buf.borrow_mut().clone_from(&self.prefix); self.parent.write_at_pathbuf(out, WriteMode::FromCursor) } } } #[derive(Copy, Clone, Eq, PartialEq)] enum UpsertMode { Normal, /// Only make sure there is a tree at the given location (requires kind tree and null-id) AssureTreeOnly, } enum WriteMode { Normal, /// Perform less cleanup to assure parent-editor still stays intact FromCursor, } fn cmp_entry_with_name(a: &tree::Entry, filename: &BStr, is_tree: bool) -> Ordering { let common = a.filename.len().min(filename.len()); a.filename[..common].cmp(&filename[..common]).then_with(|| { let a = a.filename.get(common).or_else(|| a.mode.is_tree().then_some(&b'/')); let b = filename.get(common).or_else(|| is_tree.then_some(&b'/')); a.cmp(&b) }) } fn filename(path: &BStr) -> &BStr { path.rfind_byte(b'/').map_or(path, |pos| &path[pos + 1..]) } fn empty_path() -> BString { BString::default() } fn push_path_component(base: &mut BString, component: &[u8]) -> usize { let prev_len = base.len(); debug_assert!(base.last() != Some(&b'/')); if !base.is_empty() { base.push_byte(b'/'); } base.push_str(component); prev_len } gix-object-0.60.0/src/tree/mod.rs000064400000000000000000000311441046102023000146200ustar 00000000000000use std::{cell::RefCell, cmp::Ordering}; use crate::{ bstr::{BStr, BString}, tree, Tree, TreeRef, }; /// pub mod editor; mod ref_iter; pub use ref_iter::next_entry; /// pub mod write; /// The state needed to apply edits instantly to in-memory trees. /// /// It's made so that each tree is looked at in the object database at most once, and held in memory for /// all edits until everything is flushed to write all changed trees. /// /// The editor is optimized to edit existing trees, but can deal with building entirely new trees as well /// with some penalties. #[doc(alias = "TreeUpdateBuilder", alias = "git2")] #[derive(Clone)] pub struct Editor<'a> { /// A way to lookup trees. find: &'a dyn crate::FindExt, /// The kind of hashes to produce> object_hash: gix_hash::Kind, /// All trees we currently hold in memory. Each of these may change while adding and removing entries. /// null-object-ids mark tree-entries whose value we don't know yet, they are placeholders that will be /// dropped when writing at the latest. trees: std::collections::HashMap, /// A buffer to build up paths when finding the tree to edit. path_buf: RefCell, /// Our buffer for storing tree-data in, right before decoding it. tree_buf: Vec, } /// The mode of items storable in a tree, similar to the file mode on a unix file system. /// /// Used in [`mutable::Entry`][crate::tree::Entry] and [`EntryRef`]. /// /// Note that even though it can be created from any `u16`, it should be preferable to /// create it by converting [`EntryKind`] into `EntryMode`. #[derive(Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct EntryMode { // Represents the value read from Git, except that "040000" is represented with 0o140000 but // "40000" is represented with 0o40000. internal: u16, } impl TryFrom for tree::EntryMode { type Error = u32; fn try_from(mode: u32) -> Result { Ok(match mode { 0o40000 | 0o120000 | 0o160000 => EntryMode { internal: mode as u16 }, blob_mode if blob_mode & 0o100000 == 0o100000 => EntryMode { internal: mode as u16 }, _ => return Err(mode), }) } } impl EntryMode { /// Expose the value as u16 (lossy, unlike the internal representation that is hidden). pub const fn value(self) -> u16 { // Demangle the hack: In the case where the second leftmost octet is 4 (Tree), the leftmost bit is // there to represent whether the bytes representation should have 5 or 6 octets. if self.internal & IFMT == 0o140000 { 0o040000 } else { self.internal } } /// Return the representation as used in the git internal format, which is octal and written /// to the `backing` buffer. The respective sub-slice that was written to is returned. pub fn as_bytes<'a>(&self, backing: &'a mut [u8; 6]) -> &'a BStr { if self.internal == 0 { std::slice::from_ref(&b'0') } else { for (idx, backing_octet) in backing.iter_mut().enumerate() { let bit_pos = 3 /* because base 8 and 2^3 == 8*/ * (6 - idx - 1); let oct_mask = 0b111 << bit_pos; let digit = (self.internal & oct_mask) >> bit_pos; *backing_octet = b'0' + digit as u8; } // Hack: `0o140000` represents `"040000"`, `0o40000` represents `"40000"`. if backing[1] == b'4' { if backing[0] == b'1' { backing[0] = b'0'; &backing[0..6] } else { &backing[1..6] } } else { &backing[0..6] } } .into() } /// Construct an EntryMode from bytes represented as in the git internal format /// Return the mode and the remainder of the bytes. pub(crate) fn extract_from_bytes(i: &[u8]) -> Option<(Self, &'_ [u8])> { let mut mode = 0; if i.is_empty() { return None; } // Happy path: space is at index 6 let space_pos = if i.get(6) == Some(&b' ') && i.get(5) != Some(&b' ') { for b in i.iter().take(6) { let b = b.wrapping_sub(b'0') as u16; // Not a pure octal input. // Performance matters here, so `!(b'0'..=b'7').contains(&b)` won't do. if b > 7 { return None; } mode = (mode << 3) + b; } 6 } // Space is not at index 6, we must find it. else { let mut idx = 0; let mut space_pos = 0; // const fn, this is why we can't have nice things (like `.iter().any()`). while idx < i.len() { let b = i[idx].wrapping_sub(b'0') as u16; // Delimiter, return what we got if b == b' '.wrapping_sub(b'0') as u16 { space_pos = idx; break; } // Not a pure octal input. // Performance matters here, so `!(b'0'..=b'7').contains(&b)` won't do. if b > 7 { return None; } // More than 6 octal digits we must have hit the delimiter or the input was malformed. if idx > 6 { return None; } mode = (mode << 3) + b; idx += 1; } space_pos }; // Hack: `0o140000` represents `"040000"`, `0o40000` represents `"40000"`. if mode == 0o040000 && i[0] == b'0' { mode += 0o100000; } Some((Self { internal: mode }, &i[(space_pos + 1)..])) } /// Construct an EntryMode from bytes represented as in the git internal format. pub fn from_bytes(i: &[u8]) -> Option { Self::extract_from_bytes(i).map(|(mode, _rest)| mode) } } impl std::fmt::Debug for EntryMode { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "EntryMode(0o{})", self.as_bytes(&mut Default::default())) } } impl std::fmt::Octal for EntryMode { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.as_bytes(&mut Default::default())) } } /// A discretized version of ideal and valid values for entry modes. /// /// Note that even though it can represent every valid [mode](EntryMode), it might /// lose information due to that as well. #[derive(Clone, Copy, PartialEq, Eq, Debug, Ord, PartialOrd, Hash)] #[repr(u16)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum EntryKind { /// A tree, or directory Tree = 0o040000u16, /// A file that is not executable Blob = 0o100644, /// A file that is executable BlobExecutable = 0o100755, /// A symbolic link Link = 0o120000, /// A commit of a git submodule Commit = 0o160000, } impl From for EntryMode { fn from(value: EntryKind) -> Self { EntryMode { internal: value as u16 } } } impl From for EntryKind { fn from(value: EntryMode) -> Self { value.kind() } } /// Serialization impl EntryKind { /// Return the representation as used in the git internal format. pub fn as_octal_str(&self) -> &'static BStr { use EntryKind::*; let bytes: &[u8] = match self { Tree => b"40000", Blob => b"100644", BlobExecutable => b"100755", Link => b"120000", Commit => b"160000", }; bytes.into() } } const IFMT: u16 = 0o170000; impl EntryMode { /// Discretize the raw mode into an enum with well-known state while dropping unnecessary details. pub const fn kind(&self) -> EntryKind { let etype = self.value() & IFMT; if etype == 0o100000 { if self.value() & 0o000100 == 0o000100 { EntryKind::BlobExecutable } else { EntryKind::Blob } } else if etype == EntryKind::Link as u16 { EntryKind::Link } else if etype == EntryKind::Tree as u16 { EntryKind::Tree } else { EntryKind::Commit } } /// Return true if this entry mode represents a Tree/directory pub const fn is_tree(&self) -> bool { self.value() & IFMT == EntryKind::Tree as u16 } /// Return true if this entry mode represents the commit of a submodule. pub const fn is_commit(&self) -> bool { self.value() & IFMT == EntryKind::Commit as u16 } /// Return true if this entry mode represents a symbolic link pub const fn is_link(&self) -> bool { self.value() & IFMT == EntryKind::Link as u16 } /// Return true if this entry mode represents anything BUT Tree/directory pub const fn is_no_tree(&self) -> bool { self.value() & IFMT != EntryKind::Tree as u16 } /// Return true if the entry is any kind of blob. pub const fn is_blob(&self) -> bool { self.value() & IFMT == 0o100000 } /// Return true if the entry is an executable blob. pub const fn is_executable(&self) -> bool { matches!(self.kind(), EntryKind::BlobExecutable) } /// Return true if the entry is any kind of blob or symlink. pub const fn is_blob_or_symlink(&self) -> bool { matches!( self.kind(), EntryKind::Blob | EntryKind::BlobExecutable | EntryKind::Link ) } /// Represent the mode as descriptive string. pub const fn as_str(&self) -> &'static str { use EntryKind::*; match self.kind() { Tree => "tree", Blob => "blob", BlobExecutable => "exe", Link => "link", Commit => "commit", } } } impl TreeRef<'_> { /// Convert this instance into its own version, creating a copy of all data. /// /// This will temporarily allocate an extra copy in memory, so at worst three copies of the tree exist /// at some intermediate point in time. Use [`Self::into_owned()`] to avoid this. pub fn to_owned(&self) -> Tree { self.clone().into() } /// Convert this instance into its own version, creating a copy of all data. pub fn into_owned(self) -> Tree { self.into() } } /// An element of a [`TreeRef`][crate::TreeRef::entries]. #[derive(PartialEq, Eq, Debug, Hash, Clone, Copy)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct EntryRef<'a> { /// The kind of object to which `oid` is pointing. pub mode: tree::EntryMode, /// The name of the file in the parent tree. pub filename: &'a BStr, /// The id of the object representing the entry. // TODO: figure out how these should be called. id or oid? It's inconsistent around the codebase. // Answer: make it 'id', as in `git2` #[cfg_attr(feature = "serde", serde(borrow))] pub oid: &'a gix_hash::oid, } impl PartialOrd for EntryRef<'_> { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for EntryRef<'_> { fn cmp(&self, b: &Self) -> Ordering { let a = self; let common = a.filename.len().min(b.filename.len()); a.filename[..common].cmp(&b.filename[..common]).then_with(|| { let a = a.filename.get(common).or_else(|| a.mode.is_tree().then_some(&b'/')); let b = b.filename.get(common).or_else(|| b.mode.is_tree().then_some(&b'/')); a.cmp(&b) }) } } /// An entry in a [`Tree`], similar to an entry in a directory. #[derive(PartialEq, Eq, Debug, Hash, Clone)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Entry { /// The kind of object to which `oid` is pointing to. pub mode: EntryMode, /// The name of the file in the parent tree. pub filename: BString, /// The id of the object representing the entry. pub oid: gix_hash::ObjectId, } impl PartialOrd for Entry { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for Entry { fn cmp(&self, b: &Self) -> Ordering { let a = self; let common = a.filename.len().min(b.filename.len()); a.filename[..common].cmp(&b.filename[..common]).then_with(|| { let a = a.filename.get(common).or_else(|| a.mode.is_tree().then_some(&b'/')); let b = b.filename.get(common).or_else(|| b.mode.is_tree().then_some(&b'/')); a.cmp(&b) }) } } gix-object-0.60.0/src/tree/ref_iter.rs000064400000000000000000000221561046102023000156430ustar 00000000000000use std::ops::ControlFlow; use bstr::BStr; use crate::{tree, tree::EntryRef, TreeRef, TreeRefIter}; /// Advance a path lookup by matching the next path component against `tree`. /// /// `components` must yield the remaining path components to resolve, and `tree` must be the /// current object to search in. /// /// The return value indicates how the caller should proceed: /// /// - [`ControlFlow::Continue`] contains the object id of the matched entry when there are more /// components left to resolve. Callers should load that object and pass it back into a subsequent /// invocation. /// - [`ControlFlow::Break`]`(Some(entry))` contains the matched entry for the final component, and /// signals that lookup completed successfully. /// - [`ControlFlow::Break`]`(None)` signals that lookup cannot continue and should stop without a /// match. This happens if `tree` is not a tree object, if `components` is already exhausted, or if /// the next component is not present in `tree`. /// /// Note that this behaviour is tuned to prefer to exhaust the entire chain of `components`, only the /// last component can yield a [`ControlFlow::Break`]. pub fn next_entry<'a, I, P>( components: &mut core::iter::Peekable, tree: crate::Data<'a>, ) -> core::ops::ControlFlow>, gix_hash::ObjectId> where I: Iterator, P: PartialEq, { if !tree.kind.is_tree() { return ControlFlow::Break(None); } let Some(component) = components.next() else { return ControlFlow::Break(None); }; let Some(entry) = TreeRefIter::from_bytes(tree.data, tree.hash_kind) .filter_map(Result::ok) .find(|entry| component.eq(entry.filename)) else { return ControlFlow::Break(None); }; if components.peek().is_none() { ControlFlow::Break(Some(entry)) } else { ControlFlow::Continue(entry.oid.to_owned()) } } impl<'a> TreeRefIter<'a> { /// Instantiate an iterator from the given tree `data` and `hash_kind`. pub fn from_bytes(data: &'a [u8], hash_kind: gix_hash::Kind) -> TreeRefIter<'a> { TreeRefIter { data, hash_kind } } /// Follow a sequence of `path` components starting from this instance, and look them up in `odb` one by one using `buffer` /// until the last component is looked up and its tree entry is returned. /// /// # Performance Notes /// /// Searching tree entries is currently done in sequence, which allows the search to be allocation free. It would be possible /// to reuse a vector and use a binary search instead, which might be able to improve performance over all. /// However, a benchmark should be created first to have some data and see which trade-off to choose here. pub fn lookup_entry( &self, odb: impl crate::Find, buffer: &'a mut Vec, path: I, ) -> Result, crate::find::Error> where I: IntoIterator, P: PartialEq, { buffer.clear(); buffer.extend_from_slice(self.data); let mut iter = path.into_iter().peekable(); let mut data = crate::Data::new(buffer, crate::Kind::Tree, self.hash_kind); loop { data = match next_entry(&mut iter, data) { ControlFlow::Continue(oid) => { let Some(next_tree) = odb.try_find(&oid, buffer)? else { break Ok(None); }; next_tree } ControlFlow::Break(v) => break Ok(v.map(Into::into)), } } } /// Like [`Self::lookup_entry()`], but takes any [`AsRef`](`std::path::Path`) directly via `relative_path`, /// a path relative to this tree. /// `odb` and `buffer` are used to lookup intermediate trees. /// /// # Note /// /// If any path component contains illformed UTF-8 and thus can't be converted to bytes on platforms which can't do so natively, /// the returned component will be empty which makes the lookup fail. pub fn lookup_entry_by_path( &self, odb: impl crate::Find, buffer: &'a mut Vec, relative_path: impl AsRef, ) -> Result, crate::find::Error> { self.lookup_entry( odb, buffer, relative_path .as_ref() .components() .map(|c| c.as_os_str().as_encoded_bytes()), ) } } impl<'a> TreeRef<'a> { /// Deserialize a Tree from `data`, assuming `hash_kind` to determine how the object ids are encoded in this particular tree. pub fn from_bytes(data: &'a [u8], hash_kind: gix_hash::Kind) -> Result, crate::decode::Error> { decode::tree(data, hash_kind.len_in_bytes()) } /// Find an entry named `name` knowing if the entry is a directory or not, using a binary search. /// /// Note that it's impossible to binary search by name alone as the sort order is special. pub fn bisect_entry(&self, name: &BStr, is_dir: bool) -> Option> { static NULL_HASH: gix_hash::ObjectId = gix_hash::Kind::shortest().null(); let search = EntryRef { mode: if is_dir { tree::EntryKind::Tree } else { tree::EntryKind::Blob } .into(), filename: name, oid: &NULL_HASH, }; self.entries .binary_search_by(|e| e.cmp(&search)) .ok() .map(|idx| self.entries[idx]) } /// Create an instance of the empty tree. /// /// It's particularly useful as static part of a program. pub const fn empty() -> TreeRef<'static> { TreeRef { entries: Vec::new() } } } impl<'a> TreeRefIter<'a> { /// Consume self and return all parsed entries. pub fn entries(self) -> Result>, crate::decode::Error> { self.collect() } /// Return the offset in bytes that our data advanced from `buf`, the original buffer /// to the beginning of the data of the tree. /// /// Then the tree-iteration can be resumed at the entry that would otherwise be returned next. pub fn offset_to_next_entry(&self, buf: &[u8]) -> usize { let before = (*buf).as_ptr(); let after = (*self.data).as_ptr(); debug_assert!( before <= after, "`TreeRefIter::offset_to_next_entry(): {after:?} <= {before:?}) violated" ); (after as usize - before as usize) / std::mem::size_of::() } } impl<'a> Iterator for TreeRefIter<'a> { type Item = Result, crate::decode::Error>; fn next(&mut self) -> Option { if self.data.is_empty() { return None; } match decode::fast_entry(self.data, self.hash_kind.len_in_bytes()) { Some((data_left, entry)) => { self.data = data_left; Some(Ok(entry)) } None => { self.data = &[]; Some(Err(crate::decode::Error)) } } } } impl<'a> TryFrom<&'a [u8]> for tree::EntryMode { type Error = &'a [u8]; fn try_from(mode: &'a [u8]) -> Result { tree::EntryMode::from_bytes(mode).ok_or(mode) } } mod decode { use bstr::ByteSlice; use crate::{tree, tree::EntryRef, TreeRef}; pub fn fast_entry(i: &[u8], hash_len: usize) -> Option<(&[u8], EntryRef<'_>)> { let (mode, i) = tree::EntryMode::extract_from_bytes(i)?; let (filename, i) = i.split_at(i.find_byte(0)?); let i = &i[1..]; let (oid, i) = match i.len() { len if len < hash_len => return None, _ => i.split_at(hash_len), }; Some(( i, EntryRef { mode, filename: filename.as_bstr(), oid: gix_hash::oid::try_from_bytes(oid) .unwrap_or_else(|_| panic!("we counted exactly {hash_len} bytes")), }, )) } pub fn tree(data: &[u8], hash_len: usize) -> Result, crate::decode::Error> { let mut i = data; // Calculate an estimate of the amount of entries to reduce // the amount of allocations necessary. // Note that this assumes that we want speed over fitting Vecs, this is a trade-off. const AVERAGE_FILENAME_LEN: usize = 24; const AVERAGE_MODE_LEN: usize = 6; const ENTRY_DELIMITER_LEN: usize = 2; // space + trailing zero const AVERAGE_TREE_ENTRIES: usize = 16 * 2; // prevent overallocation beyond what's meaningful or what could be dangerous let average_entry_len = ENTRY_DELIMITER_LEN + hash_len + AVERAGE_MODE_LEN + AVERAGE_FILENAME_LEN; let upper_bound = i.len() / average_entry_len; let mut out = Vec::with_capacity(upper_bound.min(AVERAGE_TREE_ENTRIES)); while !i.is_empty() { let Some((rest, entry)) = fast_entry(i, hash_len) else { return Err(crate::decode::Error); }; i = rest; out.push(entry); } Ok(TreeRef { entries: out }) } } gix-object-0.60.0/src/tree/write.rs000064400000000000000000000063661046102023000152030ustar 00000000000000use std::io; use bstr::{BString, ByteSlice}; use crate::{ encode::SPACE, tree::{Entry, EntryRef}, Kind, Tree, TreeRef, }; /// The Error used in [`Tree::write_to()`][crate::WriteTo::write_to()]. #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum Error { #[error("Nullbytes are invalid in file paths as they are separators: {name:?}")] NullbyteInFilename { name: BString }, } impl From for io::Error { fn from(err: Error) -> Self { io::Error::other(err) } } /// Serialization impl crate::WriteTo for Tree { /// Serialize this tree to `out` in the git internal format. fn write_to(&self, out: &mut dyn io::Write) -> io::Result<()> { debug_assert_eq!( &self.entries, &{ let mut entries_sorted = self.entries.clone(); entries_sorted.sort(); entries_sorted }, "entries for serialization must be sorted by filename" ); let mut buf = Default::default(); for Entry { mode, filename, oid } in &self.entries { out.write_all(mode.as_bytes(&mut buf))?; out.write_all(SPACE)?; if filename.find_byte(0).is_some() { return Err(Error::NullbyteInFilename { name: (*filename).to_owned(), } .into()); } out.write_all(filename)?; out.write_all(b"\0")?; out.write_all(oid.as_bytes())?; } Ok(()) } fn kind(&self) -> Kind { Kind::Tree } fn size(&self) -> u64 { let mut buf = Default::default(); self.entries .iter() .map(|Entry { mode, filename, oid }| { (mode.as_bytes(&mut buf).len() + 1 + filename.len() + 1 + oid.as_bytes().len()) as u64 }) .sum() } } /// Serialization impl crate::WriteTo for TreeRef<'_> { /// Serialize this tree to `out` in the git internal format. fn write_to(&self, out: &mut dyn io::Write) -> io::Result<()> { debug_assert_eq!( &{ let mut entries_sorted = self.entries.clone(); entries_sorted.sort(); entries_sorted }, &self.entries, "entries for serialization must be sorted by filename" ); let mut buf = Default::default(); for EntryRef { mode, filename, oid } in &self.entries { out.write_all(mode.as_bytes(&mut buf))?; out.write_all(SPACE)?; if filename.find_byte(0).is_some() { return Err(Error::NullbyteInFilename { name: (*filename).to_owned(), } .into()); } out.write_all(filename)?; out.write_all(b"\0")?; out.write_all(oid.as_bytes())?; } Ok(()) } fn kind(&self) -> Kind { Kind::Tree } fn size(&self) -> u64 { let mut buf = Default::default(); self.entries .iter() .map(|EntryRef { mode, filename, oid }| { (mode.as_bytes(&mut buf).len() + 1 + filename.len() + 1 + oid.as_bytes().len()) as u64 }) .sum() } }