diff --git a/Cargo.lock b/Cargo.lock index 3fcfc30..742e3a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -37,12 +37,41 @@ dependencies = [ "memchr", ] +[[package]] +name = "aligned-vec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1" + [[package]] name = "anyhow" version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" +[[package]] +name = "arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.57", +] + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + [[package]] name = "async-trait" version = "0.1.79" @@ -60,6 +89,29 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" +[[package]] +name = "av1-grain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6678909d8c5d46a42abcf571271e15fdbc0a225e3646cf23762cd415046c78bf" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876c75a42f6364451a033496a14c44bffe41f5f4a8236f697391f11024e596d2" +dependencies = [ + "arrayvec", +] + [[package]] name = "backtrace" version = "0.3.71" @@ -89,9 +141,9 @@ checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "base64" -version = "0.22.0" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bech32" @@ -114,6 +166,12 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +[[package]] +name = "bit_field" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" + [[package]] name = "bitflags" version = "1.3.2" @@ -126,6 +184,12 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +[[package]] +name = "bitstream-io" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c12d1856e42f0d817a835fe55853957c85c8c8a470114029143d3f12671446e" + [[package]] name = "blake2b-ref" version = "0.3.1" @@ -160,18 +224,36 @@ dependencies = [ "generic-array", ] +[[package]] +name = "built" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6a6c0b39c38fd754ac338b00a88066436389c0f029da5d37d1e01091d9b7c17" + [[package]] name = "bumpalo" version = "3.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" +[[package]] +name = "bytemuck" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78834c15cb5d5efe3452d58b1e8ba890dd62d21907f867f383358198e56ebca5" + [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.6.0" @@ -213,6 +295,20 @@ name = "cc" version = "1.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" +dependencies = [ + "jobserver", + "libc", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] [[package]] name = "cfg-if" @@ -346,7 +442,7 @@ dependencies = [ "ckb-fixed-hash", "ckb-hash", "ckb-occupied-capacity", - "molecule", + "molecule 0.7.5", "numext-fixed-uint", ] @@ -574,7 +670,7 @@ dependencies = [ "derive_more", "golomb-coded-set", "merkle-cbt", - "molecule", + "molecule 0.7.5", "numext-fixed-uint", "once_cell", "paste", @@ -631,6 +727,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "convert_case" version = "0.4.0" @@ -680,12 +782,37 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +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.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-common" version = "0.1.6" @@ -762,6 +889,7 @@ dependencies = [ name = "dob-decoder-server" version = "0.1.0" dependencies = [ + "base64 0.22.1", "ckb-hash", "ckb-jsonrpc-types", "ckb-sdk", @@ -769,9 +897,11 @@ dependencies = [ "ckb-vm", "futures", "hex", + "image", "jsonrpc-core", "jsonrpsee", - "lazy_static", + "lazy-regex", + "molecule 0.8.0", "reqwest 0.12.4", "serde", "serde_json", @@ -837,6 +967,22 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "exr" +version = "1.72.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "887d93f60543e9a9362ef8a21beedd0a833c5d9610e18c67abe15a5963dcb1a4" +dependencies = [ + "bit_field", + "flume", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + [[package]] name = "faster-hex" version = "0.6.1" @@ -849,6 +995,15 @@ version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" +[[package]] +name = "fdeflate" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" +dependencies = [ + "simd-adler32", +] + [[package]] name = "flate2" version = "1.0.28" @@ -859,6 +1014,15 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "flume" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +dependencies = [ + "spin", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1010,6 +1174,16 @@ dependencies = [ "wasi 0.11.0+wasi-snapshot-preview1", ] +[[package]] +name = "gif" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "gimli" version = "0.28.1" @@ -1085,6 +1259,16 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +dependencies = [ + "cfg-if 1.0.0", + "crunchy", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1115,6 +1299,12 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.3.9" @@ -1298,6 +1488,45 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "image" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd54d660e773627692c524beaad361aca785a4f9f5730ce91f42aabe5bce3d11" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "exr", + "gif", + "image-webp", + "num-traits", + "png", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d730b085583c4d789dfd07fdcf185be59501666a90c97c40162b37e4fdad272d" +dependencies = [ + "byteorder-lite", + "thiserror", +] + +[[package]] +name = "imgref" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44feda355f4159a7c757171a77de25daf6411e217b4cabd03bd6650690468126" + [[package]] name = "includedir" version = "0.6.0" @@ -1329,18 +1558,53 @@ dependencies = [ "hashbrown 0.14.3", ] +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.57", +] + [[package]] name = "ipnet" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "jobserver" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" +dependencies = [ + "libc", +] + +[[package]] +name = "jpeg-decoder" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" + [[package]] name = "js-sys" version = "0.3.69" @@ -1407,7 +1671,7 @@ version = "0.22.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d7c2416c400c94b2e864603c51a5bbd5b103386da1f5e58cbf01e7bb3ef0833" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro-crate", "proc-macro2", "quote", @@ -1460,18 +1724,58 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "lazy-regex" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d12be4595afdf58bd19e4a9f4e24187da2a66700786ff660a418e9059937a4c" +dependencies = [ + "lazy-regex-proc_macros", + "once_cell", + "regex", +] + +[[package]] +name = "lazy-regex-proc_macros" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44bcd58e6c97a7fcbaffcdc95728b393b8d98933bfadad49ed4097845b57ef0b" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn 2.0.57", +] + [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lebe" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + [[package]] name = "libc" version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +[[package]] +name = "libfuzzer-sys" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7" +dependencies = [ + "arbitrary", + "cc", + "once_cell", +] + [[package]] name = "linux-raw-sys" version = "0.4.13" @@ -1494,6 +1798,15 @@ version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + [[package]] name = "lru" version = "0.7.8" @@ -1512,6 +1825,16 @@ dependencies = [ "regex-automata 0.1.10", ] +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if 1.0.0", + "rayon", +] + [[package]] name = "memchr" version = "2.7.2" @@ -1565,6 +1888,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.7.2" @@ -1572,6 +1901,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" dependencies = [ "adler", + "simd-adler32", ] [[package]] @@ -1596,6 +1926,17 @@ dependencies = [ "faster-hex", ] +[[package]] +name = "molecule" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6efe1c7efcd0bdf4ca590e104bcb13087d9968956ae4ae98e92fb8c1da0f3730" +dependencies = [ + "bytes", + "cfg-if 1.0.0", + "faster-hex", +] + [[package]] name = "native-tls" version = "0.2.11" @@ -1614,6 +1955,28 @@ dependencies = [ "tempfile", ] +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1624,6 +1987,56 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c165a9ab64cf766f73521c0dd2cfdff64f488b8f0b3e621face3462d3db536d7" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.57", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.16.0" @@ -1868,6 +2281,19 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +[[package]] +name = "png" +version = "0.17.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -1916,6 +2342,40 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "profiling" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d84d1d7a6ac92673717f9f6d1518374ef257669c24ebc5ac25d5033828be58" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd" +dependencies = [ + "quote", + "syn 2.0.57", +] + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quote" version = "1.0.35" @@ -2006,6 +2466,76 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "rav1e" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9" +dependencies = [ + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av1-grain", + "bitstream-io", + "built", + "cfg-if 1.0.0", + "interpolate_name", + "itertools", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "once_cell", + "paste", + "profiling", + "rand 0.8.5", + "rand_chacha 0.3.1", + "simd_helpers", + "system-deps", + "thiserror", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc13288f5ab39e6d7c9d501759712e6969fcc9734220846fc9ed26cae2cc4234" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.4.1" @@ -2116,7 +2646,7 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10" dependencies = [ - "base64 0.22.0", + "base64 0.22.1", "bytes", "encoding_rs", "futures-core", @@ -2152,6 +2682,15 @@ dependencies = [ "winreg 0.52.0", ] +[[package]] +name = "rgb" +version = "0.8.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05aaa8004b64fd573fc9d002f4e632d51ad4f026c2b5ba95fcb6c2f32c2c47d8" +dependencies = [ + "bytemuck", +] + [[package]] name = "route-recognizer" version = "0.3.1" @@ -2207,7 +2746,7 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" dependencies = [ - "base64 0.22.0", + "base64 0.22.1", "rustls-pki-types", ] @@ -2451,6 +2990,21 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + [[package]] name = "siphasher" version = "0.3.11" @@ -2509,12 +3063,21 @@ dependencies = [ "cfg-if 0.1.10", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + [[package]] name = "spore-types" version = "0.1.0" source = "git+https://github.com/sporeprotocol/spore-contract?rev=81315ca#81315ca8c3865719a5ab71eb2dcc4bf4332cb09c" dependencies = [ - "molecule", + "molecule 0.7.5", ] [[package]] @@ -2583,6 +3146,25 @@ dependencies = [ "libc", ] +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.12", + "version-compare", +] + +[[package]] +name = "target-lexicon" +version = "0.12.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f" + [[package]] name = "tempfile" version = "3.10.1" @@ -2625,6 +3207,17 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tiff" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -2900,6 +3493,17 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "v_frame" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f32aaa24bacd11e488aa9ba66369c7cd514885742c9fe08cfe85884db3e92b" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.0" @@ -2912,6 +3516,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version-compare" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" + [[package]] name = "version_check" version = "0.9.4" @@ -3025,6 +3635,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "weezl" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" + [[package]] name = "winapi" version = "0.3.9" @@ -3260,3 +3876,27 @@ name = "xxhash-rust" version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "927da81e25be1e1a2901d59b81b37dd2efd1fc9c9345a55007f09bf5a2d3ee03" + +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec866b44a2a1fd6133d363f073ca1b179f438f99e7e5bfb1e33f7181facfe448" +dependencies = [ + "zune-core", +] diff --git a/Cargo.toml b/Cargo.toml index 2edf7a2..c2781a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,19 +6,22 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +base64 = "0.22.1" ckb-sdk = "3.2.0" ckb-types = "0.116.1" ckb-jsonrpc-types = "0.116.1" ckb-hash = "0.116.1" +ckb-vm = { version = "0.24", features = ["asm"] } thiserror = "1.0" serde_json = "1.0" hex = "0.4.3" +lazy-regex = "3.1.0" +image = "0.25.1" reqwest = { version = "0.12.4", features = ["json"] } jsonrpc-core = "18.0" serde = { version = "1.0", features = ["serde_derive"] } futures = "0.3" -lazy_static = { version = "1.4" } -ckb-vm = { version = "0.24", features = ["asm"] } +molecule = "0.8.0" spore-types = { git = "https://github.com/sporeprotocol/spore-contract", rev = "81315ca" } diff --git a/README.md b/README.md index 2e7fbec..f2d59f3 100644 --- a/README.md +++ b/README.md @@ -90,3 +90,13 @@ refer to error definitions [here](https://github.com/sporeprotocol/dob-decoder-s | 1026 | DecoderBinaryNotFoundInCell | | 1027 | JsonRpcRequestError | | 1028 | SystemTimeError | +| 1029 | JsonRpcReques | +| 1030 | FetchFromBtcNodeError | +| 1031 | InvalidBtcTransactionFormat | +| 1032 | InvalidInscriptionFormat | +| 1033 | InvalidInscriptionContentHexFormat | +| 1034 | EmptyInscriptionContent | +| 1035 | ExceededInscriptionIndex | +| 1036 | InvalidOnchainFsuriFormat | +| 1037 | FsuriNotFoundInConfig | +| 1038 | FetchFromIpfsError | diff --git a/cache/decoders/code_hash_ac35b0e6178dc4a89fb85194b4ac0b60eed2b6ce9f10bf7bf2ee76190c3a0071.bin b/cache/decoders/code_hash_ac35b0e6178dc4a89fb85194b4ac0b60eed2b6ce9f10bf7bf2ee76190c3a0071.bin new file mode 100755 index 0000000..abaa32e Binary files /dev/null and b/cache/decoders/code_hash_ac35b0e6178dc4a89fb85194b4ac0b60eed2b6ce9f10bf7bf2ee76190c3a0071.bin differ diff --git a/settings.mainnet.toml b/settings.mainnet.toml index 18b3567..864640e 100644 --- a/settings.mainnet.toml +++ b/settings.mainnet.toml @@ -6,6 +6,9 @@ protocol_versions = [ # connect to the RPC of CKB node ckb_rpc = "https://mainnet.ckb.dev/" +# connect to the image fetcher service +image_fetcher_url = { btcfs = "https://mempool.space/api/tx/", ipfs = "https://ipfs.io/ipfs/" } + # address that rpc server running at in case of standalone server mode rpc_server_address = "0.0.0.0:8090" @@ -21,6 +24,12 @@ dobs_cache_directory = "cache/dobs" # expiration time indicator for cleaning whole dobs cache, zero means never clean dobs_cache_expiration_sec = 300 +# identifier that represents the maximum combination of DOB/1 +dob1_max_combination = 5 + +# identifier that represents the maximum number of caching images +dob1_max_cache_size = 100 + # all deployed on-chain Spore contracts binary hash (order from new to old) # refer to: https://github.com/sporeprotocol/spore-contract/blob/master/docs/VERSIONS.md [[available_spores]] diff --git a/settings.toml b/settings.toml index 0ff1ae0..33aa9ea 100644 --- a/settings.toml +++ b/settings.toml @@ -6,6 +6,9 @@ protocol_versions = [ # connect to the RPC of CKB node ckb_rpc = "https://testnet.ckbapp.dev/" +# connect to the image fetcher service +image_fetcher_url = { btcfs = "https://mempool.space/testnet/api/tx/", ipfs = "https://ipfs.io/ipfs/" } + # address that rpc server running at in case of standalone server mode rpc_server_address = "0.0.0.0:8090" @@ -21,6 +24,12 @@ dobs_cache_directory = "cache/dobs" # expiration time indicator for cleaning whole dobs cache, zero means never clean dobs_cache_expiration_sec = 300 +# identifier that represents the maximum combination of DOB/1 +dob1_max_combination = 5 + +# identifier that represents the maximum number of caching images +dob1_max_cache_size = 100 + # all deployed on-chain Spore contracts binary hash (order from new to old) # refer to: https://github.com/sporeprotocol/spore-contract/blob/master/docs/VERSIONS.md [[available_spores]] @@ -47,6 +56,10 @@ hash_type = "data1" # associate `code_hash` with the corresponding onchain information about `tx_hash` and `out_index` # server will firstly search onchain decoders by `code_hash` in this configuration, if not found, cache will be used instead + +# +# DOB/0 +# [[onchain_decoder_deployment]] code_hash = "0xb82abd59ade361a014f0abb692f71b0feb880693c3ccb95b9137b73551d872ce" tx_hash = "0xb2497dc3e616055125ef8276be7ee21986d2cd4b2ce90992725386cabcb6ea7f" @@ -66,3 +79,11 @@ out_index = 0 code_hash = "0x13cac78ad8482202f18f9df4ea707611c35f994375fa03ae79121312dda9925c" tx_hash = "0x4a8a0d079f8438bed89e0ece1b14e67ab68e2aa7688a5f4917a59a185e0f8fd5" out_index = 0 + +# +# DOB/1 +# +[[onchain_decoder_deployment]] +code_hash = "0xac35b0e6178dc4a89fb85194b4ac0b60eed2b6ce9f10bf7bf2ee76190c3a0071" +tx_hash = "0x1918a656f7c52ca5fbe7a903903c9bbc89d3e05525b4bb9f323fceb1a5bde51f" +out_index = 0 diff --git a/src/client.rs b/src/client.rs index 46860da..a64dd43 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,3 +1,6 @@ +#![allow(clippy::assigning_clones)] + +use std::collections::{HashMap, VecDeque}; use std::future::Future; use std::pin::Pin; use std::sync::atomic::{AtomicU64, Ordering}; @@ -6,7 +9,9 @@ use std::sync::Arc; use ckb_jsonrpc_types::{CellWithStatus, JsonBytes, OutPoint, Uint32}; use ckb_sdk::rpc::ckb_indexer::{Cell, Order, Pagination, SearchKey}; use jsonrpc_core::futures::FutureExt; +use lazy_regex::regex_replace_all; use reqwest::{Client, Url}; +use serde_json::Value; use crate::types::Error; @@ -77,9 +82,7 @@ impl RpcClient { id: Arc::new(AtomicU64::new(0)), } } -} -impl RpcClient { pub fn get_live_cell(&self, out_point: &OutPoint, with_data: bool) -> Rpc { jsonrpc!( "get_live_cell", @@ -114,3 +117,168 @@ impl RpcClient { .boxed() } } + +pub struct ImageFetchClient { + base_url: HashMap, + images_cache: VecDeque<(Url, Vec)>, + max_cache_size: usize, +} + +impl ImageFetchClient { + pub fn new(base_url: &HashMap, cache_size: usize) -> Self { + let base_url = base_url + .iter() + .map(|(k, v)| (k.clone(), Url::parse(v).expect("url"))) + .collect::>(); + Self { + base_url, + images_cache: VecDeque::new(), + max_cache_size: cache_size, + } + } + + pub async fn fetch_images(&mut self, images_uri: &[String]) -> Result>, Error> { + let mut requests = vec![]; + for uri in images_uri { + match uri.try_into()? { + URI::BTCFS(tx_hash, index) => { + let url = self + .base_url + .get("btcfs") + .ok_or(Error::FsuriNotFoundInConfig)? + .join(&tx_hash) + .expect("image url"); + let cached_image = self.images_cache.iter().find(|(v, _)| v == &url); + if let Some((_, image)) = cached_image { + requests.push(async { Ok((url, true, image.clone())) }.boxed()); + } else { + requests.push( + async move { + let image = parse_image_from_btcfs(&url, index).await?; + Ok((url, false, image)) + } + .boxed(), + ); + } + } + URI::IPFS(cid) => { + let url = self + .base_url + .get("ipfs") + .ok_or(Error::FsuriNotFoundInConfig)? + .join(&cid) + .expect("image url"); + let cached_image = self.images_cache.iter().find(|(v, _)| v == &url); + if let Some((_, image)) = cached_image { + requests.push(async { Ok((url, true, image.clone())) }.boxed()); + } else { + requests.push( + async move { + let image = reqwest::get(url.clone()) + .await + .map_err(|_| Error::FetchFromIpfsError)? + .bytes() + .await + .map_err(|_| Error::FetchFromIpfsError)? + .to_vec(); + Ok((url, false, image)) + } + .boxed(), + ); + } + } + } + } + let mut images = vec![]; + let responses = futures::future::join_all(requests).await; + for response in responses { + let (url, from_cache, result) = response?; + images.push(result.to_vec()); + if !from_cache { + self.images_cache.push_back((url, result)); + if self.images_cache.len() > self.max_cache_size { + self.images_cache.pop_front(); + } + } + } + Ok(images) + } +} + +#[allow(clippy::upper_case_acronyms)] +enum URI { + BTCFS(String, usize), + IPFS(String), +} + +impl TryFrom<&String> for URI { + type Error = Error; + + fn try_from(uri: &String) -> Result { + if uri.starts_with("btcfs://") { + let body = uri.chars().skip("btcfs://".len()).collect::(); + let parts: Vec<&str> = body.split('i').collect::>(); + if parts.len() != 2 { + return Err(Error::InvalidOnchainFsuriFormat); + } + let tx_hash = parts[0].to_string(); + let index = parts[1] + .parse() + .map_err(|_| Error::InvalidOnchainFsuriFormat)?; + Ok(URI::BTCFS(tx_hash, index)) + } else if uri.starts_with("ipfs://") { + let hash = uri.chars().skip("ipfs://".len()).collect::(); + Ok(URI::IPFS(hash)) + } else { + Err(Error::InvalidOnchainFsuriFormat) + } + } +} + +async fn parse_image_from_btcfs(url: &Url, index: usize) -> Result, Error> { + // parse btc transaction + let btc_tx = reqwest::get(url.clone()) + .await + .map_err(|_| Error::FetchFromBtcNodeError)? + .json::() + .await + .map_err(|_| Error::FetchFromBtcNodeError)?; + let vin = btc_tx + .get("vin") + .ok_or(Error::InvalidBtcTransactionFormat)? + .as_array() + .ok_or(Error::InvalidBtcTransactionFormat)? + .first() + .ok_or(Error::InvalidBtcTransactionFormat)?; + let mut witness = vin + .get("inner_witnessscript_asm") + .ok_or(Error::InvalidBtcTransactionFormat)? + .as_str() + .ok_or(Error::InvalidBtcTransactionFormat)? + .to_owned(); + + // parse inscription body + let mut images = vec![]; + let header = "OP_IF OP_PUSHBYTES_3 444f42 OP_PUSHBYTES_1 01 OP_PUSHBYTES_9 696d6167652f706e67 OP_0 OP_PUSHDATA2 "; + while let (Some(start), Some(end)) = (witness.find("OP_IF"), witness.find("OP_ENDIF")) { + let inscription = &witness[start..end + "OP_ENDIF".len()]; + if !inscription.contains(header) { + return Err(Error::InvalidInscriptionFormat); + } + let base_removed = inscription.replace(header, ""); + let hexed = regex_replace_all!(r#"\s?OP\_\w+\s?"#, &base_removed, ""); + let image = + hex::decode(hexed.as_bytes()).map_err(|_| Error::InvalidInscriptionContentHexFormat)?; + images.push(image); + witness = witness[end + "OP_ENDIF".len()..].to_owned(); + } + if images.is_empty() { + return Err(Error::EmptyInscriptionContent); + } + + let image = images + .get(index) + .cloned() + .ok_or(Error::ExceededInscriptionIndex)?; + Ok(image) +} diff --git a/src/decoder.rs b/src/decoder.rs deleted file mode 100644 index e9a9a50..0000000 --- a/src/decoder.rs +++ /dev/null @@ -1,341 +0,0 @@ -use ckb_sdk::{constants::TYPE_ID_CODE_HASH, traits::CellQueryOptions}; -use ckb_types::{ - core::ScriptHashType, - packed::{OutPoint, Script}, - prelude::{Builder, Entity, Pack}, - H256, -}; -use serde_json::Value; -use spore_types::generated::spore::{ClusterData, SporeData}; - -use crate::{ - client::RpcClient, - types::{ClusterDescriptionField, DecoderLocationType, Error, ScriptId, Settings}, -}; - -type DecodeResult = Result; - -pub struct DOBDecoder { - rpc: RpcClient, - settings: Settings, -} - -impl DOBDecoder { - pub fn new(settings: Settings) -> Self { - Self { - rpc: RpcClient::new(&settings.ckb_rpc, &settings.ckb_rpc), - settings, - } - } - - pub fn protocol_versions(&self) -> Vec { - self.settings.protocol_versions.clone() - } - - pub fn setting(&self) -> &Settings { - &self.settings - } - - pub async fn fetch_decode_ingredients( - &self, - spore_id: [u8; 32], - ) -> DecodeResult<((Value, String), ClusterDescriptionField)> { - let (content, cluster_id) = self.fetch_dob_content(spore_id).await?; - let dob_metadata = self.fetch_dob_metadata(cluster_id).await?; - Ok((content, dob_metadata)) - } - - // decode DNA under target spore_id - pub async fn decode_dna( - &self, - dna: &str, - dob_metadata: ClusterDescriptionField, - ) -> DecodeResult { - let decoder_path = match dob_metadata.dob.decoder.location { - DecoderLocationType::CodeHash => { - let mut decoder_path = self.settings.decoders_cache_directory.clone(); - decoder_path.push(format!( - "code_hash_{}.bin", - hex::encode(&dob_metadata.dob.decoder.hash) - )); - if !decoder_path.exists() { - let onchain_decoder = - self.settings - .onchain_decoder_deployment - .iter() - .find_map(|deployment| { - if deployment.code_hash == dob_metadata.dob.decoder.hash { - Some(self.fetch_decoder_binary_directly( - deployment.tx_hash.clone(), - deployment.out_index, - )) - } else { - None - } - }); - let Some(decoder_binary) = onchain_decoder else { - return Err(Error::NativeDecoderNotFound); - }; - let decoder_file_content = decoder_binary.await?; - if ckb_hash::blake2b_256(&decoder_file_content) - != dob_metadata.dob.decoder.hash.0 - { - return Err(Error::DecoderBinaryHashInvalid); - } - std::fs::write(decoder_path.clone(), decoder_file_content) - .map_err(|_| Error::DecoderBinaryPathInvalid)?; - } - decoder_path - } - DecoderLocationType::TypeId => { - let mut decoder_path = self.settings.decoders_cache_directory.clone(); - decoder_path.push(format!( - "type_id_{}.bin", - hex::encode(&dob_metadata.dob.decoder.hash) - )); - if !decoder_path.exists() { - let decoder_binary = self - .fetch_decoder_binary(dob_metadata.dob.decoder.hash.into()) - .await?; - std::fs::write(decoder_path.clone(), decoder_binary) - .map_err(|_| Error::DecoderBinaryPathInvalid)?; - } - decoder_path - } - }; - let pattern = match &dob_metadata.dob.pattern { - Value::String(string) => string.to_owned(), - pattern => pattern.to_string(), - }; - let raw_render_result = { - let (exit_code, outputs) = crate::vm::execute_riscv_binary( - &decoder_path.to_string_lossy(), - vec![dna.to_owned().into(), pattern.into()], - ) - .map_err(|_| Error::DecoderExecutionError)?; - #[cfg(feature = "render_debug")] - { - println!("-------- DECODE RESULT ({exit_code}) ---------"); - outputs.iter().for_each(|output| println!("{output}")); - println!("-------- DECODE RESULT END ---------"); - } - if exit_code != 0 { - return Err(Error::DecoderExecutionInternalError); - } - outputs.first().ok_or(Error::DecoderOutputInvalid)?.clone() - }; - Ok(raw_render_result) - } - - // // invoke `ckb-vm-runner` in native machine and collect console output as result - // #[cfg(not(feature = "embeded_vm"))] - // fn execute_externally( - // &self, - // decoder_path: std::path::PathBuf, - // dna: &str, - // pattern: &str, - // ) -> DecodeResult { - // let output = std::process::Command::new(&self.settings.ckb_vm_runner) - // .arg(decoder_path) - // .arg(dna) - // .arg(pattern) - // .output() - // .map_err(|_| Error::DecoderExecutionError)?; - // let raw_render_result = { - // let console_output = String::from_utf8_lossy(&output.stdout) - // .to_string() - // .replace('\\', ""); - // let lines = console_output - // .split('\n') - // .map(|line| line.trim_matches('\"')) - // .collect::>(); - // #[cfg(feature = "render_debug")] - // { - // println!("-------- DECODE RESULT ---------"); - // lines.iter().for_each(|line| println!("{line}")); - // println!("-------- DECODE RESULT END ---------"); - // } - // lines - // .first() - // .ok_or(Error::DecoderOutputInvalid)? - // .to_string() - // }; - // Ok(raw_render_result) - // } - - // search on-chain spore cell and return its content field, which represents dob content - async fn fetch_dob_content( - &self, - spore_id: [u8; 32], - ) -> DecodeResult<((Value, String), [u8; 32])> { - let mut spore_cell = None; - for spore_search_option in - build_batch_search_options(spore_id, &self.settings.available_spores) - { - spore_cell = self - .rpc - .get_cells(spore_search_option.into(), 1, None) - .await - .map_err(|_| Error::FetchLiveCellsError)? - .objects - .first() - .cloned(); - if spore_cell.is_some() { - break; - } - } - let Some(spore_cell) = spore_cell else { - return Err(Error::SporeIdNotFound); - }; - let molecule_spore_data = - SporeData::from_compatible_slice(spore_cell.output_data.unwrap_or_default().as_bytes()) - .map_err(|_| Error::SporeDataUncompatible)?; - let content_type = - String::from_utf8(molecule_spore_data.content_type().raw_data().to_vec()) - .map_err(|_| Error::SporeDataContentTypeUncompatible)?; - if !content_type.is_empty() - && !self - .settings - .protocol_versions - .iter() - .any(|version| content_type.starts_with(version)) - { - return Err(Error::DOBVersionUnexpected); - } - let cluster_id = molecule_spore_data - .cluster_id() - .to_opt() - .ok_or(Error::ClusterIdNotSet)? - .raw_data(); - let dob_content = decode_spore_data(&molecule_spore_data.content().raw_data())?; - Ok((dob_content, cluster_id.to_vec().try_into().unwrap())) - } - - // search on-chain cluster cell and return its description field, which contains dob metadata - async fn fetch_dob_metadata( - &self, - cluster_id: [u8; 32], - ) -> DecodeResult { - let mut cluster_cell = None; - for cluster_search_option in - build_batch_search_options(cluster_id, &self.settings.available_clusters) - { - cluster_cell = self - .rpc - .get_cells(cluster_search_option.into(), 1, None) - .await - .map_err(|_| Error::FetchLiveCellsError)? - .objects - .first() - .cloned(); - if cluster_cell.is_some() { - break; - } - } - let Some(cluster_cell) = cluster_cell else { - return Err(Error::ClusterIdNotFound); - }; - let molecule_cluster_data = ClusterData::from_compatible_slice( - cluster_cell.output_data.unwrap_or_default().as_bytes(), - ) - .map_err(|_| Error::ClusterDataUncompatible)?; - let dob_metadata = serde_json::from_slice(&molecule_cluster_data.description().raw_data()) - .map_err(|_| Error::DOBMetadataUnexpected)?; - Ok(dob_metadata) - } - - // search on-chain decoder cell, deployed with type_id feature enabled - async fn fetch_decoder_binary(&self, decoder_id: [u8; 32]) -> DecodeResult> { - let decoder_search_option = build_type_id_search_option(decoder_id); - let decoder_cell = self - .rpc - .get_cells(decoder_search_option.into(), 1, None) - .await - .map_err(|_| Error::FetchLiveCellsError)? - .objects - .first() - .cloned() - .ok_or(Error::DecoderIdNotFound)?; - Ok(decoder_cell - .output_data - .unwrap_or_default() - .as_bytes() - .into()) - } - - // search on-chain decoder cell, directly by its tx_hash and out_index - async fn fetch_decoder_binary_directly( - &self, - tx_hash: H256, - out_index: u32, - ) -> DecodeResult> { - let decoder_cell = self - .rpc - .get_live_cell(&OutPoint::new(tx_hash.pack(), out_index).into(), true) - .await - .map_err(|_| Error::FetchTransactionError)?; - let decoder_binary = decoder_cell - .cell - .ok_or(Error::NoOutputCellInTransaction)? - .data - .ok_or(Error::DecoderBinaryNotFoundInCell)? - .content; - Ok(decoder_binary.as_bytes().to_vec()) - } -} - -fn build_type_id_search_option(type_id_args: [u8; 32]) -> CellQueryOptions { - let type_script = Script::new_builder() - .code_hash(TYPE_ID_CODE_HASH.0.pack()) - .hash_type(ScriptHashType::Type.into()) - .args(type_id_args.to_vec().pack()) - .build(); - CellQueryOptions::new_type(type_script) -} - -fn build_batch_search_options( - type_args: [u8; 32], - available_script_ids: &[ScriptId], -) -> Vec { - available_script_ids - .iter() - .map( - |ScriptId { - code_hash, - hash_type, - }| { - let hash_type: ScriptHashType = hash_type.into(); - let type_script = Script::new_builder() - .code_hash(code_hash.0.pack()) - .hash_type(hash_type.into()) - .args(type_args.to_vec().pack()) - .build(); - CellQueryOptions::new_type(type_script) - }, - ) - .collect() -} - -pub(crate) fn decode_spore_data(spore_data: &[u8]) -> Result<(Value, String), Error> { - if spore_data[0] == 0u8 { - let dna = hex::encode(&spore_data[1..]); - return Ok((serde_json::Value::String(dna.clone()), dna)); - } - - let value: Value = - serde_json::from_slice(spore_data).map_err(|_| Error::DOBContentUnexpected)?; - let dna = match &value { - serde_json::Value::String(_) => &value, - serde_json::Value::Array(array) => array.first().ok_or(Error::DOBContentUnexpected)?, - serde_json::Value::Object(object) => { - object.get("dna").ok_or(Error::DOBContentUnexpected)? - } - _ => return Err(Error::DOBContentUnexpected), - }; - let dna = match dna { - serde_json::Value::String(string) => string.to_owned(), - _ => return Err(Error::DOBContentUnexpected), - }; - - Ok((value, dna)) -} diff --git a/src/decoder/helpers.rs b/src/decoder/helpers.rs new file mode 100644 index 0000000..69b1a5f --- /dev/null +++ b/src/decoder/helpers.rs @@ -0,0 +1,237 @@ +use std::path::PathBuf; + +use ckb_sdk::{constants::TYPE_ID_CODE_HASH, traits::CellQueryOptions}; +use ckb_types::{ + core::ScriptHashType, + packed::{OutPoint, Script}, + prelude::{Builder, Entity, Pack}, + H256, +}; +use serde_json::Value; +use spore_types::{generated::spore::ClusterData, SporeData}; + +use crate::{ + client::RpcClient, + types::{ + ClusterDescriptionField, DOBDecoderFormat, DecoderLocationType, Error, ScriptId, Settings, + }, +}; + +pub fn build_type_id_search_option(type_id_args: [u8; 32]) -> CellQueryOptions { + let type_script = Script::new_builder() + .code_hash(TYPE_ID_CODE_HASH.0.pack()) + .hash_type(ScriptHashType::Type.into()) + .args(type_id_args.to_vec().pack()) + .build(); + CellQueryOptions::new_type(type_script) +} + +pub fn build_batch_search_options( + type_args: [u8; 32], + available_script_ids: &[ScriptId], +) -> Vec { + available_script_ids + .iter() + .map( + |ScriptId { + code_hash, + hash_type, + }| { + let hash_type: ScriptHashType = hash_type.into(); + let type_script = Script::new_builder() + .code_hash(code_hash.0.pack()) + .hash_type(hash_type.into()) + .args(type_args.to_vec().pack()) + .build(); + CellQueryOptions::new_type(type_script) + }, + ) + .collect() +} + +pub fn decode_spore_data(spore_data: &[u8]) -> Result<(Value, String), Error> { + if spore_data[0] == 0u8 { + let dna = hex::encode(&spore_data[1..]); + return Ok((serde_json::Value::String(dna.clone()), dna)); + } + + let value: Value = + serde_json::from_slice(spore_data).map_err(|_| Error::DOBContentUnexpected)?; + let dna = match &value { + serde_json::Value::String(_) => &value, + serde_json::Value::Array(array) => array.first().ok_or(Error::DOBContentUnexpected)?, + serde_json::Value::Object(object) => { + object.get("dna").ok_or(Error::DOBContentUnexpected)? + } + _ => return Err(Error::DOBContentUnexpected), + }; + let dna = match dna { + serde_json::Value::String(string) => string.to_owned(), + _ => return Err(Error::DOBContentUnexpected), + }; + + Ok((value, dna)) +} + +// search on-chain spore cell and return its content field, which represents dob content +pub async fn fetch_dob_content( + rpc: &RpcClient, + settings: &Settings, + spore_id: [u8; 32], +) -> Result<((Value, String), [u8; 32]), Error> { + let mut spore_cell = None; + for spore_search_option in build_batch_search_options(spore_id, &settings.available_spores) { + spore_cell = rpc + .get_cells(spore_search_option.into(), 1, None) + .await + .map_err(|_| Error::FetchLiveCellsError)? + .objects + .first() + .cloned(); + if spore_cell.is_some() { + break; + } + } + let Some(spore_cell) = spore_cell else { + return Err(Error::SporeIdNotFound); + }; + let molecule_spore_data = + SporeData::from_compatible_slice(spore_cell.output_data.unwrap_or_default().as_bytes()) + .map_err(|_| Error::SporeDataUncompatible)?; + let content_type = String::from_utf8(molecule_spore_data.content_type().raw_data().to_vec()) + .map_err(|_| Error::SporeDataContentTypeUncompatible)?; + if !settings + .protocol_versions + .iter() + .any(|version| content_type.starts_with(version)) + { + return Err(Error::DOBVersionUnexpected); + } + let cluster_id = molecule_spore_data + .cluster_id() + .to_opt() + .ok_or(Error::ClusterIdNotSet)? + .raw_data(); + let dob_content = decode_spore_data(&molecule_spore_data.content().raw_data())?; + Ok((dob_content, cluster_id.to_vec().try_into().unwrap())) +} + +// search on-chain cluster cell and return its description field, which contains dob metadata +pub async fn fetch_dob_metadata( + rpc: &RpcClient, + settings: &Settings, + cluster_id: [u8; 32], +) -> Result { + let mut cluster_cell = None; + for cluster_search_option in + build_batch_search_options(cluster_id, &settings.available_clusters) + { + cluster_cell = rpc + .get_cells(cluster_search_option.into(), 1, None) + .await + .map_err(|_| Error::FetchLiveCellsError)? + .objects + .first() + .cloned(); + if cluster_cell.is_some() { + break; + } + } + let Some(cluster_cell) = cluster_cell else { + return Err(Error::ClusterIdNotFound); + }; + let molecule_cluster_data = + ClusterData::from_compatible_slice(cluster_cell.output_data.unwrap_or_default().as_bytes()) + .map_err(|_| Error::ClusterDataUncompatible)?; + let dob_metadata = serde_json::from_slice(&molecule_cluster_data.description().raw_data()) + .map_err(|_| Error::DOBMetadataUnexpected)?; + Ok(dob_metadata) +} + +// search on-chain decoder cell, deployed with type_id feature enabled +pub async fn fetch_decoder_binary(rpc: &RpcClient, decoder_id: [u8; 32]) -> Result, Error> { + let decoder_search_option = build_type_id_search_option(decoder_id); + let decoder_cell = rpc + .get_cells(decoder_search_option.into(), 1, None) + .await + .map_err(|_| Error::FetchLiveCellsError)? + .objects + .first() + .cloned() + .ok_or(Error::DecoderIdNotFound)?; + Ok(decoder_cell + .output_data + .unwrap_or_default() + .as_bytes() + .into()) +} + +// search on-chain decoder cell, directly by its tx_hash and out_index +pub async fn fetch_decoder_binary_directly( + rpc: &RpcClient, + tx_hash: H256, + out_index: u32, +) -> Result, Error> { + let decoder_cell = rpc + .get_live_cell(&OutPoint::new(tx_hash.pack(), out_index).into(), true) + .await + .map_err(|_| Error::FetchTransactionError)?; + let decoder_binary = decoder_cell + .cell + .ok_or(Error::NoOutputCellInTransaction)? + .data + .ok_or(Error::DecoderBinaryNotFoundInCell)? + .content; + Ok(decoder_binary.as_bytes().to_vec()) +} + +pub async fn parse_decoder_path( + rpc: &RpcClient, + decoder: &DOBDecoderFormat, + settings: &Settings, +) -> Result { + let decoder_path = match decoder.location { + DecoderLocationType::CodeHash => { + let mut decoder_path = settings.decoders_cache_directory.clone(); + decoder_path.push(format!("code_hash_{}.bin", hex::encode(&decoder.hash))); + if !decoder_path.exists() { + let onchain_decoder = + settings + .onchain_decoder_deployment + .iter() + .find_map(|deployment| { + if deployment.code_hash == decoder.hash { + Some(fetch_decoder_binary_directly( + rpc, + deployment.tx_hash.clone(), + deployment.out_index, + )) + } else { + None + } + }); + let Some(decoder_binary) = onchain_decoder else { + return Err(Error::NativeDecoderNotFound); + }; + let decoder_file_content = decoder_binary.await?; + if ckb_hash::blake2b_256(&decoder_file_content) != decoder.hash.0 { + return Err(Error::DecoderBinaryHashInvalid); + } + std::fs::write(decoder_path.clone(), decoder_file_content) + .map_err(|_| Error::DecoderBinaryPathInvalid)?; + } + decoder_path + } + DecoderLocationType::TypeId => { + let mut decoder_path = settings.decoders_cache_directory.clone(); + decoder_path.push(format!("type_id_{}.bin", hex::encode(&decoder.hash))); + if !decoder_path.exists() { + let decoder_binary = fetch_decoder_binary(rpc, decoder.hash.clone().into()).await?; + std::fs::write(decoder_path.clone(), decoder_binary) + .map_err(|_| Error::DecoderBinaryPathInvalid)?; + } + decoder_path + } + }; + Ok(decoder_path) +} diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs new file mode 100644 index 0000000..ab83ae6 --- /dev/null +++ b/src/decoder/mod.rs @@ -0,0 +1,113 @@ +use serde_json::Value; + +use crate::{ + client::RpcClient, + types::{ + ClusterDescriptionField, DOBClusterFormatV0, DOBClusterFormatV1, Dob, Error, Settings, + }, +}; + +pub(crate) mod helpers; +use helpers::*; + +pub struct DOBDecoder { + rpc: RpcClient, + settings: Settings, +} + +impl DOBDecoder { + pub fn new(settings: Settings) -> Self { + Self { + rpc: RpcClient::new(&settings.ckb_rpc, &settings.ckb_rpc), + settings, + } + } + + pub fn protocol_versions(&self) -> Vec { + self.settings.protocol_versions.clone() + } + + pub fn setting(&self) -> &Settings { + &self.settings + } + + pub async fn fetch_decode_ingredients( + &self, + spore_id: [u8; 32], + ) -> Result<((Value, String), ClusterDescriptionField), Error> { + let (content, cluster_id) = fetch_dob_content(&self.rpc, &self.settings, spore_id).await?; + let dob_metadata = fetch_dob_metadata(&self.rpc, &self.settings, cluster_id).await?; + Ok((content, dob_metadata)) + } + + // decode DNA under target spore_id + pub async fn decode_dna( + &self, + dna: &str, + dob_metadata: ClusterDescriptionField, + ) -> Result { + let dob = dob_metadata.unbox_dob()?; + match dob { + Dob::V0(dob0) => self.decode_dob0_dna(dna, dob0).await, + Dob::V1(dob1) => self.decode_dob1_dna(dna, dob1).await, + } + } + + // decode specificly for objects under DOB/0 protocol + async fn decode_dob0_dna(&self, dna: &str, dob0: &DOBClusterFormatV0) -> Result { + let decoder_path = parse_decoder_path(&self.rpc, &dob0.decoder, &self.settings).await?; + let pattern = match &dob0.pattern { + Value::String(string) => string.to_owned(), + pattern => pattern.to_string(), + }; + let raw_render_result = { + let (exit_code, outputs) = crate::vm::execute_riscv_binary( + &decoder_path.to_string_lossy(), + vec![dna.to_owned().into(), pattern.into()], + &self.settings, + ) + .map_err(|_| Error::DecoderExecutionError)?; + #[cfg(feature = "render_debug")] + { + println!("\n-------- DOB/0 DECODE RESULT ({exit_code}) ---------"); + outputs.iter().for_each(|output| println!("{output}")); + println!("-------- DOB/0 DECODE RESULT END ---------"); + } + if exit_code != 0 { + return Err(Error::DecoderExecutionInternalError); + } + outputs.first().ok_or(Error::DecoderOutputInvalid)?.clone() + }; + Ok(raw_render_result) + } + + // decode specificly for objects under DOB/1 protocol + async fn decode_dob1_dna(&self, dna: &str, dob1: &DOBClusterFormatV1) -> Result { + let traits_output = self.decode_dob0_dna(dna, &dob1.traits).await?; + let image_decoder_path = + parse_decoder_path(&self.rpc, &dob1.images.decoder, &self.settings).await?; + let image_pattern = match &dob1.images.pattern { + Value::String(string) => string.to_owned(), + pattern => pattern.to_string(), + }; + let raw_render_result = { + let (exit_code, outputs) = crate::vm::execute_riscv_binary( + &image_decoder_path.to_string_lossy(), + vec![traits_output.into(), image_pattern.into()], + &self.settings, + ) + .map_err(|_| Error::DecoderExecutionError)?; + #[cfg(feature = "render_debug")] + { + println!("\n-------- DOB/1 DECODE RESULT ({exit_code}) ---------"); + outputs.iter().for_each(|output| println!("{output}")); + println!("-------- DOB/1 DECODE RESULT END ---------"); + } + if exit_code != 0 { + return Err(Error::DecoderExecutionInternalError); + } + outputs.first().ok_or(Error::DecoderOutputInvalid)?.clone() + }; + Ok(raw_render_result) + } +} diff --git a/src/tests/decoder.rs b/src/tests/dob0/decoder.rs similarity index 95% rename from src/tests/decoder.rs rename to src/tests/dob0/decoder.rs index ea4ba14..07ce6f7 100644 --- a/src/tests/decoder.rs +++ b/src/tests/dob0/decoder.rs @@ -4,7 +4,8 @@ use serde_json::{json, Value}; use crate::decoder::DOBDecoder; use crate::tests::prepare_settings; use crate::types::{ - ClusterDescriptionField, DOBClusterFormat, DOBDecoderFormat, DecoderLocationType, + ClusterDescriptionField, DOBClusterFormat, DOBClusterFormatV0, DOBDecoderFormat, + DecoderLocationType, }; const EXPECTED_UNICORN_RENDER_RESULT: &str = "[{\"name\":\"wuxing_yinyang\",\"traits\":[{\"String\":\"3<_>\"}]},{\"name\":\"prev.bgcolor\",\"traits\":[{\"String\":\"(%wuxing_yinyang):['#DBAB00', '#09D3FF', '#A028E9', '#FF3939', '#(135deg, #FE4F4F, #66C084, #00E2E2, #E180E2, #F4EC32)']\"}]},{\"name\":\"prev<%v>\",\"traits\":[{\"String\":\"(%wuxing_yinyang):['#000000', '#000000', '#000000', '#000000', '#000000', '#FFFFFF', '#FFFFFF', '#FFFFFF', '#FFFFFF', '#FFFFFF'])\"}]},{\"name\":\"Spirits\",\"traits\":[{\"String\":\"(%wuxing_yinyang):['Metal, Golden Body', 'Wood, Blue Body', 'Water, White Body', 'Fire, Red Body', 'Earth, Colorful Body']\"}]},{\"name\":\"Yin Yang\",\"traits\":[{\"String\":\"(%wuxing_yinyang):['Yin, Long hair', 'Yin, Long hair', 'Yin, Long hair', 'Yin, Long hair', 'Yin, Long hair', 'Yang, Short Hair', 'Yang, Short Hair', 'Yang, Short Hair', 'Yang, Short Hair', 'Yang, Short Hair']\"}]},{\"name\":\"Talents\",\"traits\":[{\"String\":\"(%wuxing_yinyang):['Guard<~>', 'Death<~>', 'Forget<~>', 'Curse<~>', 'Hermit<~>', 'Attack<~>', 'Revival<~>', 'Summon<~>', 'Prophet<~>', 'Crown<~>']\"}]},{\"name\":\"Horn\",\"traits\":[{\"String\":\"(%wuxing_yinyang):['Praetorian Horn', 'Hel Horn', 'Lethe Horn', 'Necromancer Horn', 'Lao Tsu Horn', 'Warrior Horn', 'Shaman Horn', 'Bard Horn', 'Sibyl Horn', 'Caesar Horn']\"}]},{\"name\":\"Wings\",\"traits\":[{\"String\":\"Sun Wings\"}]},{\"name\":\"Tail\",\"traits\":[{\"String\":\"Meteor Tail\"}]},{\"name\":\"Horseshoes\",\"traits\":[{\"String\":\"Silver Horseshoes\"}]},{\"name\":\"Destiny Number\",\"traits\":[{\"Number\":65321}]},{\"name\":\"Lucky Number\",\"traits\":[{\"Number\":35}]}]"; @@ -34,17 +35,16 @@ fn generate_unicorn_dob_ingredients(onchain_decoder: bool) -> (Value, ClusterDes }; let unicorn_metadata = ClusterDescriptionField { description: "Unicorns are the first series of digital objects generated based on time and space on CKB. Combining the Birth Time-location Determining Destiny Theory, Five Element Theory and YinYang Theory, it provide a special way for people to get Unicorn's on-chain DNA. Now all the seeds(DNAs) are on chain, and a magic world can expand.".to_string(), - dob: DOBClusterFormat { - ver: Some(0), + dob: DOBClusterFormat::new_dob0(DOBClusterFormatV0 { decoder, pattern: serde_json::from_str("[[\"wuxing_yinyang\",\"string\",0,1,\"options\",[\"0<_>\",\"1<_>\",\"2<_>\",\"3<_>\",\"4<_>\",\"5<_>\",\"6<_>\",\"7<_>\",\"8<_>\",\"9<_>\"]],[\"prev.bgcolor\",\"string\",1,1,\"options\",[\"(%wuxing_yinyang):['#DBAB00', '#09D3FF', '#A028E9', '#FF3939', '#(135deg, #FE4F4F, #66C084, #00E2E2, #E180E2, #F4EC32)']\"]],[\"prev<%v>\",\"string\",2,1,\"options\",[\"(%wuxing_yinyang):['#000000', '#000000', '#000000', '#000000', '#000000', '#FFFFFF', '#FFFFFF', '#FFFFFF', '#FFFFFF', '#FFFFFF'])\"]],[\"Spirits\",\"string\",3,1,\"options\",[\"(%wuxing_yinyang):['Metal, Golden Body', 'Wood, Blue Body', 'Water, White Body', 'Fire, Red Body', 'Earth, Colorful Body']\"]],[\"Yin Yang\",\"string\",4,1,\"options\",[\"(%wuxing_yinyang):['Yin, Long hair', 'Yin, Long hair', 'Yin, Long hair', 'Yin, Long hair', 'Yin, Long hair', 'Yang, Short Hair', 'Yang, Short Hair', 'Yang, Short Hair', 'Yang, Short Hair', 'Yang, Short Hair']\"]],[\"Talents\",\"string\",5,1,\"options\",[\"(%wuxing_yinyang):['Guard<~>', 'Death<~>', 'Forget<~>', 'Curse<~>', 'Hermit<~>', 'Attack<~>', 'Revival<~>', 'Summon<~>', 'Prophet<~>', 'Crown<~>']\"]],[\"Horn\",\"string\",6,1,\"options\",[\"(%wuxing_yinyang):['Praetorian Horn', 'Hel Horn', 'Lethe Horn', 'Necromancer Horn', 'Lao Tsu Horn', 'Warrior Horn', 'Shaman Horn', 'Bard Horn', 'Sibyl Horn', 'Caesar Horn']\"]],[\"Wings\",\"string\",7,1,\"options\",[\"Wind Wings\",\"Night Shadow Wings\",\"Lightning Wings\",\"Sun Wings\",\"Golden Wings\",\"Cloud Wings\",\"Morning Glow Wings\",\"Star Wings\",\"Spring Wings\",\"Moon Wings\",\"Angel Wings\"]],[\"Tail\",\"string\",8,1,\"options\",[\"Meteor Tail\",\"Rainbow Tail\",\"Willow Tail\",\"Phoenix Tail\",\"Sunset Shadow Tail\",\"Socrates Tail\",\"Dumbledore Tail\",\"Venus Tail\",\"Gaia Tail\"]],[\"Horseshoes\",\"string\",9,1,\"options\",[\"Ice Horseshoes\",\"Crystal Horseshoes\",\"Maple Horseshoes\",\"Flame Horseshoes\",\"Thunder Horseshoes\",\"Lotus Horseshoes\",\"Silver Horseshoes\"]],[\"Destiny Number\",\"number\",10,4,\"range\",[50000,100000]],[\"Lucky Number\",\"number\",14,1,\"range\",[1,49]]]").unwrap(), - }, + }), }; (unicorn_content, unicorn_metadata) } fn generate_example_dob_ingredients(onchain_decoder: bool) -> (Value, ClusterDescriptionField) { - let unicorn_content = json!({ + let example_content = json!({ "block_number": 120, "cell_id": 11844, "dna": "df4ffcb5e7a283ea7e6f09a504d0e256" @@ -60,15 +60,14 @@ fn generate_example_dob_ingredients(onchain_decoder: bool) -> (Value, ClusterDes hash: h256!("0x32f29aba4b17f3d05bec8cec55d50ef86766fd0bf82fdedaa14269f344d3784a"), } }; - let unicorn_metadata = ClusterDescriptionField { + let example_metadata = ClusterDescriptionField { description: "DOB/0 example.".to_string(), - dob: DOBClusterFormat { - ver: Some(0), + dob: DOBClusterFormat::new_dob0(DOBClusterFormatV0 { decoder, pattern: serde_json::from_str("[[\"Name\",\"string\",0,1,\"options\",[\"Alice\",\"Bob\",\"Charlie\",\"David\",\"Ethan\",\"Florence\",\"Grace\",\"Helen\"]],[\"Age\",\"number\",1,1,\"range\",[0,100]],[\"Score\",\"number\",2,1,\"raw\"],[\"DNA\",\"string\",3,3,\"raw\"],[\"URL\",\"string\",6,21,\"utf8\"],[\"Value\",\"number\",3,3,\"raw\"]]").unwrap(), - }, + }), }; - (unicorn_content, unicorn_metadata) + (example_content, example_metadata) } #[tokio::test] diff --git a/src/tests/legacy_decoder.rs b/src/tests/dob0/legacy_decoder.rs similarity index 98% rename from src/tests/legacy_decoder.rs rename to src/tests/dob0/legacy_decoder.rs index 2d435c6..d511e9b 100644 --- a/src/tests/legacy_decoder.rs +++ b/src/tests/dob0/legacy_decoder.rs @@ -1,9 +1,10 @@ use ckb_types::{h256, H256}; -use crate::decoder::{decode_spore_data, DOBDecoder}; +use crate::decoder::{helpers::decode_spore_data, DOBDecoder}; use crate::tests::prepare_settings; use crate::types::{ - ClusterDescriptionField, DOBClusterFormat, DOBDecoderFormat, DecoderLocationType, + ClusterDescriptionField, DOBClusterFormat, DOBClusterFormatV0, DOBDecoderFormat, + DecoderLocationType, }; use serde_json::{json, Value}; @@ -30,11 +31,10 @@ fn generate_nervape_dob_ingredients(onchain_decoder: bool) -> (Value, ClusterDes }; let nervape_metadata = ClusterDescriptionField { description: "Nervape, multi-chain composable digital objects built on Bitcoin.".to_string(), - dob: DOBClusterFormat { - ver: Some(0), + dob: DOBClusterFormat::new_dob0(DOBClusterFormatV0 { decoder, pattern: Value::String("830900004400000087000000370500004206000085060000c2060000050700004807000089070000c6070000060800004408000081080000c00800000209000043090000430000000c0000001900000009000000707265762e747970652a00000008000000220000000c0000000d0000000100000000110000000800000005000000696d616765b00400000c0000001700000007000000707265762e62679904000008000000910400000c0000000d0000000100000000800400003c0000008a000000d80000002601000074010000c2010000100200005e020000ac020000fa0200004803000096030000e4030000320400004a00000062746366733a2f2f3162633234333531613064663265363836353734636431623633343661316635356638316366663061326535323037386136653361643061333563666238333369304a00000062746366733a2f2f3634663536326431366532613461323965386334383231333730666666343733656466613232633236656635383038616462323430346533396463303133653569304a00000062746366733a2f2f6332396665636436643764376565633063623361326233646664636236616132363038316462386639383531313130623763323061306633633631373239396169304a00000062746366733a2f2f3539653837636131373765663066643435376538376539663933363237363630303232636635313962353331653166346533613664646139653565333338323769304a00000062746366733a2f2f6133353839646463663462376133633664613532666536616534656433323936663165646531333966653931323766323639376365306463663237303362363169304a00000062746366733a2f2f3739393732396666366131366464366166353764623161386361363134366435363733613330616439613539373664643836316433343861356565633238633469304a00000062746366733a2f2f3838646432616230356262386639633732646134326166633730363737616330356634373665313765306631363535316463303036333561653765393534366569304a00000062746366733a2f2f6233326533626262373363623837376339623431313532393933306135623665623332383039323762323832633132343836636532363930316233633232393169304a00000062746366733a2f2f6138623139646461623333386462306335326639613238346237643935666665616130646533346530623837343137373930316562393265306639663964386469304a00000062746366733a2f2f6261386231626239643862616565346266323461303666616132356235363934313066326462393662343633396638653038636362656330356338386437396269304a00000062746366733a2f2f6161383938366630656636363738303764346232333937306536343834346464653366303632323534326237396135633330323533396465306333356233316569304a00000062746366733a2f2f3130306637653066303936356463353435313561333833316133323038383133313563663563613634616430316265643262343232363136623135666433313469304a00000062746366733a2f2f6238346563306337373061613139363161336439343938656138613637653132383235333239313366633163313365336561663561343864653231363466623969304a00000062746366733a2f2f6130366261326531363134613530393931373665356363346439356465373663626562343730356138626437653134323333363237386562633239306664623369300b0100000c0000001c0000000c000000707265762e6267636f6c6f72ef00000008000000e70000000c0000000d0000000100000000d60000003c00000047000000520000005d00000068000000730000007e00000089000000940000009f000000aa000000b5000000c0000000cb00000007000000234646453345420700000023464643324645070000002343454241463707000000234237453646390700000023414246344430070000002345304446424407000000234639463741370700000023453242453931070000002346394336363207000000234637443642320700000023464341383633070000002346394143414307000000234530453145320700000023413341374141430000000c0000001a0000000a0000004261636b67726f756e642900000008000000210000000c0000000d00000001030000000000000000000000ff000000000000003d0000000c0000001400000004000000537569742900000008000000210000000c0000000d00000001030000000000000000000000ff00000000000000430000000c0000001a0000000a000000557070657220626f64792900000008000000210000000c0000000d00000001030000000000000000000000ff00000000000000430000000c0000001a0000000a0000004c6f77657220626f64792900000008000000210000000c0000000d00000001030000000000000000000000ff00000000000000410000000c000000180000000800000048656164776561722900000008000000210000000c0000000d00000001030000000000000000000000ff000000000000003d0000000c00000014000000040000004d61736b2900000008000000210000000c0000000d00000001030000000000000000000000ff00000000000000400000000c0000001700000007000000457965776561722900000008000000210000000c0000000d00000001030000000000000000000000ff000000000000003e0000000c00000015000000050000004d6f7574682900000008000000210000000c0000000d00000001030000000000000000000000ff000000000000003d0000000c0000001400000004000000456172732900000008000000210000000c0000000d00000001030000000000000000000000ff000000000000003f0000000c0000001600000006000000546174746f6f2900000008000000210000000c0000000d00000001030000000000000000000000ff00000000000000420000000c00000019000000090000004163636573736f72792900000008000000210000000c0000000d00000001030000000000000000000000ff00000000000000410000000c000000180000000800000048616e6468656c642900000008000000210000000c0000000d00000001030000000000000000000000ff00000000000000400000000c00000017000000070000005370656369616c2900000008000000210000000c0000000d00000001030000000000000000000000ff00000000000000".to_string()), - }, + }), }; (nervape_content, nervape_metadata) } @@ -58,11 +58,10 @@ fn generate_unicorn_dob_ingredients(onchain_decoder: bool) -> (Value, ClusterDes }; let unicorn_metadata = ClusterDescriptionField { description: "Unicorns are the first series of digital objects generated based on time and space on CKB. Combining the Birth Time-location Determining Destiny Theory, Five Element Theory and YinYang Theory, it provide a special way for people to get Unicorn's on-chain DNA. Now all the seeds(DNAs) are on chain, and a magic world can expand.".to_string(), - dob: DOBClusterFormat { - ver: Some(0), + dob: DOBClusterFormat::new_dob0(DOBClusterFormatV0 { decoder, pattern: Value::String("3d09000034000000e7000000a00100005e0200001403000021040000ef040000d4050000e6060000cf070000b1080000f8080000b30000000c0000001e0000000e000000777578696e675f79696e79616e6795000000080000008d0000000c0000000d00000001000000007c0000002c000000340000003c000000440000004c000000540000005c000000640000006c0000007400000004000000303c5f3e04000000313c5f3e04000000323c5f3e04000000333c5f3e04000000343c5f3e04000000353c5f3e04000000363c5f3e04000000373c5f3e04000000383c5f3e04000000393c5f3eb90000000c0000001c0000000c000000707265762e6267636f6c6f729d00000008000000950000000c0000000d00000001000000008400000008000000780000002825777578696e675f79696e79616e67293a5b2723444241423030272c202723303944334646272c202723413032384539272c202723464633393339272c202723283133356465672c20234645344634462c20233636433038342c20233030453245322c20234531383045322c202346344543333229275dbe0000000c0000001800000008000000707265763c25763ea6000000080000009e0000000c0000000d00000001000000008d00000008000000810000002825777578696e675f79696e79616e67293a5b2723303030303030272c202723303030303030272c202723303030303030272c202723303030303030272c202723303030303030272c202723464646464646272c202723464646464646272c202723464646464646272c202723464646464646272c202723464646464646275d29b60000000c0000001700000007000000537069726974739f00000008000000970000000c0000000d000000010000000086000000080000007a0000002825777578696e675f79696e79616e67293a5b274d6574616c2c20476f6c64656e20426f6479272c2027576f6f642c20426c756520426f6479272c202757617465722c20576869746520426f6479272c2027466972652c2052656420426f6479272c202745617274682c20436f6c6f7266756c20426f6479275d0d0100000c000000180000000800000059696e2059616e67f500000008000000ed0000000c0000000d0000000100000000dc00000008000000d00000002825777578696e675f79696e79616e67293a5b2759696e2c204c6f6e672068616972272c202759696e2c204c6f6e672068616972272c202759696e2c204c6f6e672068616972272c202759696e2c204c6f6e672068616972272c202759696e2c204c6f6e672068616972272c202759616e672c2053686f72742048616972272c202759616e672c2053686f72742048616972272c202759616e672c2053686f72742048616972272c202759616e672c2053686f72742048616972272c202759616e672c2053686f72742048616972275dce0000000c000000170000000700000054616c656e7473b700000008000000af0000000c0000000d00000001000000009e00000008000000920000002825777578696e675f79696e79616e67293a5b2747756172643c7e3e272c202744656174683c7e3e272c2027466f726765743c7e3e272c202743757273653c7e3e272c20274865726d69743c7e3e272c202741747461636b3c7e3e272c20275265766976616c3c7e3e272c202753756d6d6f6e3c7e3e272c202750726f706865743c7e3e272c202743726f776e3c7e3e275de50000000c0000001400000004000000486f726ed100000008000000c90000000c0000000d0000000100000000b800000008000000ac0000002825777578696e675f79696e79616e67293a5b2750726165746f7269616e20486f726e272c202748656c20486f726e272c20274c6574686520486f726e272c20274e6563726f6d616e63657220486f726e272c20274c616f2054737520486f726e272c202757617272696f7220486f726e272c20275368616d616e20486f726e272c20274261726420486f726e272c2027536962796c20486f726e272c202743616573617220486f726e275d120100000c000000150000000500000057696e6773fd00000008000000f50000000c0000000d0000000100000000e4000000300000003e0000005400000067000000740000008400000093000000a9000000b7000000c7000000d50000000a00000057696e642057696e6773120000004e6967687420536861646f772057696e67730f0000004c696768746e696e672057696e67730900000053756e2057696e67730c000000476f6c64656e2057696e67730b000000436c6f75642057696e6773120000004d6f726e696e6720476c6f772057696e67730a000000537461722057696e67730c000000537072696e672057696e67730a0000004d6f6f6e2057696e67730b000000416e67656c2057696e6773e90000000c00000015000000050000005461696c73d400000008000000cc0000000c0000000d0000000100000000bb00000028000000370000004700000056000000660000007c0000008d000000a0000000ae0000000b0000004d6574656f72205461696c0c0000005261696e626f77205461696c0b00000057696c6c6f77205461696c0c00000050686f656e6978205461696c1200000053756e73657420536861646f77205461696c0d000000536f637261746573205461696c0f00000044756d626c65646f7265205461696c0a00000056656e7573205461696c0900000047616961205461696ce20000000c0000001a0000000a000000486f72736573686f6573c800000008000000c00000000c0000000d0000000100000000af0000002000000032000000480000005c00000070000000860000009a0000000e00000049636520486f72736573686f6573120000004372797374616c20486f72736573686f6573100000004d61706c6520486f72736573686f657310000000466c616d6520486f72736573686f6573120000005468756e64657220486f72736573686f6573100000004c6f74757320486f72736573686f65731100000053696c76657220486f72736573686f6573470000000c0000001e0000000e00000044657374696e79204e756d6265722900000008000000210000000c0000000d000000040300000050c3000000000000a086010000000000450000000c0000001c0000000c0000004c75636b79204e756d6265722900000008000000210000000c0000000d000000010300000001000000000000003100000000000000".to_string()), - }, + }), }; (unicorn_content, unicorn_metadata) } diff --git a/src/tests/dob0/mod.rs b/src/tests/dob0/mod.rs new file mode 100644 index 0000000..4685c1b --- /dev/null +++ b/src/tests/dob0/mod.rs @@ -0,0 +1,2 @@ +mod decoder; +mod legacy_decoder; diff --git a/src/tests/dob1/decoder.rs b/src/tests/dob1/decoder.rs new file mode 100644 index 0000000..eeb306f --- /dev/null +++ b/src/tests/dob1/decoder.rs @@ -0,0 +1,152 @@ +use std::collections::HashMap; +use std::io::Cursor; + +use ckb_types::h256; +use image::codecs::png::{CompressionType, FilterType, PngEncoder}; +use serde_json::{json, Value}; + +use crate::client::ImageFetchClient; +use crate::decoder::DOBDecoder; +use crate::hashmap; +use crate::tests::prepare_settings; +use crate::types::{ + ClusterDescriptionField, DOBClusterFormat, DOBClusterFormatV0, DOBClusterFormatV1, + DOBDecoderFormat, DecoderLocationType, +}; + +fn generate_dob1_ingredients() -> (Value, ClusterDescriptionField) { + let content = json!({ + "dna": "ac7b88aabbcc687474703a2f2f3132372e302e302e313a383039300000" + }); + let metadata = ClusterDescriptionField { + description: "DOB/1 Test".to_string(), + dob: DOBClusterFormat::new_dob1(DOBClusterFormatV1 { + traits: DOBClusterFormatV0 { + decoder: DOBDecoderFormat { + location: DecoderLocationType::CodeHash, + hash: h256!( + "0x32f29aba4b17f3d05bec8cec55d50ef86766fd0bf82fdedaa14269f344d3784a" + ), + }, + pattern: serde_json::from_str("[[\"Name\",\"string\",0,1,\"options\",[\"Alice\",\"Bob\",\"Charlie\",\"David\",\"Ethan\",\"Florence\",\"Grace\",\"Helen\"]],[\"Age\",\"number\",1,1,\"range\",[0,100]],[\"Score\",\"number\",2,1,\"raw\"],[\"DNA\",\"string\",3,3,\"raw\"],[\"URL\",\"string\",6,21,\"utf8\"],[\"Value\",\"number\",3,3,\"raw\"]]").unwrap(), + }, + images: DOBClusterFormatV0 { + decoder: DOBDecoderFormat { + location: DecoderLocationType::CodeHash, + hash: h256!( + "0xac35b0e6178dc4a89fb85194b4ac0b60eed2b6ce9f10bf7bf2ee76190c3a0071" + ), + }, + pattern: serde_json::from_str("[[\"0\",\"color\",\"Name\",\"options\",[[\"Alice\",\"#0000FF\"],[\"Bob\",\"#00FF00\"],[\"Ethan\",\"#FF0000\"],[[\"*\"],\"#FFFFFF\"]]],[\"0\",\"uri\",\"Age\",\"range\",[[[0,50],\"btcfs://b2f4560f17679d3e3fca66209ac425c660d28a252ef72444c3325c6eb0364393i0\"],[[51,100],\"btcfs://eb3910b3e32a5ed9460bd0d75168c01ba1b8f00cc0faf83e4d8b67b48ea79676i0\"],[[\"*\"],\"btcfs://11b6303eb7d887d7ade459ac27959754cd55f9f9e50345ced8e1e8f47f4581fai0\"]]],[\"0\",\"uri\",\"Score\",\"range\",[[[0,1000],\"btcfs://11d6cc654f4c0759bfee520966937a4304db2b33880c88c2a6c649e30c7b9aaei1\"],[[\"*\"],\"btcfs://e1484915b27e45b120239080fe5032580550ff9ff759eb26ee86bf8aaf90068bi1\"]]],[\"1\",\"uri\",\"Value\",\"range\",[[[0,100000],\"btcfs://11d6cc654f4c0759bfee520966937a4304db2b33880c88c2a6c649e30c7b9aaei0\"],[[\"*\"],\"btcfs://e1484915b27e45b120239080fe5032580550ff9ff759eb26ee86bf8aaf90068bi0\"]]]]").unwrap(), + }, + }), + }; + (content, metadata) +} + +#[test] +fn test_print_dob1_ingreidents() { + let (_, dob_metadata) = generate_dob1_ingredients(); + println!( + "cluster_description: {}", + serde_json::to_string(&dob_metadata).unwrap() + ); +} + +macro_rules! png_decoder { + ($image:expr) => {{ + let rgba = image::load_from_memory(&$image).expect("load image"); + let mut inner_buffer = Vec::new(); + let buffer = Cursor::new(&mut inner_buffer); + let encoder = + PngEncoder::new_with_quality(buffer, CompressionType::Best, FilterType::NoFilter); + rgba.write_with_encoder(encoder).expect("write image"); + inner_buffer + }}; + () => {}; +} + +#[tokio::test] +async fn check_fetched_image() { + let url = hashmap!(("btcfs", "https://mempool.space/api/tx/")); + let mut fetcher = ImageFetchClient::new(&url, 100); + let uris = vec![ + "btcfs://11d6cc654f4c0759bfee520966937a4304db2b33880c88c2a6c649e30c7b9aaei0".to_string(), + ]; + let images = fetcher.fetch_images(&uris).await.expect("fetch images"); + let image_raw_bytes = images.first().expect("image"); + let png = png_decoder!(image_raw_bytes); + println!("image size: {:?}", png.len()); + // std::fs::write("dob1.png", &png).expect("write image"); +} + +#[tokio::test] +async fn check_ipfs_image() { + let url = hashmap!(("ipfs", "https://ipfs.io/ipfs/")); + let mut fetcher = ImageFetchClient::new(&url, 100); + let uris = vec!["ipfs://QmeQ6TfqzsjJCMtYmpbyZeMxiSzQGc6Aqg6NyJTeLYrrJr".to_string()]; + let images = fetcher.fetch_images(&uris).await.expect("fetch images"); + let image_raw_bytes = images.first().expect("image"); + let png = png_decoder!(image_raw_bytes); + println!("image size: {:?}", png.len()); + // std::fs::write("dob1.png", &png).expect("write image"); +} + +#[tokio::test] +async fn test_dob1() { + let settings = prepare_settings("text/plain"); + let decoder = DOBDecoder::new(settings); + let (content, metadata) = generate_dob1_ingredients(); + let _output = decoder + .decode_dna(&content["dna"].as_str().unwrap(), metadata) + .await + .expect("decode dob/1"); + // use base64::{engine::general_purpose::STANDARD, Engine}; + // let dob1_output: Value = serde_json::from_str(&_output).unwrap(); + // let base64_image = dob1_output + // .get("images") + // .unwrap() + // .as_array() + // .unwrap() + // .first() + // .unwrap() + // .as_object() + // .unwrap() + // .get("content") + // .unwrap() + // .as_str() + // .unwrap(); + // let image = STANDARD.decode(base64_image).expect("decode base64 image"); + // std::fs::write("dob1.png", &image).expect("write image"); +} + +#[tokio::test] +async fn test_fetch_dob1() { + let settings = prepare_settings("text/plain"); + let decoder = DOBDecoder::new(settings); + let dob1_spore_id = + hex::decode("2fa44c408dfb78f5f032f6cb30966f9122df7e906c38bcbbd1fe9751ad3b2083").unwrap(); + let ((_, dna), metadata) = decoder + .fetch_decode_ingredients(dob1_spore_id.try_into().unwrap()) + .await + .unwrap(); + let output = decoder.decode_dna(&dna, metadata).await.unwrap(); + println!("dob1 output: {}", output); + // use base64::{engine::general_purpose::STANDARD, Engine}; + // let dob1_output: Value = serde_json::from_str(&output).unwrap(); + // let base64_image = dob1_output + // .get("images") + // .unwrap() + // .as_array() + // .unwrap() + // .first() + // .unwrap() + // .as_object() + // .unwrap() + // .get("content") + // .unwrap() + // .as_str() + // .unwrap(); + // let image = STANDARD.decode(base64_image).expect("decode base64 image"); + // std::fs::write("dob1.png", &image).expect("write image"); +} diff --git a/src/tests/dob1/mod.rs b/src/tests/dob1/mod.rs new file mode 100644 index 0000000..f201bd0 --- /dev/null +++ b/src/tests/dob1/mod.rs @@ -0,0 +1 @@ +mod decoder; diff --git a/src/tests/mod.rs b/src/tests/mod.rs index dcc0f7d..ba07649 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -1,17 +1,36 @@ +use std::collections::HashMap; + use ckb_types::h256; use crate::types::{HashType, OnchainDecoderDeployment, ScriptId, Settings}; -mod decoder; -mod legacy_decoder; +mod dob0; +mod dob1; + +#[macro_export] +macro_rules! hashmap { + ($(($key:expr, $value:expr) $(,)?)+) => {{ + let mut map = HashMap::new(); + $( + map.insert($key.to_owned(), $value.to_owned()); + )+ + map + }}; +} fn prepare_settings(version: &str) -> Settings { Settings { ckb_rpc: "https://testnet.ckbapp.dev/".to_string(), + image_fetcher_url: hashmap!( + ("btcfs", "https://mempool.space/api/tx/"), + ("ipfs", "https://ipfs.io/ipfs/") + ), protocol_versions: vec![version.to_string()], ckb_vm_runner: "ckb-vm-runner".to_string(), decoders_cache_directory: "cache/decoders".parse().unwrap(), dobs_cache_directory: "cache/dobs".parse().unwrap(), + dob1_max_combination: 5, + dob1_max_cache_size: 100, available_spores: vec![ ScriptId { code_hash: h256!( @@ -59,6 +78,15 @@ fn prepare_settings(version: &str) -> Settings { ), out_index: 0, }, + OnchainDecoderDeployment { + code_hash: h256!( + "0xac35b0e6178dc4a89fb85194b4ac0b60eed2b6ce9f10bf7bf2ee76190c3a0071" + ), + tx_hash: h256!( + "0x1918a656f7c52ca5fbe7a903903c9bbc89d3e05525b4bb9f323fceb1a5bde51f" + ), + out_index: 0, + }, ], ..Default::default() } diff --git a/src/types/error.rs b/src/types/error.rs new file mode 100644 index 0000000..d239c58 --- /dev/null +++ b/src/types/error.rs @@ -0,0 +1,79 @@ +#[allow(clippy::enum_variant_names)] +#[derive(thiserror::Error, Debug)] +#[repr(i32)] +pub enum Error { + #[error("DNA bytes length not match the requirement in Cluster")] + DnaLengthNotMatch = 1001, + #[error("spore id length should equal to 32")] + SporeIdLengthInvalid, + #[error("natvie decoder not found")] + NativeDecoderNotFound, + #[error("spore id not exist on-chain")] + SporeIdNotFound, + #[error("uncompatible spore data")] + SporeDataUncompatible, + #[error("uncompatible spore data content_type")] + SporeDataContentTypeUncompatible, + #[error("unexpected DOB protocol version")] + DOBVersionUnexpected, + #[error("miss cluster id in spore data")] + ClusterIdNotSet, + #[error("cluster id not exist on-chain")] + ClusterIdNotFound, + #[error("uncompatible cluster data")] + ClusterDataUncompatible, + #[error("decoder id not exist on-chain")] + DecoderIdNotFound, + #[error("output of decoder should contain at least one line")] + DecoderOutputInvalid, + #[error("DNA string is not in hex format")] + HexedDNAParseError, + #[error("spore id string is not in hex format")] + HexedSporeIdParseError, + #[error("invalid decoder path to persist")] + DecoderBinaryPathInvalid, + #[error("encounter error while executing DNA decoding")] + DecoderExecutionError, + #[error("decoding program triggered an error")] + DecoderExecutionInternalError, + #[error("encounter error while searching live cells")] + FetchLiveCellsError, + #[error("encounter error while searching transaction by hash")] + FetchTransactionError, + #[error("not found specific output_cell in transaction")] + NoOutputCellInTransaction, + #[error("spore content cannot parse to DOB content")] + DOBContentUnexpected, + #[error("cluster description cannot parse to DOB metadata")] + DOBMetadataUnexpected, + #[error("DOB render cache folder not found")] + DOBRenderCacheNotFound, + #[error("cached DOB render result file has changed unexpectedly")] + DOBRenderCacheModified, + #[error("invalid deployed on-chain decoder code_hash")] + DecoderBinaryHashInvalid, + #[error("no binary found in cell for decoder")] + DecoderBinaryNotFoundInCell, + #[error("error ocurred while requesing json-rpc")] + JsonRpcRequestError, + #[error("system time calculation error")] + SystemTimeError, + #[error("BTC node responsed bad")] + FetchFromBtcNodeError, + #[error("BTC transaction format broken")] + InvalidBtcTransactionFormat, + #[error("Inscription format broken")] + InvalidInscriptionFormat, + #[error("Inscription content must be hex format")] + InvalidInscriptionContentHexFormat, + #[error("Inscription content must be filled")] + EmptyInscriptionContent, + #[error("Inscription index flag exceeded")] + ExceededInscriptionIndex, + #[error("fs header like 'btcfs://' and 'ckbfs://' are not contained")] + InvalidOnchainFsuriFormat, + #[error("fs header like 'btcfs://' and 'ckbfs://' are not configured in config file")] + FsuriNotFoundInConfig, + #[error("IPFS Gateway responsed bad")] + FetchFromIpfsError, +} diff --git a/src/types/generated/dob1_syscall.mol b/src/types/generated/dob1_syscall.mol new file mode 100644 index 0000000..74d718b --- /dev/null +++ b/src/types/generated/dob1_syscall.mol @@ -0,0 +1,11 @@ +vector URI ; +vector Color ; +vector RawImage ; + +union Item { + URI, + Color, + RawImage, +} + +vector ItemVec ; diff --git a/src/types/generated/mod.rs b/src/types/generated/mod.rs new file mode 100644 index 0000000..237a7cc --- /dev/null +++ b/src/types/generated/mod.rs @@ -0,0 +1,1456 @@ +// Generated by Molecule 0.8.0 +#![allow(dead_code)] +#![allow(clippy::all)] + +use molecule::prelude::*; +#[derive(Clone)] +pub struct URI(molecule::bytes::Bytes); +impl ::core::fmt::LowerHex for URI { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + use molecule::hex_string; + if f.alternate() { + write!(f, "0x")?; + } + write!(f, "{}", hex_string(self.as_slice())) + } +} +impl ::core::fmt::Debug for URI { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + write!(f, "{}({:#x})", Self::NAME, self) + } +} +impl ::core::fmt::Display for URI { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + use molecule::hex_string; + let raw_data = hex_string(&self.raw_data()); + write!(f, "{}(0x{})", Self::NAME, raw_data) + } +} +impl ::core::default::Default for URI { + fn default() -> Self { + let v = molecule::bytes::Bytes::from_static(&Self::DEFAULT_VALUE); + URI::new_unchecked(v) + } +} +impl URI { + const DEFAULT_VALUE: [u8; 4] = [0, 0, 0, 0]; + pub const ITEM_SIZE: usize = 1; + pub fn total_size(&self) -> usize { + molecule::NUMBER_SIZE + Self::ITEM_SIZE * self.item_count() + } + pub fn item_count(&self) -> usize { + molecule::unpack_number(self.as_slice()) as usize + } + pub fn len(&self) -> usize { + self.item_count() + } + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + pub fn get(&self, idx: usize) -> Option { + if idx >= self.len() { + None + } else { + Some(self.get_unchecked(idx)) + } + } + pub fn get_unchecked(&self, idx: usize) -> Byte { + let start = molecule::NUMBER_SIZE + Self::ITEM_SIZE * idx; + let end = start + Self::ITEM_SIZE; + Byte::new_unchecked(self.0.slice(start..end)) + } + pub fn raw_data(&self) -> molecule::bytes::Bytes { + self.0.slice(molecule::NUMBER_SIZE..) + } + pub fn as_reader<'r>(&'r self) -> URIReader<'r> { + URIReader::new_unchecked(self.as_slice()) + } +} +impl molecule::prelude::Entity for URI { + type Builder = URIBuilder; + const NAME: &'static str = "URI"; + fn new_unchecked(data: molecule::bytes::Bytes) -> Self { + URI(data) + } + fn as_bytes(&self) -> molecule::bytes::Bytes { + self.0.clone() + } + fn as_slice(&self) -> &[u8] { + &self.0[..] + } + fn from_slice(slice: &[u8]) -> molecule::error::VerificationResult { + URIReader::from_slice(slice).map(|reader| reader.to_entity()) + } + fn from_compatible_slice(slice: &[u8]) -> molecule::error::VerificationResult { + URIReader::from_compatible_slice(slice).map(|reader| reader.to_entity()) + } + fn new_builder() -> Self::Builder { + ::core::default::Default::default() + } + fn as_builder(self) -> Self::Builder { + Self::new_builder().extend(self.into_iter()) + } +} +#[derive(Clone, Copy)] +pub struct URIReader<'r>(&'r [u8]); +impl<'r> ::core::fmt::LowerHex for URIReader<'r> { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + use molecule::hex_string; + if f.alternate() { + write!(f, "0x")?; + } + write!(f, "{}", hex_string(self.as_slice())) + } +} +impl<'r> ::core::fmt::Debug for URIReader<'r> { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + write!(f, "{}({:#x})", Self::NAME, self) + } +} +impl<'r> ::core::fmt::Display for URIReader<'r> { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + use molecule::hex_string; + let raw_data = hex_string(&self.raw_data()); + write!(f, "{}(0x{})", Self::NAME, raw_data) + } +} +impl<'r> URIReader<'r> { + pub const ITEM_SIZE: usize = 1; + pub fn total_size(&self) -> usize { + molecule::NUMBER_SIZE + Self::ITEM_SIZE * self.item_count() + } + pub fn item_count(&self) -> usize { + molecule::unpack_number(self.as_slice()) as usize + } + pub fn len(&self) -> usize { + self.item_count() + } + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + pub fn get(&self, idx: usize) -> Option> { + if idx >= self.len() { + None + } else { + Some(self.get_unchecked(idx)) + } + } + pub fn get_unchecked(&self, idx: usize) -> ByteReader<'r> { + let start = molecule::NUMBER_SIZE + Self::ITEM_SIZE * idx; + let end = start + Self::ITEM_SIZE; + ByteReader::new_unchecked(&self.as_slice()[start..end]) + } + pub fn raw_data(&self) -> &'r [u8] { + &self.as_slice()[molecule::NUMBER_SIZE..] + } +} +impl<'r> molecule::prelude::Reader<'r> for URIReader<'r> { + type Entity = URI; + const NAME: &'static str = "URIReader"; + fn to_entity(&self) -> Self::Entity { + Self::Entity::new_unchecked(self.as_slice().to_owned().into()) + } + fn new_unchecked(slice: &'r [u8]) -> Self { + URIReader(slice) + } + fn as_slice(&self) -> &'r [u8] { + self.0 + } + fn verify(slice: &[u8], _compatible: bool) -> molecule::error::VerificationResult<()> { + use molecule::verification_error as ve; + let slice_len = slice.len(); + if slice_len < molecule::NUMBER_SIZE { + return ve!(Self, HeaderIsBroken, molecule::NUMBER_SIZE, slice_len); + } + let item_count = molecule::unpack_number(slice) as usize; + if item_count == 0 { + if slice_len != molecule::NUMBER_SIZE { + return ve!(Self, TotalSizeNotMatch, molecule::NUMBER_SIZE, slice_len); + } + return Ok(()); + } + let total_size = molecule::NUMBER_SIZE + Self::ITEM_SIZE * item_count; + if slice_len != total_size { + return ve!(Self, TotalSizeNotMatch, total_size, slice_len); + } + Ok(()) + } +} +#[derive(Clone, Debug, Default)] +pub struct URIBuilder(pub(crate) Vec); +impl URIBuilder { + pub const ITEM_SIZE: usize = 1; + pub fn set(mut self, v: Vec) -> Self { + self.0 = v; + self + } + pub fn push(mut self, v: Byte) -> Self { + self.0.push(v); + self + } + pub fn extend>(mut self, iter: T) -> Self { + for elem in iter { + self.0.push(elem); + } + self + } + pub fn replace(&mut self, index: usize, v: Byte) -> Option { + self.0 + .get_mut(index) + .map(|item| ::core::mem::replace(item, v)) + } +} +impl molecule::prelude::Builder for URIBuilder { + type Entity = URI; + const NAME: &'static str = "URIBuilder"; + fn expected_length(&self) -> usize { + molecule::NUMBER_SIZE + Self::ITEM_SIZE * self.0.len() + } + fn write(&self, writer: &mut W) -> molecule::io::Result<()> { + writer.write_all(&molecule::pack_number(self.0.len() as molecule::Number))?; + for inner in &self.0[..] { + writer.write_all(inner.as_slice())?; + } + Ok(()) + } + fn build(&self) -> Self::Entity { + let mut inner = Vec::with_capacity(self.expected_length()); + self.write(&mut inner) + .unwrap_or_else(|_| panic!("{} build should be ok", Self::NAME)); + URI::new_unchecked(inner.into()) + } +} +pub struct URIIterator(URI, usize, usize); +impl ::core::iter::Iterator for URIIterator { + type Item = Byte; + fn next(&mut self) -> Option { + if self.1 >= self.2 { + None + } else { + let ret = self.0.get_unchecked(self.1); + self.1 += 1; + Some(ret) + } + } +} +impl ::core::iter::ExactSizeIterator for URIIterator { + fn len(&self) -> usize { + self.2 - self.1 + } +} +impl ::core::iter::IntoIterator for URI { + type Item = Byte; + type IntoIter = URIIterator; + fn into_iter(self) -> Self::IntoIter { + let len = self.len(); + URIIterator(self, 0, len) + } +} +impl ::core::iter::FromIterator for URI { + fn from_iter>(iter: T) -> Self { + Self::new_builder().extend(iter).build() + } +} +impl ::core::iter::FromIterator for URI { + fn from_iter>(iter: T) -> Self { + Self::new_builder() + .extend(iter.into_iter().map(Into::into)) + .build() + } +} +#[derive(Clone)] +pub struct Color(molecule::bytes::Bytes); +impl ::core::fmt::LowerHex for Color { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + use molecule::hex_string; + if f.alternate() { + write!(f, "0x")?; + } + write!(f, "{}", hex_string(self.as_slice())) + } +} +impl ::core::fmt::Debug for Color { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + write!(f, "{}({:#x})", Self::NAME, self) + } +} +impl ::core::fmt::Display for Color { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + use molecule::hex_string; + let raw_data = hex_string(&self.raw_data()); + write!(f, "{}(0x{})", Self::NAME, raw_data) + } +} +impl ::core::default::Default for Color { + fn default() -> Self { + let v = molecule::bytes::Bytes::from_static(&Self::DEFAULT_VALUE); + Color::new_unchecked(v) + } +} +impl Color { + const DEFAULT_VALUE: [u8; 4] = [0, 0, 0, 0]; + pub const ITEM_SIZE: usize = 1; + pub fn total_size(&self) -> usize { + molecule::NUMBER_SIZE + Self::ITEM_SIZE * self.item_count() + } + pub fn item_count(&self) -> usize { + molecule::unpack_number(self.as_slice()) as usize + } + pub fn len(&self) -> usize { + self.item_count() + } + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + pub fn get(&self, idx: usize) -> Option { + if idx >= self.len() { + None + } else { + Some(self.get_unchecked(idx)) + } + } + pub fn get_unchecked(&self, idx: usize) -> Byte { + let start = molecule::NUMBER_SIZE + Self::ITEM_SIZE * idx; + let end = start + Self::ITEM_SIZE; + Byte::new_unchecked(self.0.slice(start..end)) + } + pub fn raw_data(&self) -> molecule::bytes::Bytes { + self.0.slice(molecule::NUMBER_SIZE..) + } + pub fn as_reader<'r>(&'r self) -> ColorReader<'r> { + ColorReader::new_unchecked(self.as_slice()) + } +} +impl molecule::prelude::Entity for Color { + type Builder = ColorBuilder; + const NAME: &'static str = "Color"; + fn new_unchecked(data: molecule::bytes::Bytes) -> Self { + Color(data) + } + fn as_bytes(&self) -> molecule::bytes::Bytes { + self.0.clone() + } + fn as_slice(&self) -> &[u8] { + &self.0[..] + } + fn from_slice(slice: &[u8]) -> molecule::error::VerificationResult { + ColorReader::from_slice(slice).map(|reader| reader.to_entity()) + } + fn from_compatible_slice(slice: &[u8]) -> molecule::error::VerificationResult { + ColorReader::from_compatible_slice(slice).map(|reader| reader.to_entity()) + } + fn new_builder() -> Self::Builder { + ::core::default::Default::default() + } + fn as_builder(self) -> Self::Builder { + Self::new_builder().extend(self.into_iter()) + } +} +#[derive(Clone, Copy)] +pub struct ColorReader<'r>(&'r [u8]); +impl<'r> ::core::fmt::LowerHex for ColorReader<'r> { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + use molecule::hex_string; + if f.alternate() { + write!(f, "0x")?; + } + write!(f, "{}", hex_string(self.as_slice())) + } +} +impl<'r> ::core::fmt::Debug for ColorReader<'r> { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + write!(f, "{}({:#x})", Self::NAME, self) + } +} +impl<'r> ::core::fmt::Display for ColorReader<'r> { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + use molecule::hex_string; + let raw_data = hex_string(&self.raw_data()); + write!(f, "{}(0x{})", Self::NAME, raw_data) + } +} +impl<'r> ColorReader<'r> { + pub const ITEM_SIZE: usize = 1; + pub fn total_size(&self) -> usize { + molecule::NUMBER_SIZE + Self::ITEM_SIZE * self.item_count() + } + pub fn item_count(&self) -> usize { + molecule::unpack_number(self.as_slice()) as usize + } + pub fn len(&self) -> usize { + self.item_count() + } + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + pub fn get(&self, idx: usize) -> Option> { + if idx >= self.len() { + None + } else { + Some(self.get_unchecked(idx)) + } + } + pub fn get_unchecked(&self, idx: usize) -> ByteReader<'r> { + let start = molecule::NUMBER_SIZE + Self::ITEM_SIZE * idx; + let end = start + Self::ITEM_SIZE; + ByteReader::new_unchecked(&self.as_slice()[start..end]) + } + pub fn raw_data(&self) -> &'r [u8] { + &self.as_slice()[molecule::NUMBER_SIZE..] + } +} +impl<'r> molecule::prelude::Reader<'r> for ColorReader<'r> { + type Entity = Color; + const NAME: &'static str = "ColorReader"; + fn to_entity(&self) -> Self::Entity { + Self::Entity::new_unchecked(self.as_slice().to_owned().into()) + } + fn new_unchecked(slice: &'r [u8]) -> Self { + ColorReader(slice) + } + fn as_slice(&self) -> &'r [u8] { + self.0 + } + fn verify(slice: &[u8], _compatible: bool) -> molecule::error::VerificationResult<()> { + use molecule::verification_error as ve; + let slice_len = slice.len(); + if slice_len < molecule::NUMBER_SIZE { + return ve!(Self, HeaderIsBroken, molecule::NUMBER_SIZE, slice_len); + } + let item_count = molecule::unpack_number(slice) as usize; + if item_count == 0 { + if slice_len != molecule::NUMBER_SIZE { + return ve!(Self, TotalSizeNotMatch, molecule::NUMBER_SIZE, slice_len); + } + return Ok(()); + } + let total_size = molecule::NUMBER_SIZE + Self::ITEM_SIZE * item_count; + if slice_len != total_size { + return ve!(Self, TotalSizeNotMatch, total_size, slice_len); + } + Ok(()) + } +} +#[derive(Clone, Debug, Default)] +pub struct ColorBuilder(pub(crate) Vec); +impl ColorBuilder { + pub const ITEM_SIZE: usize = 1; + pub fn set(mut self, v: Vec) -> Self { + self.0 = v; + self + } + pub fn push(mut self, v: Byte) -> Self { + self.0.push(v); + self + } + pub fn extend>(mut self, iter: T) -> Self { + for elem in iter { + self.0.push(elem); + } + self + } + pub fn replace(&mut self, index: usize, v: Byte) -> Option { + self.0 + .get_mut(index) + .map(|item| ::core::mem::replace(item, v)) + } +} +impl molecule::prelude::Builder for ColorBuilder { + type Entity = Color; + const NAME: &'static str = "ColorBuilder"; + fn expected_length(&self) -> usize { + molecule::NUMBER_SIZE + Self::ITEM_SIZE * self.0.len() + } + fn write(&self, writer: &mut W) -> molecule::io::Result<()> { + writer.write_all(&molecule::pack_number(self.0.len() as molecule::Number))?; + for inner in &self.0[..] { + writer.write_all(inner.as_slice())?; + } + Ok(()) + } + fn build(&self) -> Self::Entity { + let mut inner = Vec::with_capacity(self.expected_length()); + self.write(&mut inner) + .unwrap_or_else(|_| panic!("{} build should be ok", Self::NAME)); + Color::new_unchecked(inner.into()) + } +} +pub struct ColorIterator(Color, usize, usize); +impl ::core::iter::Iterator for ColorIterator { + type Item = Byte; + fn next(&mut self) -> Option { + if self.1 >= self.2 { + None + } else { + let ret = self.0.get_unchecked(self.1); + self.1 += 1; + Some(ret) + } + } +} +impl ::core::iter::ExactSizeIterator for ColorIterator { + fn len(&self) -> usize { + self.2 - self.1 + } +} +impl ::core::iter::IntoIterator for Color { + type Item = Byte; + type IntoIter = ColorIterator; + fn into_iter(self) -> Self::IntoIter { + let len = self.len(); + ColorIterator(self, 0, len) + } +} +impl ::core::iter::FromIterator for Color { + fn from_iter>(iter: T) -> Self { + Self::new_builder().extend(iter).build() + } +} +impl ::core::iter::FromIterator for Color { + fn from_iter>(iter: T) -> Self { + Self::new_builder() + .extend(iter.into_iter().map(Into::into)) + .build() + } +} +#[derive(Clone)] +pub struct RawImage(molecule::bytes::Bytes); +impl ::core::fmt::LowerHex for RawImage { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + use molecule::hex_string; + if f.alternate() { + write!(f, "0x")?; + } + write!(f, "{}", hex_string(self.as_slice())) + } +} +impl ::core::fmt::Debug for RawImage { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + write!(f, "{}({:#x})", Self::NAME, self) + } +} +impl ::core::fmt::Display for RawImage { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + use molecule::hex_string; + let raw_data = hex_string(&self.raw_data()); + write!(f, "{}(0x{})", Self::NAME, raw_data) + } +} +impl ::core::default::Default for RawImage { + fn default() -> Self { + let v = molecule::bytes::Bytes::from_static(&Self::DEFAULT_VALUE); + RawImage::new_unchecked(v) + } +} +impl RawImage { + const DEFAULT_VALUE: [u8; 4] = [0, 0, 0, 0]; + pub const ITEM_SIZE: usize = 1; + pub fn total_size(&self) -> usize { + molecule::NUMBER_SIZE + Self::ITEM_SIZE * self.item_count() + } + pub fn item_count(&self) -> usize { + molecule::unpack_number(self.as_slice()) as usize + } + pub fn len(&self) -> usize { + self.item_count() + } + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + pub fn get(&self, idx: usize) -> Option { + if idx >= self.len() { + None + } else { + Some(self.get_unchecked(idx)) + } + } + pub fn get_unchecked(&self, idx: usize) -> Byte { + let start = molecule::NUMBER_SIZE + Self::ITEM_SIZE * idx; + let end = start + Self::ITEM_SIZE; + Byte::new_unchecked(self.0.slice(start..end)) + } + pub fn raw_data(&self) -> molecule::bytes::Bytes { + self.0.slice(molecule::NUMBER_SIZE..) + } + pub fn as_reader<'r>(&'r self) -> RawImageReader<'r> { + RawImageReader::new_unchecked(self.as_slice()) + } +} +impl molecule::prelude::Entity for RawImage { + type Builder = RawImageBuilder; + const NAME: &'static str = "RawImage"; + fn new_unchecked(data: molecule::bytes::Bytes) -> Self { + RawImage(data) + } + fn as_bytes(&self) -> molecule::bytes::Bytes { + self.0.clone() + } + fn as_slice(&self) -> &[u8] { + &self.0[..] + } + fn from_slice(slice: &[u8]) -> molecule::error::VerificationResult { + RawImageReader::from_slice(slice).map(|reader| reader.to_entity()) + } + fn from_compatible_slice(slice: &[u8]) -> molecule::error::VerificationResult { + RawImageReader::from_compatible_slice(slice).map(|reader| reader.to_entity()) + } + fn new_builder() -> Self::Builder { + ::core::default::Default::default() + } + fn as_builder(self) -> Self::Builder { + Self::new_builder().extend(self.into_iter()) + } +} +#[derive(Clone, Copy)] +pub struct RawImageReader<'r>(&'r [u8]); +impl<'r> ::core::fmt::LowerHex for RawImageReader<'r> { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + use molecule::hex_string; + if f.alternate() { + write!(f, "0x")?; + } + write!(f, "{}", hex_string(self.as_slice())) + } +} +impl<'r> ::core::fmt::Debug for RawImageReader<'r> { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + write!(f, "{}({:#x})", Self::NAME, self) + } +} +impl<'r> ::core::fmt::Display for RawImageReader<'r> { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + use molecule::hex_string; + let raw_data = hex_string(&self.raw_data()); + write!(f, "{}(0x{})", Self::NAME, raw_data) + } +} +impl<'r> RawImageReader<'r> { + pub const ITEM_SIZE: usize = 1; + pub fn total_size(&self) -> usize { + molecule::NUMBER_SIZE + Self::ITEM_SIZE * self.item_count() + } + pub fn item_count(&self) -> usize { + molecule::unpack_number(self.as_slice()) as usize + } + pub fn len(&self) -> usize { + self.item_count() + } + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + pub fn get(&self, idx: usize) -> Option> { + if idx >= self.len() { + None + } else { + Some(self.get_unchecked(idx)) + } + } + pub fn get_unchecked(&self, idx: usize) -> ByteReader<'r> { + let start = molecule::NUMBER_SIZE + Self::ITEM_SIZE * idx; + let end = start + Self::ITEM_SIZE; + ByteReader::new_unchecked(&self.as_slice()[start..end]) + } + pub fn raw_data(&self) -> &'r [u8] { + &self.as_slice()[molecule::NUMBER_SIZE..] + } +} +impl<'r> molecule::prelude::Reader<'r> for RawImageReader<'r> { + type Entity = RawImage; + const NAME: &'static str = "RawImageReader"; + fn to_entity(&self) -> Self::Entity { + Self::Entity::new_unchecked(self.as_slice().to_owned().into()) + } + fn new_unchecked(slice: &'r [u8]) -> Self { + RawImageReader(slice) + } + fn as_slice(&self) -> &'r [u8] { + self.0 + } + fn verify(slice: &[u8], _compatible: bool) -> molecule::error::VerificationResult<()> { + use molecule::verification_error as ve; + let slice_len = slice.len(); + if slice_len < molecule::NUMBER_SIZE { + return ve!(Self, HeaderIsBroken, molecule::NUMBER_SIZE, slice_len); + } + let item_count = molecule::unpack_number(slice) as usize; + if item_count == 0 { + if slice_len != molecule::NUMBER_SIZE { + return ve!(Self, TotalSizeNotMatch, molecule::NUMBER_SIZE, slice_len); + } + return Ok(()); + } + let total_size = molecule::NUMBER_SIZE + Self::ITEM_SIZE * item_count; + if slice_len != total_size { + return ve!(Self, TotalSizeNotMatch, total_size, slice_len); + } + Ok(()) + } +} +#[derive(Clone, Debug, Default)] +pub struct RawImageBuilder(pub(crate) Vec); +impl RawImageBuilder { + pub const ITEM_SIZE: usize = 1; + pub fn set(mut self, v: Vec) -> Self { + self.0 = v; + self + } + pub fn push(mut self, v: Byte) -> Self { + self.0.push(v); + self + } + pub fn extend>(mut self, iter: T) -> Self { + for elem in iter { + self.0.push(elem); + } + self + } + pub fn replace(&mut self, index: usize, v: Byte) -> Option { + self.0 + .get_mut(index) + .map(|item| ::core::mem::replace(item, v)) + } +} +impl molecule::prelude::Builder for RawImageBuilder { + type Entity = RawImage; + const NAME: &'static str = "RawImageBuilder"; + fn expected_length(&self) -> usize { + molecule::NUMBER_SIZE + Self::ITEM_SIZE * self.0.len() + } + fn write(&self, writer: &mut W) -> molecule::io::Result<()> { + writer.write_all(&molecule::pack_number(self.0.len() as molecule::Number))?; + for inner in &self.0[..] { + writer.write_all(inner.as_slice())?; + } + Ok(()) + } + fn build(&self) -> Self::Entity { + let mut inner = Vec::with_capacity(self.expected_length()); + self.write(&mut inner) + .unwrap_or_else(|_| panic!("{} build should be ok", Self::NAME)); + RawImage::new_unchecked(inner.into()) + } +} +pub struct RawImageIterator(RawImage, usize, usize); +impl ::core::iter::Iterator for RawImageIterator { + type Item = Byte; + fn next(&mut self) -> Option { + if self.1 >= self.2 { + None + } else { + let ret = self.0.get_unchecked(self.1); + self.1 += 1; + Some(ret) + } + } +} +impl ::core::iter::ExactSizeIterator for RawImageIterator { + fn len(&self) -> usize { + self.2 - self.1 + } +} +impl ::core::iter::IntoIterator for RawImage { + type Item = Byte; + type IntoIter = RawImageIterator; + fn into_iter(self) -> Self::IntoIter { + let len = self.len(); + RawImageIterator(self, 0, len) + } +} +impl ::core::iter::FromIterator for RawImage { + fn from_iter>(iter: T) -> Self { + Self::new_builder().extend(iter).build() + } +} +impl ::core::iter::FromIterator for RawImage { + fn from_iter>(iter: T) -> Self { + Self::new_builder() + .extend(iter.into_iter().map(Into::into)) + .build() + } +} +#[derive(Clone)] +pub struct Item(molecule::bytes::Bytes); +impl ::core::fmt::LowerHex for Item { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + use molecule::hex_string; + if f.alternate() { + write!(f, "0x")?; + } + write!(f, "{}", hex_string(self.as_slice())) + } +} +impl ::core::fmt::Debug for Item { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + write!(f, "{}({:#x})", Self::NAME, self) + } +} +impl ::core::fmt::Display for Item { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + write!(f, "{}(", Self::NAME)?; + self.to_enum().display_inner(f)?; + write!(f, ")") + } +} +impl ::core::default::Default for Item { + fn default() -> Self { + let v = molecule::bytes::Bytes::from_static(&Self::DEFAULT_VALUE); + Item::new_unchecked(v) + } +} +impl Item { + const DEFAULT_VALUE: [u8; 8] = [0, 0, 0, 0, 0, 0, 0, 0]; + pub const ITEMS_COUNT: usize = 3; + pub fn item_id(&self) -> molecule::Number { + molecule::unpack_number(self.as_slice()) + } + pub fn to_enum(&self) -> ItemUnion { + let inner = self.0.slice(molecule::NUMBER_SIZE..); + match self.item_id() { + 0 => URI::new_unchecked(inner).into(), + 1 => Color::new_unchecked(inner).into(), + 2 => RawImage::new_unchecked(inner).into(), + _ => panic!("{}: invalid data", Self::NAME), + } + } + pub fn as_reader<'r>(&'r self) -> ItemReader<'r> { + ItemReader::new_unchecked(self.as_slice()) + } +} +impl molecule::prelude::Entity for Item { + type Builder = ItemBuilder; + const NAME: &'static str = "Item"; + fn new_unchecked(data: molecule::bytes::Bytes) -> Self { + Item(data) + } + fn as_bytes(&self) -> molecule::bytes::Bytes { + self.0.clone() + } + fn as_slice(&self) -> &[u8] { + &self.0[..] + } + fn from_slice(slice: &[u8]) -> molecule::error::VerificationResult { + ItemReader::from_slice(slice).map(|reader| reader.to_entity()) + } + fn from_compatible_slice(slice: &[u8]) -> molecule::error::VerificationResult { + ItemReader::from_compatible_slice(slice).map(|reader| reader.to_entity()) + } + fn new_builder() -> Self::Builder { + ::core::default::Default::default() + } + fn as_builder(self) -> Self::Builder { + Self::new_builder().set(self.to_enum()) + } +} +#[derive(Clone, Copy)] +pub struct ItemReader<'r>(&'r [u8]); +impl<'r> ::core::fmt::LowerHex for ItemReader<'r> { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + use molecule::hex_string; + if f.alternate() { + write!(f, "0x")?; + } + write!(f, "{}", hex_string(self.as_slice())) + } +} +impl<'r> ::core::fmt::Debug for ItemReader<'r> { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + write!(f, "{}({:#x})", Self::NAME, self) + } +} +impl<'r> ::core::fmt::Display for ItemReader<'r> { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + write!(f, "{}(", Self::NAME)?; + self.to_enum().display_inner(f)?; + write!(f, ")") + } +} +impl<'r> ItemReader<'r> { + pub const ITEMS_COUNT: usize = 3; + pub fn item_id(&self) -> molecule::Number { + molecule::unpack_number(self.as_slice()) + } + pub fn to_enum(&self) -> ItemUnionReader<'r> { + let inner = &self.as_slice()[molecule::NUMBER_SIZE..]; + match self.item_id() { + 0 => URIReader::new_unchecked(inner).into(), + 1 => ColorReader::new_unchecked(inner).into(), + 2 => RawImageReader::new_unchecked(inner).into(), + _ => panic!("{}: invalid data", Self::NAME), + } + } +} +impl<'r> molecule::prelude::Reader<'r> for ItemReader<'r> { + type Entity = Item; + const NAME: &'static str = "ItemReader"; + fn to_entity(&self) -> Self::Entity { + Self::Entity::new_unchecked(self.as_slice().to_owned().into()) + } + fn new_unchecked(slice: &'r [u8]) -> Self { + ItemReader(slice) + } + fn as_slice(&self) -> &'r [u8] { + self.0 + } + fn verify(slice: &[u8], compatible: bool) -> molecule::error::VerificationResult<()> { + use molecule::verification_error as ve; + let slice_len = slice.len(); + if slice_len < molecule::NUMBER_SIZE { + return ve!(Self, HeaderIsBroken, molecule::NUMBER_SIZE, slice_len); + } + let item_id = molecule::unpack_number(slice); + let inner_slice = &slice[molecule::NUMBER_SIZE..]; + match item_id { + 0 => URIReader::verify(inner_slice, compatible), + 1 => ColorReader::verify(inner_slice, compatible), + 2 => RawImageReader::verify(inner_slice, compatible), + _ => ve!(Self, UnknownItem, Self::ITEMS_COUNT, item_id), + }?; + Ok(()) + } +} +#[derive(Clone, Debug, Default)] +pub struct ItemBuilder(pub(crate) ItemUnion); +impl ItemBuilder { + pub const ITEMS_COUNT: usize = 3; + pub fn set(mut self, v: I) -> Self + where + I: ::core::convert::Into, + { + self.0 = v.into(); + self + } +} +impl molecule::prelude::Builder for ItemBuilder { + type Entity = Item; + const NAME: &'static str = "ItemBuilder"; + fn expected_length(&self) -> usize { + molecule::NUMBER_SIZE + self.0.as_slice().len() + } + fn write(&self, writer: &mut W) -> molecule::io::Result<()> { + writer.write_all(&molecule::pack_number(self.0.item_id()))?; + writer.write_all(self.0.as_slice()) + } + fn build(&self) -> Self::Entity { + let mut inner = Vec::with_capacity(self.expected_length()); + self.write(&mut inner) + .unwrap_or_else(|_| panic!("{} build should be ok", Self::NAME)); + Item::new_unchecked(inner.into()) + } +} +#[derive(Debug, Clone)] +pub enum ItemUnion { + URI(URI), + Color(Color), + RawImage(RawImage), +} +#[derive(Debug, Clone, Copy)] +pub enum ItemUnionReader<'r> { + URI(URIReader<'r>), + Color(ColorReader<'r>), + RawImage(RawImageReader<'r>), +} +impl ::core::default::Default for ItemUnion { + fn default() -> Self { + ItemUnion::URI(::core::default::Default::default()) + } +} +impl ::core::fmt::Display for ItemUnion { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + match self { + ItemUnion::URI(ref item) => { + write!(f, "{}::{}({})", Self::NAME, URI::NAME, item) + } + ItemUnion::Color(ref item) => { + write!(f, "{}::{}({})", Self::NAME, Color::NAME, item) + } + ItemUnion::RawImage(ref item) => { + write!(f, "{}::{}({})", Self::NAME, RawImage::NAME, item) + } + } + } +} +impl<'r> ::core::fmt::Display for ItemUnionReader<'r> { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + match self { + ItemUnionReader::URI(ref item) => { + write!(f, "{}::{}({})", Self::NAME, URI::NAME, item) + } + ItemUnionReader::Color(ref item) => { + write!(f, "{}::{}({})", Self::NAME, Color::NAME, item) + } + ItemUnionReader::RawImage(ref item) => { + write!(f, "{}::{}({})", Self::NAME, RawImage::NAME, item) + } + } + } +} +impl ItemUnion { + pub(crate) fn display_inner(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + match self { + ItemUnion::URI(ref item) => write!(f, "{}", item), + ItemUnion::Color(ref item) => write!(f, "{}", item), + ItemUnion::RawImage(ref item) => write!(f, "{}", item), + } + } +} +impl<'r> ItemUnionReader<'r> { + pub(crate) fn display_inner(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + match self { + ItemUnionReader::URI(ref item) => write!(f, "{}", item), + ItemUnionReader::Color(ref item) => write!(f, "{}", item), + ItemUnionReader::RawImage(ref item) => write!(f, "{}", item), + } + } +} +impl ::core::convert::From for ItemUnion { + fn from(item: URI) -> Self { + ItemUnion::URI(item) + } +} +impl ::core::convert::From for ItemUnion { + fn from(item: Color) -> Self { + ItemUnion::Color(item) + } +} +impl ::core::convert::From for ItemUnion { + fn from(item: RawImage) -> Self { + ItemUnion::RawImage(item) + } +} +impl<'r> ::core::convert::From> for ItemUnionReader<'r> { + fn from(item: URIReader<'r>) -> Self { + ItemUnionReader::URI(item) + } +} +impl<'r> ::core::convert::From> for ItemUnionReader<'r> { + fn from(item: ColorReader<'r>) -> Self { + ItemUnionReader::Color(item) + } +} +impl<'r> ::core::convert::From> for ItemUnionReader<'r> { + fn from(item: RawImageReader<'r>) -> Self { + ItemUnionReader::RawImage(item) + } +} +impl ItemUnion { + pub const NAME: &'static str = "ItemUnion"; + pub fn as_bytes(&self) -> molecule::bytes::Bytes { + match self { + ItemUnion::URI(item) => item.as_bytes(), + ItemUnion::Color(item) => item.as_bytes(), + ItemUnion::RawImage(item) => item.as_bytes(), + } + } + pub fn as_slice(&self) -> &[u8] { + match self { + ItemUnion::URI(item) => item.as_slice(), + ItemUnion::Color(item) => item.as_slice(), + ItemUnion::RawImage(item) => item.as_slice(), + } + } + pub fn item_id(&self) -> molecule::Number { + match self { + ItemUnion::URI(_) => 0, + ItemUnion::Color(_) => 1, + ItemUnion::RawImage(_) => 2, + } + } + pub fn item_name(&self) -> &str { + match self { + ItemUnion::URI(_) => "URI", + ItemUnion::Color(_) => "Color", + ItemUnion::RawImage(_) => "RawImage", + } + } + pub fn as_reader<'r>(&'r self) -> ItemUnionReader<'r> { + match self { + ItemUnion::URI(item) => item.as_reader().into(), + ItemUnion::Color(item) => item.as_reader().into(), + ItemUnion::RawImage(item) => item.as_reader().into(), + } + } +} +impl<'r> ItemUnionReader<'r> { + pub const NAME: &'r str = "ItemUnionReader"; + pub fn as_slice(&self) -> &'r [u8] { + match self { + ItemUnionReader::URI(item) => item.as_slice(), + ItemUnionReader::Color(item) => item.as_slice(), + ItemUnionReader::RawImage(item) => item.as_slice(), + } + } + pub fn item_id(&self) -> molecule::Number { + match self { + ItemUnionReader::URI(_) => 0, + ItemUnionReader::Color(_) => 1, + ItemUnionReader::RawImage(_) => 2, + } + } + pub fn item_name(&self) -> &str { + match self { + ItemUnionReader::URI(_) => "URI", + ItemUnionReader::Color(_) => "Color", + ItemUnionReader::RawImage(_) => "RawImage", + } + } +} +impl From for Item { + fn from(value: URI) -> Self { + Self::new_builder().set(value).build() + } +} +impl From for Item { + fn from(value: Color) -> Self { + Self::new_builder().set(value).build() + } +} +impl From for Item { + fn from(value: RawImage) -> Self { + Self::new_builder().set(value).build() + } +} +#[derive(Clone)] +pub struct ItemVec(molecule::bytes::Bytes); +impl ::core::fmt::LowerHex for ItemVec { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + use molecule::hex_string; + if f.alternate() { + write!(f, "0x")?; + } + write!(f, "{}", hex_string(self.as_slice())) + } +} +impl ::core::fmt::Debug for ItemVec { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + write!(f, "{}({:#x})", Self::NAME, self) + } +} +impl ::core::fmt::Display for ItemVec { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + write!(f, "{} [", Self::NAME)?; + for i in 0..self.len() { + if i == 0 { + write!(f, "{}", self.get_unchecked(i))?; + } else { + write!(f, ", {}", self.get_unchecked(i))?; + } + } + write!(f, "]") + } +} +impl ::core::default::Default for ItemVec { + fn default() -> Self { + let v = molecule::bytes::Bytes::from_static(&Self::DEFAULT_VALUE); + ItemVec::new_unchecked(v) + } +} +impl ItemVec { + const DEFAULT_VALUE: [u8; 4] = [4, 0, 0, 0]; + pub fn total_size(&self) -> usize { + molecule::unpack_number(self.as_slice()) as usize + } + pub fn item_count(&self) -> usize { + if self.total_size() == molecule::NUMBER_SIZE { + 0 + } else { + (molecule::unpack_number(&self.as_slice()[molecule::NUMBER_SIZE..]) as usize / 4) - 1 + } + } + pub fn len(&self) -> usize { + self.item_count() + } + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + pub fn get(&self, idx: usize) -> Option { + if idx >= self.len() { + None + } else { + Some(self.get_unchecked(idx)) + } + } + pub fn get_unchecked(&self, idx: usize) -> Item { + let slice = self.as_slice(); + let start_idx = molecule::NUMBER_SIZE * (1 + idx); + let start = molecule::unpack_number(&slice[start_idx..]) as usize; + if idx == self.len() - 1 { + Item::new_unchecked(self.0.slice(start..)) + } else { + let end_idx = start_idx + molecule::NUMBER_SIZE; + let end = molecule::unpack_number(&slice[end_idx..]) as usize; + Item::new_unchecked(self.0.slice(start..end)) + } + } + pub fn as_reader<'r>(&'r self) -> ItemVecReader<'r> { + ItemVecReader::new_unchecked(self.as_slice()) + } +} +impl molecule::prelude::Entity for ItemVec { + type Builder = ItemVecBuilder; + const NAME: &'static str = "ItemVec"; + fn new_unchecked(data: molecule::bytes::Bytes) -> Self { + ItemVec(data) + } + fn as_bytes(&self) -> molecule::bytes::Bytes { + self.0.clone() + } + fn as_slice(&self) -> &[u8] { + &self.0[..] + } + fn from_slice(slice: &[u8]) -> molecule::error::VerificationResult { + ItemVecReader::from_slice(slice).map(|reader| reader.to_entity()) + } + fn from_compatible_slice(slice: &[u8]) -> molecule::error::VerificationResult { + ItemVecReader::from_compatible_slice(slice).map(|reader| reader.to_entity()) + } + fn new_builder() -> Self::Builder { + ::core::default::Default::default() + } + fn as_builder(self) -> Self::Builder { + Self::new_builder().extend(self.into_iter()) + } +} +#[derive(Clone, Copy)] +pub struct ItemVecReader<'r>(&'r [u8]); +impl<'r> ::core::fmt::LowerHex for ItemVecReader<'r> { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + use molecule::hex_string; + if f.alternate() { + write!(f, "0x")?; + } + write!(f, "{}", hex_string(self.as_slice())) + } +} +impl<'r> ::core::fmt::Debug for ItemVecReader<'r> { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + write!(f, "{}({:#x})", Self::NAME, self) + } +} +impl<'r> ::core::fmt::Display for ItemVecReader<'r> { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + write!(f, "{} [", Self::NAME)?; + for i in 0..self.len() { + if i == 0 { + write!(f, "{}", self.get_unchecked(i))?; + } else { + write!(f, ", {}", self.get_unchecked(i))?; + } + } + write!(f, "]") + } +} +impl<'r> ItemVecReader<'r> { + pub fn total_size(&self) -> usize { + molecule::unpack_number(self.as_slice()) as usize + } + pub fn item_count(&self) -> usize { + if self.total_size() == molecule::NUMBER_SIZE { + 0 + } else { + (molecule::unpack_number(&self.as_slice()[molecule::NUMBER_SIZE..]) as usize / 4) - 1 + } + } + pub fn len(&self) -> usize { + self.item_count() + } + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + pub fn get(&self, idx: usize) -> Option> { + if idx >= self.len() { + None + } else { + Some(self.get_unchecked(idx)) + } + } + pub fn get_unchecked(&self, idx: usize) -> ItemReader<'r> { + let slice = self.as_slice(); + let start_idx = molecule::NUMBER_SIZE * (1 + idx); + let start = molecule::unpack_number(&slice[start_idx..]) as usize; + if idx == self.len() - 1 { + ItemReader::new_unchecked(&self.as_slice()[start..]) + } else { + let end_idx = start_idx + molecule::NUMBER_SIZE; + let end = molecule::unpack_number(&slice[end_idx..]) as usize; + ItemReader::new_unchecked(&self.as_slice()[start..end]) + } + } +} +impl<'r> molecule::prelude::Reader<'r> for ItemVecReader<'r> { + type Entity = ItemVec; + const NAME: &'static str = "ItemVecReader"; + fn to_entity(&self) -> Self::Entity { + Self::Entity::new_unchecked(self.as_slice().to_owned().into()) + } + fn new_unchecked(slice: &'r [u8]) -> Self { + ItemVecReader(slice) + } + fn as_slice(&self) -> &'r [u8] { + self.0 + } + fn verify(slice: &[u8], compatible: bool) -> molecule::error::VerificationResult<()> { + use molecule::verification_error as ve; + let slice_len = slice.len(); + if slice_len < molecule::NUMBER_SIZE { + return ve!(Self, HeaderIsBroken, molecule::NUMBER_SIZE, slice_len); + } + let total_size = molecule::unpack_number(slice) as usize; + if slice_len != total_size { + return ve!(Self, TotalSizeNotMatch, total_size, slice_len); + } + if slice_len == molecule::NUMBER_SIZE { + return Ok(()); + } + if slice_len < molecule::NUMBER_SIZE * 2 { + return ve!( + Self, + TotalSizeNotMatch, + molecule::NUMBER_SIZE * 2, + slice_len + ); + } + let offset_first = molecule::unpack_number(&slice[molecule::NUMBER_SIZE..]) as usize; + if offset_first % molecule::NUMBER_SIZE != 0 || offset_first < molecule::NUMBER_SIZE * 2 { + return ve!(Self, OffsetsNotMatch); + } + if slice_len < offset_first { + return ve!(Self, HeaderIsBroken, offset_first, slice_len); + } + let mut offsets: Vec = slice[molecule::NUMBER_SIZE..offset_first] + .chunks_exact(molecule::NUMBER_SIZE) + .map(|x| molecule::unpack_number(x) as usize) + .collect(); + offsets.push(total_size); + if offsets.windows(2).any(|i| i[0] > i[1]) { + return ve!(Self, OffsetsNotMatch); + } + for pair in offsets.windows(2) { + let start = pair[0]; + let end = pair[1]; + ItemReader::verify(&slice[start..end], compatible)?; + } + Ok(()) + } +} +#[derive(Clone, Debug, Default)] +pub struct ItemVecBuilder(pub(crate) Vec); +impl ItemVecBuilder { + pub fn set(mut self, v: Vec) -> Self { + self.0 = v; + self + } + pub fn push(mut self, v: Item) -> Self { + self.0.push(v); + self + } + pub fn extend>(mut self, iter: T) -> Self { + for elem in iter { + self.0.push(elem); + } + self + } + pub fn replace(&mut self, index: usize, v: Item) -> Option { + self.0 + .get_mut(index) + .map(|item| ::core::mem::replace(item, v)) + } +} +impl molecule::prelude::Builder for ItemVecBuilder { + type Entity = ItemVec; + const NAME: &'static str = "ItemVecBuilder"; + fn expected_length(&self) -> usize { + molecule::NUMBER_SIZE * (self.0.len() + 1) + + self + .0 + .iter() + .map(|inner| inner.as_slice().len()) + .sum::() + } + fn write(&self, writer: &mut W) -> molecule::io::Result<()> { + let item_count = self.0.len(); + if item_count == 0 { + writer.write_all(&molecule::pack_number( + molecule::NUMBER_SIZE as molecule::Number, + ))?; + } else { + let (total_size, offsets) = self.0.iter().fold( + ( + molecule::NUMBER_SIZE * (item_count + 1), + Vec::with_capacity(item_count), + ), + |(start, mut offsets), inner| { + offsets.push(start); + (start + inner.as_slice().len(), offsets) + }, + ); + writer.write_all(&molecule::pack_number(total_size as molecule::Number))?; + for offset in offsets.into_iter() { + writer.write_all(&molecule::pack_number(offset as molecule::Number))?; + } + for inner in self.0.iter() { + writer.write_all(inner.as_slice())?; + } + } + Ok(()) + } + fn build(&self) -> Self::Entity { + let mut inner = Vec::with_capacity(self.expected_length()); + self.write(&mut inner) + .unwrap_or_else(|_| panic!("{} build should be ok", Self::NAME)); + ItemVec::new_unchecked(inner.into()) + } +} +pub struct ItemVecIterator(ItemVec, usize, usize); +impl ::core::iter::Iterator for ItemVecIterator { + type Item = Item; + fn next(&mut self) -> Option { + if self.1 >= self.2 { + None + } else { + let ret = self.0.get_unchecked(self.1); + self.1 += 1; + Some(ret) + } + } +} +impl ::core::iter::ExactSizeIterator for ItemVecIterator { + fn len(&self) -> usize { + self.2 - self.1 + } +} +impl ::core::iter::IntoIterator for ItemVec { + type Item = Item; + type IntoIter = ItemVecIterator; + fn into_iter(self) -> Self::IntoIter { + let len = self.len(); + ItemVecIterator(self, 0, len) + } +} +impl<'r> ItemVecReader<'r> { + pub fn iter<'t>(&'t self) -> ItemVecReaderIterator<'t, 'r> { + ItemVecReaderIterator(&self, 0, self.len()) + } +} +pub struct ItemVecReaderIterator<'t, 'r>(&'t ItemVecReader<'r>, usize, usize); +impl<'t: 'r, 'r> ::core::iter::Iterator for ItemVecReaderIterator<'t, 'r> { + type Item = ItemReader<'t>; + fn next(&mut self) -> Option { + if self.1 >= self.2 { + None + } else { + let ret = self.0.get_unchecked(self.1); + self.1 += 1; + Some(ret) + } + } +} +impl<'t: 'r, 'r> ::core::iter::ExactSizeIterator for ItemVecReaderIterator<'t, 'r> { + fn len(&self) -> usize { + self.2 - self.1 + } +} +impl ::core::iter::FromIterator for ItemVec { + fn from_iter>(iter: T) -> Self { + Self::new_builder().extend(iter).build() + } +} diff --git a/src/types/mod.rs b/src/types/mod.rs new file mode 100644 index 0000000..65c2782 --- /dev/null +++ b/src/types/mod.rs @@ -0,0 +1,6 @@ +mod error; +mod object; + +pub mod generated; +pub use error::Error; +pub use object::*; diff --git a/src/types.rs b/src/types/object.rs similarity index 53% rename from src/types.rs rename to src/types/object.rs index ce84d6f..83e53f4 100644 --- a/src/types.rs +++ b/src/types/object.rs @@ -1,4 +1,4 @@ -use std::path::PathBuf; +use std::{collections::HashMap, path::PathBuf}; use ckb_types::{core::ScriptHashType, H256}; use serde::Deserialize; @@ -9,66 +9,11 @@ use jsonrpsee::types::ErrorCode; #[cfg(feature = "standalone_server")] use serde::Serialize; -#[allow(clippy::enum_variant_names)] -#[derive(thiserror::Error, Debug)] -#[repr(i32)] -pub enum Error { - #[error("DNA bytes length not match the requirement in Cluster")] - DnaLengthNotMatch = 1001, - #[error("spore id length should equal to 32")] - SporeIdLengthInvalid, - #[error("natvie decoder not found")] - NativeDecoderNotFound, - #[error("spore id not exist on-chain")] - SporeIdNotFound, - #[error("uncompatible spore data")] - SporeDataUncompatible, - #[error("uncompatible spore data content_type")] - SporeDataContentTypeUncompatible, - #[error("unexpected DOB protocol version")] - DOBVersionUnexpected, - #[error("miss cluster id in spore data")] - ClusterIdNotSet, - #[error("cluster id not exist on-chain")] - ClusterIdNotFound, - #[error("uncompatible cluster data")] - ClusterDataUncompatible, - #[error("decoder id not exist on-chain")] - DecoderIdNotFound, - #[error("output of decoder should contain at least one line")] - DecoderOutputInvalid, - #[error("DNA string is not in hex format")] - HexedDNAParseError, - #[error("spore id string is not in hex format")] - HexedSporeIdParseError, - #[error("invalid decoder path to persist")] - DecoderBinaryPathInvalid, - #[error("encounter error while executing DNA decoding")] - DecoderExecutionError, - #[error("decoding program triggered an error")] - DecoderExecutionInternalError, - #[error("encounter error while searching live cells")] - FetchLiveCellsError, - #[error("encounter error while searching transaction by hash")] - FetchTransactionError, - #[error("not found specific output_cell in transaction")] - NoOutputCellInTransaction, - #[error("spore content cannot parse to DOB content")] - DOBContentUnexpected, - #[error("cluster description cannot parse to DOB metadata")] - DOBMetadataUnexpected, - #[error("DOB render cache folder not found")] - DOBRenderCacheNotFound, - #[error("cached DOB render result file has changed unexpectedly")] - DOBRenderCacheModified, - #[error("invalid deployed on-chain decoder code_hash")] - DecoderBinaryHashInvalid, - #[error("no binary found in cell for decoder")] - DecoderBinaryNotFoundInCell, - #[error("error ocurred while requesing json-rpc")] - JsonRpcRequestError, - #[error("error ocurred while requiring system timestamp")] - SystemTimeError, +use crate::types::Error; + +pub enum Dob<'a> { + V0(&'a DOBClusterFormatV0), + V1(&'a DOBClusterFormatV1), } #[cfg(feature = "standalone_server")] @@ -80,26 +25,94 @@ impl From for ErrorCode { // value on `description` field in Cluster data, adapting for DOB protocol in JSON format #[derive(Deserialize)] -#[cfg_attr(test, derive(serde::Serialize, PartialEq, Debug))] +#[cfg_attr(feature = "standalone_server", derive(Serialize))] +#[cfg_attr(test, derive(PartialEq, Debug))] pub struct ClusterDescriptionField { pub description: String, pub dob: DOBClusterFormat, } +impl ClusterDescriptionField { + pub fn unbox_dob(&self) -> Result { + match self.dob.ver { + Some(0) | None => { + let dob0 = self + .dob + .dob_ver_0 + .as_ref() + .ok_or(Error::ClusterDataUncompatible)?; + Ok(Dob::V0(dob0)) + } + Some(1) => { + let dob1 = self + .dob + .dob_ver_1 + .as_ref() + .ok_or(Error::ClusterDataUncompatible)?; + Ok(Dob::V1(dob1)) + } + _ => Err(Error::DOBVersionUnexpected), + } + } +} + // contains `decoder` and `pattern` identifiers +// +// note: if `ver` is empty, `dob_ver_0` must uniquely exist #[derive(Deserialize)] -#[cfg_attr(test, derive(serde::Serialize, PartialEq, Debug))] +#[cfg_attr(feature = "standalone_server", derive(Serialize))] +#[cfg_attr(test, derive(PartialEq, Debug))] pub struct DOBClusterFormat { #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] pub ver: Option, + #[serde(flatten)] + pub dob_ver_0: Option, + #[serde(flatten)] + pub dob_ver_1: Option, +} + +#[cfg(test)] +impl DOBClusterFormat { + #[allow(dead_code)] + pub fn new_dob0(dob_ver_0: DOBClusterFormatV0) -> Self { + Self { + ver: Some(0), + dob_ver_0: Some(dob_ver_0), + dob_ver_1: None, + } + } + + #[allow(dead_code)] + pub fn new_dob1(dob_ver_1: DOBClusterFormatV1) -> Self { + Self { + ver: Some(1), + dob_ver_0: None, + dob_ver_1: Some(dob_ver_1), + } + } +} + +#[derive(Deserialize)] +#[cfg_attr(feature = "standalone_server", derive(Serialize))] +#[cfg_attr(test, derive(PartialEq, Debug))] +pub struct DOBClusterFormatV0 { pub decoder: DOBDecoderFormat, pub pattern: Value, } +#[derive(Deserialize)] +#[cfg_attr(feature = "standalone_server", derive(Serialize))] +#[cfg_attr(test, derive(PartialEq, Debug))] +pub struct DOBClusterFormatV1 { + pub traits: DOBClusterFormatV0, + pub images: DOBClusterFormatV0, +} + // restricted decoder locator type #[derive(Deserialize)] -#[cfg_attr(test, derive(serde::Serialize, PartialEq, Debug))] +#[cfg_attr(feature = "standalone_server", derive(Serialize))] +#[cfg_attr(test, derive(PartialEq, Debug))] pub enum DecoderLocationType { #[serde(rename(serialize = "type_id", deserialize = "type_id"))] TypeId, @@ -109,7 +122,8 @@ pub enum DecoderLocationType { // decoder location information #[derive(Deserialize)] -#[cfg_attr(test, derive(serde::Serialize, PartialEq, Debug))] +#[cfg_attr(feature = "standalone_server", derive(Serialize))] +#[cfg_attr(test, derive(PartialEq, Debug))] pub struct DOBDecoderFormat { #[serde(rename(serialize = "type", deserialize = "type"))] pub location: DecoderLocationType, @@ -140,8 +154,8 @@ pub enum HashType { } impl From<&HashType> for ScriptHashType { - fn from(value: &HashType) -> Self { - match value { + fn from(hash_type: &HashType) -> ScriptHashType { + match hash_type { HashType::Data => ScriptHashType::Data, HashType::Data1 => ScriptHashType::Data1, HashType::Data2 => ScriptHashType::Data2, @@ -163,11 +177,14 @@ pub struct ScriptId { pub struct Settings { pub protocol_versions: Vec, pub ckb_rpc: String, + pub image_fetcher_url: HashMap, pub rpc_server_address: String, pub ckb_vm_runner: String, pub decoders_cache_directory: PathBuf, pub dobs_cache_directory: PathBuf, pub dobs_cache_expiration_sec: u64, + pub dob1_max_combination: usize, + pub dob1_max_cache_size: usize, pub onchain_decoder_deployment: Vec, pub available_spores: Vec, pub available_clusters: Vec, diff --git a/src/vm.rs b/src/vm.rs index 537dacd..284b079 100644 --- a/src/vm.rs +++ b/src/vm.rs @@ -1,11 +1,32 @@ // refer to https://github.com/nervosnetwork/ckb-vm/blob/develop/examples/ckb-vm-runner.rs -use std::sync::{Arc, Mutex}; +use std::io::Cursor; +use std::sync::{mpsc, Arc, Mutex}; +use std::thread; +use base64::{engine::general_purpose::STANDARD, Engine}; use ckb_vm::cost_model::estimate_cycles; -use ckb_vm::registers::{A0, A7}; +use ckb_vm::registers::{A0, A1, A2, A3, A7}; use ckb_vm::{Bytes, Memory, Register, SupportMachine, Syscalls}; +use image::codecs::png::{CompressionType, FilterType, PngEncoder}; +use image::{imageops, load_from_memory, DynamicImage, Pixel, Rgb, RgbaImage}; +use molecule::prelude::Entity; +use crate::client::ImageFetchClient; +use crate::types::{generated, Error, Settings}; + +macro_rules! error { + ($err: expr) => {{ + let error = $err.to_string(); + #[cfg(test)] + println!("[ERROR] {error}"); + #[cfg(not(test))] + jsonrpsee::tracing::error!("{error}"); + ckb_vm::error::Error::Unexpected(error) + }}; +} + +// ckb-vm syscall for printing debug information struct DebugSyscall { output: Arc>>, } @@ -40,20 +61,164 @@ impl Syscalls for DebugSyscall { .clone() .lock() .unwrap() - .push(String::from_utf8(buffer).unwrap()); + .push(String::from_utf8(buffer).map_err(|err| error!(err))?); Ok(true) } } +// ckb-vm syscall for image combination +struct ImageCombinationSyscall { + client: Arc>, + max_combination: usize, +} + +impl ImageCombinationSyscall { + // fetch one image synchronously + // + // note: for saving space purpose, we take time to trade memory space through + // fetching images one by one + fn fetch_one_image_sync(&mut self, image_uri: String) -> Result, Error> { + let (tx, rx) = mpsc::channel(); + let client = self.client.clone(); + thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().unwrap(); + let mut client = client.lock().unwrap(); + tx.send(rt.block_on(client.fetch_images(&[image_uri]))) + .expect("send"); + }); + Ok(rx.recv().expect("recv")?.remove(0)) + } +} + +impl Syscalls for ImageCombinationSyscall { + fn initialize(&mut self, _machine: &mut Mac) -> Result<(), ckb_vm::error::Error> { + Ok(()) + } + + fn ecall(&mut self, machine: &mut Mac) -> Result { + let code = &machine.registers()[A7]; + if code.to_i32() != 2077 { + return Ok(false); + } + + // prepare input arguments + let buffer_addr = machine.registers()[A0].to_u64(); + let buffer_size_addr = machine.registers()[A1].clone(); + let mut buffer_size = machine.memory_mut().load64(&buffer_size_addr)?.to_u64(); + let molecule_addr = machine.registers()[A2].to_u64(); + let molecule_size = machine.registers()[A3].to_u64(); + + // parse all of images uri/color/raw + let pattern_bytes = machine + .memory_mut() + .load_bytes(molecule_addr, molecule_size)?; + let pattern = + generated::ItemVec::from_compatible_slice(&pattern_bytes).map_err(|err| error!(err))?; + if pattern.len() > self.max_combination { + return Err(error!("too many combine operations")); + } + + // handle DOB/1 pattern + #[cfg(feature = "render_debug")] + { + println!("\n-------- DOB/1 IMAGES ---------"); + } + let mut combination = image::DynamicImage::new_rgba8(1, 1); + for item in pattern.into_iter() { + match item.to_enum() { + generated::ItemUnion::Color(color) => { + let color_code = String::from_utf8_lossy(&color.raw_data()).to_string(); + #[cfg(feature = "render_debug")] + { + println!("COLOR => {color_code}"); + } + let rgb = hex::decode(color_code.trim_start_matches('#')) + .map_err(|err| error!(err))?; + if rgb.len() != 3 { + return Err(error!("invalid color code")); + } + let pixel = Rgb([rgb[0], rgb[1], rgb[2]]).to_rgba(); + let image = DynamicImage::ImageRgba8(RgbaImage::from_pixel(1, 1, pixel)); + overlay_both_images(&mut combination, image); + } + generated::ItemUnion::RawImage(raw_image) => { + #[cfg(feature = "render_debug")] + { + println!("IMAGE => (base64 len: {})", raw_image.raw_data().len()); + } + let image_bytes = STANDARD + .decode(raw_image.raw_data()) + .map_err(|err| error!(err))?; + let image = load_from_memory(&image_bytes).map_err(|err| error!(err))?; + overlay_both_images(&mut combination, image); + } + generated::ItemUnion::URI(uri) => { + let uri = String::from_utf8_lossy(&uri.raw_data()).to_string(); + #[cfg(feature = "render_debug")] + { + println!("FSURI => {uri}"); + } + let raw_image = self.fetch_one_image_sync(uri).map_err(|err| error!(err))?; + let image = load_from_memory(&raw_image).map_err(|err| error!(err))?; + overlay_both_images(&mut combination, image); + } + } + } + #[cfg(feature = "render_debug")] + { + println!("-------- DOB/1 IMAGES END ---------"); + } + + // return output + let mut output = Vec::new(); + let cursor = Cursor::new(&mut output); + let png = PngEncoder::new_with_quality(cursor, CompressionType::Best, FilterType::NoFilter); + combination + .write_with_encoder(png) + .map_err(|err| error!(err))?; + if buffer_size > 0 { + buffer_size = buffer_size.min(output.len() as u64); + machine + .memory_mut() + .store_bytes(buffer_addr, &output[..buffer_size as usize])?; + } else { + buffer_size = output.len() as u64; + } + machine + .memory_mut() + .store64(&buffer_size_addr, &Mac::REG::from_u64(buffer_size))?; + + Ok(true) + } +} + +fn overlay_both_images(base: &mut DynamicImage, mut overlayer: DynamicImage) { + let width = base.width().max(overlayer.width()); + let height = base.height().max(overlayer.height()); + if base.width() != width || base.height() != height { + *base = base.resize(width, height, imageops::FilterType::Nearest); + } + if overlayer.width() != width || overlayer.height() != height { + overlayer = overlayer.resize(width, height, imageops::FilterType::Nearest); + } + imageops::overlay(base, &overlayer, 0, 0); +} + fn main_asm( code: Bytes, args: Vec, + settings: &Settings, ) -> Result<(i8, Vec), Box> { let debug_result = Arc::new(Mutex::new(Vec::new())); let debug = Box::new(DebugSyscall { output: debug_result.clone(), }); + let client = ImageFetchClient::new(&settings.image_fetcher_url, settings.dob1_max_cache_size); + let image = Box::new(ImageCombinationSyscall { + client: Arc::new(Mutex::new(client)), + max_combination: settings.dob1_max_combination, + }); let asm_core = ckb_vm::machine::asm::AsmCoreMachine::new( ckb_vm::ISA_IMC | ckb_vm::ISA_B | ckb_vm::ISA_MOP | ckb_vm::ISA_A, @@ -63,6 +228,7 @@ fn main_asm( let core = ckb_vm::DefaultMachineBuilder::new(asm_core) .instruction_cycle_func(Box::new(estimate_cycles)) .syscall(debug) + .syscall(image) .build(); let mut machine = ckb_vm::machine::asm::AsmMachine::new(core); machine.load_program(&code, &args)?; @@ -75,7 +241,8 @@ fn main_asm( pub fn execute_riscv_binary( binary_path: &str, args: Vec, + settings: &Settings, ) -> Result<(i8, Vec), Box> { let code = std::fs::read(binary_path)?.into(); - main_asm(code, args) + main_asm(code, args, settings) }